Add built-in user interface with randomize & legality options (#9 & #16)

This commit is contained in:
kuroppoi 2025-04-03 03:16:01 +02:00
parent ff8615401e
commit 356900d591
49 changed files with 12548 additions and 156 deletions

View File

@ -30,6 +30,8 @@ dependencies {
implementation 'com.formdev:flatlaf:3.1.1'
implementation 'com.formdev:flatlaf-extras:3.1.1'
implementation 'com.formdev:flatlaf-intellij-themes:3.1.1'
implementation 'com.miglayout:miglayout-swing:4.2' // Finally, a good fucking layout manager.
implementation 'org.swinglabs.swingx:swingx-autocomplete:1.6.5-1'
}
sourceSets {

View File

@ -14,7 +14,7 @@ import org.apache.logging.log4j.Logger;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import entralinked.gui.MainView;
import entralinked.gui.view.MainView;
import entralinked.model.dlc.DlcList;
import entralinked.model.player.PlayerManager;
import entralinked.model.user.UserManager;
@ -119,7 +119,6 @@ public class Entralinked {
if(mainView != null) {
SwingUtilities.invokeLater(() -> {
mainView.setDashboardButtonEnabled(true);
mainView.setStatusLabelText("Configure your DS to use the following DNS server: %s".formatted(hostIpAddress));
});
}

View File

@ -4,7 +4,7 @@ import java.util.HashMap;
import java.util.Map;
public enum GameVersion {
// ==================================
// Black Version & White Version
// ==================================
@ -29,21 +29,35 @@ public enum GameVersion {
// Black Version 2 & White Version 2
// ==================================
BLACK_2_JAPANESE(23, 1, "IREJ", "ブラック2", true),
BLACK_2_ENGLISH(23, 2, "IREO", "Black Version 2", true),
BLACK_2_FRENCH(23, 3, "IREF", "Version Noire 2", true),
BLACK_2_ITALIAN(23, 4, "IREI", "Versione Nera 2", true),
BLACK_2_GERMAN(23, 5, "IRED", "Schwarze Edition 2", true),
BLACK_2_SPANISH(23, 7, "IRES", "Edicion Negra 2", true),
BLACK_2_KOREAN(23, 8, "IREK", "블랙2", true),
BLACK_2_JAPANESE(23, 1, "IREJ", "ブラック2"),
BLACK_2_ENGLISH(23, 2, "IREO", "Black Version 2"),
BLACK_2_FRENCH(23, 3, "IREF", "Version Noire 2"),
BLACK_2_ITALIAN(23, 4, "IREI", "Versione Nera 2"),
BLACK_2_GERMAN(23, 5, "IRED", "Schwarze Edition 2"),
BLACK_2_SPANISH(23, 7, "IRES", "Edicion Negra 2"),
BLACK_2_KOREAN(23, 8, "IREK", "블랙2"),
WHITE_2_JAPANESE(22, 1, "IRDJ", "ホワイト2", true),
WHITE_2_ENGLISH(22, 2, "IRDO", "White Version 2", true),
WHITE_2_FRENCH(22, 3, "IRDF", "Version Blanche 2", true),
WHITE_2_ITALIAN(22, 4, "IRDI", "Versione Bianca 2", true),
WHITE_2_GERMAN(22, 5, "IRDD", "Weisse Edition 2", true),
WHITE_2_SPANISH(22, 7, "IRDS", "Edicion Blanca 2", true),
WHITE_2_KOREAN(22, 8, "IRDK", "화이트2", true);
WHITE_2_JAPANESE(22, 1, "IRDJ", "ホワイト2"),
WHITE_2_ENGLISH(22, 2, "IRDO", "White Version 2"),
WHITE_2_FRENCH(22, 3, "IRDF", "Version Blanche 2"),
WHITE_2_ITALIAN(22, 4, "IRDI", "Versione Bianca 2"),
WHITE_2_GERMAN(22, 5, "IRDD", "Weisse Edition 2"),
WHITE_2_SPANISH(22, 7, "IRDS", "Edicion Blanca 2"),
WHITE_2_KOREAN(22, 8, "IRDK", "화이트2");
// Masks
public static final int BW_MASK = 0b110011111111;
public static final int B2W2_MASK = 0b001111111111;
public static final int ALL_MASK = BW_MASK | B2W2_MASK;
public static final int JAP_MASK = 0b111100000001;
public static final int ENG_MASK = 0b111100000010;
public static final int FRE_MASK = 0b111100000100;
public static final int ITA_MASK = 0b111100001000;
public static final int GER_MASK = 0b111100010000;
public static final int SPA_MASK = 0b111101000000;
public static final int KOR_MASK = 0b111110000000;
public static final int JAP_KOR_MASK = JAP_MASK | KOR_MASK;
public static final int NA_EUR_MASK = ENG_MASK | FRE_MASK | ITA_MASK | GER_MASK | SPA_MASK;
// Lookup maps
private static final Map<String, GameVersion> mapBySerial = new HashMap<>();
@ -52,7 +66,7 @@ public enum GameVersion {
static {
for(GameVersion version : values()) {
mapBySerial.put(version.getSerial(), version);
mapByCodes.put(version.getRomCode() << version.getLanguageCode(), version);
mapByCodes.put(version.getBits(), version);
}
}
@ -60,18 +74,12 @@ public enum GameVersion {
private final int languageCode; // Values are not tested
private final String serial;
private final String displayName;
private final boolean isVersion2;
private GameVersion(int romCode, int languageCode, String serial, String displayName, boolean isVersion2) {
private GameVersion(int romCode, int languageCode, String serial, String displayName) {
this.romCode = romCode;
this.languageCode = languageCode;
this.serial = serial;
this.displayName = displayName;
this.isVersion2 = isVersion2;
}
private GameVersion(int romCode, int languageCode, String serial, String displayName) {
this(romCode, languageCode, serial, displayName, false);
}
public static GameVersion lookup(String serial) {
@ -79,7 +87,11 @@ public enum GameVersion {
}
public static GameVersion lookup(int romCode, int languageCode) {
return mapByCodes.get(romCode << languageCode);
return mapByCodes.get(getBits(romCode, languageCode));
}
private static int getBits(int romCode, int languageCode) {
return (1 << (8 - (romCode - 23))) | (1 << languageCode - 1) & 0b111111111111;
}
public int getRomCode() {
@ -99,6 +111,15 @@ public enum GameVersion {
}
public boolean isVersion2() {
return isVersion2;
return checkMask(B2W2_MASK);
}
public boolean checkMask(int mask) {
int bits = getBits();
return (bits & mask) == bits;
}
public int getBits() {
return getBits(romCode, languageCode);
}
}

View File

@ -0,0 +1,55 @@
package entralinked.gui;
import java.awt.Component;
import java.io.File;
import java.util.Collections;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Function;
import javax.swing.JFileChooser;
import javax.swing.filechooser.FileFilter;
public class FileChooser {
private static final JFileChooser fileChooser = new JFileChooser(".");
public static void showFileOpenDialog(Component parent, Consumer<FileSelection> handler) {
showFileOpenDialog(parent, Collections.emptyList(), handler);
}
public static void showFileOpenDialog(Component parent, FileFilter fileFilter, Consumer<FileSelection> handler) {
showFileOpenDialog(parent, List.of(fileFilter), handler);
}
public static void showFileOpenDialog(Component parent, List<FileFilter> fileFilters, Consumer<FileSelection> handler) {
showDialog(parent, fileFilters, fileChooser::showOpenDialog, handler);
}
private static void showDialog(Component parent, List<FileFilter> fileFilters, Function<Component, Integer> dialogFunction, Consumer<FileSelection> handler) {
FileFilter currentFilter = fileChooser.getFileFilter();
fileChooser.resetChoosableFileFilters();
fileChooser.setAcceptAllFileFilterUsed(fileFilters.isEmpty());
fileFilters.forEach(fileChooser::addChoosableFileFilter);
if(fileFilters.contains(currentFilter)) {
fileChooser.setFileFilter(currentFilter);
}
if(dialogFunction.apply(parent) == JFileChooser.APPROVE_OPTION) {
File file = fileChooser.getSelectedFile();
handler.accept(new FileSelection(file, fileChooser.getFileFilter(), getFileExtension(file)));
}
}
public static String getFileExtension(File file) {
String name = file.getName();
int index = name.lastIndexOf('.');
if(index == -1 || index + 1 == name.length()) {
return null;
}
return name.substring(index + 1).toLowerCase();
}
}

View File

@ -0,0 +1,7 @@
package entralinked.gui;
import java.io.File;
import javax.swing.filechooser.FileFilter;
public record FileSelection(File file, FileFilter filter, String extension) {}

View File

@ -0,0 +1,60 @@
package entralinked.gui;
import java.awt.FontMetrics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.image.BufferedImage;
import java.util.HashMap;
import java.util.Map;
import javax.imageio.ImageIO;
import entralinked.gui.data.DataManager;
public class ImageLoader {
private static final Map<String, BufferedImage> cache = new HashMap<>();
public static BufferedImage getImage(String path) {
return cache.computeIfAbsent(path, ImageLoader::loadImage);
}
private static BufferedImage loadImage(String path) {
try {
return ImageIO.read(DataManager.class.getResource(path));
} catch(Exception e) {
BufferedImage image = new BufferedImage(128, 128, BufferedImage.TYPE_INT_ARGB);
Graphics2D graphics = image.createGraphics();
graphics.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
graphics.drawString("IMAGE LOAD ERROR", 0, 16);
graphics.drawString("Please report this!", 0, 124);
drawWrappedString(graphics, path, 0, 32, image.getWidth());
drawWrappedString(graphics, e.getMessage(), 0, 80, image.getWidth());
graphics.dispose();
return image;
}
}
private static void drawWrappedString(Graphics2D graphics, String string, int x, int y, int width) {
int length = string.length();
FontMetrics metrics = graphics.getFontMetrics();
String line = "";
int currentY = y;
for(int i = 0; i < length; i++) {
char next = string.charAt(i);
if((!line.isEmpty() && x + metrics.stringWidth(line + next) >= width)) {
graphics.drawString(line, x, currentY);
line = "";
currentY += metrics.getHeight();
}
line += next;
if(i + 1 == length) {
graphics.drawString(line, x, currentY);
}
}
}
}

View File

@ -0,0 +1,25 @@
package entralinked.gui;
import javax.swing.DefaultCellEditor;
import javax.swing.InputVerifier;
import javax.swing.JTextField;
@SuppressWarnings("serial")
public class InputVerifierCellEditor extends DefaultCellEditor {
private final InputVerifier verifier;
public InputVerifierCellEditor(InputVerifier verifier) {
this(verifier, new JTextField());
}
public InputVerifierCellEditor(InputVerifier verifier, JTextField textField) {
super(textField);
this.verifier = verifier;
}
@Override
public boolean stopCellEditing() {
return verifier.verify(editorComponent) && super.stopCellEditing();
}
}

View File

@ -0,0 +1,38 @@
package entralinked.gui;
import java.awt.Component;
import java.awt.Font;
import java.util.function.Function;
import javax.swing.DefaultListCellRenderer;
import javax.swing.JList;
import entralinked.utility.SwingUtility;
@SuppressWarnings({"serial", "unchecked"})
public class ModelListCellRenderer<T> extends DefaultListCellRenderer {
private final Class<T> type;
private final Function<T, String> textSupplier;
private final String nullValue;
public ModelListCellRenderer(Class<T> type, Function<T, String> textSupplier, String nullValue) {
this.type = type;
this.textSupplier = textSupplier;
this.nullValue = nullValue;
}
@Override
public Component getListCellRendererComponent(JList<?> list, Object value, int index, boolean isSelected, boolean cellHasFocus) {
Component component = super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
setText(value == null ? String.valueOf(nullValue) : type.isAssignableFrom(value.getClass()) ? textSupplier.apply((T)value) : String.valueOf(value));
Font font = component.getFont();
String text = getText();
if(font.canDisplayUpTo(text) != -1) {
component.setFont(SwingUtility.findSupportingFont(text, font));
}
return component;
}
}

View File

@ -0,0 +1,38 @@
package entralinked.gui;
import java.awt.Component;
import java.awt.Font;
import java.util.function.Function;
import javax.swing.JTable;
import javax.swing.table.DefaultTableCellRenderer;
import entralinked.utility.SwingUtility;
@SuppressWarnings({"serial", "unchecked"})
public class ModelTableCellRenderer<T> extends DefaultTableCellRenderer {
private final Class<T> type;
private final Function<T, String> textSupplier;
private final String nullValue;
public ModelTableCellRenderer(Class<T> type, Function<T, String> textSupplier, String nullValue) {
this.type = type;
this.textSupplier = textSupplier;
this.nullValue = nullValue;
}
@Override
public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
Component component = super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
setText(value == null ? String.valueOf(nullValue) : type.isAssignableFrom(value.getClass()) ? textSupplier.apply((T)value) : String.valueOf(value));
Font font = component.getFont();
String text = getText();
if(font.canDisplayUpTo(text) != -1) {
component.setFont(SwingUtility.findSupportingFont(text, font));
}
return component;
}
}

View File

@ -0,0 +1,229 @@
package entralinked.gui.component;
import java.awt.Component;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Vector;
import java.util.function.Function;
import javax.swing.CellEditor;
import javax.swing.DefaultCellEditor;
import javax.swing.JComboBox;
import javax.swing.JTable;
import javax.swing.JTextField;
import javax.swing.ListCellRenderer;
import javax.swing.table.TableCellEditor;
import javax.swing.table.TableCellRenderer;
import org.jdesktop.swingx.autocomplete.AutoCompleteDecorator;
import org.jdesktop.swingx.autocomplete.ComboBoxCellEditor;
import org.jdesktop.swingx.autocomplete.ObjectToStringConverter;
import entralinked.gui.ModelListCellRenderer;
import entralinked.gui.ModelTableCellRenderer;
/**
* TODO look into custom table models because this current implementation is absolutely ass.
*/
@SuppressWarnings("serial")
public class ConfigTable extends JTable {
private final Map<Integer, TableCellRenderer> tableCellRenderers = new HashMap<>();
private final Map<Integer, ListCellRenderer<Object>> listCellRenderers = new HashMap<>();
private final Map<Integer, ObjectToStringConverter> converters = new HashMap<>();
private TableCellEditor[][] cellEditors;
private boolean[][] disabledCells;
@Override
public TableCellEditor getCellEditor(int row, int column) {
resizeArrays();
return cellEditors[row][column];
}
@Override
public TableCellRenderer getCellRenderer(int row, int column) {
return tableCellRenderers.getOrDefault(column, super.getCellRenderer(row, column));
}
@Override
public boolean isCellEditable(int row, int column) {
resizeArrays();
return !disabledCells[row][column] && cellEditors[row][column] != null;
}
public void setCellEditable(int row, int column, boolean editable) {
resizeArrays();
disabledCells[row][column] = !editable;
}
public void setCellEditor(int row, int column, TableCellEditor editor) {
resizeArrays();
cellEditors[row][column] = editor;
}
public <T> void setCellRenderers(int column, Class<T> type, Function<T, String> supplier) {
setCellRenderers(column, type, supplier, null);
}
@SuppressWarnings("unchecked")
public <T> void setCellRenderers(int column, Class<T> type, Function<T, String> supplier, String nullValue) {
tableCellRenderers.put(column, new ModelTableCellRenderer<T>(type, supplier, nullValue));
listCellRenderers.put(column, new ModelListCellRenderer<T>(type, supplier, nullValue));
converters.put(column, new ObjectToStringConverter() {
@Override
public String getPreferredStringForItem(Object item) {
return (item == null || !type.isAssignableFrom(item.getClass())) ? null : supplier.apply((T)item);
}
});
}
public void enableOption(int row, int column) {
if(isCellEditable(row, column)) {
return;
}
Object initialValue = "N/A";
CellEditor editor = getCellEditor(row, column);
if(editor instanceof DefaultCellEditor) {
Component component = ((DefaultCellEditor)editor).getComponent();
if(component instanceof JComboBox) {
JComboBox<?> comboBox = (JComboBox<?>)component;
if(comboBox.getItemCount() > 0) {
initialValue = comboBox.getItemAt(0);
}
} else if(component instanceof JTextField) {
initialValue = "";
}
}
enableOption(row, column, initialValue);
}
public void enableOption(int row, int column, Object initialValue) {
if(isCellEditable(row, column)) {
return;
}
getModel().setValueAt(initialValue, row, column);
setCellEditable(row, column, true);
}
public void disableOption(int row, int column) {
setCellEditable(row, column, false);
getModel().setValueAt("", row, column);
}
public void setOptionsAt(int row, int column, Collection<?> options) {
setOptionsAt(row, column, options, false);
}
public void setOptionsAt(int row, int column, Collection<?> options, boolean includeNullOption) {
if(options.isEmpty()) {
setCellEditor(row, column, null);
getModel().setValueAt("N/A", row, column);
return;
}
Vector<?> vector = new Vector<>(options);
if(includeNullOption) {
vector.add(0, null);
}
Object currentValue = getValueAt(row, column);
if(currentValue == null || !vector.contains(currentValue)) {
currentValue = vector.firstElement();
getModel().setValueAt(currentValue, row, column);
}
JComboBox<?> comboBox = new JComboBox<>(vector);
AutoCompleteDecorator.decorate(comboBox, converters.getOrDefault(column, ObjectToStringConverter.DEFAULT_IMPLEMENTATION));
if(listCellRenderers.containsKey(column)) {
comboBox.setRenderer(listCellRenderers.get(column));
}
setCellEditor(row, column, new ComboBoxCellEditor(comboBox));
}
public void randomizeSelection(int row, int column) {
randomizeSelection(row, column, true);
}
public void randomizeSelection(int row, int column, boolean allowNull) {
CellEditor editor = getCellEditor(row, column);
if(!(editor instanceof DefaultCellEditor)) {
return;
}
Component component = ((DefaultCellEditor)editor).getComponent();
if(component instanceof JComboBox) {
JComboBox<?> comboBox = (JComboBox<?>)component;
int count = comboBox.getItemCount();
if(count <= 1) {
return;
}
int index = (int)(Math.random() * count);
Object item = comboBox.getItemAt(index);
if(!allowNull) {
// TODO potential infinite loop
while(item == null) {
item = comboBox.getItemAt((int)(Math.random() * count));
}
}
getModel().setValueAt(item, row, column);
}
}
public <T> T getValueAt(int row, int column, Class<T> type) {
return getValueAt(row, column, type, null);
}
@SuppressWarnings("unchecked")
public <T> T getValueAt(int row, int column, Class<T> type, T def) {
Object value = getValueAt(row, column);
if(value != null && type.isAssignableFrom(value.getClass())) {
return (T)value;
}
return def;
}
private void resizeArrays() {
int rows = getRowCount();
int columns = getColumnCount();
if(cellEditors == null) {
cellEditors = new TableCellEditor[rows][columns];
disabledCells = new boolean[rows][columns];
return;
}
// Check row size
if(rows != cellEditors.length) {
cellEditors = Arrays.copyOf(cellEditors, rows);
disabledCells = Arrays.copyOf(disabledCells, rows);
}
// Check column size
if(rows > 0 && cellEditors[0].length != columns) {
for(int i = 0; i < rows; i++) {
cellEditors[i] = Arrays.copyOf(cellEditors[i], columns);
disabledCells[i] = Arrays.copyOf(disabledCells[i], columns);
}
}
}
}

View File

@ -0,0 +1,90 @@
package entralinked.gui.component;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JPasswordField;
import javax.swing.JTextField;
import com.formdev.flatlaf.FlatClientProperties;
import net.miginfocom.swing.MigLayout;
@SuppressWarnings("serial")
public class PropertyDisplay extends JPanel {
private final Map<Integer, String> keys = new HashMap<>();
private final Map<String, JLabel> labels = new HashMap<>();
private final Map<String, JTextField> fields = new HashMap<>();
public PropertyDisplay() {
setLayout(new MigLayout("insets 0, fill"));
}
public void addProperty(int x, int y, String key, String label) {
addProperty(x, y, key, label, false);
}
public void addProperty(int x, int y, String key, String label, boolean hidden) {
int coordHash = Objects.hash(x, y);
String existingKey = keys.get(coordHash);
if(existingKey != null) {
removeProperty(existingKey);
}
removeProperty(key);
String labelConstraints = "cell %s %s, sg proplabel";
if(x > 0) {
labelConstraints += ", gapx 4";
}
JLabel labelComponent = new JLabel(label);
labels.put(key, labelComponent);
add(labelComponent, labelConstraints.formatted(x * 2, y));
add(createValueField(key, hidden), "cell %s %s, sg propfield, width 96".formatted(x * 2 + 1, y));
keys.put(coordHash, key);
}
public boolean removeProperty(String key) {
if(!labels.containsKey(key)) {
return false;
}
remove(labels.remove(key));
remove(fields.remove(key));
keys.values().remove(key);
return true;
}
public void setValue(String key, Object value) {
JTextField textField = fields.get(key);
if(textField != null) {
textField.setText(String.valueOf(value));
}
}
private JTextField createValueField(String key, boolean hidden) {
JTextField textField = null;
if(!hidden) {
textField = new JTextField();
} else {
textField = new JPasswordField();
textField.putClientProperty(FlatClientProperties.STYLE, "showRevealButton: true; showCapsLock: false");
}
textField.setEditable(false);
fields.put(key, textField);
return textField;
}
public JTextField getValueField(String key) {
return fields.get(key);
}
}

View File

@ -0,0 +1,84 @@
package entralinked.gui.component;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.image.BufferedImage;
import javax.swing.JComponent;
@SuppressWarnings("serial")
public class ShadowedSprite extends JComponent {
private BufferedImage image;
private BufferedImage shadowImage;
private int scale;
public ShadowedSprite() {
setOpaque(false);
}
@Override
public void paintComponent(Graphics graphics) {
super.paintComponent(graphics);
if(image == null) {
return;
}
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);
graphics.drawImage(image, x, y, width, height, null);
}
private void imageChanged() {
if(image == null) {
return;
}
int width = image.getWidth();
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);
}
}
}
private void updateSize(int width, int height) {
Dimension oldSize = getPreferredSize();
Dimension size = new Dimension(width, height);
setMinimumSize(size);
setPreferredSize(size);
if(oldSize.width != width || oldSize.height != height) {
revalidate();
}
}
public void setImage(BufferedImage image) {
setImage(image, 1);
}
public void setImage(BufferedImage image, int width, int height) {
setImage(image, 1, width, height);
}
public void setImage(BufferedImage image, int scale) {
setImage(image, scale, image == null ? 0 : image.getWidth(), image == null ? 0 : image.getHeight());
}
public void setImage(BufferedImage image, int scale, int width, int height) {
this.image = image;
this.scale = scale;
imageChanged();
updateSize(width, height);
repaint();
}
}

View File

@ -0,0 +1,16 @@
package entralinked.gui.data;
import java.util.List;
import com.fasterxml.jackson.annotation.JsonProperty;
public record Country(
@JsonProperty(required = true) int id,
@JsonProperty(required = true) String name,
List<Region> regions) {
@JsonProperty
public boolean hasRegions() {
return regions != null && !regions.isEmpty();
}
}

View File

@ -0,0 +1,185 @@
package entralinked.gui.data;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import entralinked.GameVersion;
import entralinked.gui.ImageLoader;
import entralinked.model.pkmn.PkmnGender;
/**
* TODO name is too generic & model management is a bit confusing.
*/
public class DataManager {
private static final Map<Integer, String> abilities = new HashMap<>();
private static final Map<Integer, String> items = new HashMap<>();
private static final Map<Integer, String> moves = new HashMap<>();
private static final Map<Integer, PkmnSpecies> species = new HashMap<>();
private static final Map<Integer, Country> countries = new HashMap<>();
private static final Map<String, DreamWorldArea> dreamWorldAreas = new HashMap<>();
private static final Map<GameVersion, List<Encounter>> encounterCache = new HashMap<>();
public static void clearData() {
abilities.clear();
items.clear();
moves.clear();
species.clear();
countries.clear();
dreamWorldAreas.clear();
encounterCache.clear();
}
public static void loadData() throws IOException {
clearData();
ObjectMapper mapper = new ObjectMapper();
abilities.putAll(mapper.readValue(DataManager.class.getResource("/data/abilities.json"), new TypeReference<Map<Integer, String>>(){}));
items.putAll(mapper.readValue(DataManager.class.getResource("/data/items.json"), new TypeReference<Map<Integer, String>>(){}));
moves.putAll(mapper.readValue(DataManager.class.getResource("/data/moves.json"), new TypeReference<Map<Integer, String>>(){}));
species.putAll(mapper.readValue(DataManager.class.getResource("/data/species.json"), new TypeReference<Map<Integer, PkmnSpecies>>(){}));
countries.putAll(mapper.readValue(DataManager.class.getResource("/data/countries.json"), new TypeReference<Map<Integer, Country>>(){}));
dreamWorldAreas.putAll(mapper.readValue(DataManager.class.getResource("/data/legality.json"), new TypeReference<Map<String, DreamWorldArea>>(){}));
}
public static BufferedImage getPokemonSprite(int species) {
return getPokemonSprite(0, 0, false, false);
}
public static BufferedImage getPokemonSprite(PkmnSpecies species, int form, PkmnGender gender, boolean shiny) {
return getPokemonSprite(species, form, gender == PkmnGender.FEMALE, shiny);
}
public static BufferedImage getPokemonSprite(int species, int form, PkmnGender gender, boolean shiny) {
return getPokemonSprite(species, form, gender == PkmnGender.FEMALE, shiny);
}
public static BufferedImage getPokemonSprite(int species, int form, boolean female, boolean shiny) {
return getPokemonSprite(getSpecies(species), form, female, shiny);
}
public static BufferedImage getPokemonSprite(PkmnSpecies species, int form, boolean female, boolean shiny) {
String path = "/sprites/pokemon/normal/0.png";
if(species != null) {
path = "/sprites/pokemon/%s/%s%s%s.png".formatted(
shiny ? "shiny" : "normal",
species.hasFemaleSprite() && female ? "female/" : "",
species.id(), form == 0 ? "" : "-" + form);
}
return ImageLoader.getImage(path);
}
public static BufferedImage getItemSprite(int item) {
return ImageLoader.getImage("/sprites/items/%s.png".formatted(item));
}
public static String getAbilityName(int id) {
return abilities.getOrDefault(id, "Unknown (#%s)".formatted(id));
}
public static Set<Integer> getAbilityIds() {
return Collections.unmodifiableSet(abilities.keySet());
}
public static String getItemName(int id) {
return items.getOrDefault(id, "Unknown (#%s)".formatted(id));
}
public static Set<Integer> getItemIds() {
return Collections.unmodifiableSet(items.keySet());
}
public static String getMoveName(int id) {
return moves.getOrDefault(id, "Unknown (#%s)".formatted(id));
}
public static Set<Integer> getMoveIds() {
return Collections.unmodifiableSet(moves.keySet());
}
public static PkmnSpecies getSpecies(int id) {
return species.get(id);
}
public static Set<Integer> getSpeciesIds() {
return Collections.unmodifiableSet(species.keySet());
}
public static Collection<PkmnSpecies> getSpecies() {
return Collections.unmodifiableCollection(species.values());
}
public static Country getCountry(int id) {
return countries.get(id);
}
public static Collection<Country> getCountries() {
return Collections.unmodifiableCollection(countries.values());
}
// TODO functions might be computationally expensive
private static List<Encounter> getEncounters(GameVersion gameVersion) {
return encounterCache.computeIfAbsent(gameVersion, version -> dreamWorldAreas.values().stream()
.map(DreamWorldArea::encounters)
.flatMap(List::stream)
.filter(x -> x.versionMask() == 0 || version.checkMask(x.versionMask()))
.toList());
}
public static List<PkmnSpecies> getDownloadableSpecies(GameVersion gameVersion) {
return species.values().stream()
.filter(x -> x.downloadable() && (gameVersion.isVersion2() || x.id() <= 493))
.collect(Collectors.toCollection(ArrayList::new));
}
public static List<PkmnSpecies> getSpeciesOptions(GameVersion gameVersion) {
return getEncounters(gameVersion).stream()
.map(x -> species.get(x.species()))
.distinct()
.collect(Collectors.toCollection(ArrayList::new));
}
public static List<PkmnGender> getGenderOptions(GameVersion gameVersion, PkmnSpecies species) {
return getEncounters(gameVersion).stream()
.filter(x -> x.species() == species.id())
.map(x -> x.isGenderLocked() ? List.of(x.gender()) : species.getGenders())
.flatMap(List::stream)
.distinct()
.sorted((a, b) -> Integer.compare(a.ordinal(), b.ordinal()))
.collect(Collectors.toCollection(ArrayList::new));
}
public static List<Integer> getMoveOptions(GameVersion gameVersion, PkmnSpecies species, PkmnGender gender) {
return getEncounters(gameVersion).stream()
.filter(x -> x.species() == species.id() && (!x.isGenderLocked() || species.gender() == gender))
.map(Encounter::moves)
.flatMap(List::stream)
.distinct()
.collect(Collectors.toCollection(ArrayList::new));
}
public static List<Integer> getDownloadableItems(GameVersion gameVersion) {
return items.keySet().stream().filter(x -> gameVersion.isVersion2() || x <= 626).toList();
}
public static List<Integer> getItemOptions() {
return dreamWorldAreas.values().stream()
.map(DreamWorldArea::items)
.flatMap(List::stream)
.distinct()
.toList();
}
}

View File

@ -0,0 +1,9 @@
package entralinked.gui.data;
import java.util.List;
import com.fasterxml.jackson.annotation.JsonProperty;
public record DreamWorldArea(
@JsonProperty(required = true) List<Encounter> encounters,
@JsonProperty(required = true) List<Integer> items) {}

View File

@ -0,0 +1,19 @@
package entralinked.gui.data;
import java.util.List;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import entralinked.model.pkmn.PkmnGender;
public record Encounter(
@JsonProperty(required = true) int species,
@JsonProperty(required = true) List<Integer> moves,
PkmnGender gender, int versionMask) {
@JsonIgnore
public boolean isGenderLocked() {
return gender != null;
}
}

View File

@ -0,0 +1,7 @@
package entralinked.gui.data;
import com.fasterxml.jackson.annotation.JsonProperty;
public record PkmnForm(
@JsonProperty(required = true) int id,
@JsonProperty(required = true) String name) {}

View File

@ -0,0 +1,29 @@
package entralinked.gui.data;
import java.util.List;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import entralinked.model.pkmn.PkmnGender;
public record PkmnSpecies(
@JsonProperty(required = true) int id,
@JsonProperty(required = true) String name,
boolean downloadable, boolean hasFemaleSprite, PkmnGender gender, PkmnForm[] forms) {
@JsonIgnore
public boolean isSingleGender() {
return gender != null;
}
@JsonIgnore
public boolean hasForms() {
return forms != null && forms.length > 0;
}
@JsonIgnore
public List<PkmnGender> getGenders() {
return isSingleGender() ? List.of(gender) : List.of(PkmnGender.MALE, PkmnGender.FEMALE);
}
}

View File

@ -0,0 +1,7 @@
package entralinked.gui.data;
import com.fasterxml.jackson.annotation.JsonProperty;
public record Region(
@JsonProperty(required = true) int id,
@JsonProperty(required = true) String name) {}

View File

@ -0,0 +1,211 @@
package entralinked.gui.panels;
import java.awt.BorderLayout;
import java.awt.CardLayout;
import java.awt.Desktop;
import java.awt.GridLayout;
import java.io.IOException;
import java.net.URISyntaxException;
import java.net.URL;
import javax.swing.JButton;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JTabbedPane;
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.data.DataManager;
import entralinked.model.player.Player;
import entralinked.model.player.PlayerStatus;
import entralinked.utility.GsidUtility;
import entralinked.utility.SwingUtility;
import net.miginfocom.swing.MigLayout;
@SuppressWarnings("serial")
public class DashboardPanel extends JPanel {
public static final String LOGIN_CARD = "login";
public static final String MAIN_CARD = "main";
private final Entralinked entralinked;
private final JLabel sessionLabel;
private final FlatTabbedPane tabbedPane;
private CardLayout layout;
private SummaryPanel summaryPanel;
private EncounterEditorPanel encounterPanel;
private ItemEditorPanel itemPanel;
private VisitorEditorPanel visitorPanel;
private MiscPanel miscPanel;
private Player player;
private boolean initialized;
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");
// Create login button
JButton loginButton = new JButton("Log in");
loginButton.addActionListener(event -> login(gsidTextField.getText()));
// Create browser dashboard button
JLabel browserButton = SwingUtility.createButtonLabel("Browser dashboard (Legacy)", () -> {
try {
Desktop.getDesktop().browse(new URL("http://127.0.0.1/dashboard/profile.html").toURI());
} catch(IOException | URISyntaxException e) {
SwingUtility.showExceptionInfo(getRootPane(), "Failed to open URL.", e);
}
});
// Create login panel
JPanel loginPanel = new JPanel(new MigLayout("wrap, align 50% 50%"));
loginPanel.add(loginTitle);
loginPanel.add(loginDescription);
loginPanel.add(new JLabel("Game Sync ID"), "gapy 8");
loginPanel.add(gsidTextField, "growx");
loginPanel.add(loginButton, "growx");
loginPanel.add(browserButton, "gapy 8, align 100%");
// Create save button
JButton saveButton = new JButton("Save profile");
saveButton.addActionListener(event -> {
// Try to save data
// TODO this is probably not thread-safe!!!
try {
encounterPanel.saveProfile(player);
itemPanel.saveProfile(player);
visitorPanel.saveProfile(player);
miscPanel.saveProfile(player);
player.setStatus(PlayerStatus.WAKE_READY);
if(!entralinked.getPlayerManager().savePlayer(player)) {
JOptionPane.showMessageDialog(getRootPane(), "Failed to write player data to disk.", "Attention", JOptionPane.WARNING_MESSAGE);
return;
}
JOptionPane.showMessageDialog(getRootPane(), "Profile data has been saved successfully!\n"
+ "Use Game Sync to wake up your Pokémon and download your selected content.");
} catch(Exception e) {
SwingUtility.showExceptionInfo(getRootPane(), "Failed to save player data.", e);
}
});
// Create log out button
JButton logoutButton = new JButton("Log out");
logoutButton.addActionListener(event -> {
gsidTextField.setText("");
layout.show(this, LOGIN_CARD);
});
// Create main footer button panel
JPanel buttonPanel = new JPanel(new GridLayout(1, 2));
buttonPanel.add(saveButton);
buttonPanel.add(logoutButton);
// Create main footer panel
sessionLabel = new JLabel("", JLabel.CENTER);
JPanel footerPanel = new JPanel(new BorderLayout());
footerPanel.add(sessionLabel);
footerPanel.add(buttonPanel, BorderLayout.LINE_END);
// Create main panel
tabbedPane = new FlatTabbedPane();
tabbedPane.setTabPlacement(JTabbedPane.LEFT);
tabbedPane.setTabAreaAlignment(TabAreaAlignment.center);
JPanel mainPanel = new JPanel(new BorderLayout());
mainPanel.add(tabbedPane);
mainPanel.add(footerPanel, BorderLayout.PAGE_END);
// Create layout
layout = new CardLayout();
setLayout(layout);
add(LOGIN_CARD, loginPanel);
add(MAIN_CARD, mainPanel);
}
private void login(String gameSyncId) {
// Check if GSID is valid
if(!GsidUtility.isValidGameSyncId(gameSyncId)) {
JOptionPane.showMessageDialog(this, "Please enter a valid Game Sync ID.", "Attention", JOptionPane.WARNING_MESSAGE);
return;
}
Player player = entralinked.getPlayerManager().getPlayer(gameSyncId);
// Check if player exists
if(player == null) {
JOptionPane.showMessageDialog(this, "This Game Sync ID does not exist.\n"
+ "If you haven't already, please tuck in a Pokémon first.", "Attention", JOptionPane.WARNING_MESSAGE);
return;
}
GameVersion version = player.getGameVersion();
// Check if necessary data is present
if(player.getDreamerInfo() == null || version == null) {
JOptionPane.showMessageDialog(this, "Please use Game Sync to tuck in a Pokémon first.", "Attention", JOptionPane.INFORMATION_MESSAGE);
player = null;
return;
}
// Try to initialize dashboard
if(!initialized && !initializeDashboard()) {
return;
}
// Load player data
sessionLabel.setText("Game Version: %s, Game Sync ID: %s".formatted(version.getDisplayName(), gameSyncId));
tabbedPane.setEnabledAt(3, version.isVersion2());
try {
summaryPanel.loadProfile(player);
encounterPanel.loadProfile(player);
itemPanel.loadProfile(player);
visitorPanel.loadProfile(player);
miscPanel.loadProfile(player);
} catch(Exception e) {
SwingUtility.showExceptionInfo(getRootPane(), "Failed to load player data.", e);
return;
}
this.player = player;
layout.show(this, MAIN_CARD);
}
private boolean initializeDashboard() {
try {
DataManager.loadData();
summaryPanel = new SummaryPanel();
encounterPanel = new EncounterEditorPanel();
itemPanel = new ItemEditorPanel();
visitorPanel = new VisitorEditorPanel();
miscPanel = new MiscPanel(entralinked);
} catch(Exception e) {
SwingUtility.showExceptionInfo(getRootPane(), "Failed to initialize dashboard.", e);
return false;
}
tabbedPane.add("Summary", summaryPanel);
tabbedPane.add("Entree Forest", encounterPanel);
tabbedPane.add("Dream Remnants", itemPanel);
tabbedPane.add("Join Avenue", visitorPanel);
tabbedPane.add("Miscellaneous", miscPanel);
initialized = true;
return true;
}
}

View File

@ -0,0 +1,210 @@
package entralinked.gui.panels;
import java.awt.Image;
import java.util.Arrays;
import java.util.Collections;
import entralinked.GameVersion;
import entralinked.gui.data.DataManager;
import entralinked.gui.data.PkmnForm;
import entralinked.gui.data.PkmnSpecies;
import entralinked.model.pkmn.PkmnGender;
import entralinked.model.player.DreamAnimation;
import entralinked.model.player.DreamEncounter;
import entralinked.model.player.Player;
@SuppressWarnings("serial")
public class EncounterEditorPanel extends TableEditorPanel {
public static final int ENCOUNTER_COUNT = 10;
public static final int SPECIES_COLUMN = 1;
public static final int MOVE_COLUMN = 2;
public static final int FORM_COLUMN = 3;
public static final int GENDER_COLUMN = 4;
public static final int ANIMATION_COLUMN = 5;
private static final String[] COLUMN_NAMES = { "Species", "Move", "Form", "Gender", "Animation" };
private static final Class<?>[] COLUMN_TYPES = { PkmnSpecies.class, Integer.class, PkmnForm.class, PkmnGender.class, DreamAnimation.class };
private GameVersion gameVersion;
private boolean optionLock; // Used to prevent double option updates
public EncounterEditorPanel() {
super(ENCOUNTER_COUNT, COLUMN_NAMES, COLUMN_TYPES);
setTitle("Entree Forest");
setDescription("""
<html>
Use the table below to configure Entree Forest encounters.<br/>
Up to 10 different encounters can be configured at a time.<br/>
Please be aware that Unova Pokémon are exclusive to B2W2.
</html>
""");
table.setCellRenderers(SPECIES_COLUMN, PkmnSpecies.class, PkmnSpecies::name, "— — — — —");
table.setCellRenderers(MOVE_COLUMN, Integer.class, DataManager::getMoveName, "None");
table.setCellRenderers(FORM_COLUMN, PkmnForm.class, PkmnForm::name);
table.setCellRenderers(GENDER_COLUMN, PkmnGender.class, PkmnGender::getDisplayName);
table.setCellRenderers(ANIMATION_COLUMN, DreamAnimation.class, DreamAnimation::getDisplayName);
initializeOptions();
}
@Override
public void dataChanged(int row, int column, Object oldValue, Object newValue) {
if(optionLock) {
return;
}
if(column == SPECIES_COLUMN) {
if(newValue == null) {
disableSecondaryOptions(row);
return;
}
optionLock = true;
table.enableOption(row, GENDER_COLUMN);
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);
optionLock = false;
} else if(column == GENDER_COLUMN) {
if(!isLegalMode()) {
return; // Gender only affects move options if legal mode is enabled
}
if(newValue == null) {
table.disableOption(row, MOVE_COLUMN);
return;
}
updateMoveOptions(row, getSpecies(row));
if(oldValue == null) {
table.enableOption(row, MOVE_COLUMN);
}
}
}
@Override
public void legalModeChanged() {
for(int i = 0; i < ENCOUNTER_COUNT; i++) {
updateSpeciesOptions(i);
PkmnSpecies species = getSpecies(i);
if(species != null) {
optionLock = true;
updateGenderOptions(i, species);
updateFormOptions(i, species);
updateMoveOptions(i, species);
optionLock = false;
}
}
}
@Override
public void initializeOptions(int row) {
setOptions(row, ANIMATION_COLUMN, Arrays.asList(DreamAnimation.values()));
disableSecondaryOptions(row);
}
@Override
public void randomizeSelections(int row) {
table.randomizeSelection(row, SPECIES_COLUMN, false);
table.randomizeSelection(row, GENDER_COLUMN);
table.randomizeSelection(row, FORM_COLUMN);
table.randomizeSelection(row, MOVE_COLUMN);
table.randomizeSelection(row, ANIMATION_COLUMN);
}
@Override
public void clearSelections(int row) {
model.setValueAt(null, row, SPECIES_COLUMN);
}
@Override
public Image getSelectionIcon(int row) {
return row == -1 ? DataManager.getPokemonSprite(0) : DataManager.getPokemonSprite(getSpecies(row), getForm(row), getGender(row), false);
}
@Override
public boolean shouldUpdateSelectionIcon(int column) {
return column == SPECIES_COLUMN || column == GENDER_COLUMN || column == FORM_COLUMN;
}
private void updateSpeciesOptions(int row) {
setOptions(row, SPECIES_COLUMN,
isLegalMode() ? DataManager.getSpeciesOptions(gameVersion) : DataManager.getDownloadableSpecies(gameVersion),
(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) {
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());
}
public void loadProfile(Player player) {
gameVersion = player.getGameVersion();
table.clearSelection();
clearSelections();
legalModeToggle.setSelected(false);
legalModeChanged();
int row = 0;
for(DreamEncounter encounter : player.getEncounters()) {
PkmnSpecies species = DataManager.getSpecies(encounter.species());
model.setValueAt(species, row, SPECIES_COLUMN);
model.setValueAt(encounter.move() == 0 ? null : encounter.move(), row, MOVE_COLUMN);
model.setValueAt(encounter.gender(), row, GENDER_COLUMN);
model.setValueAt(encounter.animation(), row, ANIMATION_COLUMN);
if(species.hasForms()) {
model.setValueAt(species.forms()[encounter.form()], row, FORM_COLUMN);
}
row++;
}
}
public void saveProfile(Player player) {
player.setEncounters(computeSelectionList(row ->
new DreamEncounter(getSpecies(row).id(), getMove(row), getForm(row), getGender(row), getAnimation(row)),
row -> getSpecies(row) != null));
}
private void disableSecondaryOptions(int row) {
table.disableOption(row, MOVE_COLUMN);
table.disableOption(row, FORM_COLUMN);
table.disableOption(row, GENDER_COLUMN);
table.disableOption(row, ANIMATION_COLUMN);
}
private PkmnSpecies getSpecies(int row) {
return table.getValueAt(row, SPECIES_COLUMN, PkmnSpecies.class);
}
private int getMove(int row) {
return table.getValueAt(row, MOVE_COLUMN, Integer.class, 0);
}
private int getForm(int row) {
PkmnForm form = table.getValueAt(row, FORM_COLUMN, PkmnForm.class);
return form == null ? 0 : form.id();
}
private PkmnGender getGender(int row) {
return table.getValueAt(row, GENDER_COLUMN, PkmnGender.class);
}
private DreamAnimation getAnimation(int row) {
return table.getValueAt(row, ANIMATION_COLUMN, DreamAnimation.class);
}
}

View File

@ -0,0 +1,132 @@
package entralinked.gui.panels;
import java.awt.Image;
import java.util.stream.IntStream;
import entralinked.GameVersion;
import entralinked.gui.data.DataManager;
import entralinked.model.player.DreamItem;
import entralinked.model.player.Player;
@SuppressWarnings("serial")
public class ItemEditorPanel extends TableEditorPanel {
public static final int ITEM_COUNT = 20;
public static final int MAX_ITEM_QUANTITY = 20;
public static final int ITEM_COLUMN = 1;
public static final int QUANTITY_COLUMN = 2;
private static final String[] COLUMN_NAMES = { "Item", "Quantity" };
private static final Class<?>[] COLUMN_TYPES = { Integer.class, Integer.class };
private GameVersion gameVersion;
public ItemEditorPanel() {
super(ITEM_COUNT, COLUMN_NAMES, COLUMN_TYPES);
setTitle("Dream Remnants");
setDescription("""
<html>
Use the table below to select the items you want to receive.<br/>
After waking up your Pokémon, talk to the boy near the<br/>
Entree Forest entrance in the Entralink to receive your items.
</html>
""");
table.setCellRenderers(ITEM_COLUMN, Integer.class, DataManager::getItemName, "— — — — —");
initializeOptions();
}
@Override
public void dataChanged(int row, int column, Object oldValue, Object newValue) {
if(column != ITEM_COLUMN) {
return;
}
if(newValue != null) {
if(oldValue != null) {
return; // Do nothing if new and old values are both non-null
}
table.enableOption(row, QUANTITY_COLUMN);
return;
}
table.disableOption(row, QUANTITY_COLUMN);
}
@Override
public void legalModeChanged() {
for(int i = 0; i < ITEM_COUNT; i++) {
updateItemOptions(i);
}
}
@Override
public void initializeOptions(int row) {
setOptions(row, QUANTITY_COLUMN, IntStream.rangeClosed(1, ITEM_COUNT).boxed().toList());
table.disableOption(row, QUANTITY_COLUMN);
}
@Override
public void randomizeSelections(int row) {
table.randomizeSelection(row, ITEM_COLUMN, false);
int quantity = 1;
while(quantity < MAX_ITEM_QUANTITY && Math.random() < 0.5) {
quantity++;
}
model.setValueAt(quantity, row, QUANTITY_COLUMN);
}
@Override
public void clearSelections(int row) {
model.setValueAt(null, row, ITEM_COLUMN);
}
@Override
public Image getSelectionIcon(int row) {
int item = row == -1 ? 0 : getItem(row);
return DataManager.getItemSprite(item);
}
@Override
public boolean shouldUpdateSelectionIcon(int column) {
return column == ITEM_COLUMN;
}
@Override
public int getSelectionIconScale() {
return 2;
}
public void loadProfile(Player player) {
gameVersion = player.getGameVersion();
table.clearSelection();
clearSelections();
legalModeToggle.setSelected(false);
legalModeChanged();
int row = 0;
for(DreamItem item : player.getItems()) {
model.setValueAt(item.id(), row, ITEM_COLUMN);
model.setValueAt(item.quantity(), row, QUANTITY_COLUMN);
row++;
}
}
public void saveProfile(Player player) {
player.setItems(computeSelectionList(row -> new DreamItem(getItem(row), getQuantity(row)), row -> getItem(row) != 0));
}
private void updateItemOptions(int row) {
setOptions(row, ITEM_COLUMN,
isLegalMode() ? DataManager.getItemOptions() : DataManager.getDownloadableItems(gameVersion),
(a, b) -> DataManager.getItemName(a).compareTo(DataManager.getItemName(b)), true);
}
private int getItem(int row) {
return table.getValueAt(row, ITEM_COLUMN, Integer.class, 0);
}
private int getQuantity(int row) {
return table.getValueAt(row, QUANTITY_COLUMN, Integer.class, 0);
}
}

View File

@ -0,0 +1,380 @@
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 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.JSpinner;
import javax.swing.SpinnerNumberModel;
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 MiscPanel 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 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;
private final JSpinner levelSpinner;
private Player player;
private GameVersion gameVersion;
private DlcOption customCGearSkin;
private DlcOption customDexSkin;
private DlcOption customMusical;
public MiscPanel(Entralinked entralinked) {
this.entralinked = entralinked;
setLayout(new MigLayout("align 50% 50%"));
// 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 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());
});
});
// 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%");
}
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());
}
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.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;
}
}

View File

@ -0,0 +1,119 @@
package entralinked.gui.panels;
import java.awt.Desktop;
import java.io.IOException;
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;
import entralinked.model.pkmn.PkmnInfo;
import entralinked.model.player.Player;
import entralinked.utility.SwingUtility;
import net.miginfocom.swing.MigLayout;
@SuppressWarnings("serial")
public class SummaryPanel extends JPanel {
public static final String KEY_NAME = "name";
public static final String KEY_OT = "trainer";
public static final String KEY_ID = "tid";
public static final String KEY_SID = "sid";
public static final String KEY_PID = "pid";
public static final String KEY_ABILITY = "ability";
public static final String KEY_NATURE = "nature";
public static final String KEY_GENDER = "gender";
public static final String KEY_LEVEL = "level";
public static final String KEY_ITEM = "item";
private static final String[] FLAVOR_TEXT = {
"*pokemon* is trying to grow berries in a nearby garden.",
"*pokemon* is itching to earn some Dream Points.",
"*pokemon* is looking at the Tree of Dreams from afar.",
"*pokemon* is playing a fun minigame (and you're not allowed to join, ha!)",
"*pokemon* is checking out the Friend Board.",
"*pokemon* is dreaming about when they first met *trainer*.",
"*pokemon* left a berry at the Tree of Dreams.",
"*pokemon* is trying to come up with more of these."
}; // TODO I think it would be fun to grab various bits of data from the save file and use them here
private final ShadowedSprite icon;
private final JLabel flavorTextLabel;
private final PropertyDisplay infoDisplay;
private Player player;
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
JPanel headerPanel = new JPanel(new MigLayout("fillx, insets 0"));
headerPanel.add(titleLabel, "wrap");
headerPanel.add(flavorTextLabel, "wrap");
headerPanel.add(subLabel, "wrap");
add(headerPanel, "spanx");
// Create icon label
icon = new ShadowedSprite();
add(icon, "gapright 4");
// Create property display
infoDisplay = new PropertyDisplay();
infoDisplay.addProperty(0, 0, KEY_NAME, "Name");
infoDisplay.addProperty(0, 1, KEY_OT, "OT");
infoDisplay.addProperty(0, 2, KEY_ID, "ID No.");
infoDisplay.addProperty(0, 3, KEY_SID, "SID No.", true);
infoDisplay.addProperty(0, 4, KEY_PID, "PID", true);
infoDisplay.addProperty(1, 0, KEY_ABILITY, "Ability");
infoDisplay.addProperty(1, 1, KEY_NATURE, "Nature");
infoDisplay.addProperty(1, 2, KEY_GENDER, "Gender");
infoDisplay.addProperty(1, 3, KEY_LEVEL, "Level");
infoDisplay.addProperty(1, 4, KEY_ITEM, "Held Item");
add(infoDisplay, "wrap");
// Create save file location button
JLabel openSaveButton = SwingUtility.createButtonLabel("Open data directory", () -> {
SwingUtility.showIgnorableHint(getRootPane(), "The game sends incomplete/corrupt save files to the server.\n"
+ "Please do not rely on Game Sync to back up your save files.", "Attention", JOptionPane.WARNING_MESSAGE);
try {
Desktop.getDesktop().open(player.getDataDirectory());
} catch(IOException e) {
SwingUtility.showExceptionInfo(getRootPane(), "Couldn't open data directory.", e);
}
});
add(openSaveButton, "spanx, align 100%, gapy 8");
}
public void loadProfile(Player player) {
this.player = player;
PkmnInfo info = player.getDreamerInfo();
icon.setImage(DataManager.getPokemonSprite(info.species(), info.form(), info.gender(), info.isShiny()));
infoDisplay.setValue(KEY_NAME, info.nickname());
infoDisplay.setValue(KEY_OT, info.trainerName());
infoDisplay.setValue(KEY_ID, "%05d".formatted(info.trainerId()));
infoDisplay.setValue(KEY_SID, "%05d".formatted(info.trainerSecretId()));
infoDisplay.setValue(KEY_PID, "%08X".formatted(info.personality()));
infoDisplay.setValue(KEY_ABILITY, DataManager.getAbilityName(info.ability()));
infoDisplay.setValue(KEY_NATURE, info.nature().getDisplayName());
infoDisplay.setValue(KEY_GENDER, info.gender().getDisplayName());
infoDisplay.setValue(KEY_LEVEL, info.level());
infoDisplay.setValue(KEY_ITEM, info.heldItem() == 0 ? "None" : DataManager.getItemName(info.heldItem()));
SwingUtility.setTextFieldToggle(infoDisplay.getValueField(KEY_SID), false);
SwingUtility.setTextFieldToggle(infoDisplay.getValueField(KEY_PID), false);
String flavorText = FLAVOR_TEXT[(int)(Math.random() * FLAVOR_TEXT.length)]
.replace("*pokemon*", info.nickname())
.replace("*trainer*", info.trainerName());
flavorTextLabel.setText(flavorText);
}
}

View File

@ -0,0 +1,251 @@
package entralinked.gui.panels;
import java.awt.Dimension;
import java.awt.Image;
import java.awt.image.BufferedImage;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
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;
import net.miginfocom.swing.MigLayout;
@SuppressWarnings("serial")
public abstract class TableEditorPanel extends JPanel {
protected final Map<Integer, Object> oldValues = new HashMap<>();
protected final int rowCount;
protected final ConfigTable table;
protected final TableModel model;
protected final JLabel titleLabel;
protected final JLabel descriptionLabel;
protected final ShadowedSprite selectionIcon;
protected JButton randomizeButton;
protected JButton clearAllButton;
protected JCheckBox legalModeToggle;
public TableEditorPanel(int rowCount, String[] columnNames, Class<?>[] columnTypes) {
this(rowCount, true, columnNames, columnTypes);
}
public TableEditorPanel(int rowCount, boolean incluceLegalMode, String[] columnNames, Class<?>[] columnTypes) {
if(columnNames.length != columnTypes.length) {
throw new IllegalArgumentException("Length mismatch between column names and column types");
}
this.rowCount = rowCount;
String[] columns = new String[columnNames.length + 1];
columns[0] = "No.";
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%)");
selectionIcon = new ShadowedSprite();
// Create buttons & toggles
randomizeButton = new JButton("Random");
randomizeButton.addActionListener(event -> randomizeSelections());
clearAllButton = new JButton("Clear all");
clearAllButton.addActionListener(event -> clearSelections());
legalModeToggle = new JCheckBox("Legal mode");
legalModeToggle.addActionListener(event -> {
boolean selected = legalModeToggle.isSelected();
// Since we're clearing illegal settings, ask for confirmation first.
if(selected && !SwingUtility.showIgnorableConfirmDialog(getRootPane(), "Enable legal mode? Illegal selections will be cleared.", "Attention")) {
legalModeToggle.setSelected(false);
return;
}
legalModeChanged();
});
// Create layout
setLayout(new MigLayout("align 50% 50%, gapy 0, insets 0 8 0 8"));
// Create header panels
JPanel labelPanel = new JPanel(new MigLayout("insets 0, fill"));
labelPanel.add(titleLabel, "wrap");
labelPanel.add(descriptionLabel, "wrap");
JPanel headerPanel = new JPanel(new MigLayout("insets n n 0 n", "[][grow]"));
headerPanel.add(labelPanel);
headerPanel.add(selectionIcon, "align 100%");
add(headerPanel, "wrap, grow");
// Create table
model = new DefaultTableModel(columns, rowCount);
model.addTableModelListener(event -> {
int column = event.getColumn();
if(column == 0) {
return; // Ignore number column
}
if(shouldUpdateSelectionIcon(column)) {
updateSelectionIcon();
}
for(int i = event.getFirstRow(); i <= event.getLastRow(); i++) {
int index = i * rowCount + column;
Object oldValue = oldValues.get(index);
Object newValue = model.getValueAt(i, column);
Class<?> type = columnTypes[column - 1];
// Set to null if types don't match
if(oldValue != null && !type.isAssignableFrom(oldValue.getClass())) {
oldValue = null;
}
if(newValue != null && !type.isAssignableFrom(newValue.getClass())) {
newValue = null;
}
// Fire data changed if new value is different from old value
if(newValue != oldValue) {
dataChanged(i, column, oldValue, newValue);
oldValues.put(index, model.getValueAt(i, column)); // We use getValueAt again because dataChanged might have updated the value
}
}
});
table = new ConfigTable();
table.putClientProperty("terminateEditOnFocusLost", Boolean.TRUE);
table.getTableHeader().setReorderingAllowed(false);
table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
table.setModel(model);
table.getSelectionModel().addListSelectionListener(event -> updateSelectionIcon());
int width = Math.max(table.getPreferredSize().width, table.getPreferredScrollableViewportSize().width);
table.setPreferredScrollableViewportSize(new Dimension(width, table.getRowHeight() * rowCount));
table.getColumnModel().getColumn(0).setResizable(false);
table.getColumnModel().getColumn(0).setMinWidth(32);
table.getColumnModel().getColumn(0).setMaxWidth(32);
add(new JScrollPane(table), "spanx, grow");
updateSelectionIcon();
// Initialize rows
for(int i = 0; i < rowCount; i++) {
model.setValueAt(i + 1, i, 0);
}
// Create footer panel
JPanel footerPanel = new JPanel(new MigLayout("insets 0", "0[]"));
footerPanel.add(randomizeButton);
footerPanel.add(clearAllButton);
if(incluceLegalMode) {
footerPanel.add(legalModeToggle);
}
add(footerPanel, "spanx, gapy 4");
}
public abstract void initializeOptions(int row);
public abstract void randomizeSelections(int row);
public abstract void clearSelections(int row);
public abstract Image getSelectionIcon(int row);
public void dataChanged(int row, int column, Object oldValue, Object newValue) {
// Override
}
public void legalModeChanged() {
// Override
}
public boolean shouldUpdateSelectionIcon(int column) {
return true; // Override
}
public int getSelectionIconScale() {
return 1;
}
public void initializeOptions() {
for(int i = 0; i < rowCount; i++) {
initializeOptions(i);
}
}
public void randomizeSelections() {
for(int i = 0; i < rowCount; i++) {
randomizeSelections(i);
}
}
public void clearSelections() {
for(int i = 0; i < rowCount; i++) {
clearSelections(i);
}
}
public void setTitle(String text) {
titleLabel.setText(text);
}
public void setDescription(String text) {
descriptionLabel.setText(text);
}
public boolean isLegalMode() {
return legalModeToggle.isSelected();
}
private void updateSelectionIcon() {
BufferedImage image = (BufferedImage)getSelectionIcon(table.getSelectedRow());
selectionIcon.setImage(image, getSelectionIconScale(), 96, 96);
}
protected <T> void setOptions(int row, int column, Collection<T> options) {
setOptions(row, column, options, null);
}
protected <T> void setOptions(int row, int column, Collection<T> options, Comparator<T> sorter) {
setOptions(row, column, options, sorter, false);
}
protected <T> void setOptions(int row, int column, Collection<T> options, boolean includeNullOption) {
setOptions(row, column, options, null, includeNullOption);
}
protected <T> void setOptions(int row, int column, Collection<T> options, Comparator<T> sorter, boolean includeNullOption) {
List<T> list = new ArrayList<>(options);
if(sorter != null) {
list.sort(sorter);
}
table.setOptionsAt(row, column, list, includeNullOption);
}
protected <T> List<T> computeSelectionList(Function<Integer, T> supplier, Function<Integer, Boolean> rowFilter) {
List<T> list = new ArrayList<>();
for(int i = 0; i < rowCount; i++) {
if(rowFilter.apply(i)) {
list.add(supplier.apply(i));
}
}
return list;
}
}

View File

@ -0,0 +1,272 @@
package entralinked.gui.panels;
import java.awt.Image;
import java.awt.Toolkit;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.stream.IntStream;
import java.util.stream.Stream;
import javax.swing.InputVerifier;
import javax.swing.JComponent;
import javax.swing.JTextField;
import entralinked.GameVersion;
import entralinked.gui.ImageLoader;
import entralinked.gui.InputVerifierCellEditor;
import entralinked.gui.data.Country;
import entralinked.gui.data.DataManager;
import entralinked.gui.data.PkmnSpecies;
import entralinked.gui.data.Region;
import entralinked.model.avenue.AvenueShopType;
import entralinked.model.avenue.AvenueVisitor;
import entralinked.model.avenue.AvenueVisitorType;
import entralinked.model.player.Player;
@SuppressWarnings("serial")
public class VisitorEditorPanel extends TableEditorPanel {
public static final int VISITOR_COUNT = 12;
public static final int TYPE_COLUMN = 1;
public static final int NAME_COLUMN = 2;
public static final int SHOP_COLUMN = 3;
public static final int GAME_COLUMN = 4;
public static final int COUNTRY_COLUMN = 5;
public static final int REGION_COLUMN = 6;
public static final int PHRASE_COLUMN = 7;
public static final int DREAMER_COLUMN = 8;
private static final String[] COLUMN_NAMES = { "Type", "Name", "Shop", "Game", "Country", "Region", "Phrase", "Pokémon" };
private static final Class<?>[] COLUMN_TYPES = { AvenueVisitorType.class, String.class, AvenueShopType.class, GameVersion.class, Country.class, Region.class, Integer.class, PkmnSpecies.class };
// Name tables for randomization
// TODO more languages would be neat but isn't really necessary
private static final String[] ENG_NAMES_M = { "Aleron", "Alpi", "Amando", "Anselmi", "Anton", "Arhippa", "Arpo", "Artos", "Assar", "Atilio", "Axel", "Azzo", "Jasper", "Jaylen", "Jephew", "Jimbo", "Joakim", "Joelle", "Jonas", "Jule", "Julian", "Julio", "Justan" };
private static final String[] ENG_NAMES_F = { "Agata", "Ainikki", "Alena", "Alibena", "Alwyn", "Anelma", "Anneli", "Annetta", "Antonie", "Armina", "Assunta", "Asta", "Jaclyn", "Jane", "Janette", "Jannis", "Jeanne", "Jenna", "Jess", "Joan", "Jocelyn", "Josie", "Judith", "Julie" };
private static final String[] JAP_NAMES_M = { "アートス", "アーポ", "アクセル", "アッサール", "アッツォ", "アッラン", "アティリオ", "アマンド", "アルピ", "アルヒッパ", "アンセルミ", "アントン", "ジェヒュー", "ジェリー", "ジェローム", "ジャコブ", "ジャスパー", "ジャレッド", "ジャン", "ジュール", "ジョエル", "ジョシュア", "ジョナス" };
private static final String[] JAP_NAMES_F = { "アーム", "アイニッキ", "アガタ", "アスタ", "アッスンタ", "アネルマ", "アラベッラ", "アリビーナ", "アレーナ", "アントニナ", "アンネッタ", "アンネリ", "ジェーン", "ジェシー", "ジェナ", "ジャサント", "ジャニス", "ジャンヌ", "ジュディス", "ジュリー", "ジョアン", "ジョイス", "ジョスリン", "ジョゼ" };
public VisitorEditorPanel() {
super(VISITOR_COUNT, false, COLUMN_NAMES, COLUMN_TYPES);
setTitle("Join Avenue");
setDescription("""
<html>
Use the table below to configure Join Avenue visitors.<br/>
Please be aware that new visitors will not appear if you have<br/>
already reached the max amount of concurrent visitors.
</html>
""");
table.setCellRenderers(TYPE_COLUMN, AvenueVisitorType.class, AvenueVisitorType::getDisplayName, "— — — — —");
table.setCellRenderers(SHOP_COLUMN, AvenueShopType.class, AvenueShopType::getDisplayName);
table.setCellRenderers(GAME_COLUMN, GameVersion.class, GameVersion::getDisplayName);
table.setCellRenderers(COUNTRY_COLUMN, Country.class, Country::name);
table.setCellRenderers(REGION_COLUMN, Region.class, Region::name);
table.setCellRenderers(DREAMER_COLUMN, PkmnSpecies.class, PkmnSpecies::name);
initializeOptions();
}
@Override
public void dataChanged(int row, int column, Object oldValue, Object newValue) {
if(column == TYPE_COLUMN) {
if(newValue != null) {
if(oldValue != null) {
return; // Do nothing if new and old values are both non-null
}
table.enableOption(row, SHOP_COLUMN);
table.enableOption(row, GAME_COLUMN, GameVersion.BLACK_2_ENGLISH); // Updates country -> region
table.enableOption(row, PHRASE_COLUMN);
table.enableOption(row, DREAMER_COLUMN);
table.enableOption(row, NAME_COLUMN, findRandomName(row));
model.setValueAt(findRandomName(row), row, NAME_COLUMN);
return;
}
disableSecondaryOptions(row);
} else if(column == GAME_COLUMN) {
if(newValue == null) {
table.disableOption(row, COUNTRY_COLUMN);
return;
}
GameVersion newVersion = (GameVersion)newValue;
boolean shouldUpdate = oldValue == null; // Update by default if previous value was null
// Check if language code changed from or to Japanese
if(oldValue != null) {
GameVersion oldVersion = (GameVersion)oldValue;
shouldUpdate |= (oldVersion.getLanguageCode() == 1 && newVersion.getLanguageCode() != 1)
|| (oldVersion.getLanguageCode() != 1 && newVersion.getLanguageCode() == 1);
}
if(shouldUpdate) {
// Japanese language visitors seem to only be able to appear if their set country is Japan
setOptions(row, COUNTRY_COLUMN, newVersion.getLanguageCode() == 1 ? Arrays.asList(DataManager.getCountry(105)) : DataManager.getCountries());
table.enableOption(row, COUNTRY_COLUMN);
}
} else if(column == COUNTRY_COLUMN) {
if(newValue == null) {
table.disableOption(row, REGION_COLUMN);
return;
}
Country country = (Country)newValue;
setOptions(row, REGION_COLUMN, country.hasRegions() ? country.regions() : Collections.emptyList(), (a, b) -> a.name().compareTo(b.name()));
table.enableOption(row, REGION_COLUMN);
}
}
@Override
public void initializeOptions(int row) {
setOptions(row, TYPE_COLUMN, Arrays.asList(AvenueVisitorType.values()), true);
setOptions(row, SHOP_COLUMN, Arrays.asList(AvenueShopType.values()));
setOptions(row, GAME_COLUMN, Arrays.asList(GameVersion.values()), (a, b) -> Integer.compare(a.getLanguageCode(), b.getLanguageCode()));
setOptions(row, PHRASE_COLUMN, IntStream.rangeClosed(0, 7).boxed().toList());
setOptions(row, DREAMER_COLUMN, DataManager.getSpecies(), (a, b) -> a.name().compareTo(b.name()));
InputVerifier nameVerifier = new InputVerifier() {
@Override
public boolean verify(JComponent input) {
String text = ((JTextField)input).getText();
String error = checkName(text, row);
if(error != null) {
// TODO show hint
Toolkit.getDefaultToolkit().beep();
return false;
}
return true;
}
};
table.setCellEditor(row, NAME_COLUMN, new InputVerifierCellEditor(nameVerifier));
disableSecondaryOptions(row);
}
@Override
public void randomizeSelections(int row) {
table.randomizeSelection(row, TYPE_COLUMN, false);
table.randomizeSelection(row, SHOP_COLUMN);
table.randomizeSelection(row, GAME_COLUMN);
table.randomizeSelection(row, COUNTRY_COLUMN);
table.randomizeSelection(row, REGION_COLUMN);
table.randomizeSelection(row, PHRASE_COLUMN);
table.randomizeSelection(row, DREAMER_COLUMN);
model.setValueAt(findRandomName(row), row, NAME_COLUMN);
}
@Override
public void clearSelections(int row) {
model.setValueAt(null, row, TYPE_COLUMN);
}
@Override
public Image getSelectionIcon(int row) {
String name = row == -1 || getType(row) == null ? "none" : getType(row).name().toLowerCase();
return ImageLoader.getImage("/sprites/trainers/%s.png".formatted(name));
}
public void loadProfile(Player player) {
table.clearSelection();
clearSelections();
int row = 0;
for(AvenueVisitor visitor : player.getAvenueVisitors()) {
model.setValueAt(visitor.type(), row, TYPE_COLUMN);
model.setValueAt(visitor.name(), row, NAME_COLUMN);
model.setValueAt(visitor.shopType(), row, SHOP_COLUMN);
model.setValueAt(visitor.gameVersion(), row, GAME_COLUMN);
Country country = DataManager.getCountry(visitor.countryCode());
model.setValueAt(country, row, COUNTRY_COLUMN);
if(country.hasRegions()) {
model.setValueAt(country.regions().get(visitor.stateProvinceCode() - 1), row, REGION_COLUMN);
}
model.setValueAt(visitor.personality(), row, PHRASE_COLUMN);
model.setValueAt(DataManager.getSpecies(visitor.dreamerSpecies()), row, DREAMER_COLUMN);
row++;
}
}
public void saveProfile(Player player) {
player.setAvenueVisitors(computeSelectionList(i -> new AvenueVisitor(
getName(i), getType(i), getShop(i), getGameVersion(i), getCountry(i).id(), getRegion(i), getPhrase(i), getDreamer(i).id()),
i -> getType(i) != null));
}
private String findRandomName(int row) {
boolean japanese = getGameVersion(row).getLanguageCode() == 1;
String[] table = null;
if(getType(row).isFemale()) {
table = japanese ? JAP_NAMES_F : ENG_NAMES_F;
} else {
table = japanese ? JAP_NAMES_M : ENG_NAMES_M;
}
List<String> existingNames = computeSelectionList(this::getName, i -> getType(row) != null);
List<String> options = Stream.of(table).filter(x -> !existingNames.contains(x)).toList();
return options.get((int)(Math.random() * options.size())); // Just make sure there are enough name options in the tables...
}
private String checkName(String name, int ignoreRow) {
if(name.isBlank()) {
return "Visitor name can't be blank.";
}
if(name.length() > 7) {
return "Visitor name can't exceed 7 characters.";
}
for(int i = 0; i < VISITOR_COUNT; i++) {
if(i != ignoreRow && name.equals(getName(i))) {
return "Visitors can't have the same name.";
}
}
return null;
}
private void disableSecondaryOptions(int row) {
table.disableOption(row, NAME_COLUMN);
table.disableOption(row, SHOP_COLUMN);
table.disableOption(row, GAME_COLUMN);
// table.disableOption(row, COUNTRY_COLUMN);
// table.disableOption(row, REGION_COLUMN);
table.disableOption(row, PHRASE_COLUMN);
table.disableOption(row, DREAMER_COLUMN);
}
private AvenueVisitorType getType(int row) {
return table.getValueAt(row, TYPE_COLUMN, AvenueVisitorType.class, null);
}
private String getName(int row) {
return table.getValueAt(row, NAME_COLUMN, String.class, "");
}
private AvenueShopType getShop(int row) {
return table.getValueAt(row, SHOP_COLUMN, AvenueShopType.class, null);
}
private GameVersion getGameVersion(int row) {
return table.getValueAt(row, GAME_COLUMN, GameVersion.class, null);
}
private Country getCountry(int row) {
return table.getValueAt(row, COUNTRY_COLUMN, Country.class, null);
}
private int getRegion(int row) {
Region region = table.getValueAt(row, REGION_COLUMN, Region.class, null);
return region == null ? 0 : region.id();
}
private int getPhrase(int row) {
return table.getValueAt(row, PHRASE_COLUMN, Integer.class, 0);
}
private PkmnSpecies getDreamer(int row) {
return table.getValueAt(row, DREAMER_COLUMN, PkmnSpecies.class, null);
}
}

View File

@ -1,4 +1,4 @@
package entralinked.gui;
package entralinked.gui.view;
import java.awt.BorderLayout;
import java.awt.Color;
@ -13,14 +13,15 @@ import java.net.URL;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import javax.swing.BorderFactory;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTabbedPane;
import javax.swing.JTextPane;
import javax.swing.UIManager;
import javax.swing.text.AttributeSet;
@ -34,8 +35,10 @@ import javax.swing.text.StyleContext;
import org.apache.logging.log4j.Level;
import com.formdev.flatlaf.intellijthemes.FlatOneDarkIJTheme;
import com.formdev.flatlaf.util.ColorFunctions;
import entralinked.Entralinked;
import entralinked.gui.panels.DashboardPanel;
import entralinked.utility.ConsumerAppender;
import entralinked.utility.SwingUtility;
@ -49,29 +52,21 @@ public class MainView {
public static final Color TEXT_COLOR_ERROR = Color.RED.darker();
private final StyleContext styleContext = StyleContext.getDefaultStyleContext();
private final AttributeSet fontAttribute = styleContext.addAttribute(SimpleAttributeSet.EMPTY, StyleConstants.FontFamily, "Consolas");
private final JButton dashboardButton;
private final JLabel statusLabel;
public MainView(Entralinked entralinked) {
// Set look and feel
FlatOneDarkIJTheme.setup();
UIManager.getDefaults().put("Component.focusedBorderColor", UIManager.get("Component.borderColor"));
// Create dashboard button
dashboardButton = new JButton("Open User Dashboard");
dashboardButton.setEnabled(false);
dashboardButton.setFocusable(false);
dashboardButton.addActionListener(event -> {
openUrl("http://127.0.0.1/dashboard/profile.html");
});
UIManager.put("Table.alternateRowColor", ColorFunctions.lighten(UIManager.getColor("Table.background"), 0.05F));
// Create status label
statusLabel = new JLabel("Entralinked is starting...", JLabel.CENTER);
statusLabel = new JLabel("Servers are starting, please wait a bit...", JLabel.CENTER);
// Create footer panel
JPanel footerPanel = new JPanel(new BorderLayout());
footerPanel.add(statusLabel, BorderLayout.CENTER);
footerPanel.add(dashboardButton, BorderLayout.LINE_END);
footerPanel.setBorder(BorderFactory.createEmptyBorder(2, 0, 5, 0));
// Create console output
JTextPane consoleOutputPane = new JTextPane() {
@ -85,7 +80,7 @@ public class MainView {
return getUI().getPreferredSize(this);
};
};
consoleOutputPane.setFont(new Font("Consola", Font.PLAIN, 12));
consoleOutputPane.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12));
consoleOutputPane.setEditable(false);
((DefaultCaret)consoleOutputPane.getCaret()).setUpdatePolicy(DefaultCaret.ALWAYS_UPDATE);
@ -109,7 +104,12 @@ public class MainView {
// Create main panel
JPanel panel = new JPanel(new BorderLayout());
panel.add(scrollPane, BorderLayout.CENTER);
panel.add(footerPanel, BorderLayout.PAGE_END);
panel.add(footerPanel, BorderLayout.PAGE_END);
// Create tabbed pane
JTabbedPane tabbedPane = new JTabbedPane();
tabbedPane.addTab("Console", panel);
tabbedPane.addTab("Dashboard", new DashboardPanel(entralinked));
// Create window
JFrame frame = new JFrame("Entralinked");
@ -118,7 +118,13 @@ public class MainView {
JMenuBar menuBar = new JMenuBar();
JMenu helpMenu = new JMenu("Help");
helpMenu.add(SwingUtility.createAction("Update PID (Error 60000)", () -> new PidToolDialog(entralinked, frame)));
helpMenu.add(SwingUtility.createAction("GitHub", () -> openUrl("https://github.com/kuroppoi/entralinked")));
helpMenu.add(SwingUtility.createAction("GitHub", () -> {
try {
Desktop.getDesktop().browse(new URL("https://github.com/kuroppoi/entralinked").toURI());
} catch(IOException | URISyntaxException e) {
SwingUtility.showExceptionInfo(frame, "Failed to open URL.", e);
}
}));
menuBar.add(helpMenu);
// Set window properties
@ -126,8 +132,7 @@ public class MainView {
@Override
public void windowClosing(WindowEvent event) {
// Update status
dashboardButton.setEnabled(false);
statusLabel.setText("Entralinked is shutting down ...");
statusLabel.setText("Servers are shutting down, please wait a bit...");
// Run asynchronously so it doesn't just awkwardly freeze
// Still scuffed but better than nothing I guess
@ -142,31 +147,16 @@ public class MainView {
new ImageIcon(getClass().getResource("/icon-32x.png")).getImage(),
new ImageIcon(getClass().getResource("/icon-16x.png")).getImage()));
frame.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
frame.setMinimumSize(new Dimension(512, 288));
frame.setJMenuBar(menuBar);
frame.add(panel);
frame.add(tabbedPane);
frame.getContentPane().setPreferredSize(new Dimension(733, 463));
frame.pack();
frame.setMinimumSize(frame.getSize());
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
public void setDashboardButtonEnabled(boolean enabled) {
dashboardButton.setEnabled(enabled);
}
public void setStatusLabelText(String text) {
statusLabel.setText(text);
}
private void openUrl(String url) {
Desktop desktop = Desktop.getDesktop();
if(desktop != null && desktop.isSupported(Desktop.Action.BROWSE)) {
try {
desktop.browse(new URL(url).toURI());
} catch(IOException | URISyntaxException e) {
e.printStackTrace();
}
}
}
}

View File

@ -1,4 +1,4 @@
package entralinked.gui;
package entralinked.gui.view;
import java.awt.GridBagLayout;
import java.util.regex.Pattern;
@ -60,7 +60,8 @@ public class PidToolDialog {
// Make sure user exists
if(user == null) {
JOptionPane.showMessageDialog(dialog, "This Wi-Fi Connection ID does not exist.", "Attention", JOptionPane.WARNING_MESSAGE);
JOptionPane.showMessageDialog(dialog, "This Wi-Fi Connection ID does not exist.\n"
+ "If you haven't attempted to connect yet, please do that first.", "Attention", JOptionPane.WARNING_MESSAGE);
return;
}

View File

@ -5,11 +5,21 @@ import com.fasterxml.jackson.annotation.JsonEnumDefaultValue;
public enum AvenueShopType {
@JsonEnumDefaultValue
RAFFLE,
FLORIST,
SALON,
ANTIQUE,
DOJO,
CAFE,
MARKET
RAFFLE("Raffle Shop"),
FLORIST("Flower Shop"),
SALON("Beauty Salon"),
ANTIQUE("Antique Shop"),
DOJO("Dojo"),
CAFE("Café"),
MARKET("Market");
private final String displayName;
private AvenueShopType(String displayName) {
this.displayName = displayName;
}
public String getDisplayName() {
return displayName;
}
}

View File

@ -6,47 +6,53 @@ public enum AvenueVisitorType {
// 0
@JsonEnumDefaultValue
YOUNGSTER(0),
LASS(0, true),
YOUNGSTER("Youngster", 0),
LASS("Lass", 0, true),
// 1
ACE_TRAINER_MALE(1),
ACE_TRAINER_FEMALE(1, true),
ACE_TRAINER_MALE("Ace Trainer♂", 1),
ACE_TRAINER_FEMALE("Ace Trainer♀", 1, true),
// 2
RANGER_MALE(2),
RANGER_FEMALE(2, true),
RANGER_MALE("Pokémon Ranger♂", 2),
RANGER_FEMALE("Pokémon Ranger♀", 2, true),
// 3
BREEDER_MALE(3),
BREEDER_FEMALE(3, true),
BREEDER_MALE("Pokémon Breeder♂", 3),
BREEDER_FEMALE("Pokémon Breeder♀", 3, true),
// 4
SCIENTIST_MALE(4),
SCIENTIST_FEMALE(4, true),
SCIENTIST_MALE("Scientist♂", 4),
SCIENTIST_FEMALE("Scientist♀", 4, true),
// 5
HIKER(5),
PARASOL_LADY(5, true),
HIKER("Hiker♂", 5),
PARASOL_LADY("Parasol Lady", 5, true),
// 6
ROUGHNECK(6),
NURSE(6, true),
ROUGHNECK("Roughneck", 6),
NURSE("Nurse", 6, true),
// 7
PRESCHOOLER_MALE(7),
PRESCHOOLER_FEMALE(7, true);
PRESCHOOLER_MALE("Preschooler♂", 7),
PRESCHOOLER_FEMALE("Preschooler♀", 7, true);
private final String displayName;
private final int clientId;
private final boolean female;
private AvenueVisitorType(int clientId, boolean female) {
private AvenueVisitorType(String displayName, int clientId, boolean female) {
this.displayName = displayName;
this.clientId = clientId;
this.female = female;
}
private AvenueVisitorType(int clientId) {
this(clientId, false);
private AvenueVisitorType(String displayName, int clientId) {
this(displayName, clientId, false);
}
public String getDisplayName() {
return displayName;
}
public int getClientId() {

View File

@ -5,7 +5,17 @@ import com.fasterxml.jackson.annotation.JsonEnumDefaultValue;
public enum PkmnGender {
@JsonEnumDefaultValue
MALE,
FEMALE,
GENDERLESS;
MALE("Male"),
FEMALE("Female"),
GENDERLESS("Genderless");
private final String displayName;
private PkmnGender(String displayName) {
this.displayName = displayName;
}
public String getDisplayName() {
return displayName;
}
}

View File

@ -5,33 +5,44 @@ import com.fasterxml.jackson.annotation.JsonEnumDefaultValue;
public enum PkmnNature {
@JsonEnumDefaultValue
HARDY,
LONELY,
BRAVE,
ADAMANT,
NAUGHTY,
BOLD,
DOCILE,
RELAXED,
IMPISH,
LAX,
TIMID,
HASTY,
SERIOUS,
JOLLY,
NAIVE,
MODEST,
MILD,
QUIET,
BASHFUL,
RASH,
CALM,
GENTLE,
SASSY,
CAREFUL,
QUIRKY;
HARDY("Hardy"),
LONELY("Lonely"),
BRAVE("Brave"),
ADAMANT("Adamant"),
NAUGHTY("Naughty"),
BOLD("Bold"),
DOCILE("Docile"),
RELAXED("Relaxed"),
IMPISH("Impish"),
LAX("Lax"),
TIMID("Timid"),
HASTY("Hasty"),
SERIOUS("Serious"),
JOLLY("Jolly"),
NAIVE("Naive"),
MODEST("Modest"),
MILD("Mild"),
QUIET("Quiet"),
BASHFUL("Bashful"),
RASH("Rash"),
CALM("Calm"),
GENTLE("Gentle"),
SASSY("Sassy"),
CAREFUL("Careful"),
QUIRKY("Quirky");
private final String displayName;
private PkmnNature(String displayName) {
this.displayName = displayName;
}
public static PkmnNature valueOf(int index) {
return index >= 0 && index < values().length ? values()[index] : null;
}
public String getDisplayName() {
return displayName;
}
}

View File

@ -8,44 +8,54 @@ public enum DreamAnimation {
* Look around, but stay in the same position.
*/
@JsonEnumDefaultValue
LOOK_AROUND,
LOOK_AROUND("Look around"),
/**
* Walk around, but never change direction without moving a step in that direction.
*/
WALK_AROUND,
WALK_AROUND("Walk around"),
/**
* Walk around and occasionally change direction without moving.
*/
WALK_LOOK_AROUND,
WALK_LOOK_AROUND("Walk and look around"),
/**
* Only walk up and down.
*/
WALK_VERTICALLY,
WALK_VERTICALLY("Walk up and down"),
/**
* Only walk left and right.
*/
WALK_HORIZONTALLY,
WALK_HORIZONTALLY("Walk left and right"),
/**
* Only walk left and right, and occasionally change direction without moving.
*/
WALK_LOOK_HORIZONTALLY,
WALK_LOOK_HORIZONTALLY("Walk left and right and look around"),
/**
* Continuously spin right.
*/
SPIN_RIGHT,
SPIN_RIGHT("Spin right"),
/**
* Continuously spin left.
*/
SPIN_LEFT;
SPIN_LEFT("Spin left");
private final String displayName;
private DreamAnimation(String displayName) {
this.displayName = displayName;
}
public static DreamAnimation valueOf(int index) {
return index >= 0 && index < values().length ? values()[index] : null;
}
public String getDisplayName() {
return displayName;
}
}

View File

@ -25,6 +25,7 @@ public class Player {
private String musical;
private String customCGearSkin;
private String customDexSkin;
private String customMusical;
private File dataDirectory;
public Player(String gameSyncId) {
@ -152,6 +153,14 @@ public class Player {
return customDexSkin;
}
public void setCustomMusical(String customMusical) {
this.customMusical = customMusical;
}
public String getCustomMusical() {
return customMusical;
}
// IO stuff
public void setDataDirectory(File dataDirectory) {
@ -177,4 +186,8 @@ public class Player {
public File getDexSkinFile() {
return new File(dataDirectory, "zukan.bin");
}
public File getMusicalFile() {
return new File(dataDirectory, "musical.bin");
}
}

View File

@ -23,16 +23,17 @@ public record PlayerDto(
String musical,
String customCGearSkin,
String customDexSkin,
String customMusical,
int levelsGained,
@JsonDeserialize(contentAs = DreamEncounter.class) Collection<DreamEncounter> encounters,
@JsonDeserialize(contentAs = DreamItem.class) Collection<DreamItem> items,
@JsonDeserialize(contentAs = AvenueVisitor.class) Collection<AvenueVisitor> avenueVisitors) {
public PlayerDto(Player player) {
this(player.getGameSyncId(), player.getGameVersion(), player.getStatus(), player.getDreamerInfo(),
this(player.getGameSyncId(), player.getGameVersion(), player.getStatus(), player.getDreamerInfo(),
player.getCGearSkin(), player.getDexSkin(), player.getMusical(), player.getCustomCGearSkin(),
player.getCustomDexSkin(), player.getLevelsGained(), player.getEncounters(), player.getItems(),
player.getAvenueVisitors());
player.getCustomDexSkin(), player.getCustomMusical(), player.getLevelsGained(), player.getEncounters(),
player.getItems(), player.getAvenueVisitors());
}
/**
@ -48,6 +49,7 @@ public record PlayerDto(
player.setMusical(musical);
player.setCustomCGearSkin(customCGearSkin);
player.setCustomDexSkin(customDexSkin);
player.setCustomMusical(customMusical);
player.setLevelsGained(levelsGained);
player.setEncounters(encounters == null ? Collections.emptyList() : encounters);
player.setItems(items == null ? Collections.emptyList() : items);

View File

@ -19,7 +19,6 @@ import javax.imageio.ImageIO;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.bouncycastle.util.Arrays;
import com.fasterxml.jackson.databind.ObjectMapper;
@ -35,7 +34,6 @@ import entralinked.model.player.Player;
import entralinked.model.player.PlayerManager;
import entralinked.model.player.PlayerStatus;
import entralinked.network.http.HttpHandler;
import entralinked.utility.ColorUtility;
import entralinked.utility.Crc16;
import entralinked.utility.GsidUtility;
import entralinked.utility.LEOutputStream;
@ -49,6 +47,8 @@ import io.javalin.json.JavalinJackson;
/**
* HTTP handler for requests made to the user dashboard.
*
* @deprecated
*/
public class DashboardHandler implements HttpHandler {
@ -310,29 +310,8 @@ public class DashboardHandler implements HttpHandler {
previewImage = TiledImageUtility.readCGearSkin(new ByteArrayInputStream(skinBytes), offsetIndices);
break;
case "ZUKAN":
// Generate some background colors based roughly on what's in the image
int[] backgroundColors = Arrays.copyOf(TiledImageUtility.DEFAULT_DEX_BACKGROUND_COLORS, 64);
int backgroundColor = image.getRGB(0, 0);
int dexColor = image.getRGB(0, 134);
int buttonColor = image.getRGB(128, 134);
// Background colors (58, 59, 60)
for(int i = 0; i < 3; i++) {
backgroundColors[i + 58] = ColorUtility.multiplyColor(backgroundColor, 0.7 + i * 0.15);
}
// Pokédex colors (48, 49, 50, 51, 52)
for(int i = 0; i < 5; i++) {
backgroundColors[i + 48] = ColorUtility.multiplyColor(dexColor, 1.15 - i * 0.15);
}
// Pokédex button colors (53, 54, 55, 56)
for(int i = 0; i < 4; i++) {
backgroundColors[i + 53] = ColorUtility.multiplyColor(buttonColor, 0.55 + i * 0.15);
}
// Process skin data
TiledImageUtility.writeDexSkin(byteOutputStream, image, backgroundColors);
TiledImageUtility.writeDexSkin(byteOutputStream, image, TiledImageUtility.generateBackgroundColors(image));
skinBytes = byteOutputStream.toByteArray();
previewImage = TiledImageUtility.readDexSkin(new ByteArrayInputStream(skinBytes), true);
break;

View File

@ -8,6 +8,7 @@ import entralinked.model.player.DreamEncounter;
import entralinked.model.player.DreamItem;
import entralinked.model.player.Player;
@Deprecated
public record DashboardProfileMessage(
String gameVersion,
String dreamerSprite,

View File

@ -9,6 +9,7 @@ import entralinked.model.avenue.AvenueVisitor;
import entralinked.model.player.DreamEncounter;
import entralinked.model.player.DreamItem;
@Deprecated
public record DashboardProfileUpdateRequest(
@JsonProperty(required = true) @JsonDeserialize(contentAs = DreamEncounter.class) List<DreamEncounter> encounters,
@JsonProperty(required = true) @JsonDeserialize(contentAs = DreamItem.class) List<DreamItem> items,

View File

@ -1,5 +1,6 @@
package entralinked.network.http.dashboard;
@Deprecated
public record DashboardStatusMessage(String message, boolean error) {
public DashboardStatusMessage(String message) {

View File

@ -220,8 +220,10 @@ public class PglHandler implements HttpHandler {
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)) {
@ -243,6 +245,17 @@ public class PglHandler implements HttpHandler {
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.
@ -264,7 +277,7 @@ public class PglHandler implements HttpHandler {
// Write misc stuff and DLC information
outputStream.writeShort(player.getLevelsGained());
outputStream.write(0); // Unknown
outputStream.write(dlcList.getDlcIndex("IRAO", "MUSICAL", player.getMusical()));
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
@ -329,7 +342,7 @@ public class PglHandler implements HttpHandler {
outputStream.writeInt(1); // [20] Ignores if 0
outputStream.write(visitor.countryCode());
outputStream.write(visitor.stateProvinceCode());
outputStream.write(0); // [26] Ignores if 1
outputStream.write(visitor.gameVersion().getLanguageCode()); // 99% sure this is the lang code because 1 seems to be ignored ONLY if country code isn't Japan (plus it's right above the rom code)
outputStream.write(visitor.gameVersion().getRomCode()); // Affects shop stock
outputStream.write(visitor.type().isFemale() ? 1 : 0);
outputStream.write(0); // [29] Does.. something

View File

@ -1,14 +1,41 @@
package entralinked.utility;
import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.GraphicsEnvironment;
import java.awt.GridBagConstraints;
import java.awt.event.ActionEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.HashSet;
import java.util.Set;
import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.BorderFactory;
import javax.swing.Icon;
import javax.swing.JCheckBox;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.JTextField;
import javax.swing.JToggleButton;
import javax.swing.SwingUtilities;
import com.formdev.flatlaf.FlatClientProperties;
import net.miginfocom.swing.MigLayout;
public class SwingUtility {
private static final Set<String> ignoredMessages = new HashSet<>();
@SuppressWarnings("serial")
public static Action createAction(String name, Icon icon, Runnable handler) {
AbstractAction action = new AbstractAction(name, icon) {
@ -55,4 +82,113 @@ public class SwingUtility {
constraints.ipady = paddingY;
return constraints;
}
public static void setTextFieldToggle(JTextField textField, boolean selected) {
for(Component component : textField.getComponents()) {
if(component instanceof JToggleButton) {
JToggleButton button = (JToggleButton)component;
if(button.isSelected() != selected) {
button.doClick();
}
}
}
}
// TODO find a good unicode font maybe?
public static Font findSupportingFont(String text, Font def) {
GraphicsEnvironment graphicsEnvironment = GraphicsEnvironment.getLocalGraphicsEnvironment();
for(Font font : graphicsEnvironment.getAllFonts()) {
if(font.canDisplayUpTo(text) == -1) {
return new Font(font.getName(), def.getStyle(), def.getSize());
}
}
return def;
}
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");
label.addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent event) {
if(SwingUtilities.isLeftMouseButton(event)) {
actionHandler.run();
}
}
});
return label;
}
public static void showIgnorableHint(Component parentComponent, String message, String title, int messageType) {
synchronized(ignoredMessages) {
// Do nothing if message has been ignored
if(ignoredMessages.contains(message)) {
return;
}
// Create ignore checkbox
JCheckBox checkBox = new JCheckBox("Don't show this again");
JOptionPane.showMessageDialog(parentComponent, createIgnorableDialogPanel(message, checkBox), title, messageType);
// Add to ignore list if checkbox is selected
if(checkBox.isSelected()) {
ignoredMessages.add(message);
}
}
}
public static boolean showIgnorableConfirmDialog(Component parentComponent, String message, String title) {
synchronized(ignoredMessages) {
// Do nothing if message has been ignored
if(ignoredMessages.contains(message)) {
return true;
}
JCheckBox checkBox = new JCheckBox("Don't ask this again");
int result = JOptionPane.showConfirmDialog(parentComponent, createIgnorableDialogPanel(message, checkBox), title, JOptionPane.YES_NO_OPTION);
// Add to ignore list if checkbox is selected
if(checkBox.isSelected()) {
ignoredMessages.add(message);
}
return result == JOptionPane.YES_OPTION;
}
}
private static JPanel createIgnorableDialogPanel(String message, JCheckBox checkBox) {
JPanel panel = new JPanel(new MigLayout("insets 0"));
panel.add(new JLabel("<html>%s</html>".formatted(message.replace("\n", "<br/>"))), "wrap"); // TODO no idea how JOptionPane does line breaks
panel.add(checkBox, "gapy 8");
return panel;
}
public static void showExceptionInfo(Component parentComponent, String message, Throwable throwable) {
// Create stacktrace string
StringWriter writer = new StringWriter();
throwable.printStackTrace(new PrintWriter(writer));
// Create text area
JTextArea area = new JTextArea(writer.toString());
area.setBorder(BorderFactory.createEmptyBorder(8, 8, 0, 0));
area.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12));
area.setEditable(false);
// Create scroll pane
int height = Math.min(200, area.getFontMetrics(area.getFont()).getHeight() * area.getLineCount() + 10);
JScrollPane scrollPane = new JScrollPane(area);
scrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS);
scrollPane.setPreferredSize(new Dimension(600, height));
scrollPane.setMaximumSize(scrollPane.getPreferredSize());
// Create dialog
String label = String.format("<html><b>%s</b><br>Exception details:<br><br></html>", message);
JPanel panel = new JPanel(new BorderLayout());
panel.add(new JLabel(label), BorderLayout.PAGE_START);
panel.add(scrollPane);
JOptionPane.showMessageDialog(parentComponent, panel, "An error has occured", JOptionPane.ERROR_MESSAGE);
}
}

View File

@ -4,6 +4,7 @@ import java.awt.image.BufferedImage;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Arrays;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
@ -363,6 +364,33 @@ public class TiledImageUtility {
}
}
/**
* Generates Pokédex skin background colors for the specified image.
*/
public static int[] generateBackgroundColors(BufferedImage image) {
int[] backgroundColors = Arrays.copyOf(DEFAULT_DEX_BACKGROUND_COLORS, 64);
int backgroundColor = image.getRGB(0, 0);
int dexColor = image.getRGB(0, 134);
int buttonColor = image.getRGB(128, 134);
// Background colors (58, 59, 60)
for(int i = 0; i < 3; i++) {
backgroundColors[i + 58] = ColorUtility.multiplyColor(backgroundColor, 0.7 + i * 0.15);
}
// Pokédex colors (48, 49, 50, 51, 52)
for(int i = 0; i < 5; i++) {
backgroundColors[i + 48] = ColorUtility.multiplyColor(dexColor, 1.15 - i * 0.15);
}
// Pokédex button colors (53, 54, 55, 56)
for(int i = 0; i < 4; i++) {
backgroundColors[i + 53] = ColorUtility.multiplyColor(buttonColor, 0.55 + i * 0.15);
}
return backgroundColors;
}
/**
* @return The index of the element in the array, or {@code -1} if no such element exists.
*/

View File

@ -266,6 +266,30 @@
<option value="WHITE_ENGLISH">White Version</option>
<option value="BLACK_2_ENGLISH">Black Version 2</option>
<option value="WHITE_2_ENGLISH">White Version 2</option>
<option value="BLACK_FRENCH">Version Noire</option>
<option value="WHITE_FRENCH">Version Blanche</option>
<option value="BLACK_2_FRENCH">Version Noire 2</option>
<option value="WHITE_2_FRENCH">Version Blanche 2</option>
<option value="BLACK_ITALIAN">Versione Nera</option>
<option value="WHITE_ITALIAN">Versione Bianca</option>
<option value="BLACK_2_ITALIAN">Versione Nera 2</option>
<option value="WHITE_2_ITALIAN">Versione Bianca 2</option>
<option value="BLACK_GERMAN">Schwarze Edition</option>
<option value="WHITE_GERMAN">Weisse Edition</option>
<option value="BLACK_2_GERMAN">Schwarze Edition 2</option>
<option value="WHITE_2_GERMAN">Weisse Edition 2</option>
<option value="BLACK_SPANISH">Edicion Negra</option>
<option value="WHITE_SPANISH">Edicion Blanca</option>
<option value="BLACK_2_SPANISH">Edicion Negra 2</option>
<option value="WHITE_2_SPANISH">Edicion Blanca 2</option>
<option value="BLACK_JAPANESE">ブラック</option>
<option value="WHITE_JAPANESE">ホワイト</option>
<option value="BLACK_2_JAPANESE">ブラック2</option>
<option value="WHITE_2_JAPANESE">ホワイト2</option>
<option value="BLACK_KOREAN">블랙</option>
<option value="WHITE_KOREAN">화이트</option>
<option value="BLACK_2_KOREAN">블랙2</option>
<option value="WHITE_2_KOREAN">화이트2</option>
</select>
<label for="visitor-form-region">Country</label>
<select id="visitor-form-region" name="region" value="1">

View File

@ -0,0 +1,166 @@
{
"1": "Stench",
"2": "Drizzle",
"3": "Speed Boost",
"4": "Battle Armor",
"5": "Sturdy",
"6": "Damp",
"7": "Limber",
"8": "Sand Veil",
"9": "Static",
"10": "Volt Absorb",
"11": "Water Absorb",
"12": "Oblivious",
"13": "Cloud Nine",
"14": "Compound Eyes",
"15": "Insomnia",
"16": "Color Change",
"17": "Immunity",
"18": "Flash Fire",
"19": "Shield Dust",
"20": "Own Tempo",
"21": "Suction Cups",
"22": "Intimidate",
"23": "Shadow Tag",
"24": "Rough Skin",
"25": "Wonder Guard",
"26": "Levitate",
"27": "Effect Spore",
"28": "Synchronize",
"29": "Clear Body",
"30": "Natural Cure",
"31": "Lightning Rod",
"32": "Serene Grace",
"33": "Swift Swim",
"34": "Chlorophyll",
"35": "Illuminate",
"36": "Trace",
"37": "Huge Power",
"38": "Poison Point",
"39": "Inner Focus",
"40": "Magma Armor",
"41": "Water Veil",
"42": "Magnet Pull",
"43": "Soundproof",
"44": "Rain Dish",
"45": "Sand Stream",
"46": "Pressure",
"47": "Thick Fat",
"48": "Early Bird",
"49": "Flame Body",
"50": "Run Away",
"51": "Keen Eye",
"52": "Hyper Cutter",
"53": "Pickup",
"54": "Truant",
"55": "Hustle",
"56": "Cute Charm",
"57": "Plus",
"58": "Minus",
"59": "Forecast",
"60": "Sticky Hold",
"61": "Shed Skin",
"62": "Guts",
"63": "Marvel Scale",
"64": "Liquid Ooze",
"65": "Overgrow",
"66": "Blaze",
"67": "Torrent",
"68": "Swarm",
"69": "Rock Head",
"70": "Drought",
"71": "Arena Trap",
"72": "Vital Spirit",
"73": "White Smoke",
"74": "Pure Power",
"75": "Shell Armor",
"76": "Air Lock",
"77": "Tangled Feet",
"78": "Motor Drive",
"79": "Rivalry",
"80": "Steadfast",
"81": "Snow Cloak",
"82": "Gluttony",
"83": "Anger Point",
"84": "Unburden",
"85": "Heatproof",
"86": "Simple",
"87": "Dry Skin",
"88": "Download",
"89": "Iron Fist",
"90": "Poison Heal",
"91": "Adaptability",
"92": "Skill Link",
"93": "Hydration",
"94": "Solar Power",
"95": "Quick Feet",
"96": "Normalize",
"97": "Sniper",
"98": "Magic Guard",
"99": "No Guard",
"100": "Stall",
"101": "Technician",
"102": "Leaf Guard",
"103": "Klutz",
"104": "Mold Breaker",
"105": "Super Luck",
"106": "Aftermath",
"107": "Anticipation",
"108": "Forewarn",
"109": "Unaware",
"110": "Tinted Lens",
"111": "Filter",
"112": "Slow Start",
"113": "Scrappy",
"114": "Storm Drain",
"115": "Ice Body",
"116": "Solid Rock",
"117": "Snow Warning",
"118": "Honey Gather",
"119": "Frisk",
"120": "Reckless",
"121": "Multitype",
"122": "Flower Gift",
"123": "Bad Dreams",
"124": "Pickpocket",
"125": "Sheer Force",
"126": "Contrary",
"127": "Unnerve",
"128": "Defiant",
"129": "Defeatist",
"130": "Cursed Body",
"131": "Healer",
"132": "Friend Guard",
"133": "Weak Armor",
"134": "Heavy Metal",
"135": "Light Metal",
"136": "Multiscale",
"137": "Toxic Boost",
"138": "Flare Boost",
"139": "Harvest",
"140": "Telepathy",
"141": "Moody",
"142": "Overcoat",
"143": "Poison Touch",
"144": "Regenerator",
"145": "Big Pecks",
"146": "Sand Rush",
"147": "Wonder Skin",
"148": "Analytic",
"149": "Illusion",
"150": "Imposter",
"151": "Infiltrator",
"152": "Mummy",
"153": "Moxie",
"154": "Justified",
"155": "Rattled",
"156": "Magic Bounce",
"157": "Sap Sipper",
"158": "Prankster",
"159": "Sand Force",
"160": "Iron Barbs",
"161": "Zen Mode",
"162": "Victory Star",
"163": "Turboblaze",
"164": "Teravolt"
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,621 @@
{
"1": "Master Ball",
"2": "Ultra Ball",
"3": "Great Ball",
"4": "Poké Ball",
"5": "Safari Ball",
"6": "Net Ball",
"7": "Dive Ball",
"8": "Nest Ball",
"9": "Repeat Ball",
"10": "Timer Ball",
"11": "Luxury Ball",
"12": "Premier Ball",
"13": "Dusk Ball",
"14": "Heal Ball",
"15": "Quick Ball",
"16": "Cherish Ball",
"17": "Potion",
"18": "Antidote",
"19": "Burn Heal",
"20": "Ice Heal",
"21": "Awakening",
"22": "Paralyze Heal",
"23": "Full Restore",
"24": "Max Potion",
"25": "Hyper Potion",
"26": "Super Potion",
"27": "Full Heal",
"28": "Revive",
"29": "Max Revive",
"30": "Fresh Water",
"31": "Soda Pop",
"32": "Lemonade",
"33": "Moomoo Milk",
"34": "Energy Powder",
"35": "Energy Root",
"36": "Heal Powder",
"37": "Revival Herb",
"38": "Ether",
"39": "Max Ether",
"40": "Elixir",
"41": "Max Elixir",
"42": "Lava Cookie",
"43": "Berry Juice",
"44": "Sacred Ash",
"45": "HP Up",
"46": "Protein",
"47": "Iron",
"48": "Carbos",
"49": "Calcium",
"50": "Rare Candy",
"51": "PP Up",
"52": "Zinc",
"53": "PP Max",
"54": "Old Gateau",
"55": "Guard Spec.",
"56": "Dire Hit",
"57": "X Attack",
"58": "X Defense",
"59": "X Speed",
"60": "X Accuracy",
"61": "X Sp. Atk",
"62": "X Sp. Def",
"63": "Poké Doll",
"64": "Fluffy Tail",
"65": "Blue Flute",
"66": "Yellow Flute",
"67": "Red Flute",
"68": "Black Flute",
"69": "White Flute",
"70": "Shoal Salt",
"71": "Shoal Shell",
"72": "Red Shard",
"73": "Blue Shard",
"74": "Yellow Shard",
"75": "Green Shard",
"76": "Super Repel",
"77": "Max Repel",
"78": "Escape Rope",
"79": "Repel",
"80": "Sun Stone",
"81": "Moon Stone",
"82": "Fire Stone",
"83": "Thunder Stone",
"84": "Water Stone",
"85": "Leaf Stone",
"86": "Tiny Mushroom",
"87": "Big Mushroom",
"88": "Pearl",
"89": "Big Pearl",
"90": "Stardust",
"91": "Star Piece",
"92": "Nugget",
"93": "Heart Scale",
"94": "Honey",
"95": "Growth Mulch",
"96": "Damp Mulch",
"97": "Stable Mulch",
"98": "Gooey Mulch",
"99": "Root Fossil",
"100": "Claw Fossil",
"101": "Helix Fossil",
"102": "Dome Fossil",
"103": "Old Amber",
"104": "Armor Fossil",
"105": "Skull Fossil",
"106": "Rare Bone",
"107": "Shiny Stone",
"108": "Dusk Stone",
"109": "Dawn Stone",
"110": "Oval Stone",
"111": "Odd Keystone",
"112": "Griseous Orb",
"116": "Douse Drive",
"117": "Shock Drive",
"118": "Burn Drive",
"119": "Chill Drive",
"134": "Sweet Heart",
"135": "Adamant Orb",
"136": "Lustrous Orb",
"137": "Greet Mail",
"138": "Favored Mail",
"139": "RSVP Mail",
"140": "Thanks Mail",
"141": "Inquiry Mail",
"142": "Like Mail",
"143": "Reply Mail",
"144": "Bridge Mail S",
"145": "Bridge Mail D",
"146": "Bridge Mail T",
"147": "Bridge Mail V",
"148": "Bridge Mail M",
"149": "Cheri Berry",
"150": "Chesto Berry",
"151": "Pecha Berry",
"152": "Rawst Berry",
"153": "Aspear Berry",
"154": "Leppa Berry",
"155": "Oran Berry",
"156": "Persim Berry",
"157": "Lum Berry",
"158": "Sitrus Berry",
"159": "Figy Berry",
"160": "Wiki Berry",
"161": "Mago Berry",
"162": "Aguav Berry",
"163": "Iapapa Berry",
"164": "Razz Berry",
"165": "Bluk Berry",
"166": "Nanab Berry",
"167": "Wepear Berry",
"168": "Pinap Berry",
"169": "Pomeg Berry",
"170": "Kelpsy Berry",
"171": "Qualot Berry",
"172": "Hondew Berry",
"173": "Grepa Berry",
"174": "Tamato Berry",
"175": "Cornn Berry",
"176": "Magost Berry",
"177": "Rabuta Berry",
"178": "Nomel Berry",
"179": "Spelon Berry",
"180": "Pamtre Berry",
"181": "Watmel Berry",
"182": "Durin Berry",
"183": "Belue Berry",
"184": "Occa Berry",
"185": "Passho Berry",
"186": "Wacan Berry",
"187": "Rindo Berry",
"188": "Yache Berry",
"189": "Chople Berry",
"190": "Kebia Berry",
"191": "Shuca Berry",
"192": "Coba Berry",
"193": "Payapa Berry",
"194": "Tanga Berry",
"195": "Charti Berry",
"196": "Kasib Berry",
"197": "Haban Berry",
"198": "Colbur Berry",
"199": "Babiri Berry",
"200": "Chilan Berry",
"201": "Liechi Berry",
"202": "Ganlon Berry",
"203": "Salac Berry",
"204": "Petaya Berry",
"205": "Apicot Berry",
"206": "Lansat Berry",
"207": "Starf Berry",
"208": "Enigma Berry",
"209": "Micle Berry",
"210": "Custap Berry",
"211": "Jaboca Berry",
"212": "Rowap Berry",
"213": "Bright Powder",
"214": "White Herb",
"215": "Macho Brace",
"216": "Exp. Share",
"217": "Quick Claw",
"218": "Soothe Bell",
"219": "Mental Herb",
"220": "Choice Band",
"221": "King's Rock",
"222": "Silver Powder",
"223": "Amulet Coin",
"224": "Cleanse Tag",
"225": "Soul Dew",
"226": "Deep Sea Tooth",
"227": "Deep Sea Scale",
"228": "Smoke Ball",
"229": "Everstone",
"230": "Focus Band",
"231": "Lucky Egg",
"232": "Scope Lens",
"233": "Metal Coat",
"234": "Leftovers",
"235": "Dragon Scale",
"236": "Light Ball",
"237": "Soft Sand",
"238": "Hard Stone",
"239": "Miracle Seed",
"240": "Black Glasses",
"241": "Black Belt",
"242": "Magnet",
"243": "Mystic Water",
"244": "Sharp Beak",
"245": "Poison Barb",
"246": "Never-Melt Ice",
"247": "Spell Tag",
"248": "Twisted Spoon",
"249": "Charcoal",
"250": "Dragon Fang",
"251": "Silk Scarf",
"252": "Up-Grade",
"253": "Shell Bell",
"254": "Sea Incense",
"255": "Lax Incense",
"256": "Lucky Punch",
"257": "Metal Powder",
"258": "Thick Club",
"259": "Stick",
"260": "Red Scarf",
"261": "Blue Scarf",
"262": "Pink Scarf",
"263": "Green Scarf",
"264": "Yellow Scarf",
"265": "Wide Lens",
"266": "Muscle Band",
"267": "Wise Glasses",
"268": "Expert Belt",
"269": "Light Clay",
"270": "Life Orb",
"271": "Power Herb",
"272": "Toxic Orb",
"273": "Flame Orb",
"274": "Quick Powder",
"275": "Focus Sash",
"276": "Zoom Lens",
"277": "Metronome",
"278": "Iron Ball",
"279": "Lagging Tail",
"280": "Destiny Knot",
"281": "Black Sludge",
"282": "Icy Rock",
"283": "Smooth Rock",
"284": "Heat Rock",
"285": "Damp Rock",
"286": "Grip Claw",
"287": "Choice Scarf",
"288": "Sticky Barb",
"289": "Power Bracer",
"290": "Power Belt",
"291": "Power Lens",
"292": "Power Band",
"293": "Power Anklet",
"294": "Power Weight",
"295": "Shed Shell",
"296": "Big Root",
"297": "Choice Specs",
"298": "Flame Plate",
"299": "Splash Plate",
"300": "Zap Plate",
"301": "Meadow Plate",
"302": "Icicle Plate",
"303": "Fist Plate",
"304": "Toxic Plate",
"305": "Earth Plate",
"306": "Sky Plate",
"307": "Mind Plate",
"308": "Insect Plate",
"309": "Stone Plate",
"310": "Spooky Plate",
"311": "Draco Plate",
"312": "Dread Plate",
"313": "Iron Plate",
"314": "Odd Incense",
"315": "Rock Incense",
"316": "Full Incense",
"317": "Wave Incense",
"318": "Rose Incense",
"319": "Luck Incense",
"320": "Pure Incense",
"321": "Protector",
"322": "Electirizer",
"323": "Magmarizer",
"324": "Dubious Disc",
"325": "Reaper Cloth",
"326": "Razor Claw",
"327": "Razor Fang",
"328": "TM01",
"329": "TM02",
"330": "TM03",
"331": "TM04",
"332": "TM05",
"333": "TM06",
"334": "TM07",
"335": "TM08",
"336": "TM09",
"337": "TM10",
"338": "TM11",
"339": "TM12",
"340": "TM13",
"341": "TM14",
"342": "TM15",
"343": "TM16",
"344": "TM17",
"345": "TM18",
"346": "TM19",
"347": "TM20",
"348": "TM21",
"349": "TM22",
"350": "TM23",
"351": "TM24",
"352": "TM25",
"353": "TM26",
"354": "TM27",
"355": "TM28",
"356": "TM29",
"357": "TM30",
"358": "TM31",
"359": "TM32",
"360": "TM33",
"361": "TM34",
"362": "TM35",
"363": "TM36",
"364": "TM37",
"365": "TM38",
"366": "TM39",
"367": "TM40",
"368": "TM41",
"369": "TM42",
"370": "TM43",
"371": "TM44",
"372": "TM45",
"373": "TM46",
"374": "TM47",
"375": "TM48",
"376": "TM49",
"377": "TM50",
"378": "TM51",
"379": "TM52",
"380": "TM53",
"381": "TM54",
"382": "TM55",
"383": "TM56",
"384": "TM57",
"385": "TM58",
"386": "TM59",
"387": "TM60",
"388": "TM61",
"389": "TM62",
"390": "TM63",
"391": "TM64",
"392": "TM65",
"393": "TM66",
"394": "TM67",
"395": "TM68",
"396": "TM69",
"397": "TM70",
"398": "TM71",
"399": "TM72",
"400": "TM73",
"401": "TM74",
"402": "TM75",
"403": "TM76",
"404": "TM77",
"405": "TM78",
"406": "TM79",
"407": "TM80",
"408": "TM81",
"409": "TM82",
"410": "TM83",
"411": "TM84",
"412": "TM85",
"413": "TM86",
"414": "TM87",
"415": "TM88",
"416": "TM89",
"417": "TM90",
"418": "TM91",
"419": "TM92",
"420": "HM01",
"421": "HM02",
"422": "HM03",
"423": "HM04",
"424": "HM05",
"425": "HM06",
"428": "Explorer Kit",
"429": "Loot Sack",
"430": "Rule Book",
"431": "Poké Radar",
"432": "Point Card",
"433": "Journal",
"434": "Seal Case",
"435": "Fashion Case",
"436": "Seal Bag",
"437": "Pal Pad",
"438": "Works Key",
"439": "Old Charm",
"440": "Galactic Key",
"441": "Red Chain",
"442": "Town Map",
"443": "Vs. Seeker",
"444": "Coin Case",
"445": "Old Rod",
"446": "Good Rod",
"447": "Super Rod",
"448": "Sprayduck",
"449": "Poffin Case",
"450": "Bike",
"451": "Suite Key",
"452": "Oak's Letter",
"453": "Lunar Wing",
"454": "Member Card",
"455": "Azure Flute",
"456": "S.S. Ticket",
"457": "Contest Pass",
"458": "Magma Stone",
"459": "Parcel",
"460": "Coupon 1",
"461": "Coupon 2",
"462": "Coupon 3",
"463": "Storage Key",
"464": "Secret Potion",
"465": "Vs. Recorder",
"466": "Gracidea",
"467": "Secret Key",
"468": "Apricorn Box",
"469": "Unown Report",
"470": "Berry Pots",
"471": "Dowsing Machine",
"472": "Blue Card",
"473": "Slowpoke Tail",
"474": "Clear Bell",
"475": "Card Key",
"476": "Basement Key",
"477": "Squirt Bottle",
"478": "Red Scale",
"479": "Lost Item",
"480": "Pass",
"481": "Machine Part",
"482": "Silver Wing",
"483": "Rainbow Wing",
"484": "Mystery Egg",
"485": "Red Apricorn",
"486": "Blue Apricorn",
"487": "Yellow Apricorn",
"488": "Green Apricorn",
"489": "Pink Apricorn",
"490": "White Apricorn",
"491": "Black Apricorn",
"492": "Fast Ball",
"493": "Level Ball",
"494": "Lure Ball",
"495": "Heavy Ball",
"496": "Love Ball",
"497": "Friend Ball",
"498": "Moon Ball",
"499": "Sport Ball",
"500": "Park Ball",
"501": "Photo Album",
"502": "GB Sounds",
"503": "Tidal Bell",
"504": "RageCandyBar",
"505": "Data Card 01",
"506": "Data Card 02",
"507": "Data Card 03",
"508": "Data Card 04",
"509": "Data Card 05",
"510": "Data Card 06",
"511": "Data Card 07",
"512": "Data Card 08",
"513": "Data Card 09",
"514": "Data Card 10",
"515": "Data Card 11",
"516": "Data Card 12",
"517": "Data Card 13",
"518": "Data Card 14",
"519": "Data Card 15",
"520": "Data Card 16",
"521": "Data Card 17",
"522": "Data Card 18",
"523": "Data Card 19",
"524": "Data Card 20",
"525": "Data Card 21",
"526": "Data Card 22",
"527": "Data Card 23",
"528": "Data Card 24",
"529": "Data Card 25",
"530": "Data Card 26",
"531": "Data Card 27",
"532": "Jade Orb",
"533": "Lock Capsule",
"534": "Red Orb",
"535": "Blue Orb",
"536": "Enigma Stone",
"537": "Prism Scale",
"538": "Eviolite",
"539": "Float Stone",
"540": "Rocky Helmet",
"541": "Air Balloon",
"542": "Red Card",
"543": "Ring Target",
"544": "Binding Band",
"545": "Absorb Bulb",
"546": "Cell Battery",
"547": "Eject Button",
"548": "Fire Gem",
"549": "Water Gem",
"550": "Electric Gem",
"551": "Grass Gem",
"552": "Ice Gem",
"553": "Fighting Gem",
"554": "Poison Gem",
"555": "Ground Gem",
"556": "Flying Gem",
"557": "Psychic Gem",
"558": "Bug Gem",
"559": "Rock Gem",
"560": "Ghost Gem",
"561": "Dragon Gem",
"562": "Dark Gem",
"563": "Steel Gem",
"564": "Normal Gem",
"565": "Health Wing",
"566": "Muscle Wing",
"567": "Resist Wing",
"568": "Genius Wing",
"569": "Clever Wing",
"570": "Swift Wing",
"571": "Pretty Wing",
"572": "Cover Fossil",
"573": "Plume Fossil",
"574": "Liberty Pass",
"575": "Pass Orb",
"576": "Dream Ball",
"577": "Poké Toy",
"578": "Prop Case",
"579": "Dragon Skull",
"580": "Balm Mushroom",
"581": "Big Nugget",
"582": "Pearl String",
"583": "Comet Shard",
"584": "Relic Copper",
"585": "Relic Silver",
"586": "Relic Gold",
"587": "Relic Vase",
"588": "Relic Band",
"589": "Relic Statue",
"590": "Relic Crown",
"591": "Casteliacone",
"592": "Dire Hit 2",
"593": "X Speed 2",
"594": "X Sp. Atk 2",
"595": "X Sp. Def 2",
"596": "X Defense 2",
"597": "X Attack 2",
"598": "X Accuracy 2",
"599": "X Speed 3",
"600": "X Sp. Atk 3",
"601": "X Sp. Def 3",
"602": "X Defense 3",
"603": "X Attack 3",
"604": "X Accuracy 3",
"605": "X Speed 6",
"606": "X Sp. Atk 6",
"607": "X Sp. Def 6",
"608": "X Defense 6",
"609": "X Attack 6",
"610": "X Accuracy 6",
"611": "Ability Urge",
"612": "Item Drop",
"613": "Item Urge",
"614": "Reset Urge",
"615": "Dire Hit 3",
"616": "Light Stone",
"617": "Dark Stone",
"618": "TM93",
"619": "TM94",
"620": "TM95",
"621": "Xtransceiver",
"622": "God Stone",
"623": "Gram 1",
"624": "Gram 2",
"625": "Gram 3",
"626": "Xtransceiver",
"627": "Medal Box",
"628": "DNA Splicers",
"629": "DNA Splicers",
"630": "Permit",
"631": "Oval Charm",
"632": "Shiny Charm",
"633": "Plasma Card",
"634": "Grubby Hanky",
"635": "Colress Machine",
"636": "Dropped Item",
"637": "Dropped Item",
"638": "Reveal Glass"
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,561 @@
{
"1": "Pound",
"2": "Karate Chop",
"3": "Double Slap",
"4": "Comet Punch",
"5": "Mega Punch",
"6": "Pay Day",
"7": "Fire Punch",
"8": "Ice Punch",
"9": "Thunder Punch",
"10": "Scratch",
"11": "Vice Grip",
"12": "Guillotine",
"13": "Razor Wind",
"14": "Swords Dance",
"15": "Cut",
"16": "Gust",
"17": "Wing Attack",
"18": "Whirlwind",
"19": "Fly",
"20": "Bind",
"21": "Slam",
"22": "Vine Whip",
"23": "Stomp",
"24": "Double Kick",
"25": "Mega Kick",
"26": "Jump Kick",
"27": "Rolling Kick",
"28": "Sand Attack",
"29": "Headbutt",
"30": "Horn Attack",
"31": "Fury Attack",
"32": "Horn Drill",
"33": "Tackle",
"34": "Body Slam",
"35": "Wrap",
"36": "Take Down",
"37": "Thrash",
"38": "Double-Edge",
"39": "Tail Whip",
"40": "Poison Sting",
"41": "Twineedle",
"42": "Pin Missile",
"43": "Leer",
"44": "Bite",
"45": "Growl",
"46": "Roar",
"47": "Sing",
"48": "Supersonic",
"49": "Sonic Boom",
"50": "Disable",
"51": "Acid",
"52": "Ember",
"53": "Flamethrower",
"54": "Mist",
"55": "Water Gun",
"56": "Hydro Pump",
"57": "Surf",
"58": "Ice Beam",
"59": "Blizzard",
"60": "Psybeam",
"61": "Bubble Beam",
"62": "Aurora Beam",
"63": "Hyper Beam",
"64": "Peck",
"65": "Drill Peck",
"66": "Submission",
"67": "Low Kick",
"68": "Counter",
"69": "Seismic Toss",
"70": "Strength",
"71": "Absorb",
"72": "Mega Drain",
"73": "Leech Seed",
"74": "Growth",
"75": "Razor Leaf",
"76": "Solar Beam",
"77": "Poison Powder",
"78": "Stun Spore",
"79": "Sleep Powder",
"80": "Petal Dance",
"81": "String Shot",
"82": "Dragon Rage",
"83": "Fire Spin",
"84": "Thunder Shock",
"85": "Thunderbolt",
"86": "Thunder Wave",
"87": "Thunder",
"88": "Rock Throw",
"89": "Earthquake",
"90": "Fissure",
"91": "Dig",
"92": "Toxic",
"93": "Confusion",
"94": "Psychic",
"95": "Hypnosis",
"96": "Meditate",
"97": "Agility",
"98": "Quick Attack",
"99": "Rage",
"100": "Teleport",
"101": "Night Shade",
"102": "Mimic",
"103": "Screech",
"104": "Double Team",
"105": "Recover",
"106": "Harden",
"107": "Minimize",
"108": "Smokescreen",
"109": "Confuse Ray",
"110": "Withdraw",
"111": "Defense Curl",
"112": "Barrier",
"113": "Light Screen",
"114": "Haze",
"115": "Reflect",
"116": "Focus Energy",
"117": "Bide",
"118": "Metronome",
"119": "Mirror Move",
"120": "Self-Destruct",
"121": "Egg Bomb",
"122": "Lick",
"123": "Smog",
"124": "Sludge",
"125": "Bone Club",
"126": "Fire Blast",
"127": "Waterfall",
"128": "Clamp",
"129": "Swift",
"130": "Skull Bash",
"131": "Spike Cannon",
"132": "Constrict",
"133": "Amnesia",
"134": "Kinesis",
"135": "Soft-Boiled",
"136": "High Jump Kick",
"137": "Glare",
"138": "Dream Eater",
"139": "Poison Gas",
"140": "Barrage",
"141": "Leech Life",
"142": "Lovely Kiss",
"143": "Sky Attack",
"144": "Transform",
"145": "Bubble",
"146": "Dizzy Punch",
"147": "Spore",
"148": "Flash",
"149": "Psywave",
"150": "Splash",
"151": "Acid Armor",
"152": "Crabhammer",
"153": "Explosion",
"154": "Fury Swipes",
"155": "Bonemerang",
"156": "Rest",
"157": "Rock Slide",
"158": "Hyper Fang",
"159": "Sharpen",
"160": "Conversion",
"161": "Tri Attack",
"162": "Super Fang",
"163": "Slash",
"164": "Substitute",
"165": "Struggle",
"166": "Sketch",
"167": "Triple Kick",
"168": "Thief",
"169": "Spider Web",
"170": "Mind Reader",
"171": "Nightmare",
"172": "Flame Wheel",
"173": "Snore",
"174": "Curse",
"175": "Flail",
"176": "Conversion 2",
"177": "Aeroblast",
"178": "Cotton Spore",
"179": "Reversal",
"180": "Spite",
"181": "Powder Snow",
"182": "Protect",
"183": "Mach Punch",
"184": "Scary Face",
"185": "Feint Attack",
"186": "Sweet Kiss",
"187": "Belly Drum",
"188": "Sludge Bomb",
"189": "Mud-Slap",
"190": "Octazooka",
"191": "Spikes",
"192": "Zap Cannon",
"193": "Foresight",
"194": "Destiny Bond",
"195": "Perish Song",
"196": "Icy Wind",
"197": "Detect",
"198": "Bone Rush",
"199": "Lock-On",
"200": "Outrage",
"201": "Sandstorm",
"202": "Giga Drain",
"203": "Endure",
"204": "Charm",
"205": "Rollout",
"206": "False Swipe",
"207": "Swagger",
"208": "Milk Drink",
"209": "Spark",
"210": "Fury Cutter",
"211": "Steel Wing",
"212": "Mean Look",
"213": "Attract",
"214": "Sleep Talk",
"215": "Heal Bell",
"216": "Return",
"217": "Present",
"218": "Frustration",
"219": "Safeguard",
"220": "Pain Split",
"221": "Sacred Fire",
"222": "Magnitude",
"223": "Dynamic Punch",
"224": "Megahorn",
"225": "Dragon Breath",
"226": "Baton Pass",
"227": "Encore",
"228": "Pursuit",
"229": "Rapid Spin",
"230": "Sweet Scent",
"231": "Iron Tail",
"232": "Metal Claw",
"233": "Vital Throw",
"234": "Morning Sun",
"235": "Synthesis",
"236": "Moonlight",
"237": "Hidden Power",
"238": "Cross Chop",
"239": "Twister",
"240": "Rain Dance",
"241": "Sunny Day",
"242": "Crunch",
"243": "Mirror Coat",
"244": "Psych Up",
"245": "Extreme Speed",
"246": "Ancient Power",
"247": "Shadow Ball",
"248": "Future Sight",
"249": "Rock Smash",
"250": "Whirlpool",
"251": "Beat Up",
"252": "Fake Out",
"253": "Uproar",
"254": "Stockpile",
"255": "Spit Up",
"256": "Swallow",
"257": "Heat Wave",
"258": "Hail",
"259": "Torment",
"260": "Flatter",
"261": "Will-O-Wisp",
"262": "Memento",
"263": "Facade",
"264": "Focus Punch",
"265": "Smelling Salts",
"266": "Follow Me",
"267": "Nature Power",
"268": "Charge",
"269": "Taunt",
"270": "Helping Hand",
"271": "Trick",
"272": "Role Play",
"273": "Wish",
"274": "Assist",
"275": "Ingrain",
"276": "Superpower",
"277": "Magic Coat",
"278": "Recycle",
"279": "Revenge",
"280": "Brick Break",
"281": "Yawn",
"282": "Knock Off",
"283": "Endeavor",
"284": "Eruption",
"285": "Skill Swap",
"286": "Imprison",
"287": "Refresh",
"288": "Grudge",
"289": "Snatch",
"290": "Secret Power",
"291": "Dive",
"292": "Arm Thrust",
"293": "Camouflage",
"294": "Tail Glow",
"295": "Luster Purge",
"296": "Mist Ball",
"297": "Feather Dance",
"298": "Teeter Dance",
"299": "Blaze Kick",
"300": "Mud Sport",
"301": "Ice Ball",
"302": "Needle Arm",
"303": "Slack Off",
"304": "Hyper Voice",
"305": "Poison Fang",
"306": "Crush Claw",
"307": "Blast Burn",
"308": "Hydro Cannon",
"309": "Meteor Mash",
"310": "Astonish",
"311": "Weather Ball",
"312": "Aromatherapy",
"313": "Fake Tears",
"314": "Air Cutter",
"315": "Overheat",
"316": "Odor Sleuth",
"317": "Rock Tomb",
"318": "Silver Wind",
"319": "Metal Sound",
"320": "Grass Whistle",
"321": "Tickle",
"322": "Cosmic Power",
"323": "Water Spout",
"324": "Signal Beam",
"325": "Shadow Punch",
"326": "Extrasensory",
"327": "Sky Uppercut",
"328": "Sand Tomb",
"329": "Sheer Cold",
"330": "Muddy Water",
"331": "Bullet Seed",
"332": "Aerial Ace",
"333": "Icicle Spear",
"334": "Iron Defense",
"335": "Block",
"336": "Howl",
"337": "Dragon Claw",
"338": "Frenzy Plant",
"339": "Bulk Up",
"340": "Bounce",
"341": "Mud Shot",
"342": "Poison Tail",
"343": "Covet",
"344": "Volt Tackle",
"345": "Magical Leaf",
"346": "Water Sport",
"347": "Calm Mind",
"348": "Leaf Blade",
"349": "Dragon Dance",
"350": "Rock Blast",
"351": "Shock Wave",
"352": "Water Pulse",
"353": "Doom Desire",
"354": "Psycho Boost",
"355": "Roost",
"356": "Gravity",
"357": "Miracle Eye",
"358": "Wake-Up Slap",
"359": "Hammer Arm",
"360": "Gyro Ball",
"361": "Healing Wish",
"362": "Brine",
"363": "Natural Gift",
"364": "Feint",
"365": "Pluck",
"366": "Tailwind",
"367": "Acupressure",
"368": "Metal Burst",
"369": "U-turn",
"370": "Close Combat",
"371": "Payback",
"372": "Assurance",
"373": "Embargo",
"374": "Fling",
"375": "Psycho Shift",
"376": "Trump Card",
"377": "Heal Block",
"378": "Wring Out",
"379": "Power Trick",
"380": "Gastro Acid",
"381": "Lucky Chant",
"382": "Me First",
"383": "Copycat",
"384": "Power Swap",
"385": "Guard Swap",
"386": "Punishment",
"387": "Last Resort",
"388": "Worry Seed",
"389": "Sucker Punch",
"390": "Toxic Spikes",
"391": "Heart Swap",
"392": "Aqua Ring",
"393": "Magnet Rise",
"394": "Flare Blitz",
"395": "Force Palm",
"396": "Aura Sphere",
"397": "Rock Polish",
"398": "Poison Jab",
"399": "Dark Pulse",
"400": "Night Slash",
"401": "Aqua Tail",
"402": "Seed Bomb",
"403": "Air Slash",
"404": "X-Scissor",
"405": "Bug Buzz",
"406": "Dragon Pulse",
"407": "Dragon Rush",
"408": "Power Gem",
"409": "Drain Punch",
"410": "Vacuum Wave",
"411": "Focus Blast",
"412": "Energy Ball",
"413": "Brave Bird",
"414": "Earth Power",
"415": "Switcheroo",
"416": "Giga Impact",
"417": "Nasty Plot",
"418": "Bullet Punch",
"419": "Avalanche",
"420": "Ice Shard",
"421": "Shadow Claw",
"422": "Thunder Fang",
"423": "Ice Fang",
"424": "Fire Fang",
"425": "Shadow Sneak",
"426": "Mud Bomb",
"427": "Psycho Cut",
"428": "Zen Headbutt",
"429": "Mirror Shot",
"430": "Flash Cannon",
"431": "Rock Climb",
"432": "Defog",
"433": "Trick Room",
"434": "Draco Meteor",
"435": "Discharge",
"436": "Lava Plume",
"437": "Leaf Storm",
"438": "Power Whip",
"439": "Rock Wrecker",
"440": "Cross Poison",
"441": "Gunk Shot",
"442": "Iron Head",
"443": "Magnet Bomb",
"444": "Stone Edge",
"445": "Captivate",
"446": "Stealth Rock",
"447": "Grass Knot",
"448": "Chatter",
"449": "Judgment",
"450": "Bug Bite",
"451": "Charge Beam",
"452": "Wood Hammer",
"453": "Aqua Jet",
"454": "Attack Order",
"455": "Defend Order",
"456": "Heal Order",
"457": "Head Smash",
"458": "Double Hit",
"459": "Roar of Time",
"460": "Spacial Rend",
"461": "Lunar Dance",
"462": "Crush Grip",
"463": "Magma Storm",
"464": "Dark Void",
"465": "Seed Flare",
"466": "Ominous Wind",
"467": "Shadow Force",
"468": "Hone Claws",
"469": "Wide Guard",
"470": "Guard Split",
"471": "Power Split",
"472": "Wonder Room",
"473": "Psyshock",
"474": "Venoshock",
"475": "Autotomize",
"476": "Rage Powder",
"477": "Telekinesis",
"478": "Magic Room",
"479": "Smack Down",
"480": "Storm Throw",
"481": "Flame Burst",
"482": "Sludge Wave",
"483": "Quiver Dance",
"484": "Heavy Slam",
"485": "Synchronoise",
"486": "Electro Ball",
"487": "Soak",
"488": "Flame Charge",
"489": "Coil",
"490": "Low Sweep",
"491": "Acid Spray",
"492": "Foul Play",
"493": "Simple Beam",
"494": "Entrainment",
"495": "After You",
"496": "Round",
"497": "Echoed Voice",
"498": "Chip Away",
"499": "Clear Smog",
"500": "Stored Power",
"501": "Quick Guard",
"502": "Ally Switch",
"503": "Scald",
"504": "Shell Smash",
"505": "Heal Pulse",
"506": "Hex",
"507": "Sky Drop",
"508": "Shift Gear",
"509": "Circle Throw",
"510": "Incinerate",
"511": "Quash",
"512": "Acrobatics",
"513": "Reflect Type",
"514": "Retaliate",
"515": "Final Gambit",
"516": "Bestow",
"517": "Inferno",
"518": "Water Pledge",
"519": "Fire Pledge",
"520": "Grass Pledge",
"521": "Volt Switch",
"522": "Struggle Bug",
"523": "Bulldoze",
"524": "Frost Breath",
"525": "Dragon Tail",
"526": "Work Up",
"527": "Electroweb",
"528": "Wild Charge",
"529": "Drill Run",
"530": "Dual Chop",
"531": "Heart Stamp",
"532": "Horn Leech",
"533": "Sacred Sword",
"534": "Razor Shell",
"535": "Heat Crash",
"536": "Leaf Tornado",
"537": "Steamroller",
"538": "Cotton Guard",
"539": "Night Daze",
"540": "Psystrike",
"541": "Tail Slap",
"542": "Hurricane",
"543": "Head Charge",
"544": "Gear Grind",
"545": "Searing Shot",
"546": "Techno Blast",
"547": "Relic Song",
"548": "Secret Sword",
"549": "Glaciate",
"550": "Bolt Strike",
"551": "Blue Flare",
"552": "Fiery Dance",
"553": "Freeze Shock",
"554": "Ice Burn",
"555": "Snarl",
"556": "Icicle Crash",
"557": "V-create",
"558": "Fusion Flare",
"559": "Fusion Bolt"
}

File diff suppressed because it is too large Load Diff