mirror of
https://github.com/kuroppoi/entralinked.git
synced 2026-04-25 15:47:00 -05:00
GUI tweaks + add option to edit décor (#6)
This commit is contained in:
parent
2385398b4b
commit
4031d833e8
46
src/main/java/entralinked/gui/GsidDocumentFilter.java
Normal file
46
src/main/java/entralinked/gui/GsidDocumentFilter.java
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
package entralinked.gui;
|
||||
|
||||
import java.awt.Toolkit;
|
||||
|
||||
import javax.swing.text.AttributeSet;
|
||||
import javax.swing.text.BadLocationException;
|
||||
|
||||
import entralinked.utility.GsidUtility;
|
||||
|
||||
public class GsidDocumentFilter extends SizeLimitDocumentFilter {
|
||||
|
||||
public GsidDocumentFilter() {
|
||||
super(10);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void insertString(FilterBypass fb, int offset, String text, AttributeSet attrs) throws BadLocationException {
|
||||
replace(fb, offset, 0, text, attrs);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void replace(FilterBypass fb, int offset, int length, String text, AttributeSet attrs) throws BadLocationException {
|
||||
if(text == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
StringBuilder builder = new StringBuilder();
|
||||
boolean shouldBeep = false;
|
||||
|
||||
for(int i = 0; i < text.length(); i++) {
|
||||
char c = Character.toUpperCase(text.charAt(i));
|
||||
|
||||
if(GsidUtility.GSID_CHARTABLE.indexOf(c) != -1) {
|
||||
builder.append(c);
|
||||
} else {
|
||||
shouldBeep = true;
|
||||
}
|
||||
}
|
||||
|
||||
if(shouldBeep) {
|
||||
Toolkit.getDefaultToolkit().beep();
|
||||
}
|
||||
|
||||
super.replace(fb, offset, length, builder.toString(), attrs);
|
||||
}
|
||||
}
|
||||
40
src/main/java/entralinked/gui/SizeLimitDocumentFilter.java
Normal file
40
src/main/java/entralinked/gui/SizeLimitDocumentFilter.java
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
package entralinked.gui;
|
||||
|
||||
import java.awt.Toolkit;
|
||||
|
||||
import javax.swing.text.AttributeSet;
|
||||
import javax.swing.text.BadLocationException;
|
||||
import javax.swing.text.DocumentFilter;
|
||||
|
||||
public class SizeLimitDocumentFilter extends DocumentFilter {
|
||||
|
||||
private final int limit;
|
||||
|
||||
public SizeLimitDocumentFilter(int limit) {
|
||||
this.limit = limit;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void insertString(FilterBypass fb, int offset, String text, AttributeSet attrs) throws BadLocationException {
|
||||
replace(fb, offset, 0, text, attrs);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void replace(FilterBypass fb, int offset, int length, String text, AttributeSet attrs) throws BadLocationException {
|
||||
if(text == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(limit <= 0) {
|
||||
super.replace(fb, offset, length, text, attrs);
|
||||
return;
|
||||
}
|
||||
|
||||
int finalLength = Math.min(text.length(), Math.max(0, limit - fb.getDocument().getLength() + length));
|
||||
super.replace(fb, offset, length, text.substring(0, finalLength), attrs);
|
||||
|
||||
if(finalLength != text.length()) {
|
||||
Toolkit.getDefaultToolkit().beep();
|
||||
}
|
||||
}
|
||||
}
|
||||
381
src/main/java/entralinked/gui/panels/CustomizationPanel.java
Normal file
381
src/main/java/entralinked/gui/panels/CustomizationPanel.java
Normal file
|
|
@ -0,0 +1,381 @@
|
|||
package entralinked.gui.panels;
|
||||
|
||||
import java.awt.Dimension;
|
||||
import java.awt.Image;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Vector;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import javax.swing.BorderFactory;
|
||||
import javax.swing.DefaultComboBoxModel;
|
||||
import javax.swing.ImageIcon;
|
||||
import javax.swing.JButton;
|
||||
import javax.swing.JComboBox;
|
||||
import javax.swing.JLabel;
|
||||
import javax.swing.JOptionPane;
|
||||
import javax.swing.JPanel;
|
||||
import javax.swing.filechooser.FileFilter;
|
||||
import javax.swing.filechooser.FileNameExtensionFilter;
|
||||
|
||||
import entralinked.Entralinked;
|
||||
import entralinked.GameVersion;
|
||||
import entralinked.gui.FileChooser;
|
||||
import entralinked.gui.ModelListCellRenderer;
|
||||
import entralinked.model.player.Player;
|
||||
import entralinked.utility.Crc16;
|
||||
import entralinked.utility.LEOutputStream;
|
||||
import entralinked.utility.SwingUtility;
|
||||
import entralinked.utility.TiledImageUtility;
|
||||
import net.miginfocom.swing.MigLayout;
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
public class CustomizationPanel extends JPanel {
|
||||
|
||||
@FunctionalInterface
|
||||
private static interface SkinWriter {
|
||||
|
||||
public void writeSkin(OutputStream outputStream, BufferedImage image) throws IOException;
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal model for combo boxes.
|
||||
*/
|
||||
private static record DlcOption(String type, String name, String path, boolean custom) {
|
||||
|
||||
public DlcOption(String type, String name, String path) {
|
||||
this(type, name, path, false);
|
||||
}
|
||||
}
|
||||
|
||||
private static final FileFilter IMAGE_FILE_FILTER = new FileNameExtensionFilter("Image Files (*.png)", "png");
|
||||
private static final FileFilter CGEAR_FILE_FILTER = new FileNameExtensionFilter("C-Gear Skin Files (*.bin, *.cgb, *.psk)", "bin", "cgb", "psk");
|
||||
private static final FileFilter ZUKAN_FILE_FILTER = new FileNameExtensionFilter("Pokédex Skin Files (*.bin, *.pds)", "bin", "pds");
|
||||
private static final byte[] NARC_HEADER = { 0x4E, 0x41, 0x52, 0x43, (byte)0xFE, (byte)0xFF, 0x00, 0x01 };
|
||||
private static final Map<String, Image> skinCache = new HashMap<>();
|
||||
private final Entralinked entralinked;
|
||||
private final JComboBox<DlcOption> cgearComboBox;
|
||||
private final JComboBox<DlcOption> zukanComboBox;
|
||||
private final JComboBox<DlcOption> musicalComboBox;
|
||||
private final JPanel optionPanel;
|
||||
private Player player;
|
||||
private GameVersion gameVersion;
|
||||
private DlcOption customCGearSkin;
|
||||
private DlcOption customDexSkin;
|
||||
private DlcOption customMusical;
|
||||
|
||||
public CustomizationPanel(Entralinked entralinked) {
|
||||
this.entralinked = entralinked;
|
||||
setLayout(new MigLayout("align 50% 50%"));
|
||||
|
||||
// Create preview labels
|
||||
JLabel cgearPreviewLabel = new JLabel("No preview available.", JLabel.CENTER);
|
||||
cgearPreviewLabel.setPreferredSize(new Dimension(TiledImageUtility.SCREEN_WIDTH, TiledImageUtility.SCREEN_HEIGHT));
|
||||
JLabel dexPreviewLabel = new JLabel("No preview available.", JLabel.CENTER);
|
||||
dexPreviewLabel.setPreferredSize(new Dimension(TiledImageUtility.SCREEN_WIDTH, TiledImageUtility.SCREEN_HEIGHT));
|
||||
|
||||
// Create preview image panels
|
||||
// Labels are added to a subpanel first otherwise the preferred size will include the border which causes issues
|
||||
JPanel cgearPreviewPanel = new JPanel(new MigLayout("insets 0"));
|
||||
cgearPreviewPanel.setBorder(BorderFactory.createTitledBorder("C-Gear Skin Preview"));
|
||||
cgearPreviewPanel.add(cgearPreviewLabel);
|
||||
JPanel dexPreviewPanel = new JPanel(new MigLayout("insets 0"));
|
||||
dexPreviewPanel.setBorder(BorderFactory.createTitledBorder("Pokédex Skin Preview"));
|
||||
dexPreviewPanel.add(dexPreviewLabel);
|
||||
JPanel previewPanel = new JPanel();
|
||||
previewPanel.add(cgearPreviewPanel);
|
||||
previewPanel.add(dexPreviewPanel);
|
||||
add(previewPanel, "spanx, align 50%, wrap");
|
||||
|
||||
// Create combo boxes
|
||||
ModelListCellRenderer<DlcOption> renderer = new ModelListCellRenderer<>(DlcOption.class, DlcOption::name, "Do not change");
|
||||
cgearComboBox = new JComboBox<>();
|
||||
cgearComboBox.setMinimumSize(cgearComboBox.getPreferredSize());
|
||||
cgearComboBox.setRenderer(renderer);
|
||||
cgearComboBox.addActionListener(event -> updateSkinPreview(cgearComboBox, cgearPreviewLabel));
|
||||
zukanComboBox = new JComboBox<>();
|
||||
zukanComboBox.setMinimumSize(zukanComboBox.getPreferredSize());
|
||||
zukanComboBox.setRenderer(renderer);
|
||||
zukanComboBox.addActionListener(event -> updateSkinPreview(zukanComboBox, dexPreviewLabel));
|
||||
musicalComboBox = new JComboBox<>();
|
||||
musicalComboBox.setMinimumSize(musicalComboBox.getPreferredSize());
|
||||
musicalComboBox.setRenderer(renderer);
|
||||
|
||||
// Create option panel
|
||||
optionPanel = new JPanel(new MigLayout());
|
||||
|
||||
// Create C-Gear skin selector
|
||||
createDlcOption("C-Gear Skin", cgearComboBox, () -> {
|
||||
FileChooser.showFileOpenDialog(getRootPane(), Arrays.asList(IMAGE_FILE_FILTER, CGEAR_FILE_FILTER), selection -> {
|
||||
File dst = player.getCGearSkinFile();
|
||||
File file = selection.file();
|
||||
FileFilter filter = selection.filter();
|
||||
|
||||
if(filter == IMAGE_FILE_FILTER) {
|
||||
if(!importSkinImage(file, dst, (stream, image) -> TiledImageUtility.writeCGearSkin(stream, image, !gameVersion.isVersion2()))) {
|
||||
return;
|
||||
}
|
||||
} else if(filter == CGEAR_FILE_FILTER) {
|
||||
if(!importSkinFile(file, dst, 9730)) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
DlcOption option = new DlcOption(gameVersion.isVersion2() ? "CGEAR2" : "CGEAR", file.getName(), dst.getAbsolutePath(), true);
|
||||
updateCustomOption(cgearComboBox, customCGearSkin, option);
|
||||
customCGearSkin = option;
|
||||
player.setCustomCGearSkin(customCGearSkin.name());
|
||||
});
|
||||
});
|
||||
|
||||
// Create Pokédex skin selector
|
||||
createDlcOption("Pokédex Skin", zukanComboBox, () -> {
|
||||
FileChooser.showFileOpenDialog(getRootPane(), Arrays.asList(IMAGE_FILE_FILTER, ZUKAN_FILE_FILTER), selection -> {
|
||||
File dst = player.getDexSkinFile();
|
||||
File file = selection.file();
|
||||
FileFilter filter = selection.filter();
|
||||
|
||||
if(filter == IMAGE_FILE_FILTER) {
|
||||
if(!importSkinImage(file, dst, (stream, image) -> TiledImageUtility.writeDexSkin(stream, image, TiledImageUtility.generateBackgroundColors(image)))) {
|
||||
return;
|
||||
}
|
||||
} else if(filter == ZUKAN_FILE_FILTER) {
|
||||
if(!importSkinFile(file, dst, 25090)) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
DlcOption option = new DlcOption("ZUKAN", file.getName(), dst.getAbsolutePath(), true);
|
||||
updateCustomOption(zukanComboBox, customDexSkin, option);
|
||||
customDexSkin = option;
|
||||
player.setCustomDexSkin(customDexSkin.name());
|
||||
});
|
||||
});
|
||||
|
||||
// Create musical show selector
|
||||
createDlcOption("Musical Show", musicalComboBox, () -> {
|
||||
SwingUtility.showIgnorableHint(getRootPane(), "Please exercise caution when importing custom musicals.\n"
|
||||
+ "Downloading invalid data might cause game crashes or other issues.", "Attention", JOptionPane.WARNING_MESSAGE);
|
||||
FileChooser.showFileOpenDialog(getRootPane(), selection -> {
|
||||
File dst = player.getMusicalFile();
|
||||
File file = selection.file();
|
||||
|
||||
if(!importNarcFile(file, dst)) {
|
||||
return;
|
||||
}
|
||||
|
||||
DlcOption option = new DlcOption("MUSICAL", file.getName(), dst.getAbsolutePath(), true);
|
||||
updateCustomOption(musicalComboBox, customMusical, option);
|
||||
customMusical = option;
|
||||
player.setCustomMusical(customMusical.name());
|
||||
});
|
||||
});
|
||||
|
||||
add(optionPanel, "spanx, align 50%");
|
||||
}
|
||||
|
||||
public void loadProfile(Player player) {
|
||||
this.player = player;
|
||||
gameVersion = player.getGameVersion();
|
||||
String cgearType = player.getGameVersion().isVersion2() ? "CGEAR2" : "CGEAR";
|
||||
customCGearSkin = player.getCustomCGearSkin() == null ? null : new DlcOption(cgearType, player.getCustomCGearSkin(), player.getCGearSkinFile().getAbsolutePath(), true);
|
||||
customDexSkin = player.getCustomDexSkin() == null ? null : new DlcOption("ZUKAN", player.getCustomDexSkin(), player.getDexSkinFile().getAbsolutePath(), true);
|
||||
customMusical = player.getCustomMusical() == null ? null : new DlcOption("MUSICAL", player.getCustomMusical(), player.getMusicalFile().getAbsolutePath(), true);
|
||||
updateDlcOptions(cgearComboBox, cgearType, player.getCGearSkin(), customCGearSkin);
|
||||
updateDlcOptions(zukanComboBox, "ZUKAN", player.getDexSkin(), customDexSkin);
|
||||
updateDlcOptions(musicalComboBox, "MUSICAL", player.getMusical(), customMusical);
|
||||
}
|
||||
|
||||
public void saveProfile(Player player) {
|
||||
DlcOption cgearSkin = (DlcOption)cgearComboBox.getSelectedItem();
|
||||
DlcOption dexSkin = (DlcOption)zukanComboBox.getSelectedItem();
|
||||
DlcOption musical = (DlcOption)musicalComboBox.getSelectedItem();
|
||||
player.setCGearSkin(cgearSkin == null ? null : cgearSkin.custom() ? "custom" : cgearSkin.name());
|
||||
player.setDexSkin(dexSkin == null ? null : dexSkin.custom() ? "custom" : dexSkin.name());
|
||||
player.setMusical(musical == null ? null : musical.custom() ? "custom" : musical.name());
|
||||
}
|
||||
|
||||
private void createDlcOption(String label, JComboBox<DlcOption> comboBox, Runnable importListener) {
|
||||
optionPanel.add(new JLabel(label), "sizegroup label");
|
||||
optionPanel.add(comboBox, "sizegroup option");
|
||||
JButton importButton = new JButton("Import");
|
||||
importButton.addActionListener(event -> importListener.run());
|
||||
optionPanel.add(importButton, "wrap");
|
||||
}
|
||||
|
||||
private void updateDlcOptions(JComboBox<DlcOption> comboBox, String type, String selectedOption, DlcOption customOption) {
|
||||
Vector<DlcOption> options = new Vector<>();
|
||||
|
||||
if(customOption != null) {
|
||||
options.add(customOption);
|
||||
}
|
||||
|
||||
entralinked.getDlcList().getDlcList("IRAO", type).forEach(dlc -> options.add(new DlcOption(type, dlc.name(), dlc.path())));
|
||||
DlcOption selection = selectedOption == null ? null : selectedOption.equals("custom") ? customOption : options.stream().filter(x -> selectedOption.equals(x.name())).findFirst().orElse(null);
|
||||
options.add(0, null); // "Do not change" option
|
||||
comboBox.setModel(new DefaultComboBoxModel<DlcOption>(options));
|
||||
comboBox.setSelectedItem(selection);
|
||||
}
|
||||
|
||||
private void updateCustomOption(JComboBox<DlcOption> comboBox, DlcOption oldValue, DlcOption newValue) {
|
||||
DefaultComboBoxModel<DlcOption> model = (DefaultComboBoxModel<DlcOption>)comboBox.getModel();
|
||||
|
||||
if(oldValue != null) {
|
||||
model.removeElement(oldValue);
|
||||
skinCache.remove(oldValue.path());
|
||||
}
|
||||
|
||||
model.insertElementAt(newValue, 1);
|
||||
model.setSelectedItem(newValue);
|
||||
}
|
||||
|
||||
private void updateSkinPreview(JComboBox<DlcOption> comboBox, JLabel previewLabel) {
|
||||
Image preview = getSkinImage((DlcOption)comboBox.getSelectedItem());
|
||||
previewLabel.setText(preview == null ? "No preview available." : "");
|
||||
previewLabel.setIcon(preview == null ? null : new ImageIcon(preview));
|
||||
}
|
||||
|
||||
private Image getSkinImage(DlcOption option) {
|
||||
return option == null ? null : skinCache.computeIfAbsent(option.path(), path -> {
|
||||
try(FileInputStream inputStream = new FileInputStream(path)) {
|
||||
return switch(option.type()) {
|
||||
case "CGEAR" -> TiledImageUtility.readCGearSkin(inputStream, true);
|
||||
case "CGEAR2" -> TiledImageUtility.readCGearSkin(inputStream, false);
|
||||
case "ZUKAN" -> TiledImageUtility.readDexSkin(inputStream, true);
|
||||
default -> throw new IllegalArgumentException("Invalid type: " + option.type());
|
||||
};
|
||||
} catch(Exception e) {
|
||||
SwingUtility.showExceptionInfo(getRootPane(), "Failed to load skin preview.", e);
|
||||
return null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private boolean importSkinFile(File src, File dst, int expectedSize) {
|
||||
int sizeWithoutChecksum = expectedSize - 2;
|
||||
int length = (int)src.length();
|
||||
|
||||
// Check content length
|
||||
if(length != expectedSize && length != sizeWithoutChecksum) {
|
||||
JOptionPane.showMessageDialog(getRootPane(), "Invalid content length, expected either %s or %s bytes."
|
||||
.formatted(sizeWithoutChecksum, expectedSize), "Attention", JOptionPane.WARNING_MESSAGE);
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
byte[] bytes = Files.readAllBytes(src.toPath());
|
||||
boolean writeChecksum = true;
|
||||
|
||||
// Validate checksum
|
||||
if(length == expectedSize) {
|
||||
int checksum = Crc16.calc(bytes, 0, sizeWithoutChecksum);
|
||||
int checksumInFile = (bytes[bytes.length - 2] & 0xFF) | ((bytes[bytes.length - 1] & 0xFF) << 8);
|
||||
|
||||
if(checksum != checksumInFile) {
|
||||
JOptionPane.showMessageDialog(getRootPane(), "File checksum doesn't match.", "Attention", JOptionPane.WARNING_MESSAGE);
|
||||
return false;
|
||||
}
|
||||
|
||||
writeChecksum = false;
|
||||
}
|
||||
|
||||
// Write to destination & append checksum if necessary
|
||||
try(LEOutputStream outputStream = new LEOutputStream(new FileOutputStream(dst))) {
|
||||
outputStream.write(bytes);
|
||||
|
||||
if(writeChecksum) {
|
||||
outputStream.writeShort(Crc16.calc(bytes));
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch(Exception e) {
|
||||
SwingUtility.showExceptionInfo(getRootPane(), "Failed to import skin.", e);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean importSkinImage(File src, File dst, SkinWriter writer) {
|
||||
try {
|
||||
BufferedImage image = ImageIO.read(src);
|
||||
int width = TiledImageUtility.SCREEN_WIDTH;
|
||||
int height = TiledImageUtility.SCREEN_HEIGHT;
|
||||
|
||||
if(image.getWidth() != width || image.getHeight() != height) {
|
||||
JOptionPane.showMessageDialog(getRootPane(), "Image size must be %sx%s pixels.".formatted(width, height), "Attention", JOptionPane.WARNING_MESSAGE);
|
||||
return false;
|
||||
}
|
||||
|
||||
ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
|
||||
writer.writeSkin(byteStream, image);
|
||||
byte[] bytes = byteStream.toByteArray();
|
||||
|
||||
try(LEOutputStream outputStream = new LEOutputStream(new FileOutputStream(dst))) {
|
||||
outputStream.write(bytes);
|
||||
outputStream.writeShort(Crc16.calc(bytes));
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch(IllegalArgumentException e) {
|
||||
JOptionPane.showMessageDialog(getRootPane(), e.getMessage(), "Attention", JOptionPane.WARNING_MESSAGE);
|
||||
} catch(Exception e) {
|
||||
SwingUtility.showExceptionInfo(getRootPane(), "Failed to import skin image.", e);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean importNarcFile(File src, File dst) {
|
||||
try {
|
||||
byte[] bytes = Files.readAllBytes(src.toPath());
|
||||
int offset = 0;
|
||||
|
||||
// Check narc header
|
||||
if(!Arrays.equals(bytes, 0, NARC_HEADER.length, NARC_HEADER, 0, NARC_HEADER.length)) {
|
||||
if(bytes.length < 16 || !Arrays.equals(bytes, 16, 16 + NARC_HEADER.length, NARC_HEADER, 0, NARC_HEADER.length)) {
|
||||
JOptionPane.showMessageDialog(getRootPane(), "Invalid or unsupported file.", "Attention", JOptionPane.WARNING_MESSAGE);
|
||||
return false;
|
||||
}
|
||||
|
||||
offset = 16;
|
||||
}
|
||||
|
||||
// TODO utility function
|
||||
int length = ((bytes[offset + NARC_HEADER.length + 3] & 0xFF) << 24)
|
||||
| ((bytes[offset + NARC_HEADER.length + 2] & 0xFF) << 16)
|
||||
| ((bytes[offset + NARC_HEADER.length + 1] & 0xFF) << 8)
|
||||
| (bytes[offset + NARC_HEADER.length] & 0xFF);
|
||||
|
||||
if(offset + length >= bytes.length) {
|
||||
JOptionPane.showMessageDialog(getRootPane(), "File data is malformed or corrupt.", "Attention", JOptionPane.WARNING_MESSAGE);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Write to destination
|
||||
try(LEOutputStream outputStream = new LEOutputStream(new FileOutputStream(dst))) {
|
||||
outputStream.write(bytes, offset, length);
|
||||
outputStream.writeShort(Crc16.calc(bytes, offset, length));
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch(Exception e) {
|
||||
SwingUtility.showExceptionInfo(getRootPane(), "Failed to import NARC file.", e);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -13,14 +13,15 @@ import javax.swing.JLabel;
|
|||
import javax.swing.JOptionPane;
|
||||
import javax.swing.JPanel;
|
||||
import javax.swing.JTabbedPane;
|
||||
import javax.swing.text.AbstractDocument;
|
||||
|
||||
import com.formdev.flatlaf.FlatClientProperties;
|
||||
import com.formdev.flatlaf.extras.components.FlatTabbedPane;
|
||||
import com.formdev.flatlaf.extras.components.FlatTabbedPane.TabAreaAlignment;
|
||||
import com.formdev.flatlaf.extras.components.FlatTextField;
|
||||
|
||||
import entralinked.Entralinked;
|
||||
import entralinked.GameVersion;
|
||||
import entralinked.gui.GsidDocumentFilter;
|
||||
import entralinked.gui.data.DataManager;
|
||||
import entralinked.model.player.Player;
|
||||
import entralinked.model.player.PlayerStatus;
|
||||
|
|
@ -41,6 +42,7 @@ public class DashboardPanel extends JPanel {
|
|||
private EncounterEditorPanel encounterPanel;
|
||||
private ItemEditorPanel itemPanel;
|
||||
private VisitorEditorPanel visitorPanel;
|
||||
private CustomizationPanel customizePanel;
|
||||
private MiscPanel miscPanel;
|
||||
private Player player;
|
||||
private boolean initialized;
|
||||
|
|
@ -48,16 +50,10 @@ public class DashboardPanel extends JPanel {
|
|||
public DashboardPanel(Entralinked entralinked) {
|
||||
this.entralinked = entralinked;
|
||||
|
||||
// Create login labels
|
||||
JLabel loginTitle = new JLabel("Log in");
|
||||
loginTitle.putClientProperty(FlatClientProperties.STYLE, "font:bold +8");
|
||||
JLabel loginDescription = new JLabel("<html>Tuck in a Pokémon and enter your Game Sync ID to continue.<br/>"
|
||||
+ "Your Game Sync ID can be found in 'Game Sync Settings' in the game's main menu.</html>");
|
||||
loginDescription.putClientProperty(FlatClientProperties.STYLE, "[dark]foreground:darken(@foreground,20%)");
|
||||
|
||||
// Create GSID field
|
||||
FlatTextField gsidTextField = new FlatTextField();
|
||||
gsidTextField.setPlaceholderText("XXXXXXXXXX");
|
||||
((AbstractDocument)gsidTextField.getDocument()).setDocumentFilter(new GsidDocumentFilter());
|
||||
|
||||
// Create login button
|
||||
JButton loginButton = new JButton("Log in");
|
||||
|
|
@ -74,8 +70,14 @@ public class DashboardPanel extends JPanel {
|
|||
|
||||
// Create login panel
|
||||
JPanel loginPanel = new JPanel(new MigLayout("wrap, align 50% 50%"));
|
||||
loginPanel.add(loginTitle);
|
||||
loginPanel.add(loginDescription);
|
||||
String loginDescription = """
|
||||
<html>
|
||||
Tuck in a Pokémon and enter your Game Sync ID to continue.<br/>
|
||||
Your Game Sync ID can be found in 'Game Sync Settings' in the game's main menu.
|
||||
</html>
|
||||
""";
|
||||
loginPanel.add(SwingUtility.createTitleLabel("Log in"));
|
||||
loginPanel.add(SwingUtility.createDescriptionLabel(loginDescription));
|
||||
loginPanel.add(new JLabel("Game Sync ID"), "gapy 8");
|
||||
loginPanel.add(gsidTextField, "growx");
|
||||
loginPanel.add(loginButton, "growx");
|
||||
|
|
@ -90,6 +92,7 @@ public class DashboardPanel extends JPanel {
|
|||
encounterPanel.saveProfile(player);
|
||||
itemPanel.saveProfile(player);
|
||||
visitorPanel.saveProfile(player);
|
||||
customizePanel.saveProfile(player);
|
||||
miscPanel.saveProfile(player);
|
||||
player.setStatus(PlayerStatus.WAKE_READY);
|
||||
|
||||
|
|
@ -177,6 +180,7 @@ public class DashboardPanel extends JPanel {
|
|||
encounterPanel.loadProfile(player);
|
||||
itemPanel.loadProfile(player);
|
||||
visitorPanel.loadProfile(player);
|
||||
customizePanel.loadProfile(player);
|
||||
miscPanel.loadProfile(player);
|
||||
} catch(Exception e) {
|
||||
SwingUtility.showExceptionInfo(getRootPane(), "Failed to load player data.", e);
|
||||
|
|
@ -195,7 +199,8 @@ public class DashboardPanel extends JPanel {
|
|||
encounterPanel = new EncounterEditorPanel();
|
||||
itemPanel = new ItemEditorPanel();
|
||||
visitorPanel = new VisitorEditorPanel();
|
||||
miscPanel = new MiscPanel(entralinked);
|
||||
customizePanel = new CustomizationPanel(entralinked);
|
||||
miscPanel = new MiscPanel();
|
||||
} catch(Exception e) {
|
||||
SwingUtility.showExceptionInfo(getRootPane(), "Failed to initialize dashboard.", e);
|
||||
return false;
|
||||
|
|
@ -205,6 +210,7 @@ public class DashboardPanel extends JPanel {
|
|||
tabbedPane.add("Entree Forest", encounterPanel);
|
||||
tabbedPane.add("Dream Remnants", itemPanel);
|
||||
tabbedPane.add("Join Avenue", visitorPanel);
|
||||
tabbedPane.add("Customization", customizePanel);
|
||||
tabbedPane.add("Miscellaneous", miscPanel);
|
||||
initialized = true;
|
||||
return true;
|
||||
|
|
|
|||
|
|
@ -1,390 +1,152 @@
|
|||
package entralinked.gui.panels;
|
||||
|
||||
import java.awt.Dimension;
|
||||
import java.awt.Image;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Vector;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import javax.swing.BorderFactory;
|
||||
import javax.swing.DefaultComboBoxModel;
|
||||
import javax.swing.ImageIcon;
|
||||
import javax.swing.JButton;
|
||||
import javax.swing.JComboBox;
|
||||
import javax.swing.JLabel;
|
||||
import javax.swing.JOptionPane;
|
||||
import javax.swing.JCheckBox;
|
||||
import javax.swing.JPanel;
|
||||
import javax.swing.JSpinner;
|
||||
import javax.swing.JTextField;
|
||||
import javax.swing.SpinnerNumberModel;
|
||||
import javax.swing.filechooser.FileFilter;
|
||||
import javax.swing.filechooser.FileNameExtensionFilter;
|
||||
import javax.swing.text.PlainDocument;
|
||||
|
||||
import entralinked.Entralinked;
|
||||
import entralinked.GameVersion;
|
||||
import entralinked.gui.FileChooser;
|
||||
import entralinked.gui.ModelListCellRenderer;
|
||||
import entralinked.gui.SizeLimitDocumentFilter;
|
||||
import entralinked.model.player.DreamDecor;
|
||||
import entralinked.model.player.Player;
|
||||
import entralinked.utility.Crc16;
|
||||
import entralinked.utility.LEOutputStream;
|
||||
import entralinked.utility.SwingUtility;
|
||||
import entralinked.utility.TiledImageUtility;
|
||||
import net.miginfocom.swing.MigLayout;
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
public class MiscPanel extends JPanel {
|
||||
|
||||
@FunctionalInterface
|
||||
private static interface SkinWriter {
|
||||
|
||||
public void writeSkin(OutputStream outputStream, BufferedImage image) throws IOException;
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal model for combo boxes.
|
||||
* Container for keeping track of Décor options.
|
||||
*/
|
||||
private static record DlcOption(String type, String name, String path, boolean custom) {
|
||||
private static class DecorOption {
|
||||
|
||||
public DlcOption(String type, String name, String path) {
|
||||
this(type, name, path, false);
|
||||
private final JCheckBox checkBox;
|
||||
private final JSpinner spinner;
|
||||
private final JTextField nameField;
|
||||
|
||||
public DecorOption(JPanel parent, String label) {
|
||||
spinner = new JSpinner(new SpinnerNumberModel(0, 0, 127, 1));
|
||||
nameField = new JTextField();
|
||||
((PlainDocument)nameField.getDocument()).setDocumentFilter(new SizeLimitDocumentFilter(12));
|
||||
checkBox = new JCheckBox(label);
|
||||
checkBox.addActionListener(event -> {
|
||||
boolean active = checkBox.isSelected();
|
||||
spinner.setEnabled(active);
|
||||
nameField.setEnabled(active);
|
||||
});
|
||||
parent.add(checkBox);
|
||||
parent.add(spinner);
|
||||
parent.add(nameField, "growx");
|
||||
setActive(true);
|
||||
}
|
||||
|
||||
public void setActive(boolean active) {
|
||||
checkBox.setSelected(active);
|
||||
spinner.setEnabled(active);
|
||||
nameField.setEnabled(active);
|
||||
}
|
||||
|
||||
public boolean isActive() {
|
||||
return checkBox.isSelected();
|
||||
}
|
||||
|
||||
public void setId(int id) {
|
||||
spinner.setValue(id);
|
||||
}
|
||||
|
||||
public int getId() {
|
||||
return (int)spinner.getValue();
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
nameField.setText(name);
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return nameField.getText();
|
||||
}
|
||||
}
|
||||
|
||||
private static final FileFilter IMAGE_FILE_FILTER = new FileNameExtensionFilter("Image Files (*.png)", "png");
|
||||
private static final FileFilter CGEAR_FILE_FILTER = new FileNameExtensionFilter("C-Gear Skin Files (*.bin, *.cgb, *.psk)", "bin", "cgb", "psk");
|
||||
private static final FileFilter ZUKAN_FILE_FILTER = new FileNameExtensionFilter("Pokédex Skin Files (*.bin, *.pds)", "bin", "pds");
|
||||
private static final byte[] NARC_HEADER = { 0x4E, 0x41, 0x52, 0x43, (byte)0xFE, (byte)0xFF, 0x00, 0x01 };
|
||||
private static final Map<String, Image> skinCache = new HashMap<>();
|
||||
private final Entralinked entralinked;
|
||||
private final JComboBox<DlcOption> cgearComboBox;
|
||||
private final JComboBox<DlcOption> zukanComboBox;
|
||||
private final JComboBox<DlcOption> musicalComboBox;
|
||||
private final JPanel optionPanel;
|
||||
public static final int DECOR_COUNT = 5;
|
||||
private final List<DecorOption> decorOptions = new ArrayList<>();
|
||||
private final JSpinner levelSpinner;
|
||||
private Player player;
|
||||
private GameVersion gameVersion;
|
||||
private DlcOption customCGearSkin;
|
||||
private DlcOption customDexSkin;
|
||||
private DlcOption customMusical;
|
||||
|
||||
public MiscPanel() {
|
||||
setLayout(new MigLayout("align 50% 50%, insets 0, wrap", "", "[]0[]"));
|
||||
|
||||
// Create decor panel
|
||||
JPanel decorPanel = new JPanel(new MigLayout("insets 0, wrap 3, fill", "[][][grow]"));
|
||||
String decorDescription = """
|
||||
<html>
|
||||
Configure Décor options to appear in Loblolly's studio.<br/>
|
||||
Due to the closure of the Dream World, this function serves no real purpose<br/>
|
||||
and is mostly meant for people who want to test or just play around with it.
|
||||
</html>
|
||||
""";
|
||||
decorPanel.add(SwingUtility.createTitleLabel("Dream Décor"), "spanx, wrap");
|
||||
decorPanel.add(SwingUtility.createDescriptionLabel(decorDescription), "spanx, wrap");
|
||||
|
||||
public MiscPanel(Entralinked entralinked) {
|
||||
this.entralinked = entralinked;
|
||||
setLayout(new MigLayout("align 50% 50%"));
|
||||
for(int i = 0; i < DECOR_COUNT; i++) {
|
||||
decorOptions.add(new DecorOption(decorPanel, "Decor %s".formatted(i + 1)));
|
||||
}
|
||||
|
||||
// Create preview labels
|
||||
JLabel cgearPreviewLabel = new JLabel("No preview available.", JLabel.CENTER);
|
||||
cgearPreviewLabel.setPreferredSize(new Dimension(TiledImageUtility.SCREEN_WIDTH, TiledImageUtility.SCREEN_HEIGHT));
|
||||
JLabel dexPreviewLabel = new JLabel("No preview available.", JLabel.CENTER);
|
||||
dexPreviewLabel.setPreferredSize(new Dimension(TiledImageUtility.SCREEN_WIDTH, TiledImageUtility.SCREEN_HEIGHT));
|
||||
|
||||
// Create preview image panels
|
||||
// Labels are added to a subpanel first otherwise the preferred size will include the border which causes issues
|
||||
JPanel cgearPreviewPanel = new JPanel(new MigLayout("insets 0"));
|
||||
cgearPreviewPanel.setBorder(BorderFactory.createTitledBorder("C-Gear Skin Preview"));
|
||||
cgearPreviewPanel.add(cgearPreviewLabel);
|
||||
JPanel dexPreviewPanel = new JPanel(new MigLayout("insets 0"));
|
||||
dexPreviewPanel.setBorder(BorderFactory.createTitledBorder("Pokédex Skin Preview"));
|
||||
dexPreviewPanel.add(dexPreviewLabel);
|
||||
JPanel previewPanel = new JPanel();
|
||||
previewPanel.add(cgearPreviewPanel);
|
||||
previewPanel.add(dexPreviewPanel);
|
||||
add(previewPanel, "spanx, align 50%, wrap");
|
||||
|
||||
// Create combo boxes
|
||||
ModelListCellRenderer<DlcOption> renderer = new ModelListCellRenderer<>(DlcOption.class, DlcOption::name, "Do not change");
|
||||
cgearComboBox = new JComboBox<>();
|
||||
cgearComboBox.setMinimumSize(cgearComboBox.getPreferredSize());
|
||||
cgearComboBox.setRenderer(renderer);
|
||||
cgearComboBox.addActionListener(event -> updateSkinPreview(cgearComboBox, cgearPreviewLabel));
|
||||
zukanComboBox = new JComboBox<>();
|
||||
zukanComboBox.setMinimumSize(zukanComboBox.getPreferredSize());
|
||||
zukanComboBox.setRenderer(renderer);
|
||||
zukanComboBox.addActionListener(event -> updateSkinPreview(zukanComboBox, dexPreviewLabel));
|
||||
musicalComboBox = new JComboBox<>();
|
||||
musicalComboBox.setMinimumSize(musicalComboBox.getPreferredSize());
|
||||
musicalComboBox.setRenderer(renderer);
|
||||
|
||||
// Create option panel
|
||||
optionPanel = new JPanel(new MigLayout());
|
||||
|
||||
// Create C-Gear skin selector
|
||||
createDlcOption("C-Gear Skin", cgearComboBox, () -> {
|
||||
FileChooser.showFileOpenDialog(getRootPane(), Arrays.asList(IMAGE_FILE_FILTER, CGEAR_FILE_FILTER), selection -> {
|
||||
File dst = player.getCGearSkinFile();
|
||||
File file = selection.file();
|
||||
FileFilter filter = selection.filter();
|
||||
|
||||
if(filter == IMAGE_FILE_FILTER) {
|
||||
if(!importSkinImage(file, dst, (stream, image) -> TiledImageUtility.writeCGearSkin(stream, image, !gameVersion.isVersion2()))) {
|
||||
return;
|
||||
}
|
||||
} else if(filter == CGEAR_FILE_FILTER) {
|
||||
if(!importSkinFile(file, dst, 9730)) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
DlcOption option = new DlcOption(gameVersion.isVersion2() ? "CGEAR2" : "CGEAR", file.getName(), dst.getAbsolutePath(), true);
|
||||
updateCustomOption(cgearComboBox, customCGearSkin, option);
|
||||
customCGearSkin = option;
|
||||
player.setCustomCGearSkin(customCGearSkin.name());
|
||||
});
|
||||
// Create decor option buttons
|
||||
JButton resetButton = new JButton("Default");
|
||||
resetButton.addActionListener(event -> {
|
||||
for(int i = 0; i < DECOR_COUNT; i++) {
|
||||
DecorOption option = decorOptions.get(i);
|
||||
DreamDecor decor = DreamDecor.DEFAULT_DECOR.get(i);
|
||||
option.setActive(true);
|
||||
option.setId(decor.id());
|
||||
option.setName(decor.name());
|
||||
}
|
||||
});
|
||||
|
||||
// Create Pokédex skin selector
|
||||
createDlcOption("Pokédex Skin", zukanComboBox, () -> {
|
||||
FileChooser.showFileOpenDialog(getRootPane(), Arrays.asList(IMAGE_FILE_FILTER, ZUKAN_FILE_FILTER), selection -> {
|
||||
File dst = player.getDexSkinFile();
|
||||
File file = selection.file();
|
||||
FileFilter filter = selection.filter();
|
||||
|
||||
if(filter == IMAGE_FILE_FILTER) {
|
||||
if(!importSkinImage(file, dst, (stream, image) -> TiledImageUtility.writeDexSkin(stream, image, TiledImageUtility.generateBackgroundColors(image)))) {
|
||||
return;
|
||||
}
|
||||
} else if(filter == ZUKAN_FILE_FILTER) {
|
||||
if(!importSkinFile(file, dst, 25090)) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
DlcOption option = new DlcOption("ZUKAN", file.getName(), dst.getAbsolutePath(), true);
|
||||
updateCustomOption(zukanComboBox, customDexSkin, option);
|
||||
customDexSkin = option;
|
||||
player.setCustomDexSkin(customDexSkin.name());
|
||||
});
|
||||
JButton clearButton = new JButton("Clear");
|
||||
clearButton.addActionListener(event -> {
|
||||
for(DecorOption option : decorOptions) {
|
||||
option.setActive(false);
|
||||
option.setId(0);
|
||||
option.setName("");
|
||||
}
|
||||
});
|
||||
|
||||
// Create musical show selector
|
||||
createDlcOption("Musical Show", musicalComboBox, () -> {
|
||||
SwingUtility.showIgnorableHint(getRootPane(), "Please exercise caution when importing custom musicals.\n"
|
||||
+ "Downloading invalid data might cause game crashes or other issues.", "Attention", JOptionPane.WARNING_MESSAGE);
|
||||
FileChooser.showFileOpenDialog(getRootPane(), selection -> {
|
||||
File dst = player.getMusicalFile();
|
||||
File file = selection.file();
|
||||
|
||||
if(!importNarcFile(file, dst)) {
|
||||
return;
|
||||
}
|
||||
|
||||
DlcOption option = new DlcOption("MUSICAL", file.getName(), dst.getAbsolutePath(), true);
|
||||
updateCustomOption(musicalComboBox, customMusical, option);
|
||||
customMusical = option;
|
||||
player.setCustomMusical(customMusical.name());
|
||||
});
|
||||
});
|
||||
// Create decor button panel
|
||||
JPanel buttonPanel = new JPanel(new MigLayout("insets 0", "0[]"));
|
||||
buttonPanel.add(resetButton);
|
||||
buttonPanel.add(clearButton);
|
||||
decorPanel.add(buttonPanel, "spanx, align right");
|
||||
add(decorPanel, "spanx, growx");
|
||||
|
||||
// Create level spinner
|
||||
levelSpinner = new JSpinner(new SpinnerNumberModel(0, 0, 99, 1)) ;
|
||||
optionPanel.add(new JLabel("Level Gain"), "sizegroup label");
|
||||
optionPanel.add(levelSpinner, "sizegroup option");
|
||||
add(optionPanel, "spanx, align 50%");
|
||||
// Create level panel
|
||||
levelSpinner = new JSpinner(new SpinnerNumberModel(0, 0, 99, 1));
|
||||
JPanel levelPanel = new JPanel(new MigLayout("insets 0, wrap"));
|
||||
levelPanel.add(SwingUtility.createTitleLabel("Level Gain"));
|
||||
levelPanel.add(SwingUtility.createDescriptionLabel("Amount of levels to gain on waking up."));
|
||||
levelPanel.add(levelSpinner, "grow");
|
||||
add(levelPanel, "");
|
||||
}
|
||||
|
||||
public void loadProfile(Player player) {
|
||||
this.player = player;
|
||||
gameVersion = player.getGameVersion();
|
||||
String cgearType = player.getGameVersion().isVersion2() ? "CGEAR2" : "CGEAR";
|
||||
customCGearSkin = player.getCustomCGearSkin() == null ? null : new DlcOption(cgearType, player.getCustomCGearSkin(), player.getCGearSkinFile().getAbsolutePath(), true);
|
||||
customDexSkin = player.getCustomDexSkin() == null ? null : new DlcOption("ZUKAN", player.getCustomDexSkin(), player.getDexSkinFile().getAbsolutePath(), true);
|
||||
customMusical = player.getCustomMusical() == null ? null : new DlcOption("MUSICAL", player.getCustomMusical(), player.getMusicalFile().getAbsolutePath(), true);
|
||||
updateDlcOptions(cgearComboBox, cgearType, player.getCGearSkin(), customCGearSkin);
|
||||
updateDlcOptions(zukanComboBox, "ZUKAN", player.getDexSkin(), customDexSkin);
|
||||
updateDlcOptions(musicalComboBox, "MUSICAL", player.getMusical(), customMusical);
|
||||
levelSpinner.setValue(player.getLevelsGained());
|
||||
List<DreamDecor> decorList = player.getDecor();
|
||||
|
||||
for(int i = 0; i < DECOR_COUNT; i++) {
|
||||
DecorOption option = decorOptions.get(i);
|
||||
DreamDecor decor = i < decorList.size() ? decorList.get(i) : null;
|
||||
option.setActive(decor != null);
|
||||
option.setId(decor == null ? 0 : decor.id());
|
||||
option.setName(decor == null ? "" : decor.name());
|
||||
}
|
||||
}
|
||||
|
||||
public void saveProfile(Player player) {
|
||||
DlcOption cgearSkin = (DlcOption)cgearComboBox.getSelectedItem();
|
||||
DlcOption dexSkin = (DlcOption)zukanComboBox.getSelectedItem();
|
||||
DlcOption musical = (DlcOption)musicalComboBox.getSelectedItem();
|
||||
player.setCGearSkin(cgearSkin == null ? null : cgearSkin.custom() ? "custom" : cgearSkin.name());
|
||||
player.setDexSkin(dexSkin == null ? null : dexSkin.custom() ? "custom" : dexSkin.name());
|
||||
player.setMusical(musical == null ? null : musical.custom() ? "custom" : musical.name());
|
||||
player.setDecor(decorOptions.stream().filter(DecorOption::isActive).map(x -> new DreamDecor(x.getId(), x.getName())).toList());
|
||||
player.setLevelsGained((int)levelSpinner.getValue());
|
||||
}
|
||||
|
||||
private void createDlcOption(String label, JComboBox<DlcOption> comboBox, Runnable importListener) {
|
||||
optionPanel.add(new JLabel(label), "sizegroup label");
|
||||
optionPanel.add(comboBox, "sizegroup option");
|
||||
JButton importButton = new JButton("Import");
|
||||
importButton.addActionListener(event -> importListener.run());
|
||||
optionPanel.add(importButton, "wrap");
|
||||
}
|
||||
|
||||
private void updateDlcOptions(JComboBox<DlcOption> comboBox, String type, String selectedOption, DlcOption customOption) {
|
||||
Vector<DlcOption> options = new Vector<>();
|
||||
|
||||
if(customOption != null) {
|
||||
options.add(customOption);
|
||||
}
|
||||
|
||||
entralinked.getDlcList().getDlcList("IRAO", type).forEach(dlc -> options.add(new DlcOption(type, dlc.name(), dlc.path())));
|
||||
DlcOption selection = selectedOption == null ? null : selectedOption.equals("custom") ? customOption : options.stream().filter(x -> selectedOption.equals(x.name())).findFirst().orElse(null);
|
||||
options.add(0, null); // "Do not change" option
|
||||
comboBox.setModel(new DefaultComboBoxModel<DlcOption>(options));
|
||||
comboBox.setSelectedItem(selection);
|
||||
}
|
||||
|
||||
private void updateCustomOption(JComboBox<DlcOption> comboBox, DlcOption oldValue, DlcOption newValue) {
|
||||
DefaultComboBoxModel<DlcOption> model = (DefaultComboBoxModel<DlcOption>)comboBox.getModel();
|
||||
|
||||
if(oldValue != null) {
|
||||
model.removeElement(oldValue);
|
||||
skinCache.remove(oldValue.path());
|
||||
}
|
||||
|
||||
model.insertElementAt(newValue, 1);
|
||||
model.setSelectedItem(newValue);
|
||||
}
|
||||
|
||||
private void updateSkinPreview(JComboBox<DlcOption> comboBox, JLabel previewLabel) {
|
||||
Image preview = getSkinImage((DlcOption)comboBox.getSelectedItem());
|
||||
previewLabel.setText(preview == null ? "No preview available." : "");
|
||||
previewLabel.setIcon(preview == null ? null : new ImageIcon(preview));
|
||||
}
|
||||
|
||||
private Image getSkinImage(DlcOption option) {
|
||||
return option == null ? null : skinCache.computeIfAbsent(option.path(), path -> {
|
||||
try(FileInputStream inputStream = new FileInputStream(path)) {
|
||||
return switch(option.type()) {
|
||||
case "CGEAR" -> TiledImageUtility.readCGearSkin(inputStream, true);
|
||||
case "CGEAR2" -> TiledImageUtility.readCGearSkin(inputStream, false);
|
||||
case "ZUKAN" -> TiledImageUtility.readDexSkin(inputStream, true);
|
||||
default -> throw new IllegalArgumentException("Invalid type: " + option.type());
|
||||
};
|
||||
} catch(Exception e) {
|
||||
SwingUtility.showExceptionInfo(getRootPane(), "Failed to load skin preview.", e);
|
||||
return null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private boolean importSkinFile(File src, File dst, int expectedSize) {
|
||||
int sizeWithoutChecksum = expectedSize - 2;
|
||||
int length = (int)src.length();
|
||||
|
||||
// Check content length
|
||||
if(length != expectedSize && length != sizeWithoutChecksum) {
|
||||
JOptionPane.showMessageDialog(getRootPane(), "Invalid content length, expected either %s or %s bytes."
|
||||
.formatted(sizeWithoutChecksum, expectedSize), "Attention", JOptionPane.WARNING_MESSAGE);
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
byte[] bytes = Files.readAllBytes(src.toPath());
|
||||
boolean writeChecksum = true;
|
||||
|
||||
// Validate checksum
|
||||
if(length == expectedSize) {
|
||||
int checksum = Crc16.calc(bytes, 0, sizeWithoutChecksum);
|
||||
int checksumInFile = (bytes[bytes.length - 2] & 0xFF) | ((bytes[bytes.length - 1] & 0xFF) << 8);
|
||||
|
||||
if(checksum != checksumInFile) {
|
||||
JOptionPane.showMessageDialog(getRootPane(), "File checksum doesn't match.", "Attention", JOptionPane.WARNING_MESSAGE);
|
||||
return false;
|
||||
}
|
||||
|
||||
writeChecksum = false;
|
||||
}
|
||||
|
||||
// Write to destination & append checksum if necessary
|
||||
try(LEOutputStream outputStream = new LEOutputStream(new FileOutputStream(dst))) {
|
||||
outputStream.write(bytes);
|
||||
|
||||
if(writeChecksum) {
|
||||
outputStream.writeShort(Crc16.calc(bytes));
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch(Exception e) {
|
||||
SwingUtility.showExceptionInfo(getRootPane(), "Failed to import skin.", e);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean importSkinImage(File src, File dst, SkinWriter writer) {
|
||||
try {
|
||||
BufferedImage image = ImageIO.read(src);
|
||||
int width = TiledImageUtility.SCREEN_WIDTH;
|
||||
int height = TiledImageUtility.SCREEN_HEIGHT;
|
||||
|
||||
if(image.getWidth() != width || image.getHeight() != height) {
|
||||
JOptionPane.showMessageDialog(getRootPane(), "Image size must be %sx%s pixels.".formatted(width, height), "Attention", JOptionPane.WARNING_MESSAGE);
|
||||
return false;
|
||||
}
|
||||
|
||||
ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
|
||||
writer.writeSkin(byteStream, image);
|
||||
byte[] bytes = byteStream.toByteArray();
|
||||
|
||||
try(LEOutputStream outputStream = new LEOutputStream(new FileOutputStream(dst))) {
|
||||
outputStream.write(bytes);
|
||||
outputStream.writeShort(Crc16.calc(bytes));
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch(IllegalArgumentException e) {
|
||||
JOptionPane.showMessageDialog(getRootPane(), e.getMessage(), "Attention", JOptionPane.WARNING_MESSAGE);
|
||||
} catch(Exception e) {
|
||||
SwingUtility.showExceptionInfo(getRootPane(), "Failed to import skin image.", e);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean importNarcFile(File src, File dst) {
|
||||
try {
|
||||
byte[] bytes = Files.readAllBytes(src.toPath());
|
||||
int offset = 0;
|
||||
|
||||
// Check narc header
|
||||
if(!Arrays.equals(bytes, 0, NARC_HEADER.length, NARC_HEADER, 0, NARC_HEADER.length)) {
|
||||
if(bytes.length < 16 || !Arrays.equals(bytes, 16, 16 + NARC_HEADER.length, NARC_HEADER, 0, NARC_HEADER.length)) {
|
||||
JOptionPane.showMessageDialog(getRootPane(), "Invalid or unsupported file.", "Attention", JOptionPane.WARNING_MESSAGE);
|
||||
return false;
|
||||
}
|
||||
|
||||
offset = 16;
|
||||
}
|
||||
|
||||
// TODO utility function
|
||||
int length = ((bytes[offset + NARC_HEADER.length + 3] & 0xFF) << 24)
|
||||
| ((bytes[offset + NARC_HEADER.length + 2] & 0xFF) << 16)
|
||||
| ((bytes[offset + NARC_HEADER.length + 1] & 0xFF) << 8)
|
||||
| (bytes[offset + NARC_HEADER.length] & 0xFF);
|
||||
|
||||
if(offset + length >= bytes.length) {
|
||||
JOptionPane.showMessageDialog(getRootPane(), "File data is malformed or corrupt.", "Attention", JOptionPane.WARNING_MESSAGE);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Write to destination
|
||||
try(LEOutputStream outputStream = new LEOutputStream(new FileOutputStream(dst))) {
|
||||
outputStream.write(bytes, offset, length);
|
||||
outputStream.writeShort(Crc16.calc(bytes, offset, length));
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch(Exception e) {
|
||||
SwingUtility.showExceptionInfo(getRootPane(), "Failed to import NARC file.", e);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,8 +7,6 @@ import javax.swing.JLabel;
|
|||
import javax.swing.JOptionPane;
|
||||
import javax.swing.JPanel;
|
||||
|
||||
import com.formdev.flatlaf.FlatClientProperties;
|
||||
|
||||
import entralinked.gui.component.PropertyDisplay;
|
||||
import entralinked.gui.component.ShadowedSprite;
|
||||
import entralinked.gui.data.DataManager;
|
||||
|
|
@ -48,19 +46,12 @@ public class SummaryPanel extends JPanel {
|
|||
public SummaryPanel() {
|
||||
setLayout(new MigLayout("align 50% 50%, gapy 0, insets 0"));
|
||||
|
||||
// Create info labels
|
||||
JLabel titleLabel = new JLabel("Summary");
|
||||
titleLabel.putClientProperty(FlatClientProperties.STYLE, "font:bold +8");
|
||||
flavorTextLabel = new JLabel();
|
||||
flavorTextLabel.putClientProperty(FlatClientProperties.STYLE, "[dark]foreground:darken(@foreground,20%)");
|
||||
JLabel subLabel = new JLabel("Tucked-in Pokémon info:");
|
||||
subLabel.putClientProperty(FlatClientProperties.STYLE, "[dark]foreground:darken(@foreground,20%)");
|
||||
|
||||
// Create header panel
|
||||
flavorTextLabel = SwingUtility.createDescriptionLabel();
|
||||
JPanel headerPanel = new JPanel(new MigLayout("fillx, insets 0"));
|
||||
headerPanel.add(titleLabel, "wrap");
|
||||
headerPanel.add(SwingUtility.createTitleLabel("Summary"), "wrap");
|
||||
headerPanel.add(flavorTextLabel, "wrap");
|
||||
headerPanel.add(subLabel, "wrap");
|
||||
headerPanel.add(SwingUtility.createDescriptionLabel("Tucked-in Pokémon info:"), "wrap");
|
||||
add(headerPanel, "spanx");
|
||||
|
||||
// Create icon label
|
||||
|
|
|
|||
|
|
@ -20,8 +20,6 @@ import javax.swing.ListSelectionModel;
|
|||
import javax.swing.table.DefaultTableModel;
|
||||
import javax.swing.table.TableModel;
|
||||
|
||||
import com.formdev.flatlaf.FlatClientProperties;
|
||||
|
||||
import entralinked.gui.component.ConfigTable;
|
||||
import entralinked.gui.component.ShadowedSprite;
|
||||
import entralinked.utility.SwingUtility;
|
||||
|
|
@ -56,10 +54,8 @@ public abstract class TableEditorPanel extends JPanel {
|
|||
System.arraycopy(columnNames, 0, columns, 1, columnNames.length);
|
||||
|
||||
// Create labels
|
||||
titleLabel = new JLabel();
|
||||
titleLabel.putClientProperty(FlatClientProperties.STYLE, "font:bold +8");
|
||||
descriptionLabel = new JLabel();
|
||||
descriptionLabel.putClientProperty(FlatClientProperties.STYLE, "[dark]foreground:darken(@foreground,20%)");
|
||||
titleLabel = SwingUtility.createTitleLabel();
|
||||
descriptionLabel = SwingUtility.createDescriptionLabel();
|
||||
selectionIcon = new ShadowedSprite();
|
||||
|
||||
// Create buttons & toggles
|
||||
|
|
|
|||
|
|
@ -1,7 +1,19 @@
|
|||
package entralinked.model.player;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
public record DreamDecor(
|
||||
@JsonProperty(required = true) int id,
|
||||
@JsonProperty(required = true) String name) {}
|
||||
@JsonProperty(required = true) String name) {
|
||||
|
||||
// TODO names probably differed per language
|
||||
public static final List<DreamDecor> DEFAULT_DECOR = List.of(
|
||||
new DreamDecor(1, "Design Table"),
|
||||
new DreamDecor(2, "Design Stool"),
|
||||
new DreamDecor(3, "Flower Vase"),
|
||||
new DreamDecor(4, "Cuddle Rug"),
|
||||
new DreamDecor(6, "Wall Poster")
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ public class Player {
|
|||
private final List<DreamEncounter> encounters = new ArrayList<>();
|
||||
private final List<DreamItem> items = new ArrayList<>();
|
||||
private final List<AvenueVisitor> avenueVisitors = new ArrayList<>();
|
||||
private final List<DreamDecor> decor = new ArrayList<>();
|
||||
private PlayerStatus status;
|
||||
private GameVersion gameVersion;
|
||||
private PkmnInfo dreamerInfo;
|
||||
|
|
@ -38,6 +39,8 @@ public class Player {
|
|||
encounters.clear();
|
||||
items.clear();
|
||||
avenueVisitors.clear();
|
||||
decor.clear();
|
||||
decor.addAll(DreamDecor.DEFAULT_DECOR);
|
||||
levelsGained = 0;
|
||||
cgearSkin = null;
|
||||
dexSkin = null;
|
||||
|
|
@ -81,6 +84,17 @@ public class Player {
|
|||
return Collections.unmodifiableList(avenueVisitors);
|
||||
}
|
||||
|
||||
public void setDecor(Collection<DreamDecor> decor) {
|
||||
if(decor.size() <= 5) {
|
||||
this.decor.clear();
|
||||
this.decor.addAll(decor);
|
||||
}
|
||||
}
|
||||
|
||||
public List<DreamDecor> getDecor() {
|
||||
return Collections.unmodifiableList(decor);
|
||||
}
|
||||
|
||||
public void setStatus(PlayerStatus status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,13 +27,14 @@ public record PlayerDto(
|
|||
int levelsGained,
|
||||
@JsonDeserialize(contentAs = DreamEncounter.class) Collection<DreamEncounter> encounters,
|
||||
@JsonDeserialize(contentAs = DreamItem.class) Collection<DreamItem> items,
|
||||
@JsonDeserialize(contentAs = AvenueVisitor.class) Collection<AvenueVisitor> avenueVisitors) {
|
||||
@JsonDeserialize(contentAs = AvenueVisitor.class) Collection<AvenueVisitor> avenueVisitors,
|
||||
@JsonDeserialize(contentAs = DreamDecor.class) Collection<DreamDecor> decor) {
|
||||
|
||||
public PlayerDto(Player player) {
|
||||
this(player.getGameSyncId(), player.getGameVersion(), player.getStatus(), player.getDreamerInfo(),
|
||||
player.getCGearSkin(), player.getDexSkin(), player.getMusical(), player.getCustomCGearSkin(),
|
||||
player.getCustomDexSkin(), player.getCustomMusical(), player.getLevelsGained(), player.getEncounters(),
|
||||
player.getItems(), player.getAvenueVisitors());
|
||||
player.getItems(), player.getAvenueVisitors(), player.getDecor());
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -54,6 +55,7 @@ public record PlayerDto(
|
|||
player.setEncounters(encounters == null ? Collections.emptyList() : encounters);
|
||||
player.setItems(items == null ? Collections.emptyList() : items);
|
||||
player.setAvenueVisitors(avenueVisitors == null ? Collections.emptyList() : avenueVisitors);
|
||||
player.setDecor(decor == null ? DreamDecor.DEFAULT_DECOR : decor);
|
||||
return player;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -53,12 +53,6 @@ public class PglHandler implements HttpHandler {
|
|||
private static final String password = "2Phfv9MY"; // Best security in the world
|
||||
private final ObjectMapper mapper = new ObjectMapper(new UrlEncodedFormFactory()
|
||||
.disable(UrlEncodedFormParser.Feature.BASE64_DECODE_VALUES));
|
||||
private final List<DreamDecor> decorList = List.of(
|
||||
new DreamDecor(1, "+----------+"),
|
||||
new DreamDecor(2, "Thank you"),
|
||||
new DreamDecor(3, "for using"),
|
||||
new DreamDecor(4, "Entralinked!"),
|
||||
new DreamDecor(5, "+----------+"));
|
||||
private final Set<Integer> sleepyList = new HashSet<>();
|
||||
private final Configuration configuration;
|
||||
private final DlcList dlcList;
|
||||
|
|
@ -216,11 +210,13 @@ public class PglHandler implements HttpHandler {
|
|||
GameVersion version = player.getGameVersion();
|
||||
List<DreamEncounter> encounters = player.getEncounters();
|
||||
List<DreamItem> items = player.getItems();
|
||||
List<DreamDecor> decorList = player.getDecor();
|
||||
|
||||
// When waking up a Pokémon, these 4 bytes are written to 0x1D304 in the save file.
|
||||
// If the bytes in the game's save file match the new bytes, they will be set to 0x00000000
|
||||
// and no content will be downloaded.
|
||||
// Looking at some old save files, this was very likely just a total tuck-in/wake-up counter.
|
||||
// Additionally, waking up sets a flag at 0x1D4A3 (seems to be a "Pokémon is tucked in" flag or something) to 0x0.
|
||||
outputStream.writeInt((int)(Math.random() * Integer.MAX_VALUE));
|
||||
|
||||
// Write encounter data (max 10)
|
||||
|
|
@ -242,7 +238,7 @@ public class PglHandler implements HttpHandler {
|
|||
outputStream.write(getDlcIndex(user, player.getMusical(), "MUSICAL", player.getMusicalFile()));
|
||||
outputStream.write(getDlcIndex(user, player.getCGearSkin(), version.isVersion2() ? "CGEAR2" : "CGEAR", player.getCGearSkinFile()));
|
||||
outputStream.write(getDlcIndex(user, player.getDexSkin(), "ZUKAN", player.getDexSkinFile()));
|
||||
outputStream.write(decorList.isEmpty() ? 0 : 1); // Seems to be a flag for indicating whether or not decor data is present
|
||||
outputStream.write(decorList.isEmpty() ? 0 : 1); // Decor flag (?) stored at 0x1D4A4
|
||||
outputStream.write(0); // Must be zero?
|
||||
|
||||
// Write item IDs
|
||||
|
|
@ -276,7 +272,11 @@ public class PglHandler implements HttpHandler {
|
|||
}
|
||||
|
||||
// Write decor padding
|
||||
outputStream.writeBytes(0, (5 - decorList.size()) * 26);
|
||||
for(int i = 0; i < (5 - decorList.size()); i++) {
|
||||
outputStream.writeShort(0x7E); // Just reset to default state
|
||||
outputStream.writeBytes(0, 24);
|
||||
}
|
||||
|
||||
outputStream.writeShort(0); // ?
|
||||
|
||||
// Join Avenue visitor data -- copied in parts to 0x2422C in the save file.
|
||||
|
|
|
|||
|
|
@ -108,6 +108,26 @@ public class SwingUtility {
|
|||
return def;
|
||||
}
|
||||
|
||||
public static JLabel createTitleLabel() {
|
||||
return createTitleLabel("");
|
||||
}
|
||||
|
||||
public static JLabel createTitleLabel(String text) {
|
||||
JLabel label = new JLabel(text);
|
||||
label.putClientProperty(FlatClientProperties.STYLE, "font:bold +8");
|
||||
return label;
|
||||
}
|
||||
|
||||
public static JLabel createDescriptionLabel() {
|
||||
return createDescriptionLabel("");
|
||||
}
|
||||
|
||||
public static JLabel createDescriptionLabel(String text) {
|
||||
JLabel label = new JLabel(text);
|
||||
label.putClientProperty(FlatClientProperties.STYLE, "[dark]foreground:darken(@foreground,20%)");
|
||||
return label;
|
||||
}
|
||||
|
||||
public static JLabel createButtonLabel(String text, Runnable actionHandler) {
|
||||
JLabel label = new JLabel("<html><u>%s</u></html>".formatted(text));
|
||||
label.putClientProperty(FlatClientProperties.STYLE, "font: -1");
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user