Implement zx0 compression

Compress data tables with the ZX0 compression algorithm
This commit is contained in:
Philippe Symons 2025-04-24 21:14:48 +02:00
parent ba8738fc4b
commit 532a095d77
38 changed files with 7255 additions and 6159 deletions

12
Dockerfile Normal file
View File

@ -0,0 +1,12 @@
FROM devkitpro/devkitarm
LABEL author="Poke Transporter GB"
USER root
ARG USER_ID
ARG GROUP_ID
ENV DEBIAN_FRONTEND="noninteractive"
RUN apt update && apt install -y build-essential

425
Makefile
View File

@ -1,204 +1,221 @@
# Build configuration (set to either 'debug' or 'release')
BUILD_TYPE := debug
#---------------------------------------------------------------------------------
.SUFFIXES:
#---------------------------------------------------------------------------------
ifeq ($(strip $(DEVKITARM)),)
$(error "Please set DEVKITARM in your environment. export DEVKITARM=<path to>devkitARM")
endif
include $(DEVKITARM)/gba_rules
#---------------------------------------------------------------------------------
# the LIBGBA path is defined in gba_rules, but we have to define LIBTONC ourselves
#---------------------------------------------------------------------------------
LIBTONC := $(DEVKITPRO)/libtonc
#---------------------------------------------------------------------------------
# TARGET is the name of the output
# BUILD is the directory where object files & intermediate files will be placed
# SOURCES is a list of directories containing source code
# INCLUDES is a list of directories containing extra header files
# DATA is a list of directories containing binary data
# GRAPHICS is a list of directories containing files to be processed by grit
#
# All directories are specified relative to the project directory where
# the makefile is found
#
#---------------------------------------------------------------------------------
TARGET := $(notdir $(CURDIR))_mb
BUILD := build
SOURCES := source
INCLUDES := include
DATA :=
MUSIC := audio
GRAPHICS := graphics
#---------------------------------------------------------------------------------
# options for code generation
#---------------------------------------------------------------------------------
ARCH := -mthumb -mthumb-interwork
CFLAGS := -g -Wall -O2\
-mcpu=arm7tdmi -mtune=arm7tdmi -masm-syntax-unified\
$(ARCH)
CFLAGS += $(INCLUDE) -ffunction-sections -fdata-sections -Os -Wall -mthumb -mcpu=arm7tdmi -mtune=arm7tdmi
CXXFLAGS := $(CFLAGS) -g0 -fno-rtti -fno-exceptions -fdata-sections -ffunction-sections -std=c++20 -Wno-volatile -D_GLIBCXX_USE_CXX20_ABI=0
ifeq ($(BUILD_TYPE), debug)
CFLAGS += -g -DDEBUG
CXXFLAGS += -g -DDEBUG
else ifeq ($(BUILD_TYPE), release)
endif
ASFLAGS := -g $(ARCH)
LDFLAGS = -Os -g $(ARCH) -Wl,-Map,$(notdir $*.map) -Wl,--gc-sections -mthumb -mcpu=arm7tdmi -mtune=arm7tdmi -Wl,-Map,output.map,--cref -nodefaultlibs
CFLAGS += -flto
LDFLAGS += -flto
#---------------------------------------------------------------------------------
# any extra libraries we wish to link with the project
#---------------------------------------------------------------------------------
LIBS := -lmm -ltonc -lgba -lc -lgcc -lsysbase
#---------------------------------------------------------------------------------
# list of directories containing libraries, this must be the top level containing
# include and lib.
# the LIBGBA path should remain in this list if you want to use maxmod
#---------------------------------------------------------------------------------
LIBDIRS := $(LIBGBA) $(LIBTONC)
#---------------------------------------------------------------------------------
# no real need to edit anything past this point unless you need to add additional
# rules for different file extensions
#---------------------------------------------------------------------------------
ifneq ($(BUILD),$(notdir $(CURDIR)))
#---------------------------------------------------------------------------------
export OUTPUT := $(CURDIR)/$(TARGET)
export VPATH := $(foreach dir,$(SOURCES),$(CURDIR)/$(dir)) \
$(foreach dir,$(DATA),$(CURDIR)/$(dir)) \
$(foreach dir,$(GRAPHICS),$(CURDIR)/$(dir))
export DEPSDIR := $(CURDIR)/$(BUILD)
CFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.c)))
CPPFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.cpp)))
SFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.s)))
PNGFILES := $(foreach dir,$(GRAPHICS),$(notdir $(wildcard $(dir)/*.png)))
BINFILES := $(foreach dir,$(DATA),$(notdir $(wildcard $(dir)/*.*)))
#ifneq ($(strip $(MUSIC)),)
# export AUDIOFILES := $(foreach dir,$(notdir $(wildcard $(MUSIC)/*.*)),$(CURDIR)/$(MUSIC)/$(dir))
# BINFILES += soundbank.bin
#endif
#---------------------------------------------------------------------------------
# use CXX for linking C++ projects, CC for standard C
#---------------------------------------------------------------------------------
ifeq ($(strip $(CPPFILES)),)
#---------------------------------------------------------------------------------
export LD := $(CC)
#---------------------------------------------------------------------------------
else
#---------------------------------------------------------------------------------
export LD := $(CXX)
#---------------------------------------------------------------------------------
endif
#---------------------------------------------------------------------------------
export OFILES_BIN := $(addsuffix .o,$(BINFILES))
export OFILES_SOURCES := $(CPPFILES:.cpp=.o) $(CFILES:.c=.o) $(SFILES:.s=.o)
export OFILES_GRAPHICS := $(PNGFILES:.png=.o)
export OFILES := $(OFILES_BIN) $(OFILES_SOURCES) $(OFILES_GRAPHICS)
export HFILES := $(addsuffix .h,$(subst .,_,$(BINFILES))) $(PNGFILES:.png=.h)
export INCLUDE := $(foreach dir,$(INCLUDES),-iquote $(CURDIR)/$(dir)) \
$(foreach dir,$(LIBDIRS),-I$(dir)/include) \
-I$(CURDIR)/$(BUILD)
export LIBPATHS := $(foreach dir,$(LIBDIRS),-L$(dir)/lib)
.PHONY: $(BUILD) clean
#---------------------------------------------------------------------------------
$(BUILD):
@[ -d $@ ] || mkdir -p $@
@$(MAKE) --no-print-directory -C $(BUILD) -f $(CURDIR)/Makefile
@mkdir -p loader/data
@cp $(TARGET).gba loader/data/multiboot_rom.bin
@$(MAKE) -C loader
#---------------------------------------------------------------------------------
clean:
@echo clean ...
@$(MAKE) -C loader clean
@rm -fr $(BUILD) $(TARGET).elf $(TARGET).gba
#---------------------------------------------------------------------------------
else
#---------------------------------------------------------------------------------
# main targets
#---------------------------------------------------------------------------------
$(OUTPUT).gba : $(OUTPUT).elf
$(OUTPUT).elf : $(OFILES)
$(OFILES_SOURCES) : $(HFILES)
#---------------------------------------------------------------------------------
# The bin2o rule should be copied and modified
# for each extension used in the data directories
#---------------------------------------------------------------------------------
#---------------------------------------------------------------------------------
# rule to build soundbank from music files
#---------------------------------------------------------------------------------
#soundbank.bin soundbank.h : $(AUDIOFILES)
#---------------------------------------------------------------------------------
# @mmutil $^ -osoundbank.bin -hsoundbank.h
#---------------------------------------------------------------------------------
# This rule links in binary data with the .bin extension
#---------------------------------------------------------------------------------
%.bin.o %_bin.h : %.bin
#---------------------------------------------------------------------------------
@echo $(notdir $<)
@$(bin2o)
#---------------------------------------------------------------------------------
# This rule creates C source files using grit
# grit takes an image file and a .grit describing how the file is to be processed
# add additional rules like this for each image extension
# you use in the graphics folders
#---------------------------------------------------------------------------------
%.c %.h: %.png %.grit
#---------------------------------------------------------------------------------
@echo "grit $(notdir $<)"
@grit $< -ftc -o$*
# make likes to delete intermediate files. This prevents it from deleting the
# files generated by grit after building the GBA ROM.
.SECONDARY:
-include $(DEPSDIR)/*.d
#---------------------------------------------------------------------------------------
endif
#---------------------------------------------------------------------------------------
# Build configuration (set to either 'debug' or 'release')
BUILD_TYPE := release
#---------------------------------------------------------------------------------
.SUFFIXES:
#---------------------------------------------------------------------------------
ifeq ($(strip $(DEVKITARM)),)
$(error "Please set DEVKITARM in your environment. export DEVKITARM=<path to>devkitARM")
endif
include $(DEVKITARM)/gba_rules
#---------------------------------------------------------------------------------
# the LIBGBA path is defined in gba_rules, but we have to define LIBTONC ourselves
#---------------------------------------------------------------------------------
LIBTONC := $(DEVKITPRO)/libtonc
#---------------------------------------------------------------------------------
# TARGET is the name of the output
# BUILD is the directory where object files & intermediate files will be placed
# SOURCES is a list of directories containing source code
# INCLUDES is a list of directories containing extra header files
# DATA is a list of directories containing binary data
# GRAPHICS is a list of directories containing files to be processed by grit
#
# All directories are specified relative to the project directory where
# the makefile is found
#
#---------------------------------------------------------------------------------
TARGET := $(notdir $(CURDIR))_mb
BUILD := build
SOURCES := source
INCLUDES := include
DATA := data
MUSIC := audio
GRAPHICS := graphics
#---------------------------------------------------------------------------------
# options for code generation
#---------------------------------------------------------------------------------
ARCH := -mthumb -mthumb-interwork
CFLAGS := -Wall -O2\
-mcpu=arm7tdmi -mtune=arm7tdmi -masm-syntax-unified\
$(ARCH)
CFLAGS += $(INCLUDE) -ffunction-sections -fdata-sections -Os -Wall -mthumb -mcpu=arm7tdmi -mtune=arm7tdmi
CXXFLAGS := $(CFLAGS) -g0 -fno-rtti -fno-exceptions -fdata-sections -ffunction-sections -std=c++20 -Wno-volatile -D_GLIBCXX_USE_CXX20_ABI=0
ifeq ($(BUILD_TYPE), debug)
CFLAGS += -g -DDEBUG
CXXFLAGS += -g -DDEBUG
else ifeq ($(BUILD_TYPE), release)
endif
ASFLAGS := $(ARCH)
LDFLAGS = -Os $(ARCH) -Wl,-Map,$(notdir $*.map) -Wl,--gc-sections -mthumb -mcpu=arm7tdmi -mtune=arm7tdmi -Wl,-Map,output.map,--cref -nodefaultlibs
CFLAGS += -flto
LDFLAGS += -flto
ifeq ($(BUILD_TYPE), debug)
ASFLAGS += -g
LDFLAGS += -g
endif
#---------------------------------------------------------------------------------
# any extra libraries we wish to link with the project
#---------------------------------------------------------------------------------
LIBS := -lmm -ltonc -lgba -lc -lgcc -lsysbase
#---------------------------------------------------------------------------------
# list of directories containing libraries, this must be the top level containing
# include and lib.
# the LIBGBA path should remain in this list if you want to use maxmod
#---------------------------------------------------------------------------------
LIBDIRS := $(LIBGBA) $(LIBTONC)
#---------------------------------------------------------------------------------
# no real need to edit anything past this point unless you need to add additional
# rules for different file extensions
#---------------------------------------------------------------------------------
ifneq ($(BUILD),$(notdir $(CURDIR)))
#---------------------------------------------------------------------------------
export OUTPUT := $(CURDIR)/$(TARGET)
export VPATH := $(foreach dir,$(SOURCES),$(CURDIR)/$(dir)) \
$(foreach dir,$(DATA),$(CURDIR)/$(dir)) \
$(foreach dir,$(GRAPHICS),$(CURDIR)/$(dir))
export DEPSDIR := $(CURDIR)/$(BUILD)
CFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.c)))
CPPFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.cpp)))
SFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.s)))
PNGFILES := $(foreach dir,$(GRAPHICS),$(notdir $(wildcard $(dir)/*.png)))
#ifneq ($(strip $(MUSIC)),)
# export AUDIOFILES := $(foreach dir,$(notdir $(wildcard $(MUSIC)/*.*)),$(CURDIR)/$(MUSIC)/$(dir))
# BINFILES += soundbank.bin
#endif
#---------------------------------------------------------------------------------
# use CXX for linking C++ projects, CC for standard C
#---------------------------------------------------------------------------------
ifeq ($(strip $(CPPFILES)),)
#---------------------------------------------------------------------------------
export LD := $(CC)
#---------------------------------------------------------------------------------
else
#---------------------------------------------------------------------------------
export LD := $(CXX)
#---------------------------------------------------------------------------------
endif
#---------------------------------------------------------------------------------
export OFILES_SOURCES := $(CPPFILES:.cpp=.o) $(CFILES:.c=.o) $(SFILES:.s=.o)
export OFILES_GRAPHICS := $(PNGFILES:.png=.o)
export OFILES := $(OFILES_SOURCES) $(OFILES_GRAPHICS)
export HFILES := $(addsuffix .h,$(subst .,_,$(BINFILES))) $(PNGFILES:.png=.h)
export INCLUDE := $(foreach dir,$(INCLUDES),-iquote $(CURDIR)/$(dir)) \
$(foreach dir,$(LIBDIRS),-I$(dir)/include) \
-I$(CURDIR)/$(BUILD)
export LIBPATHS := $(foreach dir,$(LIBDIRS),-L$(dir)/lib)
.PHONY: $(BUILD) generate_data clean
all: $(BUILD)
generate_data:
mkdir -p data
@env -i PATH=$(PATH) $(MAKE) -C tools/compressZX0
@env -i PATH=$(PATH) $(MAKE) -C tools/data-generator
@cd tools/data-generator && ./data-generator
@find tools/data-generator -name *.bin | xargs -i tools/compressZX0/compressZX0 {} data/
#---------------------------------------------------------------------------------
$(BUILD): generate_data
@[ -d $@ ] || mkdir -p $@
@$(MAKE) --no-print-directory -C $(BUILD) -f $(CURDIR)/Makefile
@mkdir -p loader/data
@cp $(TARGET).gba loader/data/multiboot_rom.bin
@$(MAKE) -C loader
#---------------------------------------------------------------------------------
clean:
@echo clean ...
@$(MAKE) -C tools/compressZX0 clean
@$(MAKE) -C tools/data-generator clean
@$(MAKE) -C loader clean
@rm -fr $(BUILD) $(TARGET).elf $(TARGET).gba data/
#---------------------------------------------------------------------------------
else
BINFILES := $(foreach dir,../$(DATA),$(notdir $(wildcard $(dir)/*.*)))
export OFILES_BIN := $(addsuffix .o,$(BINFILES))
OFILES += $(OFILES_BIN)
#---------------------------------------------------------------------------------
# main targets
#---------------------------------------------------------------------------------
$(OUTPUT).gba : $(OUTPUT).elf
$(OUTPUT).elf : $(OFILES)
$(OFILES_SOURCES) : $(HFILES)
#---------------------------------------------------------------------------------
# The bin2o rule should be copied and modified
# for each extension used in the data directories
#---------------------------------------------------------------------------------
#---------------------------------------------------------------------------------
# rule to build soundbank from music files
#---------------------------------------------------------------------------------
#soundbank.bin soundbank.h : $(AUDIOFILES)
#---------------------------------------------------------------------------------
# @mmutil $^ -osoundbank.bin -hsoundbank.h
#---------------------------------------------------------------------------------
# This rule links in binary data with the .bin extension
#---------------------------------------------------------------------------------
%.bin.o %_bin.h : %.bin
#---------------------------------------------------------------------------------
@echo $(notdir $<)
@$(bin2o)
#---------------------------------------------------------------------------------
# This rule creates C source files using grit
# grit takes an image file and a .grit describing how the file is to be processed
# add additional rules like this for each image extension
# you use in the graphics folders
#---------------------------------------------------------------------------------
%.c %.h: %.png %.grit
#---------------------------------------------------------------------------------
@echo "grit $(notdir $<)"
@grit $< -ftc -o$*
# make likes to delete intermediate files. This prevents it from deleting the
# files generated by grit after building the GBA ROM.
.SECONDARY:
-include $(DEPSDIR)/*.d
#---------------------------------------------------------------------------------------
endif
#---------------------------------------------------------------------------------------

5
buildDockerImg.sh Executable file
View File

@ -0,0 +1,5 @@
#!/bin/bash
mkdir -p docker-build && cd docker-build
docker build -t ptgb-builder:latest -f ../Dockerfile .
cd .. && rm -rf docker-build

View File

@ -1,27 +1,27 @@
# This docker-compose file allows you to pull the devkitpro/devkitarm docker container
# and use it to build the Poke Transporter GB rom(s).
#
# To launch the container itself in the background:
# docker compose up -d
# Note the container name being printed in the terminal. In my case it was this: poke_transporter_gb-build-1
#
# Then get a terminal into the container like this:
# docker exec -it poke_transporter_gb-build-1 /bin/bash
#
# Now you can build the project inside this terminal.
version: "3.5"
services:
build:
image: devkitpro/devkitarm
working_dir: /usr/Poke_Transporter_GB
command: tail -F anything
volumes:
- type: bind
source: ${SSH_AUTH_SOCK}
target: ${SSH_AUTH_SOCK}
- type: bind
source: ${PWD}
target: /usr/Poke_Transporter_GB
# This docker-compose file allows you to pull the devkitpro/devkitarm docker container
# and use it to build the Poke Transporter GB rom(s).
#
# To launch the container itself in the background:
# docker compose up -d
# Note the container name being printed in the terminal. In my case it was this: poke_transporter_gb-build-1
#
# Then get a terminal into the container like this:
# docker exec -it poke_transporter_gb-build-1 /bin/bash
#
# Now you can build the project inside this terminal.
version: "3.5"
services:
build:
image: ptgb-builder:latest
working_dir: /usr/Poke_Transporter_GB
command: tail -F anything
volumes:
- type: bind
source: ${SSH_AUTH_SOCK}
target: ${SSH_AUTH_SOCK}
- type: bind
source: ${PWD}
target: /usr/Poke_Transporter_GB

View File

@ -43,7 +43,7 @@ namespace ptgb
class vector
{
public:
static constexpr size_t default_capacity = 10;
static constexpr size_t default_capacity = 4;
vector()
: buffer_()

View File

@ -256,6 +256,7 @@
#define CPU_SET_32BIT 0x04000000
class mystery_gift_script
{
PokemonTables &data_tables;
int curr_mg_index;
int curr_section30_index;
u8 mg_script[MG_SCRIPT_SIZE] = {};
@ -264,7 +265,7 @@ class mystery_gift_script
u8 four_align_value = 0;
public:
mystery_gift_script();
mystery_gift_script(PokemonTables &data_tables);
void build_script(Pokemon_Party &incoming_box_data);
//void build_script_old(Pokemon_Party &incoming_box_data);
u8 get_script_value_at(int index);

View File

@ -14,6 +14,8 @@
#define GEN2_JPN_SIZE 383
#define GEN2_INT_SIZE 444
class PokemonTables;
struct Simplified_Pokemon
{
byte dex_number;
@ -37,18 +39,18 @@ public:
int unown_letter = -1;
Pokemon();
void load_data(int index, const byte *party_data, int game, int lang);
void convert_to_gen_three(bool simplified, bool stabilize_mythical);
void convert_to_gen_three(PokemonTables& data_tables, bool simplified, bool stabilize_mythical);
void copy_from_to(const byte *source, byte *destination, int size, bool reverse_endian);
void alocate_data_chunks(byte *G, byte *A, byte *E, byte *M);
void insert_data(byte *first, byte *second, byte *third, byte *fourth);
byte get_gen_3_data(int index);
byte *get_full_gen_3_array();
byte get_unencrypted_data(int index);
byte *convert_text(byte *text_array, int size, int gen, int lang);
u32 generate_pid_save_iv(byte pid_species_index, byte nature, byte *pid_dvs);
u32 generate_pid_iv_match(byte pid_species_index, byte nature, byte *pid_dvs);
byte *convert_text(PokemonTables& data_tables, byte *text_array, int size);
u32 generate_pid_save_iv(PokemonTables &data_tables, byte pid_species_index, byte nature, byte *pid_dvs);
u32 generate_pid_iv_match(PokemonTables& data_tables, byte pid_species_index, byte nature, byte *pid_dvs);
byte rand_reverse_mod(byte modulo_divisor, byte target_mod);
byte get_rand_gender_byte(byte index_num, byte attack_DVs);
byte get_rand_gender_byte(PokemonTables &data_tables, byte index_num, byte attack_DVs);
byte get_dex_number();
bool get_validity();
bool get_is_new();
@ -57,7 +59,7 @@ public:
u8 get_letter_from_pid(u32 pid);
u8 get_nature_from_pid(u32 pid);
u8 get_gender_from_pid(u32 pid);
void set_to_event(byte nature);
void set_to_event(PokemonTables &data_tables, byte nature);
int num_in_box;
int index_in_box;
bool is_missingno = false;

View File

@ -39,37 +39,85 @@
#define NUM_POKEMON 252
#define POKEMON_ARRAY_SIZE NUM_POKEMON + 1
extern const u8 EXP_GROUPS[POKEMON_ARRAY_SIZE];
extern const u32 EXP_MAXIMUMS[6];
extern const int GENDER_THRESHOLDS[2][8];
extern const u8 GENDER_RATIO[POKEMON_ARRAY_SIZE];
extern const bool NUM_ABILITIES[POKEMON_ARRAY_SIZE];
extern const byte MOVESETS[POKEMON_ARRAY_SIZE][32];
extern const byte FIRST_MOVES[POKEMON_ARRAY_SIZE];
extern const char* NAMES[POKEMON_ARRAY_SIZE];
extern const u16 JPN_NAMES[POKEMON_ARRAY_SIZE][6];
extern const u8 EVOLUTIONS[POKEMON_ARRAY_SIZE];
extern const u8 POWER_POINTS[252];
extern const u8 MENU_SPRITE_PALS[POKEMON_ARRAY_SIZE + 26][2];
extern const byte gen_1_index_array[191];
extern const u16 gen_1_Jpn_char_array[256];
extern const u16 gen_1_Eng_char_array[256];
extern const u16 gen_1_FreGer_char_array[256];
extern const u16 gen_1_ItaSpa_char_array[256];
extern const u16 gen_2_Jpn_char_array[256];
extern const u16 gen_2_Eng_char_array[256];
extern const u16 gen_2_FreGer_char_array[256];
extern const u16 gen_2_ItaSpa_char_array[256];
extern const u16 gen_3_Jpn_char_array[256];
extern const u16 gen_3_Intern_char_array[256];
extern const byte EVENT_PKMN[8][80];
extern const u8 TYPES[POKEMON_ARRAY_SIZE][2];
u32 get_max_exp(int index_num);
u8 get_gender_threshold(int index_num, bool is_gen_3);
bool get_num_abilities(int index_num);
bool can_learn_move(int pkmn_index, int move_index);
byte get_earliest_move(int index_num);
byte get_gen_3_char(u16 input_char, bool is_jpn);
/**
* Okay, here's the thing: to reduce the rom size, we compressed a bunch of data with ZX0
* Among this data are various data tables that were previously just stored as const arrays.
*
* But, during the mystery_gift_builder/mystery_gift_injector execution,
* these data tables are used intensely, because you're not using them for single pokémon, but rather for boxes of pokémon.
*
* Decompression is not cheap, so we can't afford to decompress the tables again and again for every pokémon we deal with
* during this flow.
*
* This is where this class comes in. It is basically a holder of these tables. The idea is to pass it along with the mystery_gift_builder
* and have it lazy decompress the tables AND charsets it needs. This way when we are dealing with multiple pokémon, we are not decompressing
* the same data every time and thereby make performance suffer.
*
* It DOES have a significant IWRAM cost though. The intention is to just allocate this struct on the stack.
* As far as I can tell, our stack is not really restricted at all. It just fills up the IWRAM as needed. So we need to be careful not to
* overwrite/corrupt any code or data we specifically tagged to be stored in IWRAM.
*/
class PokemonTables
{
public:
bool exp_groups_loaded;
bool gender_ratios_loaded;
bool num_abilities_loaded;
bool first_moves_loaded;
bool evolutions_loaded;
bool power_points_loaded;
bool gen_1_index_array_loaded;
bool event_pkmn_loaded;
bool types_loaded;
// a number representing the unique combination of gen 1/2 and the specific language
// 0 means not loaded
u8 input_charset_type;
// 0 means not loaded, 1=JPN, 2=Intern
u8 gen3_charset_type;
bool movesets_loaded;
u8 EXP_GROUPS[POKEMON_ARRAY_SIZE];
u8 GENDER_RATIO[POKEMON_ARRAY_SIZE];
bool NUM_ABILITIES[POKEMON_ARRAY_SIZE];
byte FIRST_MOVES[POKEMON_ARRAY_SIZE];
u8 EVOLUTIONS[POKEMON_ARRAY_SIZE];
u8 POWER_POINTS[252];
byte gen_1_index_array[191];
byte EVENT_PKMN[8][80];
u8 TYPES[POKEMON_ARRAY_SIZE][2];
u16 input_charset[256];
u16 gen3_charset[256];
byte MOVESETS[POKEMON_ARRAY_SIZE][32];
PokemonTables();
void load_exp_groups();
void load_gender_ratios();
void load_num_abilities();
void load_first_moves();
void load_evolutions();
void load_power_points();
void load_gen1_index_array();
void load_event_pkmn();
void load_types();
void load_input_charset(byte gen, byte lang);
void load_gen3_charset(byte lang);
void load_movesets();
u32 get_max_exp(int index_num);
u8 get_gender_threshold(int index_num, bool is_gen_3);
bool get_num_abilities(int index_num);
bool can_learn_move(int pkmn_index, int move_index);
byte get_earliest_move(int index_num);
byte get_gen_3_char(u16 input_char);
};
/**
* Loads the charset for <gen> and <lang> into <output_char_array>
*/
void load_localized_charset(u16 *output_char_array, byte gen, byte lang);
byte get_char_from_charset(const u16 *charset, u16 input_char);
#endif

View File

@ -11,7 +11,7 @@ public:
void start_link();
void continue_link(bool cancel_connection);
int get_last_error();
Pokemon get_converted_pkmn(int index);
Pokemon get_converted_pkmn(PokemonTables &data_tables, int index);
bool get_has_new_pkmn();
void set_game(int nGame);
void set_lang(int nLang);
@ -21,7 +21,7 @@ public:
void show_sprites();
Simplified_Pokemon simple_pkmn_array[30];
Simplified_Pokemon get_simple_pkmn(int index);
bool fill_simple_pkmn_array();
bool fill_simple_pkmn_array(PokemonTables &data_tables);
bool get_contains_mythical();
void set_mythic_stabilization(bool stabilize);
bool contains_valid = false;

View File

@ -8,21 +8,21 @@ class script_obj
{
public:
script_obj();
script_obj(const byte* nText, int nNext); // For dialogue
script_obj(int nRun, int nNext); // For commands
script_obj(int nRun, int nNext_if_true, int nNext_if_false); // for conditionals
script_obj(const byte* nText, uint16_t nNext); // For dialogue
script_obj(uint16_t nRun, uint16_t nNext); // For commands
script_obj(uint16_t nRun, uint16_t nNext_if_true, uint16_t nNext_if_false); // for conditionals
const byte* get_text();
int get_true_index();
int get_false_index();
int get_cond_id();
uint16_t get_true_index();
uint16_t get_false_index();
uint16_t get_cond_id();
private:
const byte* text;
bool has_text = false;
int next_index;
int conditional_index;
int next_false_index;
uint16_t next_index;
uint16_t conditional_index;
uint16_t next_false_index;
};
#endif

View File

@ -49,9 +49,9 @@ class textbox_var : public xse_var
public:
using xse_var::xse_var;
void set_text(const byte nText[]);
void insert_text(u8 mg_array[]);
void insert_text(const u16 *charset, u8 mg_array[]);
void set_start();
void insert_virtual_text(u8 mg_array[]);
void insert_virtual_text(const u16 *charset, u8 mg_array[]);
void set_virtual_start();
const byte *text;
int text_length;

View File

@ -11,21 +11,21 @@
class Select_Menu
{
public:
Select_Menu(bool enable_cancel, int nMenu_type, int nStartX, int nStartY);
Select_Menu(bool enable_cancel, u8 nMenu_type, int nStartX, int nStartY);
int select_menu_main();
void hide_menu();
void show_menu();
void clear_options();
void add_option(const byte *option, int return_value);
void set_lang(int nLang);
void add_option(const byte *option, u8 return_value);
void set_lang(u8 nLang);
private:
ptgb::vector<const byte*> menu_options;
ptgb::vector<int> return_values;
unsigned int curr_selection;
ptgb::vector<u8> return_values;
u16 curr_selection;
bool cancel_enabled;
int menu_type;
int lang;
u8 menu_type;
u8 lang;
int startTileX;
int startTileY;
};

View File

@ -125,11 +125,11 @@ void load_textbox_background();
void load_flex_background(int background_id, int layer);
void load_eternal_sprites();
void load_temp_box_sprites(Pokemon_Party *party_data);
void load_type_sprites(int pkmn_index, int dex_offset, bool is_caught);
void load_type_sprites(const u8* pkmn_type_table, int pkmn_index, int dex_offset, bool is_caught);
void add_menu_box(int options, int startTileX, int startTileY);
void add_menu_box(int startTileX, int startTileY, int width, int height);
void reload_textbox_background();
void load_select_sprites(int game_id, int lang);
void load_select_sprites(u8 game_id, u8 lang);
void fennel_blink(int frame);
void fennel_speak(int frame);
int get_curr_flex_background();

View File

@ -28,7 +28,7 @@ void set_text_exit();
int ptgb_write(const char *text);
int ptgb_write(const byte *text, bool instant);
int ptgb_write(const byte *text, bool instant, int length);
int ptgb_write_debug(const char *text, bool instant);
int ptgb_write_debug(const u16* charset, const char *text, bool instant);
void wait_for_user_to_continue(bool clear_text);
#endif

View File

@ -0,0 +1,50 @@
#ifndef _ZX0_DECODE_H
#define _ZX0_DECODE_H
#include <cstdint>
// The ZX0 decompressor offers functionality to decompress data
// compressed with the ZX0 algorithm (see tools/compressZX0)
// This algorithm was invented by Einar Saukas
// Original implementation can be found here: https://github.com/einar-saukas/ZX0
// However, we've implemented a custom variant of this algorithm.
// (for instance: we're storing the uncompressed size in the first 2 bytes in little endian)
// Our implementation "streams" the decompression: the decompression buffer is only 2 KB, so it can't fit the entire
// uncompressed file at once. Therefore it uses a ringbuffer to stream the decompression on-demand.
extern "C"
{
/**
* @brief This function slots the specified input_data buffer into the zx0 decompressor.
* Calling this function effectively resets the ZX0 decompressors' internal state.
*/
void zx0_decompressor_set_input(const uint8_t *input_data);
/**
* @brief This function returns the uncompressed size of the current input_data buffer.
* It reads this from the first 2 bytes of input_data
*/
uint16_t zx0_decompressor_get_decompressed_size();
/**
* @brief This function seeks to the specified OUTPUT buffer position.
* NOTE: this is an expensive operation!
* ZX0 doesn't actually support random access.
*
* So if we're seeking forward, we're actually uncompressing the data until we reach the desired output buffer position.
* And if we're seeking backward, we're actually starting to uncompress from scratch until the desired output buffer position!
*
* So handle this with care!
* @param output_byte_pos
*/
void zx0_decompressor_seek(uint16_t output_byte_pos);
/**
* @brief This function copies <num_bytes> of decompressed data into the specified <output_buffer>
* It will trigger decompression on the go (streaming basis)
*/
uint16_t zx0_decompressor_read(uint8_t *output_buffer, uint16_t num_bytes);
}
#endif

View File

@ -2,6 +2,7 @@
#include "libstd_replacements.h"
#include "flash_mem.h"
#include "pokemon.h"
#include "pokemon_data.h"
#include "rom_data.h"
#include "libraries/Pokemon-Gen3-to-Gen-X/include/save.h"
#include "text_engine.h"
@ -96,10 +97,14 @@ void initalize_memory_locations()
void print_mem_section()
{
return; // This function isn't really needed now
uint16_t charset[256];
byte out[4] = {0, 0, 0, 0xFF};
out[0] = get_gen_3_char(mem_name, false);
out[1] = get_gen_3_char('-', false);
out[2] = get_gen_3_char(mem_id + 0xA1, false); // Kinda a dumb way to
load_localized_charset(charset, 3, ENG_ID);
out[0] = get_char_from_charset(charset, mem_name);
out[1] = get_char_from_charset(charset, '-');
out[2] = get_char_from_charset(charset, mem_id + 0xA1); // Kinda a dumb way to
tte_set_pos(0, 0);
ptgb_write(out, true);
}

View File

@ -11,6 +11,7 @@
#include "text_engine.h"
#include "background_engine.h"
#include "pokemon_party.h"
#include "pokemon_data.h"
#include "script_array.h"
#include "sprite_data.h"
#include "button_handler.h"
@ -253,6 +254,8 @@ int credits()
}
if (ENABLE_DEBUG_SCREEN && key_hit(KEY_SELECT))
{
uint16_t charset[256];
load_localized_charset(charset, 3, ENG_ID);
if (key_held(KEY_UP) && key_held(KEY_L) && key_held(KEY_R))
{
set_treecko(true);
@ -270,48 +273,48 @@ int credits()
int def_lang = get_def_lang_num();
create_textbox(4, 1, 160, 80, true);
ptgb_write_debug("Debug info:\n\nG: ", true);
ptgb_write_debug(ptgb::to_string(curr_rom.language), true);
ptgb_write_debug(charset, "Debug info:\n\nG: ", true);
ptgb_write_debug(charset, ptgb::to_string(curr_rom.language), true);
switch (curr_rom.gamecode)
{
case RUBY_ID:
ptgb_write_debug("-R-", true);
ptgb_write_debug(charset, "-R-", true);
break;
case SAPPHIRE_ID:
ptgb_write_debug("-S-", true);
ptgb_write_debug(charset, "-S-", true);
break;
case FIRERED_ID:
ptgb_write_debug("-F-", true);
ptgb_write_debug(charset, "-F-", true);
break;
case LEAFGREEN_ID:
ptgb_write_debug("-L-", true);
ptgb_write_debug(charset, "-L-", true);
break;
case EMERALD_ID:
ptgb_write_debug("-E-", true);
ptgb_write_debug(charset, "-E-", true);
break;
}
ptgb_write_debug(ptgb::to_string(curr_rom.version), true);
ptgb_write_debug(charset, ptgb::to_string(curr_rom.version), true);
ptgb_write_debug("\nF: ", true);
ptgb_write_debug(ptgb::to_string(e4_flag), true);
ptgb_write_debug(ptgb::to_string(mg_flag), true);
ptgb_write_debug(ptgb::to_string(all_collected_flag), true);
ptgb_write_debug("-", true);
ptgb_write_debug(charset, "\nF: ", true);
ptgb_write_debug(charset, ptgb::to_string(e4_flag), true);
ptgb_write_debug(charset, ptgb::to_string(mg_flag), true);
ptgb_write_debug(charset, ptgb::to_string(all_collected_flag), true);
ptgb_write_debug(charset, "-", true);
n2hexstr(hexBuffer, pkmn_flags);
ptgb_write_debug(hexBuffer, true);
ptgb_write_debug("\nS: ", true);
ptgb_write_debug(ptgb::to_string(tutorial), true);
ptgb_write_debug("-", true);
ptgb_write_debug(charset, hexBuffer, true);
ptgb_write_debug(charset, "\nS: ", true);
ptgb_write_debug(charset, ptgb::to_string(tutorial), true);
ptgb_write_debug(charset, "-", true);
n2hexstr(hexBuffer, def_lang);
ptgb_write_debug(hexBuffer, true);
ptgb_write_debug(charset, hexBuffer, true);
ptgb_write_debug("\n", true);
ptgb_write_debug(VERSION, true);
ptgb_write_debug(charset, "\n", true);
ptgb_write_debug(charset, VERSION, true);
if (get_treecko_enabled())
{
ptgb_write_debug(".T", true);
ptgb_write_debug(charset, ".T", true);
}
while (true)
{

View File

@ -40,7 +40,8 @@ int var_script_ptr_low = (VAR_ID_START + 0x01);
int var_script_ptr_high = (VAR_ID_START + 0x02);
int var_call_return_1 = (VAR_ID_START + 0x03);
mystery_gift_script::mystery_gift_script()
mystery_gift_script::mystery_gift_script(PokemonTables &data_tables)
: data_tables(data_tables)
{
curr_mg_index = NPC_LOCATION_OFFSET;
curr_section30_index = 0;
@ -331,7 +332,7 @@ void mystery_gift_script::build_script(Pokemon_Party &incoming_box_data)
for (int i = 0; i < MAX_PKMN_IN_BOX; i++) // Add in the Pokemon data
{
Pokemon curr_pkmn = incoming_box_data.get_converted_pkmn(i);
Pokemon curr_pkmn = incoming_box_data.get_converted_pkmn(data_tables, i);
if (curr_pkmn.get_validity())
{
for (int curr_byte = 0; curr_byte < POKEMON_SIZE; curr_byte++)
@ -354,14 +355,16 @@ void mystery_gift_script::build_script(Pokemon_Party &incoming_box_data)
}
// insert text
textThank.insert_text(save_section_30);
textPCFull.insert_text(save_section_30);
textWeHere.insert_text(save_section_30);
textPCConvo.insert_text(save_section_30);
textPCThanks.insert_text(save_section_30);
textLookerFull.insert_text(save_section_30);
textMoveBox.insert_text(save_section_30);
textReceived.insert_text(save_section_30);
data_tables.load_gen3_charset(ENG_ID);
textThank.insert_text(data_tables.gen3_charset, save_section_30);
textPCFull.insert_text(data_tables.gen3_charset, save_section_30);
textWeHere.insert_text(data_tables.gen3_charset, save_section_30);
textPCConvo.insert_text(data_tables.gen3_charset, save_section_30);
textPCThanks.insert_text(data_tables.gen3_charset, save_section_30);
textLookerFull.insert_text(data_tables.gen3_charset, save_section_30);
textMoveBox.insert_text(data_tables.gen3_charset, save_section_30);
textReceived.insert_text(data_tables.gen3_charset, save_section_30);
movementSlowSpin.insert_movement(save_section_30);
movementFastSpin.insert_movement(save_section_30);
@ -783,9 +786,9 @@ void mystery_gift_script::build_script(Pokemon_Party &incoming_box_data)
add_word(flashBuffer_ptr.place_word());
add_word(readFlashSector_ptr.place_word());
textGreet.insert_virtual_text(mg_script);
textYouMustBe.insert_virtual_text(mg_script);
textIAm.insert_virtual_text(mg_script);
textGreet.insert_virtual_text(data_tables.gen3_charset, mg_script);
textYouMustBe.insert_virtual_text(data_tables.gen3_charset, mg_script);
textIAm.insert_virtual_text(data_tables.gen3_charset, mg_script);
for (unsigned int i = 0; i < mg_variable_list.size(); i++) // Fill all the refrences for script variables in the mg
{

View File

@ -16,7 +16,8 @@ static u8 frlg_wonder_card[0x14E] = {
bool inject_mystery(Pokemon_Party &incoming_box_data)
{
mystery_gift_script script;
PokemonTables data_tables;
mystery_gift_script script(data_tables);
if (ENABLE_OLD_EVENT)
{
// script.build_script_old(incoming_box_data);
@ -83,7 +84,7 @@ bool inject_mystery(Pokemon_Party &incoming_box_data)
{
for (int i = 0; i < MAX_PKMN_IN_BOX; i++) // Add in the Pokemon data
{
Pokemon curr_pkmn = incoming_box_data.get_converted_pkmn(i);
Pokemon curr_pkmn = incoming_box_data.get_converted_pkmn(data_tables, i);
if (curr_pkmn.get_validity())
{

View File

@ -11,6 +11,8 @@
#include "button_handler.h"
#include "translated_text.h"
#include "text_engine.h"
#include "zx0_decompressor.h"
#include "TYPES_zx0_bin.h"
Dex dex_array[DEX_MAX];
int dex_shift = 0;
@ -71,6 +73,11 @@ void pokedex_init()
int pokedex_loop()
{
u8 TYPES[POKEMON_ARRAY_SIZE * 2];
zx0_decompressor_set_input(TYPES_zx0_bin);
zx0_decompressor_read((uint8_t*)TYPES, zx0_decompressor_get_decompressed_size());
pokedex_init();
pokedex_show();
bool update = true;
@ -198,7 +205,7 @@ int pokedex_loop()
// Eventually it could be optimized to move the labels around, but this honestly makes the most sense. Less code but one frame different
for (int i = 0; i < DEX_MAX; i++)
{
load_type_sprites(dex_shift + i + 1 + mythic_skip, i, is_caught(dex_shift + i + 1 + mythic_skip));
load_type_sprites(TYPES, dex_shift + i + 1 + mythic_skip, i, is_caught(dex_shift + i + 1 + mythic_skip));
}
update = false;
}

View File

@ -6,6 +6,8 @@
#include "save_data_manager.h"
#include "debug_mode.h"
#include "text_engine.h"
#include "zx0_decompressor.h"
#include "JPN_NAMES_zx0_bin.h"
Pokemon::Pokemon() {};
@ -159,7 +161,7 @@ void Pokemon::load_data(int index, const byte *party_data, int game, int lang)
global_next_frame();
}
}
void Pokemon::convert_to_gen_three(bool simplified, bool stabilize_mythical)
void Pokemon::convert_to_gen_three(PokemonTables& data_tables, bool simplified, bool stabilize_mythical)
{
// Convert the species indexes
if (gen == 1)
@ -170,7 +172,8 @@ void Pokemon::convert_to_gen_three(bool simplified, bool stabilize_mythical)
}
else
{
species_index_struct = gen_1_index_array[species_index_struct];
data_tables.load_gen1_index_array();
species_index_struct = data_tables.gen_1_index_array[species_index_struct];
if (species_index_struct == 0xFF)
{
is_missingno = true;
@ -206,13 +209,21 @@ void Pokemon::convert_to_gen_three(bool simplified, bool stabilize_mythical)
// Set nickname
if (language == KOR_ID)
{
gen_3_pkmn[18] = JPN_ID; // Set to JPN
byte new_nickname[10];
byte new_ot[7];
u16 cur_char;
data_tables.load_gen3_charset(language);
// setup the zx0 decompressor to decompress the JPN_NAMES table
zx0_decompressor_set_input(JPN_NAMES_zx0_bin);
// seek to the right pokemon
zx0_decompressor_seek(sizeof(u16) * 6 * species_index_struct);
for (int i = 0; i < 6; i++)
{ // Read the JPN name and convert it
new_nickname[i] = get_gen_3_char(JPN_NAMES[species_index_struct][i], true);
zx0_decompressor_read((u8*)&cur_char, 2);
new_nickname[i] = data_tables.get_gen_3_char(cur_char);
}
if (gen == 1)
@ -247,13 +258,16 @@ void Pokemon::convert_to_gen_three(bool simplified, bool stabilize_mythical)
}
else
{
gen_3_pkmn[18] = language; // Language
copy_from_to(convert_text(&nickname[0], 10, gen, language), &gen_3_pkmn[8], 10, false); // Nickname
copy_from_to(convert_text(&trainer_name[0], 7, gen, language), &gen_3_pkmn[20], 7, false); // OT Name
gen_3_pkmn[18] = language; // Language
data_tables.load_input_charset(gen, language);
data_tables.load_gen3_charset(language);
copy_from_to(convert_text(data_tables, &nickname[0], 10), &gen_3_pkmn[8], 10, false); // Nickname
copy_from_to(convert_text(data_tables, &trainer_name[0], 7), &gen_3_pkmn[20], 7, false); // OT Name
}
// Make sure Level is not over 100 based on EXP
u32 max_exp = get_max_exp(species_index_struct);
u32 max_exp = data_tables.get_max_exp(species_index_struct);
if (exp > max_exp)
{
exp = max_exp;
@ -320,7 +334,7 @@ void Pokemon::convert_to_gen_three(bool simplified, bool stabilize_mythical)
{
for (int i = 0; i < 4; i++)
{
if ((!can_learn_move(species_index_struct, moves[i])) && (moves[i] != 0))
if ((!data_tables.can_learn_move(species_index_struct, moves[i])) && (moves[i] != 0))
{
moves[i] = 0; // Remove the move
pp_bonus[i] = 0; // Remove the PP bonus
@ -331,7 +345,7 @@ void Pokemon::convert_to_gen_three(bool simplified, bool stabilize_mythical)
// Make sure it has at least one move
if (moves[0] + moves[1] + moves[2] + moves[3] == 0)
{
moves[0] = get_earliest_move(species_index_struct);
moves[0] = data_tables.get_earliest_move(species_index_struct);
}
// Bubble valid moves to the top
@ -360,15 +374,16 @@ void Pokemon::convert_to_gen_three(bool simplified, bool stabilize_mythical)
}
// Restore the PP values
data_tables.load_power_points();
for (int i = 0; i < 4; i++)
{
pure_pp_values[i] = POWER_POINTS[moves[i]] + ((POWER_POINTS[moves[i]] / 5) * pp_bonus[i]);
pure_pp_values[i] = data_tables.POWER_POINTS[moves[i]] + ((data_tables.POWER_POINTS[moves[i]] / 5) * pp_bonus[i]);
}
// This is everything the mythical needs, don't change anything else
if (stabilize_mythical && (species_index_struct == 151 || species_index_struct == 251))
{
set_to_event(nature_mod);
set_to_event(data_tables, nature_mod);
return;
}
@ -377,7 +392,7 @@ void Pokemon::convert_to_gen_three(bool simplified, bool stabilize_mythical)
u32 n_pid;
if (ENABLE_MATCH_PID)
{
n_pid = generate_pid_iv_match(species_index_struct, nature_mod, &dvs[0]);
n_pid = generate_pid_iv_match(data_tables, species_index_struct, nature_mod, &dvs[0]);
u16 curr_rand = get_rand_u16();
ivs[0] = (curr_rand >> 0) & 0b11111;
@ -395,7 +410,7 @@ void Pokemon::convert_to_gen_three(bool simplified, bool stabilize_mythical)
}
else
{
n_pid = generate_pid_save_iv(species_index_struct, nature_mod, &dvs[0]);
n_pid = generate_pid_save_iv(data_tables, species_index_struct, nature_mod, &dvs[0]);
// Convert and set IVs
int hp_iv = 0;
@ -420,7 +435,7 @@ void Pokemon::convert_to_gen_three(bool simplified, bool stabilize_mythical)
enable_auto_random();
// Determine and set Ability
iv_egg_ability |= ((pid[0] & 0x1) ? get_num_abilities(species_index_struct) : 0) << 31;
iv_egg_ability |= ((pid[0] & 0x1) ? data_tables.get_num_abilities(species_index_struct) : 0) << 31;
// Origin info
origin_info |= ((caught_data[1] & 0b10000000) << 8); // OT gender - We would shift left 15 bits, but the bit is already shifted over 7
@ -672,66 +687,23 @@ byte Pokemon::get_unencrypted_data(int index)
return unencrypted_data[index];
}
byte *Pokemon::convert_text(byte *text_array, int size, int gen, int lang)
byte *Pokemon::convert_text(PokemonTables& data_tables, byte *text_array, int size)
{
for (int i = 0; i < size; i++)
{
switch (gen)
{
case 1:
switch (lang)
{
case JPN_ID:
text_array[i] = get_gen_3_char(gen_1_Jpn_char_array[text_array[i]], true);
break;
case ENG_ID:
default:
text_array[i] = get_gen_3_char(gen_1_Eng_char_array[text_array[i]], false);
break;
case FRE_ID:
case GER_ID:
text_array[i] = get_gen_3_char(gen_1_FreGer_char_array[text_array[i]], false);
break;
case SPA_ID:
case ITA_ID:
text_array[i] = get_gen_3_char(gen_1_ItaSpa_char_array[text_array[i]], false);
break;
}
break;
case 2:
default:
switch (lang)
{
case JPN_ID:
text_array[i] = get_gen_3_char(gen_2_Jpn_char_array[text_array[i]], true);
break;
case ENG_ID:
default:
text_array[i] = get_gen_3_char(gen_2_Eng_char_array[text_array[i]], false);
break;
case FRE_ID:
case GER_ID:
text_array[i] = get_gen_3_char(gen_2_FreGer_char_array[text_array[i]], false);
break;
case SPA_ID:
case ITA_ID:
text_array[i] = get_gen_3_char(gen_2_ItaSpa_char_array[text_array[i]], false);
break;
}
break;
}
text_array[i] = data_tables.get_gen_3_char(data_tables.input_charset[text_array[i]]);
}
return text_array;
}
u32 Pokemon::generate_pid_iv_match(byte pid_species_index, byte nature, byte *pid_dvs)
u32 Pokemon::generate_pid_iv_match(PokemonTables& data_tables, byte pid_species_index, byte nature, byte *pid_dvs)
{
u32 new_pid = 0;
byte new_nature = 0;
byte new_gender = 0;
byte new_letter = 0;
int gen2_gender_threshold = get_gender_threshold(pid_species_index, false);
int gen3_gender_threshold = get_gender_threshold(pid_species_index, true);
int gen2_gender_threshold = data_tables.get_gender_threshold(pid_species_index, false);
int gen3_gender_threshold = data_tables.get_gender_threshold(pid_species_index, true);
bool gender = (((pid_dvs[0] >> 4) & 0b1111) < gen2_gender_threshold);
do
@ -768,7 +740,7 @@ u8 Pokemon::get_gender_from_pid(u32 pid)
return (pid & 0xFF);
};
u32 Pokemon::generate_pid_save_iv(byte pid_species_index, byte nature, byte *pid_dvs)
u32 Pokemon::generate_pid_save_iv(PokemonTables &data_tables, byte pid_species_index, byte nature, byte *pid_dvs)
{
// Set Unown Letter
u32 new_pid = 0;
@ -795,7 +767,7 @@ u32 Pokemon::generate_pid_save_iv(byte pid_species_index, byte nature, byte *pid
else
{
// Set the correct gender for the Pokemon
new_pid |= get_rand_gender_byte(pid_species_index, ((pid_dvs[0] >> 4) & 0b1111));
new_pid |= get_rand_gender_byte(data_tables, pid_species_index, ((pid_dvs[0] >> 4) & 0b1111));
// Randomize rest of PID
new_pid |= get_rand_u32() & 0xFFFFFF00;
@ -814,10 +786,10 @@ byte Pokemon::rand_reverse_mod(byte modulo_divisor, byte target_mod)
return (modulo_divisor * get_rand_range(0, (255 - target_mod) / modulo_divisor)) + target_mod;
}
byte Pokemon::get_rand_gender_byte(byte index_num, byte attack_DVs)
byte Pokemon::get_rand_gender_byte(PokemonTables &data_tables, byte index_num, byte attack_DVs)
{
byte gen2_threshold = get_gender_threshold(index_num, false);
byte gen3_threshold = get_gender_threshold(index_num, true);
byte gen2_threshold = data_tables.get_gender_threshold(index_num, false);
byte gen3_threshold = data_tables.get_gender_threshold(index_num, true);
if (gen2_threshold == -1) // Is one gender or is genderless
{
return get_rand_range(0, 256);
@ -869,7 +841,7 @@ Simplified_Pokemon Pokemon::get_simple_pkmn()
return curr_pkmn;
}
void Pokemon::set_to_event(byte nature)
void Pokemon::set_to_event(PokemonTables &data_tables, byte nature)
{
int event_id = 0;
if (species_index_struct == 151)
@ -915,6 +887,7 @@ void Pokemon::set_to_event(byte nature)
}
}
data_tables.load_event_pkmn();
// Load the event into the Pokemon array and unencrypted data array
for (int i = 0; i < 0x20; i++)
{
@ -922,15 +895,15 @@ void Pokemon::set_to_event(byte nature)
{
i += 10; // Skip over the nickname
}
gen_3_pkmn[i] = EVENT_PKMN[event_id][i];
gen_3_pkmn[i] = data_tables.EVENT_PKMN[event_id][i];
}
for (int i = 0; i < 12; i++)
{
data_section_G[i] = EVENT_PKMN[event_id][i + 0x20 + 0];
data_section_A[i] = EVENT_PKMN[event_id][i + 0x20 + 12];
data_section_E[i] = EVENT_PKMN[event_id][i + 0x20 + 24];
data_section_M[i] = EVENT_PKMN[event_id][i + 0x20 + 36];
data_section_G[i] = data_tables.EVENT_PKMN[event_id][i + 0x20 + 0];
data_section_A[i] = data_tables.EVENT_PKMN[event_id][i + 0x20 + 12];
data_section_E[i] = data_tables.EVENT_PKMN[event_id][i + 0x20 + 24];
data_section_M[i] = data_tables.EVENT_PKMN[event_id][i + 0x20 + 36];
}
// insert moves and PP bonuses
@ -975,7 +948,8 @@ void Pokemon::set_to_event(byte nature)
enable_auto_random();
// Determine and set Ability
iv_egg_ability |= ((pid[0] & 0x1) ? get_num_abilities(species_index_struct) : 0) << 31;
data_tables.load_num_abilities();
iv_egg_ability |= ((pid[0] & 0x1) ? data_tables.get_num_abilities(species_index_struct) : 0) << 31;
// Set IVs, Egg, and Ability
for (int i = 0; i < 4; i++)

File diff suppressed because it is too large Load Diff

View File

@ -204,11 +204,11 @@ int Pokemon_Party::get_last_error()
return last_error;
}
Pokemon Pokemon_Party::get_converted_pkmn(int index)
Pokemon Pokemon_Party::get_converted_pkmn(PokemonTables& data_tables, int index)
{
Pokemon converted_mon;
converted_mon.load_data(index, box_data_array, game, lang);
converted_mon.convert_to_gen_three(false, stabilize_mythic);
converted_mon.convert_to_gen_three(data_tables, false, stabilize_mythic);
has_new_pkmn = has_new_pkmn || converted_mon.get_is_new();
simple_pkmn_array[index] = converted_mon.get_simple_pkmn();
return converted_mon;
@ -281,14 +281,14 @@ Simplified_Pokemon Pokemon_Party::get_simple_pkmn(int index)
return simple_pkmn_array[index];
}
bool Pokemon_Party::fill_simple_pkmn_array()
bool Pokemon_Party::fill_simple_pkmn_array(PokemonTables &data_tables)
{
contains_mythical = false;
for (int index = 0; index < get_num_pkmn(); index++)
{
Pokemon converted_mon;
converted_mon.load_data(index, box_data_array, game, lang);
converted_mon.convert_to_gen_three(true, stabilize_mythic);
converted_mon.convert_to_gen_three(data_tables, true, stabilize_mythic);
has_new_pkmn = has_new_pkmn || converted_mon.get_is_new();
contains_mythical = contains_mythical ||
converted_mon.get_dex_number() == 151 || converted_mon.get_dex_number() == 251;

View File

@ -139,7 +139,7 @@ void populate_lang_menu()
langs.add_option(option_german, GER_ID);
langs.add_option(option_italian, ITA_ID);
langs.add_option(option_korean, KOR_ID);
langs.add_option(option_cancel, -1);
langs.add_option(option_cancel, UINT8_MAX);
}
void populate_game_menu(int lang)
@ -154,13 +154,13 @@ void populate_game_menu(int lang)
games.add_option(option_gold, GOLD_ID);
games.add_option(option_silver, SILVER_ID);
games.add_option(option_crystal, CRYSTAL_ID);
games.add_option(option_cancel, -1);
games.add_option(option_cancel, UINT8_MAX);
break;
case (KOR_ID):
games.add_option(option_gold, GOLD_ID);
games.add_option(option_silver, SILVER_ID);
games.add_option(option_cancel, -1);
games.add_option(option_cancel, UINT8_MAX);
break;
default:
@ -170,7 +170,7 @@ void populate_game_menu(int lang)
games.add_option(option_gold, GOLD_ID);
games.add_option(option_silver, SILVER_ID);
games.add_option(option_crystal, CRYSTAL_ID);
games.add_option(option_cancel, -1);
games.add_option(option_cancel, UINT8_MAX);
break;
}
}
@ -300,8 +300,8 @@ bool run_conditional(int index)
{
return false;
}
games.set_lang(lang);
party_data.set_lang(lang);
games.set_lang(static_cast<u8>(lang));
party_data.set_lang(static_cast<u8>(lang));
return true;
case CMD_GAME_MENU:
@ -348,8 +348,10 @@ bool run_conditional(int index)
return true;
case CMD_LOAD_SIMP:
return party_data.fill_simple_pkmn_array();
{
PokemonTables data_tables;
return party_data.fill_simple_pkmn_array(data_tables);
}
case CMD_CANCEL_LINK:
party_data.continue_link(true);
return true;

View File

@ -4,27 +4,29 @@
#include "pokemon.h"
#include "pokemon_party.h"
#include "script_array.h"
#include "translated_text.h"
script_obj::script_obj(){};
script_obj::script_obj(const byte* nText, int nNext)
script_obj::script_obj(const byte* nText, uint16_t nNext)
{
text = nText;
has_text = true;
next_index = nNext;
conditional_index = 0;
next_false_index = 0;
}
script_obj::script_obj(int nRun, int nNext)
script_obj::script_obj(uint16_t nRun, uint16_t nNext)
{
text = nullptr;
next_index = nNext;
conditional_index = nRun;
next_false_index = nNext;
}
script_obj::script_obj(int nRun, int nNext_if_true, int nNext_if_false)
script_obj::script_obj(uint16_t nRun, uint16_t nNext_if_true, uint16_t nNext_if_false)
{
text = nullptr;
next_index = nNext_if_true;
conditional_index = nRun;
next_false_index = nNext_if_false;
@ -32,27 +34,20 @@ script_obj::script_obj(int nRun, int nNext_if_true, int nNext_if_false)
const byte* script_obj::get_text()
{
if (has_text)
{
return text;
}
else
{
return NULL;
}
return text;
}
int script_obj::get_true_index()
uint16_t script_obj::get_true_index()
{
return next_index;
}
int script_obj::get_false_index()
uint16_t script_obj::get_false_index()
{
return next_false_index;
}
int script_obj::get_cond_id()
uint16_t script_obj::get_cond_id()
{
return conditional_index;
}

View File

@ -153,12 +153,12 @@ void textbox_var::set_virtual_start()
start_location_in_script = *curr_loc_ptr - 4;
}
void textbox_var::insert_text(u8 mg_array[])
void textbox_var::insert_text(const u16 *charset, u8 mg_array[])
{
set_start();
for (int parser = 0; parser < text_length; parser++)
{
if (curr_rom.is_hoenn() && (text[parser] == 0xFC) && (get_gen_3_char((char16_t)(text[parser + 1]), false) == 0x01)) // Removes colored text
if (curr_rom.is_hoenn() && (text[parser] == 0xFC) && (get_char_from_charset(charset, (char16_t)(text[parser + 1])) == 0x01)) // Removes colored text
{
parser += 2;
}
@ -172,12 +172,12 @@ void textbox_var::insert_text(u8 mg_array[])
(*curr_loc_ptr)++;
}
void textbox_var::insert_virtual_text(u8 mg_array[])
void textbox_var::insert_virtual_text(const u16 *charset, u8 mg_array[])
{
set_virtual_start();
for (int parser = 0; parser < text_length; parser++)
{
if (curr_rom.is_hoenn() && (text[parser] == 0xFC) && (get_gen_3_char((char16_t)(text[parser + 1]), false) == 0x01)) // Removes colored text
if (curr_rom.is_hoenn() && (text[parser] == 0xFC) && (get_char_from_charset(charset, (char16_t)(text[parser + 1])) == 0x01)) // Removes colored text
{
parser += 2;
}

View File

@ -6,7 +6,7 @@
#define TILE_HEIGHT 8
#define TILE_WIDTH 8
Select_Menu::Select_Menu(bool enable_cancel, int nMenu_type, int nStartTileX, int nStartTileY)
Select_Menu::Select_Menu(bool enable_cancel, u8 nMenu_type, int nStartTileX, int nStartTileY)
{
cancel_enabled = enable_cancel;
menu_type = nMenu_type;
@ -14,7 +14,7 @@ Select_Menu::Select_Menu(bool enable_cancel, int nMenu_type, int nStartTileX, in
startTileY = nStartTileY;
}
void Select_Menu::add_option(const byte *option, int return_value)
void Select_Menu::add_option(const byte *option, u8 return_value)
{
menu_options.push_back(option);
return_values.push_back(return_value);
@ -66,7 +66,7 @@ int Select_Menu::select_menu_main()
if (update)
{
if (return_values[curr_selection] == -1)
if (return_values[curr_selection] == UINT8_MAX)
{
switch (menu_type)
{
@ -140,7 +140,7 @@ void Select_Menu::clear_options()
return_values.clear();
}
void Select_Menu::set_lang(int nLang)
void Select_Menu::set_lang(u8 nLang)
{
lang = nLang;
}

View File

@ -514,13 +514,13 @@ void load_temp_box_sprites(Pokemon_Party *party_data)
load_sprite_compressed(button_confirm_right, button_edgeTiles, curr_tile_id, BTN_PAL, ATTR0_TALL, ATTR1_SIZE_8x32, 1);
}
void load_type_sprites(int pkmn_index, int dex_offset, bool is_caught)
void load_type_sprites(const u8* pkmn_type_table, int pkmn_index, int dex_offset, bool is_caught)
{
if (is_caught)
{
u32 curr_tile_id = global_tile_id_end + (dex_offset * 2 * 4);
int type1 = TYPES[pkmn_index][0];
int type2 = TYPES[pkmn_index][1];
u16 type1 = pkmn_type_table[pkmn_index * 2];
u16 type2 = pkmn_type_table[pkmn_index * 2 + 1];
load_sprite(type_sprites[(dex_offset * 2) + 0], &typesTiles[(type1 * 32)], 128, curr_tile_id, (type1 < 13 ? TYPES_PAL1 : TYPES_PAL2), ATTR0_WIDE, ATTR1_SIZE_32x8, 1);
load_sprite(type_sprites[(dex_offset * 2) + 1], &typesTiles[(type2 * 32)], 128, curr_tile_id, (type2 < 13 ? TYPES_PAL1 : TYPES_PAL2), ATTR0_WIDE, ATTR1_SIZE_32x8, 1);
@ -561,7 +561,7 @@ void load_sprite_compressed(OBJ_ATTR *sprite, const unsigned int objTiles[],
obj_hide(sprite);
};
void load_select_sprites(int game_id, int lang)
void load_select_sprites(u8 game_id, u8 lang)
{
u32 curr_tile_id = global_tile_id_end;
// Alpha Shadow Main Color Grey Black Mid

View File

@ -223,7 +223,7 @@ int ptgb_write(const byte *text, bool instant, int length)
return 0; // str - text;
}
// This is mostly used for debug stuff, I shouldn't rely it on it much.
int ptgb_write_debug(const char *text, bool instant)
int ptgb_write_debug(const u16* charset, const char *text, bool instant)
{
byte temp_holding[256];
int i;
@ -240,7 +240,7 @@ int ptgb_write_debug(const char *text, bool instant)
}
else
{
temp_holding[i] = get_gen_3_char(text[i], false);
temp_holding[i] = get_char_from_charset(charset, text[i]);
}
}
return ptgb_write(temp_holding, instant);

475
source/zx0_decompressor.cpp Normal file
View File

@ -0,0 +1,475 @@
#include "zx0_decompressor.h"
#include <cstring>
// The following code is a custom implementation of the ZX0 decompression algorithm invented by Einar Saukas
// Original implementation can be found here: https://github.com/einar-saukas/ZX0
// It uses classes, but keeps them completely hidden in the .cpp file with an anonymous namespace for internal linkage.
// The header provides a C facade to access the relevant methods, but the rest of Poke Transporter GB
// doesn't need to be aware of all the datatypes/classes defined here.
namespace
{
/**
* This class implements a ringbuffer. This is useful for ZX0 decompression
* because it allows you to look back easily.
*
* WARNING: the specified bufferSize MUST be a power of 2!
* This is needed because I optimized a modulo division (for wraparound)
* with a bitmask AND. (and it simply won't work correctly if the bufferSize is not a power of 2!)
*/
class RingBuffer
{
public:
RingBuffer(uint8_t *buffer, const uint16_t bufferSize);
/**
* Returns the buffer size.
*/
uint16_t getBufferSize() const;
uint16_t read(uint8_t *outputBuffer, uint16_t bytesToRead);
uint8_t readByte();
void writeByte(uint8_t value);
/**
* This function seeks backwards from the current end of the buffer
*/
void seekBackwardsFromBufferEnd(uint16_t offset);
void reset();
protected:
private:
uint8_t *buffer_;
uint16_t ringStartPos_;
uint16_t ringReadPos_;
uint16_t ringEndPos_;
uint16_t bufferSize_;
};
/**
* This class makes reading on a per-bit basis much easier.
*/
class BitReader
{
public:
BitReader(const uint8_t* buffer);
uint8_t readBit();
uint8_t read(uint8_t numBits);
uint8_t readByte();
protected:
private:
const uint8_t* buffer_;
const uint8_t* curBuffer_;
uint8_t currentByte_;
uint8_t bitsLeft_;
};
enum class ZX0OperationType
{
NONE,
LITERAL_BLOCK,
COPY_LAST_OFFSET,
COPY_NEW_OFFSET
};
typedef struct ZX0Command
{
ZX0OperationType cmdType;
uint16_t length;
uint16_t offset;
uint16_t bytePos;
} ZX0Command;
/**
* @brief This class implements the actual ZX0 decompression.
*/
class ZX0Decompressor
{
public:
ZX0Decompressor(uint8_t* decompressionBuffer, uint16_t decompressionBufferSize);
/**
* @brief This function prepares the ZX0Decompressor instance
* for decompressing the specified inputData
* @param compressedData
*/
void setInput(const uint8_t *inputData);
/**
* @brief Retrieves the size of the data when it is fully decompressed
* This is read from the first 2 bytes of the inputData
*/
uint16_t getDecompressedSize() const;
/**
* @brief This function reads <numBytes> of data into <outputBuffer>
*/
uint16_t read(uint8_t *outputBuffer, uint16_t numBytes);
/**
* @brief This function uncompresses from the current point until the
* specified outputBytePos lies inside the decompressionBuffer
*/
void seek(uint16_t outputBytePos);
protected:
private:
void readNextCommand();
uint16_t copy_block(uint8_t *outputBuffer, uint16_t numBytes);
RingBuffer buffer_;
BitReader reader_;
ZX0Command cur_command_;
const uint8_t *inputData_;
uint16_t bytesDecompressed_;
uint16_t lastOffset_;
};
RingBuffer::RingBuffer(uint8_t *buffer, const uint16_t bufferSize)
: buffer_(buffer)
, ringStartPos_(0)
, ringReadPos_(0)
, ringEndPos_(0)
, bufferSize_(bufferSize)
{
}
__attribute__((unused))
uint16_t RingBuffer::getBufferSize() const
{
return bufferSize_;
}
__attribute__((unused))
uint16_t RingBuffer::read(uint8_t *outputBuffer, uint16_t bytesToRead)
{
if(ringReadPos_ == ringEndPos_) return 0; // Early exit if empty
const uint16_t wrapMask = bufferSize_ - 1;
uint16_t available;
uint16_t bytesRead;
uint16_t chunkSize;
available = (ringReadPos_ < ringEndPos_) ? (ringEndPos_ - ringReadPos_) : (bufferSize_ - ringReadPos_);
chunkSize = (bytesToRead < available) ? bytesToRead : available;
memcpy(outputBuffer, buffer_ + ringReadPos_, chunkSize);
bytesRead = chunkSize;
ringReadPos_ = (ringReadPos_ + chunkSize) & wrapMask; // wraparound done by bitmask
bytesToRead -= chunkSize;
if(bytesToRead > 0 && ringReadPos_ != ringEndPos_)
{
// we need more bytes from the start of the buffer
available = ringEndPos_;
chunkSize = (bytesToRead <= ringEndPos_) ? bytesToRead : available;
memcpy(outputBuffer + bytesRead, buffer_, chunkSize);
bytesRead += chunkSize;
ringReadPos_ = chunkSize & wrapMask; // Since we wrapped around to start
}
return bytesRead;
}
uint8_t RingBuffer::readByte()
{
uint8_t value;
if(ringReadPos_ == ringEndPos_) return 0; // Early exit if empty
value = buffer_[ringReadPos_];
ringReadPos_ = (ringReadPos_ + 1) & (bufferSize_ - 1);
return value;
}
void RingBuffer::writeByte(uint8_t value)
{
buffer_[ringEndPos_] = value;
ringEndPos_ = (ringEndPos_ + 1) & (bufferSize_ - 1); // wraparound done by bitmask
if(ringEndPos_ == ringStartPos_)
{
// buffer is full, overwrite oldest byte
ringStartPos_ = (ringStartPos_ + 1) & (bufferSize_ - 1); // wraparound done by bitmask
}
}
void RingBuffer::seekBackwardsFromBufferEnd(uint16_t offset)
{
ringReadPos_ = (ringEndPos_ - offset) & (bufferSize_ - 1);
}
void RingBuffer::reset()
{
ringStartPos_ = 0;
ringReadPos_ = 0;
ringEndPos_ = 0;
}
static inline uint16_t read_elias_gamma(BitReader& reader)
{
uint16_t num_non_leading_bits = 0;
uint16_t value;
while (!reader.readBit())
{
++num_non_leading_bits; // Count leading zeros
}
// reconstruct the most significant bit of value
value = 1 << num_non_leading_bits; // Start with MSB
// now apply the binary part (the actual value)
while(num_non_leading_bits--)
{
value |= (reader.readBit() << num_non_leading_bits);
}
// Adjust back to zero-based
return value - 1;
}
static inline void read_new_offset(BitReader& reader, uint16_t& offset)
{
const uint8_t has_msb = reader.readBit();
const uint16_t lsb = reader.read(7);
const uint16_t msb = (has_msb) ? read_elias_gamma(reader) : 0;
offset = ((msb << 7) | lsb) + 1;
}
BitReader::BitReader(const uint8_t* buffer)
: buffer_(buffer)
, curBuffer_(buffer)
, currentByte_(0)
, bitsLeft_(0)
{
}
uint8_t BitReader::readBit()
{
// Pre-decrement and check underflow
if (--bitsLeft_ == 0xFF)
{
currentByte_ = (*curBuffer_);
++curBuffer_;
bitsLeft_ = 7;
}
return (currentByte_ >> bitsLeft_) & 1;
}
uint8_t BitReader::read(uint8_t numBits)
{
uint8_t result = 0;
while (numBits--)
{
result = (result << 1) | readBit();
}
return result;
}
uint8_t BitReader::readByte()
{
if(bitsLeft_ == 0)
{
const uint8_t value = (*curBuffer_);
++curBuffer_;
return value;
}
// Handle byte reads across bit boundaries
return read(8);
}
ZX0Decompressor::ZX0Decompressor(uint8_t* decompressionBuffer, uint16_t decompressionBufferSize)
: buffer_(decompressionBuffer, decompressionBufferSize)
, reader_(nullptr)
, cur_command_({ZX0OperationType::NONE, 0, 0, 0})
, inputData_(nullptr)
, bytesDecompressed_(0)
, lastOffset_(UINT16_MAX)
{
}
void ZX0Decompressor::setInput(const uint8_t *inputData)
{
buffer_.reset();
reader_ = BitReader(inputData + 2);
cur_command_ = {ZX0OperationType::NONE, 0, 0, 0};
inputData_ = inputData;
bytesDecompressed_ = 0;
lastOffset_ = UINT16_MAX;
}
uint16_t ZX0Decompressor::getDecompressedSize() const
{
if(!inputData_)
{
return 0;
}
return *((uint16_t*)inputData_);
}
uint16_t ZX0Decompressor::read(uint8_t *outputBuffer, uint16_t numBytes)
{
const uint16_t decompressed_size = getDecompressedSize();
const uint16_t bytesDecompressedBefore = bytesDecompressed_;
uint16_t bytesRead;
while(numBytes)
{
if(bytesDecompressed_ == decompressed_size)
{
break;
}
// Check if we have finished processing the previous pending command
// if we have, we need to read a new operation
if(cur_command_.cmdType == ZX0OperationType::NONE || cur_command_.bytePos >= cur_command_.length)
{
readNextCommand();
}
bytesRead = copy_block(outputBuffer + bytesDecompressed_, numBytes);
numBytes -= bytesRead;
bytesDecompressed_ += bytesRead;
}
return bytesDecompressed_ - bytesDecompressedBefore;
}
void ZX0Decompressor::seek(uint16_t outputBytePos)
{
uint8_t read_buffer[32];
uint16_t bytesToRead;
uint16_t chunkSize;
// NOTE: outputBytePos denotes the index of the byte in the output (decompressed data) buffer!!
// ZX0 doesn't actually have random access
// so we'll have to simulate it.
// first check if the specified position is in the backwards direction.
if(outputBytePos < bytesDecompressed_)
{
// the specified position is in earlier decoded data.
// this will be expensive, because we will have to start decompression from the beginning
// until the specified position.
// reset the decoder
setInput(inputData_);
bytesToRead = outputBytePos;
}
else
{
// The specified position is in future -to-be-decoded- decompressed data.
// so we just need to keep decoding until we're there.
bytesToRead = outputBytePos - bytesDecompressed_;
}
// Start decompressing until we're at the desired point
do
{
chunkSize = (bytesToRead > sizeof(read_buffer)) ? sizeof(read_buffer) : bytesToRead;
bytesToRead -= read(read_buffer, chunkSize);
}
while(bytesToRead);
}
void ZX0Decompressor::readNextCommand()
{
const uint8_t cmdBit = reader_.readBit();
// the "COPY_NEW_OFFSET" command adds + 1 to the length, but the other commands don't.
// given that read_elias_gamma() function is marked "inline", the way I set the length
// is to avoid having multiple calls to it here. (for code size)
if(cmdBit)
{
read_new_offset(reader_, lastOffset_);
cur_command_.cmdType = ZX0OperationType::COPY_NEW_OFFSET;
cur_command_.length = 1;
cur_command_.offset = lastOffset_;
cur_command_.bytePos = 0;
}
else if(cur_command_.cmdType == ZX0OperationType::LITERAL_BLOCK)
{
cur_command_.cmdType = ZX0OperationType::COPY_LAST_OFFSET;
// copy from new offset and last offset differs in the sense that with the new offset the encoded length is reduced by one
// and for last offset it isn't. This is likely because you still need to be able to insert a dummy "copy-from-last-offset" operation.
cur_command_.length = 0;
cur_command_.offset = lastOffset_;
}
else
{
cur_command_.cmdType = ZX0OperationType::LITERAL_BLOCK;
cur_command_.length = 0;
}
cur_command_.length += read_elias_gamma(reader_);
cur_command_.bytePos = 0;
}
uint16_t ZX0Decompressor::copy_block(uint8_t *outputBuffer, uint16_t numBytes)
{
const uint16_t available = cur_command_.length - cur_command_.bytePos;
const uint16_t bytesToRead = (numBytes > available) ? available : numBytes;
uint16_t bytesRemaining = bytesToRead;
if(cur_command_.cmdType == ZX0OperationType::LITERAL_BLOCK)
{
// Literal copy
do
{
(*outputBuffer) = reader_.readByte();
buffer_.writeByte((*outputBuffer));
++outputBuffer;
}
while(--bytesRemaining);
}
else
{
if(!cur_command_.bytePos)
{
buffer_.seekBackwardsFromBufferEnd(cur_command_.offset);
}
do
{
*outputBuffer = buffer_.readByte();
buffer_.writeByte(*outputBuffer);
++outputBuffer;
}
while(--bytesRemaining);
}
cur_command_.bytePos += bytesToRead;
return bytesToRead;
}
}
// define a global 2 KB decompression buffer in IWRAM.
// IWRAM is much faster than EXRAM, so it's ideally suited for decompression.
// 2 KB is a modest/reasonable size to reserve.
// But this also means we can only have one instance of ZX0Decompressor.
// This is one of the reasons why it is implemented in the way that it is.
__attribute__((section(".iwram")))
static uint8_t decompression_buffer[2048];
__attribute__((section(".iwram")))
static ZX0Decompressor decompressor(decompression_buffer, sizeof(decompression_buffer));
extern "C"
{
void zx0_decompressor_set_input(const uint8_t *input_data)
{
decompressor.setInput(input_data);
}
uint16_t zx0_decompressor_get_decompressed_size()
{
return decompressor.getDecompressedSize();
}
void zx0_decompressor_seek(uint16_t output_byte_pos)
{
decompressor.seek(output_byte_pos);
}
uint16_t zx0_decompressor_read(uint8_t *output_buffer, uint16_t num_bytes)
{
return decompressor.read(output_buffer, num_bytes);
}
}

View File

@ -0,0 +1,43 @@
# # Compiler flags
CXXFLAGS := -std=c++11 -fno-rtti -fno-exceptions -fno-unwind-tables -Wall -Wextra -I $(CURDIR) -g
# Source files directory
SRC_DIR := .
# Build directory
BUILD_DIR := build
# Source files (add more as needed)
SRCS := $(shell find $(SRC_DIR) -type f -name '*.cpp')
# Object files
OBJS := $(patsubst $(SRC_DIR)/%.cpp,$(BUILD_DIR)/%.o,$(SRCS))
# Ensure necessary directories exist
# This function ensures the directory for the target exists
define make_directory
@mkdir -p $(dir $@)
endef
# Target executable
TARGET := compressZX0
# Phony targets
.PHONY: all clean
# Default target
all: $(TARGET)
$(TARGET): $(OBJS)
$(CXX) $(CXXFLAGS) $^ -o $@
# Rule to compile source files
$(BUILD_DIR)/%.o: $(SRC_DIR)/%.cpp | $(BUILD_DIR)
$(make_directory)
$(CXX) $(CXXFLAGS) -c $< -o $@
# Create the build directory if it doesn't exist
$(BUILD_DIR):
mkdir -p $(BUILD_DIR)
# Clean rule
clean:
rm -rf $(BUILD_DIR) $(TARGET)

433
tools/compressZX0/main.cpp Normal file
View File

@ -0,0 +1,433 @@
#include <cstdio>
#include <cstdint>
#include <cstring>
// @author risingPhil
// This file implements the zx0 compression algorithm.
// It serves as a test to see if this algorithm could suit us for the compression of several static arrays/buffers in Poke Transporter GB.
//#define LOG_OPERATIONS 1
#define MAX_OFFSET 2048 // Maximum backward offset (ZX0 limit), tuned for Poke Transporter GB to be able to use a 2 KB decompression buffer
#define MAX_LEN 255 // Maximum match length (ZX0 limit)
#define OUTPUT_BUFFER_SIZE 256 * 1024 // Maximum output buffer size (artificial limitation, I suppose)
#ifdef LOG_OPERATIONS
#define LOG_OP(...) printf(__VA_ARGS__)
#else
#define LOG_OP(...)
#endif
// anonymous namespace for internal linkage
namespace
{
/**
* This class makes writing on a per-bit basis much easier
*/
class BitWriter
{
public:
BitWriter(uint8_t* buffer);
void write(uint8_t value, uint8_t numBits);
void write_bit(uint8_t value);
void write_byte(uint8_t value);
size_t get_bits_written() const;
protected:
private:
uint8_t* buffer_;
uint8_t* cur_buffer_;
uint8_t bit_index_;
};
BitWriter::BitWriter(uint8_t* buffer)
: buffer_(buffer)
, cur_buffer_(buffer)
, bit_index_(0)
{
}
void BitWriter::write_bit(uint8_t value)
{
value &= 0x1;
*cur_buffer_ |= (value << (7 - bit_index_));
if(bit_index_ == 7)
{
++cur_buffer_;
bit_index_ = 0;
}
else
{
++bit_index_;
}
}
void BitWriter::write_byte(uint8_t value)
{
if(bit_index_)
{
write(value, 8);
return;
}
*cur_buffer_ = value;
++cur_buffer_;
}
void BitWriter::write(uint8_t value, uint8_t numBits)
{
for(int i=numBits - 1; i >= 0; --i)
{
write_bit(value >> i);
}
}
size_t BitWriter::get_bits_written() const
{
return (cur_buffer_ - buffer_) * 8 + bit_index_;
}
}
/**
* @brief Find the best match for the current position (LZ77-style)
* We simply try to find the longest matching bytes backwards in the buffer.
*/
static void find_backwards_match(const unsigned char *buffer, size_t buffer_size, int pos, int *best_offset, int *best_len)
{
*best_offset = 0;
*best_len = 0;
const size_t max_offset = (pos > MAX_OFFSET) ? MAX_OFFSET : pos;
const int max_len = (buffer_size - pos > MAX_LEN) ? MAX_LEN : buffer_size - pos;
int len;
for (size_t offset = 1; offset <= max_offset; offset++)
{
len = 0;
while (len < max_len && buffer[pos - offset + len] == buffer[pos + len])
{
++len;
}
if (len > *best_len)
{
*best_len = len;
*best_offset = offset;
}
}
}
/**
* @brief This function encodes the specified value with gamma encoding.
*
* The way it works is that we first determine of how many bits the value consists, except for the leading bits. (=num_non_leading_bits)
* Then we write <num_non_leading_bits> zeros.
* We also write the original value in <num_non_leading_bits + 1> bits
*
* For decoding, we can determine the number of zeros and that will indicate how many bits we need to read for the actual value.
*
*/
static void write_elias_gamma(BitWriter& writer, int value)
{
value++; // Adjust because Gamma only encodes n ≥ 1
int num_non_leading_bits = 0;
int i;
// Calculate floor(log2(value))
int tmp = value >> 1;
while(tmp)
{
++num_non_leading_bits;
tmp >>= 1;
}
// Write unary part (k zeros)
for (i = 0; i < num_non_leading_bits; i++)
{
writer.write_bit(0);
}
// Write binary part (num_non_leading_bits+1 bits of value)
for (int i = num_non_leading_bits; i >= 0; i--)
{
writer.write_bit(value >> i);
}
}
/**
* This struct represents a buffer to hold a number of pending "literal" bytes
* before they actually get written to the output
*/
typedef struct LiteralBuffer
{
uint8_t buffer[1024];
uint16_t size;
} LiteralBuffer;
/**
* @brief This function writes a command for the decompressor to start copying <length> bytes
* from the last offset specified with the write_copy_from_new_offset_block() function
*/
static void write_copy_from_last_offset_block(BitWriter& writer, int length)
{
LOG_OP("copy_last: %d\n", length);
writer.write_bit(0);
write_elias_gamma(writer, length);
}
/**
* @brief Writes a command to copy the bytes in LiteralBuffer to the decompressed buffer.
*/
static void write_literal_block(BitWriter& writer, LiteralBuffer& literal_buffer)
{
uint16_t i = 0;
if(!literal_buffer.size)
{
return;
}
LOG_OP("copy_literal: %hu\n", literal_buffer.size);
// flag that this is a literal block
writer.write_bit(0);
write_elias_gamma(writer, literal_buffer.size);
while(i < literal_buffer.size)
{
writer.write_byte(literal_buffer.buffer[i]);
++i;
}
literal_buffer.size = 0;
}
/**
* @brief Writes a command to indicate that the decompressor must copy <length> bytes from the given backwards offset.
*/
static void write_copy_from_new_offset_block(BitWriter& writer, int offset, int length)
{
LOG_OP("copy_new: offset: %d, length: %d\n", offset, length);
writer.write_bit(1); // Match flag
// Encode offset (Elias Gamma + 7-bit LSB)
const int msb = ((offset - 1) >> 7) & 0xFF;
const int lsb = (offset - 1) & 0x7F;
// first bit of LSB indicates whether the MSB follows.
writer.write_bit((msb > 0));
// write 7 bit LSB raw bits
writer.write(lsb, 7);
if (msb > 0)
{
write_elias_gamma(writer, msb);
}
// Encode length (Elias Gamma)
write_elias_gamma(writer, length - 1);
}
static void literal_buffer_push(BitWriter& writer, LiteralBuffer& literal_buffer, uint8_t byte)
{
if(literal_buffer.size == 1024)
{
// EDGE case: buffer is full.
// back-to-back literal blocks are forbidden,
// so we must insert a dummy "use last offset" block
write_literal_block(writer, literal_buffer);
write_copy_from_last_offset_block(writer, 0);
}
literal_buffer.buffer[literal_buffer.size] = byte;
++literal_buffer.size;
}
/**
* This function encodes the specified buffer with the ZX0 compression algorithm
* and stores the result into output_buffer.
*
* Please make sure the output_buffer is sufficiently large enough before calling this function.
*/
static size_t encodeZX0(uint8_t* output_buffer, const uint8_t* buffer, size_t buffer_size)
{
BitWriter writer(output_buffer);
LiteralBuffer literal_buffer = {
.buffer = {0},
.size = 0
};
int pos = 0;
int last_offset = 0x7FFFFFFF;
int offset;
int length;
int numBytes = buffer_size;
// first write the size of the input in little endian format in the output buffer
writer.write_byte(static_cast<uint8_t>(buffer_size));
writer.write_byte(static_cast<uint8_t>(buffer_size >> 8));
while(pos < numBytes)
{
find_backwards_match(buffer, numBytes, pos, &offset, &length);
// important rules: You cannot have 2 consecutive literal blocks.
// reusing the last offset can only happen after a literal block!
if(length < 2)
{
// we must buffer the literals because we can only start writing them when we know the "length"
literal_buffer_push(writer, literal_buffer, buffer[pos]);
++pos;
}
else if(offset == last_offset)
{
// write any pending literal bytes
write_literal_block(writer, literal_buffer);
write_copy_from_last_offset_block(writer, length);
pos += length;
}
else
{
// write any pending literal bytes
write_literal_block(writer, literal_buffer);
write_copy_from_new_offset_block(writer, offset, length);
last_offset = offset;
pos += length;
}
}
return writer.get_bits_written();
}
/**
* @brief Reads the given file completely into the specified buffer.
* The buffer is allocated by this function, but should be delete[]'d by the caller.
*/
static bool read_file(const char* filename, uint8_t*& out_buffer, size_t& out_size)
{
FILE* file;
long size;
size_t read;
uint8_t* buffer;
file = fopen(filename, "rb");
if (!file) return false;
// Seek to end to determine size
if (fseek(file, 0, SEEK_END) != 0)
{
fclose(file);
return false;
}
size = ftell(file);
if (size < 0)
{
fclose(file);
return false;
}
rewind(file);
buffer = new uint8_t[size];
read = fread(buffer, 1, size, file);
fclose(file);
if (read != (size_t)size) {
delete[] buffer;
return false;
}
out_buffer = buffer;
out_size = size;
return true;
}
static void print_usage()
{
const char* usageString = R"delim(
Usage: compressZX0 <path/to/file> <output_path>
This program will compress the given file with the ZX0 compression algorithm and store the output in
<output_path>/<filename_without_extension>_zx0.bin
)delim";
printf(usageString);
}
int main(int argc, char** argv)
{
// Reserve 256KB buffer, which is already much larger than the maximum file size we'd allow for PTGB.
// (the reason why I'm using a buffer instead of writing directly to a file is simply because I'm lazy.
// I wrote a test of the algorithm using buffers first. And I know that for Poke Transporter GB specifically
// we'll never exceed the 256KB filesize. So I'm not going to rework this code, because there's currently no need)
uint8_t output_buffer[OUTPUT_BUFFER_SIZE] = {0};
uint8_t *input_buffer = nullptr;
char *filename;
char *extension_dot;
size_t input_buffer_size;
size_t bits_written;
size_t num_bytes;
double compress_ratio;
char output_path[4096];
FILE* f;
if(argc < 3)
{
print_usage();
return 1;
}
if(!read_file(argv[1], input_buffer, input_buffer_size))
{
perror("Could not open file: ");
return 1;
}
// make sure the input_buffer_size is not larger than our output_buffer we statically allocated
// This is a bit of an artificial limitation though.
if(input_buffer_size > sizeof(output_buffer))
{
fprintf(stderr, "ERROR: The input file should not be larger than %zu KB!\n", sizeof(output_buffer));
return 1;
}
// get the filename part of the given file
// and remove the extension.
// basename uses statically allocated memory that gets overwritten by each call.
// but it returns a modifiable char*
// so we might as well just edit that buffer directly because no-one will depend on this value later.
filename = basename(argv[1]);
printf("Compressing %s...", filename);
bits_written = encodeZX0(output_buffer, input_buffer, input_buffer_size);
delete[] input_buffer;
input_buffer = nullptr;
num_bytes = (bits_written + 7) / 8;
printf("done\n");
// if we have an extension in the filename, just end the string at the '.' position.
extension_dot = strchr(filename, '.');
if(extension_dot)
{
*extension_dot = '\0';
}
// argv[2] should be the output directory
snprintf(output_path, sizeof(output_path), "%s/%s_zx0.bin", argv[2], filename);
f = fopen(output_path, "wb+");
fwrite(output_buffer, 1, num_bytes, f);
fclose(f);
compress_ratio = static_cast<double>(num_bytes) / input_buffer_size;
printf("Compressed size: %zu bytes/%zu bytes, Compression ratio: %f%%\n", num_bytes, input_buffer_size, compress_ratio * 100.f);
return 0;
}

View File

@ -0,0 +1,43 @@
# # Compiler flags
CXXFLAGS := -std=c++11 -fno-rtti -fno-exceptions -fno-unwind-tables -Wall -Wextra -I $(CURDIR)/include -g
# Source files directory
SRC_DIR := ./src
# Build directory
BUILD_DIR := build
# Source files (add more as needed)
SRCS := $(shell find $(SRC_DIR) -type f -name '*.cpp')
# Object files
OBJS := $(patsubst $(SRC_DIR)/%.cpp,$(BUILD_DIR)/%.o,$(SRCS))
# Ensure necessary directories exist
# This function ensures the directory for the target exists
define make_directory
@mkdir -p $(dir $@)
endef
# Target executable
TARGET := data-generator
# Phony targets
.PHONY: all clean
# Default target
all: $(TARGET)
$(TARGET): $(OBJS)
$(CXX) $(CXXFLAGS) $^ -o $@
# Rule to compile source files
$(BUILD_DIR)/%.o: $(SRC_DIR)/%.cpp | $(BUILD_DIR)
$(make_directory)
$(CXX) $(CXXFLAGS) -c $< -o $@
# Create the build directory if it doesn't exist
$(BUILD_DIR):
mkdir -p $(BUILD_DIR)
# Clean rule
clean:
rm -rf $(BUILD_DIR) $(TARGET) *.bin

View File

@ -0,0 +1,9 @@
#ifndef _COMMON_H
#define _COMMON_H
#include <cstdint>
#include <cstddef>
void writeTable(const char* filename, const uint8_t *buffer, size_t buffer_size);
#endif

View File

@ -0,0 +1,6 @@
#ifndef _POKEMON_DATA_H
#define _POKEMON_DATA_H
void generate_pokemon_data();
#endif

View File

@ -0,0 +1,12 @@
#include "common.h"
#include <cstdio>
void writeTable(const char* filename, const uint8_t *buffer, size_t buffer_size)
{
FILE* f;
f = fopen(filename, "wb+");
fwrite(buffer, 1, buffer_size, f);
fclose(f);
}

View File

@ -0,0 +1,11 @@
#include "pokemon_data.h"
// This application holds the various long static data arrays that Poke Transporter GB uses
// and it writes them to .bin files that can be compressed with compressZX0 later.
// it's useful to do it this way because it keeps this data easy to view, edit and document
int main(int /*argc*/, char **/*argv*/)
{
generate_pokemon_data();
return 0;
}

File diff suppressed because it is too large Load Diff