mirror of
https://github.com/kuroppoi/entralinked.git
synced 2026-09-08 00:35:12 -05:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
afc79c39ad | ||
|
|
15c42165ed | ||
|
|
e663d91559 | ||
|
|
4031d833e8 | ||
|
|
2385398b4b | ||
|
|
840d5e956a | ||
|
|
bf2cc1f6bc |
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,8 @@ import javax.swing.JComponent;
|
||||
@SuppressWarnings("serial")
|
||||
public class ShadowedSprite extends JComponent {
|
||||
|
||||
public static final int OFFSET_X = 4;
|
||||
public static final int OFFSET_Y = 2;
|
||||
private BufferedImage image;
|
||||
private BufferedImage shadowImage;
|
||||
private int scale;
|
||||
@@ -28,9 +30,9 @@ public class ShadowedSprite extends JComponent {
|
||||
Dimension size = getSize();
|
||||
int width = image.getWidth() * scale;
|
||||
int height = image.getHeight() * scale;
|
||||
int x = size.width / 2 - width / 2;
|
||||
int y = size.height / 2 - height / 2;
|
||||
graphics.drawImage(shadowImage, x + 4, y + 2, width, height, null);
|
||||
int x = size.width / 2 - width / 2 - OFFSET_X / 2;
|
||||
int y = size.height / 2 - height / 2 - OFFSET_Y / 2;
|
||||
graphics.drawImage(shadowImage, x + OFFSET_X, y + OFFSET_Y, width, height, null);
|
||||
graphics.drawImage(image, x, y, width, height, null);
|
||||
}
|
||||
|
||||
@@ -43,7 +45,6 @@ public class ShadowedSprite extends JComponent {
|
||||
int height = image.getHeight();
|
||||
shadowImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
|
||||
|
||||
// TODO shadow can be cut off if pixels touch the border
|
||||
for(int i = 0; i < image.getWidth(); i++) {
|
||||
for(int j = 0; j < image.getHeight(); j++) {
|
||||
shadowImage.setRGB(i, j, image.getRGB(i, j) & 0x7F000000);
|
||||
@@ -78,7 +79,7 @@ public class ShadowedSprite extends JComponent {
|
||||
this.image = image;
|
||||
this.scale = scale;
|
||||
imageChanged();
|
||||
updateSize(width, height);
|
||||
updateSize(width + OFFSET_X, height + OFFSET_Y);
|
||||
repaint();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
@@ -162,9 +163,17 @@ public class DataManager {
|
||||
.collect(Collectors.toCollection(ArrayList::new));
|
||||
}
|
||||
|
||||
public static List<Integer> getMoveOptions(GameVersion gameVersion, PkmnSpecies species, PkmnGender gender) {
|
||||
public static List<PkmnForm> getFormOptions(GameVersion gameVersion, PkmnSpecies species, PkmnGender gender) {
|
||||
return getEncounters(gameVersion).stream()
|
||||
.filter(x -> x.species() == species.id() && (!x.isGenderLocked() || species.gender() == gender))
|
||||
.filter(x -> x.species() == species.id() && (!x.isGenderLocked() || x.gender() == gender))
|
||||
.flatMap(x -> species.hasForms() ? Stream.of(species.forms()).filter(form -> x.hasForm(form.id())) : Stream.empty())
|
||||
.distinct()
|
||||
.collect(Collectors.toCollection(ArrayList::new));
|
||||
}
|
||||
|
||||
public static List<Integer> getMoveOptions(GameVersion gameVersion, PkmnSpecies species, PkmnGender gender, int form) {
|
||||
return getEncounters(gameVersion).stream()
|
||||
.filter(x -> x.species() == species.id() && (!x.isGenderLocked() || x.gender() == gender) && x.hasForm(form))
|
||||
.map(Encounter::moves)
|
||||
.flatMap(List::stream)
|
||||
.distinct()
|
||||
|
||||
@@ -10,10 +10,21 @@ import entralinked.model.pkmn.PkmnGender;
|
||||
public record Encounter(
|
||||
@JsonProperty(required = true) int species,
|
||||
@JsonProperty(required = true) List<Integer> moves,
|
||||
PkmnGender gender, int versionMask) {
|
||||
PkmnGender gender, int formMask, int versionMask) {
|
||||
|
||||
@JsonIgnore
|
||||
public boolean isGenderLocked() {
|
||||
return gender != null;
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
public boolean isFormLocked() {
|
||||
return formMask != 0;
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
public boolean hasForm(int form) {
|
||||
int bits = 1 << form;
|
||||
return !isFormLocked() || (bits & formMask) == bits;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
@@ -62,14 +62,28 @@ public class EncounterEditorPanel extends TableEditorPanel {
|
||||
table.enableOption(row, MOVE_COLUMN);
|
||||
table.enableOption(row, FORM_COLUMN);
|
||||
table.enableOption(row, ANIMATION_COLUMN);
|
||||
PkmnSpecies species = (PkmnSpecies)newValue;
|
||||
updateGenderOptions(row, species);
|
||||
updateFormOptions(row, species);
|
||||
updateMoveOptions(row, species);
|
||||
updateGenderOptions(row);
|
||||
updateFormOptions(row);
|
||||
updateMoveOptions(row);
|
||||
optionLock = false;
|
||||
} else if(column == GENDER_COLUMN) {
|
||||
if(!isLegalMode()) {
|
||||
return; // Gender only affects move options if legal mode is enabled
|
||||
return; // Gender only affects form options if legal mode is enabled
|
||||
}
|
||||
|
||||
if(newValue == null) {
|
||||
table.disableOption(row, FORM_COLUMN);
|
||||
return;
|
||||
}
|
||||
|
||||
updateFormOptions(row);
|
||||
|
||||
if(oldValue == null) {
|
||||
table.enableOption(row, FORM_COLUMN);
|
||||
}
|
||||
} else if(column == FORM_COLUMN) {
|
||||
if(!isLegalMode()) {
|
||||
return; // Form only affects move options if legal mode is enabled
|
||||
}
|
||||
|
||||
if(newValue == null) {
|
||||
@@ -77,7 +91,7 @@ public class EncounterEditorPanel extends TableEditorPanel {
|
||||
return;
|
||||
}
|
||||
|
||||
updateMoveOptions(row, getSpecies(row));
|
||||
updateMoveOptions(row);
|
||||
|
||||
if(oldValue == null) {
|
||||
table.enableOption(row, MOVE_COLUMN);
|
||||
@@ -93,9 +107,9 @@ public class EncounterEditorPanel extends TableEditorPanel {
|
||||
|
||||
if(species != null) {
|
||||
optionLock = true;
|
||||
updateGenderOptions(i, species);
|
||||
updateFormOptions(i, species);
|
||||
updateMoveOptions(i, species);
|
||||
updateGenderOptions(i);
|
||||
updateFormOptions(i);
|
||||
updateMoveOptions(i);
|
||||
optionLock = false;
|
||||
}
|
||||
}
|
||||
@@ -137,18 +151,21 @@ public class EncounterEditorPanel extends TableEditorPanel {
|
||||
(a, b) -> a.name().compareTo(b.name()), true);
|
||||
}
|
||||
|
||||
private void updateMoveOptions(int row, PkmnSpecies species) {
|
||||
setOptions(row, MOVE_COLUMN,
|
||||
isLegalMode() ? DataManager.getMoveOptions(gameVersion, species, getGender(row)) : DataManager.getMoveIds(),
|
||||
(a, b) -> DataManager.getMoveName(a).compareTo(DataManager.getMoveName(b)), true);
|
||||
}
|
||||
|
||||
private void updateGenderOptions(int row, PkmnSpecies species) {
|
||||
private void updateGenderOptions(int row) {
|
||||
PkmnSpecies species = getSpecies(row);
|
||||
setOptions(row, GENDER_COLUMN, isLegalMode() ? DataManager.getGenderOptions(gameVersion, species) : species.getGenders());
|
||||
}
|
||||
|
||||
private void updateFormOptions(int row, PkmnSpecies species) {
|
||||
setOptions(row, FORM_COLUMN, species.hasForms() ? Arrays.asList(species.forms()) : Collections.emptyList());
|
||||
private void updateFormOptions(int row) {
|
||||
PkmnSpecies species = getSpecies(row);
|
||||
setOptions(row, FORM_COLUMN, species.hasForms() ? (isLegalMode() ? DataManager.getFormOptions(gameVersion, species, getGender(row))
|
||||
: Arrays.asList(species.forms())) : Collections.emptyList());
|
||||
}
|
||||
|
||||
private void updateMoveOptions(int row) {
|
||||
setOptions(row, MOVE_COLUMN,
|
||||
isLegalMode() ? DataManager.getMoveOptions(gameVersion, getSpecies(row), getGender(row), getForm(row)) : DataManager.getMoveIds(),
|
||||
(a, b) -> DataManager.getMoveName(a).compareTo(DataManager.getMoveName(b)), true);
|
||||
}
|
||||
|
||||
public void loadProfile(Player player) {
|
||||
|
||||
@@ -1,380 +1,152 @@
|
||||
package entralinked.gui.panels;
|
||||
|
||||
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 BufferedImage EMPTY_IMAGE = new BufferedImage(TiledImageUtility.SCREEN_WIDTH, TiledImageUtility.SCREEN_HEIGHT, BufferedImage.TYPE_INT_RGB);
|
||||
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("", JLabel.CENTER);
|
||||
cgearPreviewLabel.setBorder(BorderFactory.createTitledBorder("C-Gear Skin Preview"));
|
||||
JLabel dexPreviewLabel = new JLabel("", JLabel.CENTER);
|
||||
dexPreviewLabel.setBorder(BorderFactory.createTitledBorder("Pokédex Skin Preview"));
|
||||
|
||||
// Create preview image panel
|
||||
JPanel previewPanel = new JPanel();
|
||||
previewPanel.add(cgearPreviewLabel);
|
||||
previewPanel.add(dexPreviewLabel);
|
||||
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 -> {
|
||||
cgearPreviewLabel.setIcon(new ImageIcon(getSkinImage((DlcOption)cgearComboBox.getSelectedItem())));
|
||||
});
|
||||
zukanComboBox = new JComboBox<>();
|
||||
zukanComboBox.setMinimumSize(zukanComboBox.getPreferredSize());
|
||||
zukanComboBox.setRenderer(renderer);
|
||||
zukanComboBox.addActionListener(event -> {
|
||||
dexPreviewLabel.setIcon(new ImageIcon(getSkinImage((DlcOption)zukanComboBox.getSelectedItem())));
|
||||
});
|
||||
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 static Image getSkinImage(DlcOption option) {
|
||||
return option == null ? EMPTY_IMAGE : 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) {
|
||||
return EMPTY_IMAGE; // TODO show feedback
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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) {
|
||||
e.printStackTrace(); // TODO show feedback
|
||||
}
|
||||
|
||||
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) {
|
||||
e.printStackTrace(); // TODO show feedback
|
||||
}
|
||||
|
||||
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) {
|
||||
e.printStackTrace(); // TODO show feedback
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
@@ -26,7 +26,7 @@ public enum AvenueVisitorType {
|
||||
SCIENTIST_FEMALE("Scientist♀", 4, true),
|
||||
|
||||
// 5
|
||||
HIKER("Hiker♂", 5),
|
||||
HIKER("Hiker", 5),
|
||||
PARASOL_LADY("Parasol Lady", 5, true),
|
||||
|
||||
// 6
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import entralinked.Configuration;
|
||||
import entralinked.Entralinked;
|
||||
import entralinked.GameVersion;
|
||||
import entralinked.model.avenue.AvenueVisitor;
|
||||
import entralinked.model.dlc.Dlc;
|
||||
import entralinked.model.dlc.DlcList;
|
||||
@@ -52,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;
|
||||
@@ -202,63 +197,26 @@ public class PglHandler implements HttpHandler {
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info("Player {} is downloading save data as user {}", player.getGameSyncId(), user.getRedactedId());
|
||||
logger.info("Player {} is downloading save data", player.getGameSyncId());
|
||||
|
||||
// Write status code
|
||||
writeStatusCode(outputStream, 0);
|
||||
|
||||
// Allow it to wake up anyway, maybe the poor sap is stuck..
|
||||
// Just don't send any other data.
|
||||
// Allow waking up but don't send any data
|
||||
if(player.getStatus() == PlayerStatus.AWAKE) {
|
||||
return;
|
||||
}
|
||||
|
||||
GameVersion version = player.getGameVersion();
|
||||
List<DreamEncounter> encounters = player.getEncounters();
|
||||
List<DreamItem> items = player.getItems();
|
||||
List<DreamDecor> decorList = player.getDecor();
|
||||
|
||||
// Prepare DLC information
|
||||
String cgearType = player.getGameVersion().isVersion2() ? "CGEAR2" : "CGEAR";
|
||||
String cgearSkin = player.getCGearSkin();
|
||||
String dexSkin = player.getDexSkin();
|
||||
String musical = player.getMusical();
|
||||
int cgearSkinIndex = 0;
|
||||
int dexSkinIndex = 0;
|
||||
int musicalIndex = 0;
|
||||
|
||||
// Create or remove custom C-Gear skin DLC override
|
||||
if("custom".equals(cgearSkin)) {
|
||||
cgearSkinIndex = 1;
|
||||
user.setDlcOverride(cgearType, new Dlc(player.getCGearSkinFile().getAbsolutePath(),
|
||||
"custom", "IRAO", cgearType, cgearSkinIndex, 9730, 0, true));
|
||||
} else {
|
||||
cgearSkinIndex = dlcList.getDlcIndex("IRAO", cgearType, cgearSkin);
|
||||
user.removeDlcOverride(cgearType);
|
||||
}
|
||||
|
||||
// Create or remove custom Pokédex skin DLC override
|
||||
if("custom".equals(dexSkin)) {
|
||||
dexSkinIndex = 1;
|
||||
user.setDlcOverride("ZUKAN", new Dlc(player.getDexSkinFile().getAbsolutePath(),
|
||||
"custom", "IRAO", "ZUKAN", dexSkinIndex, 25090, 0, true));
|
||||
} else {
|
||||
dexSkinIndex = dlcList.getDlcIndex("IRAO", "ZUKAN", dexSkin);
|
||||
user.removeDlcOverride("ZUKAN");
|
||||
}
|
||||
|
||||
// Create or remove custom musical DLC override
|
||||
if("custom".equals(musical)) {
|
||||
musicalIndex = 1;
|
||||
File file = player.getMusicalFile();
|
||||
user.setDlcOverride("MUSICAL", new Dlc(file.getAbsolutePath(),
|
||||
"custom", "IRAO", "MUSICAL", musicalIndex, (int)file.length(), 0, true));
|
||||
} else {
|
||||
musicalIndex = dlcList.getDlcIndex("IRAO", "MUSICAL", musical);
|
||||
user.removeDlcOverride("MUSICAL");
|
||||
}
|
||||
|
||||
// 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.
|
||||
// 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)
|
||||
@@ -277,10 +235,10 @@ public class PglHandler implements HttpHandler {
|
||||
// Write misc stuff and DLC information
|
||||
outputStream.writeShort(player.getLevelsGained());
|
||||
outputStream.write(0); // Unknown
|
||||
outputStream.write(musicalIndex);
|
||||
outputStream.write(cgearSkinIndex);
|
||||
outputStream.write(dexSkinIndex);
|
||||
outputStream.write(decorList.isEmpty() ? 0 : 1); // Seems to be a flag for indicating whether or not decor data is present
|
||||
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); // Decor flag (?) stored at 0x1D4A4
|
||||
outputStream.write(0); // Must be zero?
|
||||
|
||||
// Write item IDs
|
||||
@@ -314,19 +272,23 @@ 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.
|
||||
// Black Version 2 and White Version 2 only.
|
||||
if(player.getGameVersion().isVersion2()) {
|
||||
if(version.isVersion2()) {
|
||||
List<AvenueVisitor> avenueVisitors = player.getAvenueVisitors();
|
||||
|
||||
for(AvenueVisitor visitor : avenueVisitors) {
|
||||
// Write visitor name + padding. Names cannot be duplicate.
|
||||
byte[] nameBytes = visitor.name().getBytes(StandardCharsets.UTF_16LE);
|
||||
outputStream.write(nameBytes, 0, Math.min(14, nameBytes.length));
|
||||
outputStream.writeBytes(-1, 14 - nameBytes.length);
|
||||
outputStream.writeBytes(-1, 16 - nameBytes.length);
|
||||
|
||||
// Full visitor type consists of a trainer class and what I call a 'personality' index
|
||||
// that, along with the trainer class, determines which phrases the visitor uses.
|
||||
@@ -335,7 +297,6 @@ public class PglHandler implements HttpHandler {
|
||||
// For example, if the visitor type is '0', then shop type '0' would be a raffle.
|
||||
// However, if the visitor type is '2', then shop type '0' results in a dojo instead.
|
||||
int visitorType = visitor.type().getClientId() + visitor.personality() * 8;
|
||||
outputStream.writeShort(-1); // Does nothing, seems to be read as part of the name.
|
||||
outputStream.write(visitorType);
|
||||
outputStream.write(visitor.shopType().ordinal() + (7 - visitorType * 2 % 7));
|
||||
outputStream.writeShort(0); // Does nothing
|
||||
@@ -451,7 +412,6 @@ public class PglHandler implements HttpHandler {
|
||||
private void handleUploadSaveData(PglRequest request, Context ctx) throws IOException {
|
||||
LEOutputStream outputStream = new LEOutputStream(ctx.outputStream());
|
||||
Player player = playerManager.getPlayer(request.gameSyncId());
|
||||
User user = ctx.attribute("user");
|
||||
|
||||
// Check if the player exists, has no Pokémon tucked in already and uses the same game version
|
||||
if(player == null
|
||||
@@ -470,7 +430,7 @@ public class PglHandler implements HttpHandler {
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info("Player {} is uploading save data as user {}", player.getGameSyncId(), user.getRedactedId());
|
||||
logger.info("Player {} is uploading save data", player.getGameSyncId());
|
||||
|
||||
// Try to store save data
|
||||
if(!playerManager.storePlayerGameSaveFile(player, ctx.bodyInputStream())) {
|
||||
@@ -575,4 +535,17 @@ public class PglHandler implements HttpHandler {
|
||||
outputStream.writeInt(status);
|
||||
outputStream.writeBytes(0, 124);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the index of the player's chosen DLC for the specified type and prepares DLC overriding if necessary.
|
||||
*/
|
||||
private int getDlcIndex(User user, String name, String type, File customFile) {
|
||||
if("custom".equals(name)) {
|
||||
user.setDlcOverride(type, new Dlc(customFile.getAbsolutePath(), name, "IRAO", type, 1, (int)customFile.length(), 0, true));
|
||||
return 1;
|
||||
} else {
|
||||
user.removeDlcOverride(type);
|
||||
return dlcList.getDlcIndex("IRAO", type, name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,6 +108,28 @@ public class SwingUtility {
|
||||
return def;
|
||||
}
|
||||
|
||||
public static JLabel createTitleLabel() {
|
||||
return createTitleLabel("");
|
||||
}
|
||||
|
||||
public static JLabel createTitleLabel(String text) {
|
||||
return createStyledLabel(text, "font:bold +8");
|
||||
}
|
||||
|
||||
public static JLabel createDescriptionLabel() {
|
||||
return createDescriptionLabel("");
|
||||
}
|
||||
|
||||
public static JLabel createDescriptionLabel(String text) {
|
||||
return createStyledLabel(text, "[dark]foreground:darken(@foreground,20%)");
|
||||
}
|
||||
|
||||
public static JLabel createStyledLabel(String text, String style) {
|
||||
JLabel label = new JLabel(text);
|
||||
label.putClientProperty(FlatClientProperties.STYLE, style);
|
||||
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");
|
||||
|
||||
@@ -1159,6 +1159,7 @@
|
||||
324,
|
||||
351
|
||||
],
|
||||
"formMask": 1,
|
||||
"versionMask": 3327
|
||||
},
|
||||
{
|
||||
@@ -1393,6 +1394,7 @@
|
||||
173,
|
||||
450
|
||||
],
|
||||
"formMask": 2,
|
||||
"versionMask": 4095
|
||||
},
|
||||
{
|
||||
@@ -2074,6 +2076,56 @@
|
||||
575
|
||||
]
|
||||
},
|
||||
"Pokémon Café Forest": {
|
||||
"encounters": [
|
||||
{
|
||||
"species": 61,
|
||||
"moves": [
|
||||
114,
|
||||
352
|
||||
],
|
||||
"versionMask": 4095
|
||||
},
|
||||
{
|
||||
"species": 133,
|
||||
"moves": [
|
||||
129,
|
||||
204
|
||||
],
|
||||
"versionMask": 4095
|
||||
},
|
||||
{
|
||||
"species": 235,
|
||||
"moves": [
|
||||
214,
|
||||
445
|
||||
],
|
||||
"versionMask": 4095
|
||||
},
|
||||
{
|
||||
"species": 412,
|
||||
"moves": [
|
||||
173,
|
||||
450
|
||||
],
|
||||
"formMask": 1,
|
||||
"versionMask": 4095
|
||||
}
|
||||
],
|
||||
"items": [
|
||||
30,
|
||||
31,
|
||||
32,
|
||||
33,
|
||||
50,
|
||||
82,
|
||||
83,
|
||||
84,
|
||||
134,
|
||||
157,
|
||||
221
|
||||
]
|
||||
},
|
||||
"Global Link Promotions": {
|
||||
"encounters": [
|
||||
{
|
||||
@@ -2327,6 +2379,7 @@
|
||||
{
|
||||
"species": 493,
|
||||
"moves": [],
|
||||
"formMask": 1,
|
||||
"versionMask": 3167
|
||||
},
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user