GUI tweaks + add option to edit décor (#6)

This commit is contained in:
kuroppoi 2025-04-09 21:45:52 +02:00
parent 2385398b4b
commit 4031d833e8
12 changed files with 661 additions and 391 deletions

View 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);
}
}

View 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();
}
}
}

View 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;
}
}

View File

@ -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;

View File

@ -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;
}
}

View File

@ -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

View File

@ -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

View File

@ -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")
);
}

View File

@ -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;
}

View File

@ -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;
}
}

View File

@ -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.

View 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");