first commit

This commit is contained in:
Holly McFarland 2023-09-29 15:07:18 -04:00
commit 104334eeb9
6 changed files with 691 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
build
*.gba
*.elf

13
LICENSE.txt Normal file
View File

@ -0,0 +1,13 @@
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
Version 2, December 2004
Copyright (C) 2004 Sam Hocevar <sam@hocevar.net>
Everyone is permitted to copy and distribute verbatim or modified
copies of this license document, and changing it is allowed as long
as the name is changed.
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. You just DO WHAT THE FUCK YOU WANT TO.

285
Makefile Normal file
View File

@ -0,0 +1,285 @@
#
# Template tonc makefile
#
# Yoinked mostly from DKP's template
#
# === SETUP ===========================================================
# --- No implicit rules ---
.SUFFIXES:
# --- Paths ---
export TONCLIB := ${DEVKITPRO}/libtonc
# === TONC RULES ======================================================
#
# Yes, this is almost, but not quite, completely like to
# DKP's base_rules and gba_rules
#
export PATH := $(DEVKITARM)/bin:$(DEVKITPRO)/tools/bin:$(PATH)
# --- Executable names ---
PREFIX ?= arm-none-eabi-
export CC := $(PREFIX)gcc
export CXX := $(PREFIX)g++
export AS := $(PREFIX)as
export AR := $(PREFIX)ar
export NM := $(PREFIX)nm
export OBJCOPY := $(PREFIX)objcopy
# LD defined in Makefile
# === LINK / TRANSLATE ================================================
%.gba : %.elf
@$(OBJCOPY) -O binary $< $@
@echo built ... $(notdir $@)
@gbafix $@ -t$(TITLE)
#----------------------------------------------------------------------
%.mb.elf :
@echo Linking multiboot
$(LD) -specs=gba_mb.specs $(LDFLAGS) $(OFILES) $(LIBPATHS) $(LIBS) -o $@
$(NM) -Sn $@ > $(basename $(notdir $@)).map
#----------------------------------------------------------------------
%.elf :
@echo Linking cartridge
$(LD) -specs=gba.specs $(LDFLAGS) $(OFILES) $(LIBPATHS) $(LIBS) -o $@
$(NM) -Sn $@ > $(basename $(notdir $@)).map
#----------------------------------------------------------------------
%.a :
@echo $(notdir $@)
@rm -f $@
$(AR) -crs $@ $^
# === OBJECTIFY =======================================================
%.iwram.o : %.iwram.cpp
@echo $(notdir $<)
$(CXX) -MMD -MP -MF $(DEPSDIR)/$*.d $(CXXFLAGS) $(IARCH) -c $< -o $@
#----------------------------------------------------------------------
%.iwram.o : %.iwram.c
@echo $(notdir $<)
$(CC) -MMD -MP -MF $(DEPSDIR)/$*.d $(CFLAGS) $(IARCH) -c $< -o $@
#----------------------------------------------------------------------
%.o : %.cpp
@echo $(notdir $<)
$(CXX) -MMD -MP -MF $(DEPSDIR)/$*.d $(CXXFLAGS) $(RARCH) -c $< -o $@
#----------------------------------------------------------------------
%.o : %.c
@echo $(notdir $<)
$(CC) -MMD -MP -MF $(DEPSDIR)/$*.d $(CFLAGS) $(RARCH) -c $< -o $@
#----------------------------------------------------------------------
%.o : %.s
@echo $(notdir $<)
$(CC) -MMD -MP -MF $(DEPSDIR)/$*.d -x assembler-with-cpp $(ASFLAGS) -c $< -o $@
#----------------------------------------------------------------------
%.o : %.S
@echo $(notdir $<)
$(CC) -MMD -MP -MF $(DEPSDIR)/$*.d -x assembler-with-cpp $(ASFLAGS) -c $< -o $@
#----------------------------------------------------------------------
# canned command sequence for binary data
#----------------------------------------------------------------------
define bin2o
bin2s $< | $(AS) -o $(@)
echo "extern const u8" `(echo $(<F) | sed -e 's/^\([0-9]\)/_\1/' | tr . _)`"_end[];" > `(echo $(<F) | tr . _)`.h
echo "extern const u8" `(echo $(<F) | sed -e 's/^\([0-9]\)/_\1/' | tr . _)`"[];" >> `(echo $(<F) | tr . _)`.h
echo "extern const u32" `(echo $(<F) | sed -e 's/^\([0-9]\)/_\1/' | tr . _)`_size";" >> `(echo $(<F) | tr . _)`.h
endef
# =====================================================================
# --- Main path ---
export PATH := $(DEVKITARM)/bin:$(PATH)
# === PROJECT DETAILS =================================================
# PROJ : Base project name
# TITLE : Title for ROM header (12 characters)
# LIBS : Libraries to use, formatted as list for linker flags
# BUILD : Directory for build process temporaries. Should NOT be empty!
# SRCDIRS : List of source file directories
# DATADIRS : List of data file directories
# INCDIRS : List of header file directories
# LIBDIRS : List of library directories
# General note: use `.' for the current dir, don't leave the lists empty.
export PROJ ?= $(notdir $(CURDIR))
TITLE := $(PROJ)
LIBS := -ltonc
BUILD := build
SRCDIRS := src lib
DATADIRS := data
INCDIRS := src
LIBDIRS := $(TONCLIB)
# --- switches ---
bMB := 1 # Multiboot build
bTEMPS := 0 # Save gcc temporaries (.i and .s files)
bDEBUG2 := 0 # Generate debug info (bDEBUG2? Not a full DEBUG flag. Yet)
# === BUILD FLAGS =====================================================
# This is probably where you can stop editing
# NOTE: I've noticed that -fgcse and -ftree-loop-optimize sometimes muck
# up things (gcse seems fond of building masks inside a loop instead of
# outside them for example). Removing them sometimes helps
# --- Architecture ---
ARCH := -mthumb-interwork -mthumb
RARCH := -mthumb-interwork -mthumb
IARCH := -mthumb-interwork -marm -mlong-calls
# --- Main flags ---
CFLAGS := -mcpu=arm7tdmi -mtune=arm7tdmi -O2
CFLAGS += -Wall
CFLAGS += $(INCLUDE)
CFLAGS += -ffast-math -fno-strict-aliasing
CXXFLAGS := $(CFLAGS) -fno-rtti -fno-exceptions
ASFLAGS := $(ARCH) $(INCLUDE)
LDFLAGS := $(ARCH) -Wl,-Map,$(PROJ).map
# --- switched additions ----------------------------------------------
# --- Multiboot ? ---
ifeq ($(strip $(bMB)), 1)
TARGET := $(PROJ).mb
else
TARGET := $(PROJ)
CXXFLAGS += -DNOMULTIBOOT
endif
# --- Save temporary files ? ---
ifeq ($(strip $(bTEMPS)), 1)
CFLAGS += -save-temps
CXXFLAGS += -save-temps
endif
# --- Debug info ? ---
ifeq ($(strip $(bDEBUG)), 1)
CFLAGS += -DDEBUG -g
CXXFLAGS += -DDEBUG -g
ASFLAGS += -DDEBUG -g
LDFLAGS += -g
else
CFLAGS += -DNDEBUG
CXXFLAGS += -DNDEBUG
ASFLAGS += -DNDEBUG
endif
# === BUILD PROC ======================================================
ifneq ($(BUILD),$(notdir $(CURDIR)))
# Still in main dir:
# * Define/export some extra variables
# * Invoke this file again from the build dir
# PONDER: what happens if BUILD == "" ?
export OUTPUT := $(CURDIR)/$(TARGET)
export VPATH := \
$(foreach dir, $(SRCDIRS) , $(CURDIR)/$(dir)) \
$(foreach dir, $(DATADIRS), $(CURDIR)/$(dir))
export DEPSDIR := $(CURDIR)/$(BUILD)
# --- List source and data files ---
CFILES := $(foreach dir, $(SRCDIRS) , $(notdir $(wildcard $(dir)/*.c)))
CPPFILES := $(foreach dir, $(SRCDIRS) , $(notdir $(wildcard $(dir)/*.cpp)))
SFILES := $(foreach dir, $(SRCDIRS) , $(notdir $(wildcard $(dir)/*.s)))
BINFILES := $(foreach dir, $(DATADIRS), $(notdir $(wildcard $(dir)/*.*)))
# --- Set linker depending on C++ file existence ---
ifeq ($(strip $(CPPFILES)),)
export LD := $(CC)
else
export LD := $(CXX)
endif
# --- Define object file list ---
export OFILES := $(addsuffix .o, $(BINFILES)) \
$(CFILES:.c=.o) $(CPPFILES:.cpp=.o) \
$(SFILES:.s=.o)
# --- Create include and library search paths ---
export INCLUDE := $(foreach dir,$(INCDIRS),-I$(CURDIR)/$(dir)) \
$(foreach dir,$(LIBDIRS),-I$(dir)/include) \
-I$(CURDIR)/$(BUILD)
export LIBPATHS := -L$(CURDIR) $(foreach dir,$(LIBDIRS),-L$(dir)/lib)
# --- Create BUILD if necessary, and run this makefile from there ---
$(BUILD):
@[ -d $@ ] || mkdir -p $@
@make --no-print-directory -C $(BUILD) -f $(CURDIR)/Makefile
arm-none-eabi-nm -Sn $(OUTPUT).elf > $(BUILD)/$(TARGET).map
all : $(BUILD)
clean:
@echo clean ...
@rm -rf $(BUILD) $(TARGET).elf $(TARGET).gba $(TARGET).sav
else # If we're here, we should be in the BUILD dir
DEPENDS := $(OFILES:.o=.d)
# --- Main targets ----
$(OUTPUT).gba : $(OUTPUT).elf
$(OUTPUT).elf : $(OFILES)
-include $(DEPENDS)
endif # End BUILD switch
# --- More targets ----------------------------------------------------
.PHONY: clean rebuild start
rebuild: clean $(BUILD)
start:
start "$(TARGET).gba"
restart: rebuild start
# EOF

15
README.md Normal file
View File

@ -0,0 +1,15 @@
# pokemon stat view
GBA homebrew for checking the stats of pokemon in the current party of a gen 3 save file, by inserting the cartridge you want to check.
![Photo of a GBA running the software. Six pokemon are listed, showing their stats, EVs, IVs, and natures.](image.jpg)
To use, run `pokemon_stat_view.mb.gba` via multiboot or a flashcart. Once it's loaded, it continuously checks the cartridge slot to see if it contains a generation 3 pokemon game. If it does, the screen is refreshed with the details of that save file's party. Simply tear out your current cartridge (if applicable) and insert the game you want to check. You can keep swapping in new cartridges for as long as the software stays running. (Hot tip: If you're loading it via multiboot, you can have the game you want to check already inserted, and then press Start+Select during startup to cancel booting to cartridge ROM.)
**Be sure to back up any precious save files.** This software *shouldn't* cause any problems for your save files (as it doesn't perform any writes beyond a few magic bytes for bank switching), and it didn't hurt any of mine during any of my testing, but hotswapping GBA cartridges is highly cursed. God only knows what kind of devine punishment one invites upon themselves by accepting this forbidden technique into their lives. Please do not come to me to say that it ruined your childhood save file; I can't fix it and it will make me sad.
# building
Should just be able to run `make clean; make build` to get it going. Flip the `bMB` flag in the makefile from 1 to 0 to get a non-multiboot build, for use in testing with an emulator (where you can supply any save file you like). This removes the code that repeatedly tests for the existence of a Pokemon cartridge, instead loading the save file once and then sitting there forever. I bet there's a way to set up make so it builds this rom with a different command, instead of making you edit a flag in the makefile, but I'll be damned if I know how to do it.
I'm not exactly a C++ developer by day, which you can also tell because the code is the way it is.

BIN
image.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

375
src/main.cpp Normal file
View File

@ -0,0 +1,375 @@
#include <tonc.h>
#include <string>
#include <cstring>
void log(std::string text) {
tte_write("#{es; P:0,0}");
tte_write(text.c_str());
}
void init() {
REG_DISPCNT= DCNT_MODE0 | DCNT_BG0;
tte_init_se_default(0, BG_CBB(0) | BG_SBB(31));
}
int get_int_from_sram(int offset) {
// I don't know why just reading an int gives me weird values
// They're close to if it was reading in big endian, but not exactly
// e.g. 0x000001DA -> 3671775962
// But whatever
// NOTE FROM FUTURE HOLLY: Is it because the port width for sram is only one byte?
// Whatever, I've decided I don't care
int out = 0;
for (int i=3; i>=0; i--) {
out += (*((char*)(MEM_SRAM + offset + i)) << (i*8));
}
return out;
}
int get_save_game_offset() {
// Get offset to the more recent of the two saved games (other is a backup)
int a = get_int_from_sram(0x0FFC);
int b = get_int_from_sram(0xEFFC);
if (a > b) {
return 0x0000;
}
return 0xE000;
}
int data_offset(int personality_value, u8 structure) {
// the order of the four substructures in a pokemon's "data"
// structure is determined by the pokemon's personality value
// mod 24
u16 structure_orders[24] = {
0x0123,
0x0132,
0x0213,
0x0231,
0x0312,
0x0321,
0x1023,
0x1032,
0x1203,
0x1230,
0x1302,
0x1320,
0x2013,
0x2031,
0x2103,
0x2130,
0x2301,
0x2310,
0x3012,
0x3021,
0x3102,
0x3120,
0x3201,
0x3210,
};
u16 this_order = structure_orders[(u32)(personality_value) % 24];
for (int i=0; i<4; i++) {
if (((this_order >> (12 - i*4)) & 0xf) == structure) {
return i*12;
}
}
// will never happen unless i really mess this up
return -1;
}
void add_num(int num, std::string* stats) {
// Write a number to stats, right-justifying it to 3 columns
if (num < 100) {
*stats += " ";
}
if (num < 10) {
*stats += " ";
}
*stats += std::to_string(num);
}
void add_num2(int num, std::string* stats) {
// Write a number to stats, right-justifying it to 2 columns
if (num < 10) {
*stats += " ";
}
*stats += std::to_string(num);
}
char get_nature_mod(char nature, char stat) {
// '+' if stat is boosed by nature, '-' if it's decreased, ' ' if neutral
int up[5] = {
0b0000000000000000000011110, // atk
0b0000000000000001110100000, // def
0b0000010111000000000000000, // spa
0b0111100000000000000000000, // spd
0b0000000000110110000000000, // spe
};
int down[5] = {
0b0000100001000010000100000, // atk
0b0001000010000100000000010, // def
0b0100000000010000100001000, // spa
0b0000010000100001000010000, // spd
0b0010000100000000010000100, // spe
};
u8 index = stat; // oh my god g++ shut UP
if ((up[index] >> nature) & 1) {
return '+';
}
if ((down[index] >> nature) & 1) {
return '-';
}
return ' ';
}
void interpret_pokemon(int offset, std::string* stats) {
// Add info about the pokemon's stats to the string
char names[445][8] = {"??????", "Bulbas", "Ivysau", "Venusa", "Charma", "Charme", "Chariz", "Squirt", "Wartor", "Blasto", "Caterp", "Metapo", "Butter", "Weedle", "Kakuna", "Beedri", "Pidgey", "Pidgeo", "Pidgeo", "Rattat", "Ratica", "Spearo", "Fearow", "Ekans", "Arbok", "Pikach", "Raichu", "Sandsh", "Sandsl", "Nidora", "Nidori", "Nidoqu", "Nidora", "Nidori", "Nidoki", "Clefai", "Clefab", "Vulpix", "Nineta", "Jiggly", "Wiggly", "Zubat", "Golbat", "Oddish", "Gloom", "Vilepl", "Paras", "Parase", "Venona", "Venomo", "Diglet", "Dugtri", "Meowth", "Persia", "Psyduc", "Golduc", "Mankey", "Primea", "Growli", "Arcani", "Poliwa", "Poliwh", "Poliwr", "Abra", "Kadabr", "Alakaz", "Machop", "Machok", "Macham", "Bellsp", "Weepin", "Victre", "Tentac", "Tentac", "Geodud", "Gravel", "Golem", "Ponyta", "Rapida", "Slowpo", "Slowbr", "Magnem", "Magnet", "Farfet", "Doduo", "Dodrio", "Seel", "Dewgon", "Grimer", "Muk", "Shelld", "Cloyst", "Gastly", "Haunte", "Gengar", "Onix", "Drowze", "Hypno", "Krabby", "Kingle", "Voltor", "Electr", "Exeggc", "Exeggu", "Cubone", "Marowa", "Hitmon", "Hitmon", "Lickit", "Koffin", "Weezin", "Rhyhor", "Rhydon", "Chanse", "Tangel", "Kangas", "Horsea", "Seadra", "Goldee", "Seakin", "Staryu", "Starmi", "Mr.", "Mime", "Scythe", "Jynx", "Electa", "Magmar", "Pinsir", "Tauros", "Magika", "Gyarad", "Lapras", "Ditto", "Eevee", "Vapore", "Jolteo", "Flareo", "Porygo", "Omanyt", "Omasta", "Kabuto", "Kabuto", "Aeroda", "Snorla", "Articu", "Zapdos", "Moltre", "Dratin", "Dragon", "Dragon", "Mewtwo", "Mew", "Chikor", "Baylee", "Megani", "Cyndaq", "Quilav", "Typhlo", "Totodi", "Crocon", "Ferali", "Sentre", "Furret", "Hootho", "Noctow", "Ledyba", "Ledian", "Spinar", "Ariado", "Crobat", "Chinch", "Lantur", "Pichu", "Cleffa", "Igglyb", "Togepi", "Togeti", "Natu", "Xatu", "Mareep", "Flaaff", "Amphar", "Bellos", "Marill", "Azumar", "Sudowo", "Polito", "Hoppip", "Skiplo", "Jumplu", "Aipom", "Sunker", "Sunflo", "Yanma", "Wooper", "Quagsi", "Espeon", "Umbreo", "Murkro", "Slowki", "Misdre", "Unown", "Wobbuf", "Girafa", "Pineco", "Forret", "Dunspa", "Gligar", "Steeli", "Snubbu", "Granbu", "Qwilfi", "Scizor", "Shuckl", "Heracr", "Snease", "Teddiu", "Ursari", "Slugma", "Magcar", "Swinub", "Pilosw", "Corsol", "Remora", "Octill", "Delibi", "Mantin", "Skarmo", "Houndo", "Houndo", "Kingdr", "Phanpy", "Donpha", "Porygo", "Stantl", "Smearg", "Tyrogu", "Hitmon", "Smooch", "Elekid", "Magby", "Miltan", "Blisse", "Raikou", "Entei", "Suicun", "Larvit", "Pupita", "Tyrani", "Lugia", "Ho-Oh", "Celebi", "?", "?", "?", "?", "?", "?", "?", "?", "?", "?", "?", "?", "?", "?", "?", "?", "?", "?", "?", "?", "?", "?", "?", "?", "?", "Treeck", "Grovyl", "Scepti", "Torchi", "Combus", "Blazik", "Mudkip", "Marsht", "Swampe", "Poochy", "Mighty", "Zigzag", "Linoon", "Wurmpl", "Silcoo", "Beauti", "Cascoo", "Dustox", "Lotad", "Lombre", "Ludico", "Seedot", "Nuzlea", "Shiftr", "Nincad", "Ninjas", "Shedin", "Taillo", "Swello", "Shroom", "Breloo", "Spinda", "Wingul", "Pelipp", "Surski", "Masque", "Wailme", "Wailor", "Skitty", "Delcat", "Kecleo", "Baltoy", "Claydo", "Nosepa", "Torkoa", "Sabley", "Barboa", "Whisca", "Luvdis", "Corphi", "Crawda", "Feebas", "Miloti", "Carvan", "Sharpe", "Trapin", "Vibrav", "Flygon", "Makuhi", "Hariya", "Electr", "Manect", "Numel", "Cameru", "Spheal", "Sealeo", "Walrei", "Cacnea", "Cactur", "Snorun", "Glalie", "Lunato", "Solroc", "Azuril", "Spoink", "Grumpi", "Plusle", "Minun", "Mawile", "Mediti", "Medich", "Swablu", "Altari", "Wynaut", "Duskul", "Dusclo", "Roseli", "Slakot", "Vigoro", "Slakin", "Gulpin", "Swalot", "Tropiu", "Whismu", "Loudre", "Explou", "Clampe", "Huntai", "Goreby", "Absol", "Shuppe", "Banett", "Sevipe", "Zangoo", "Relica", "Aron", "Lairon", "Aggron", "Castfo", "Volbea", "Illumi", "Lileep", "Cradil", "Anorit", "Armald", "Ralts", "Kirlia", "Gardev", "Bagon", "Shelgo", "Salame", "Beldum", "Metang", "Metagr", "Regiro", "Regice", "Regist", "Kyogre", "Groudo", "Rayqua", "Latias", "Latios", "Jirach", "Deoxys", "Chimec", "Pokémo", "Egg", "Unown", "Unown", "Unown", "Unown", "Unown", "Unown", "Unown", "Unown", "Unown", "Unown", "Unown", "Unown", "Unown", "Unown", "Unown", "Unown", "Unown", "Unown", "Unown", "Unown", "Unown", "Unown", "Unown", "Unown", "Unown", "Unown", "Unown", "??????", "Bonsly", "Munchl"};
int personality_value = get_int_from_sram(offset);
int ot_id = get_int_from_sram(offset+4);
int evs = data_offset(personality_value, 2);
int ivs = data_offset(personality_value, 3) + 4;
int species_id = (get_int_from_sram(offset+32+data_offset(personality_value, 0)) ^ ot_id ^ personality_value) & 0xffff;
int hpatk = get_int_from_sram(offset+88);
int atk = hpatk >> 0x10;
int hp = hpatk & 0xff;
int defspe = get_int_from_sram(offset+92);
int spe = defspe >> 0x10;
int def = defspe & 0xff;
int spaspd = get_int_from_sram(offset+96);
int spd = spaspd >> 0x10;
int spa = spaspd & 0xff;
char nature = ((u32)(personality_value)) % 25;
// all these places where a value is XORed with the OT ID and the personality
// value are because the *whole "data" structure* is repeatedly XORed with them.
// i could have done the "decryption" somewhere else but every value i need out
// of the structure happens to be aligned to four bytes, the length of the key,
// so fuck it basically
int iv_offset = offset+32+ivs;
u32 iv = (u32)(get_int_from_sram(iv_offset) ^ ot_id ^ personality_value);
int hp_iv = iv & 31;
int atk_iv = (iv >> 5) & 31;
int def_iv = (iv >> 10) & 31;
int spe_iv = (iv >> 15) & 31;
int spa_iv = (iv >> 20) & 31;
int spd_iv = (iv >> 25) & 31;
int ev_offset = offset+32+evs;
u32 ev1 = (u32)(get_int_from_sram(ev_offset) ^ ot_id ^ personality_value);
int hp_ev = ev1 & 0xf;
int atk_ev = (ev1 >> 8) & 0xf;
int def_ev = (ev1 >> 16) & 0xf;
int spe_ev = (ev1 >> 24) & 0xf;
u32 ev2 = (u32)(get_int_from_sram(ev_offset + 4) ^ ot_id ^ personality_value);
int spa_ev = ev2 & 0xf;
int spd_ev = (ev2 >> 8) & 0xf;
// this is how you're supposed to write c++ right?
char* name = names[species_id + 1];
const char* padding = " " + strlen(name);
*stats += padding;
*stats += name;
*stats += " ";
//add_num(species_id, stats); *stats += " ";
add_num(hp, stats); *stats += " ";
add_num(atk, stats); *stats += " ";
add_num(def, stats); *stats += " ";
add_num(spa, stats); *stats += " ";
add_num(spd, stats); *stats += " ";
add_num(spe, stats); *stats += "\n";
*stats += "Nat/IV ";
add_num(hp_iv, stats); *stats += " ";
*stats += get_nature_mod(nature, 0); add_num2(atk_iv, stats); *stats += " ";
*stats += get_nature_mod(nature, 1); add_num2(def_iv, stats); *stats += " ";
*stats += get_nature_mod(nature, 2); add_num2(spa_iv, stats); *stats += " ";
*stats += get_nature_mod(nature, 3); add_num2(spd_iv, stats); *stats += " ";
*stats += get_nature_mod(nature, 4); add_num2(spe_iv, stats); *stats += "\n";
*stats += " EV ";
add_num(hp_ev, stats); *stats += " ";
add_num(atk_ev, stats); *stats += " ";
add_num(def_ev, stats); *stats += " ";
add_num(spa_ev, stats); *stats += " ";
add_num(spd_ev, stats); *stats += " ";
add_num(spe_ev, stats); *stats += "\n";
}
u64 get_section_offsets(int save_game) {
// Find party/items section and trainer info section, since these move each time the game is saved
// Once upon a time this returned the offset to both sections packed into a u64, but that caused
// problems with bank switching. Since I only need four bytes out of the trainer info section, I
// instead just grabbed those bytes when I knew I'd be able to and packed them into the return
// value. I have not yet thought of a better name for the function.
int section_num = 0;
int sectionid;
u32 section;
int sections_found = 0;
u64 section_offsets = 0;
bool bank_switched = false;
bool switch_back = false;
while (sections_found != 2) {
section = (0x1000 * section_num++);
sectionid = *(u8*)(MEM_SRAM + (bank_switched ? 0x0000 : save_game) + section + 0x0FF4);
if (sectionid == 0) {
// We actually only need one word out of this section
// Might as well just grab it now
section_offsets |= ((u64)(get_int_from_sram(section + 0x00AC)) << 32);
sections_found++;
} else if (sectionid == 1) {
section_offsets |= (section & 0xffffffff) + (bank_switched ? 0x0000 : save_game);
sections_found++;
// If we haven't yet switched banks, but we do later, we need to know to switch
// back so this pointer still makes sense
switch_back = !bank_switched;
}
if (section_num == 2 && save_game && !bank_switched) {
// If we're using save game B and the sections we need don't
// happen to be sections zero and one, then they won't fit in
// the GBA's 64KiB SRAM address space
// ∧_∧
// (。・ω・。)つ━☆・*。 I cast Spell of Switch Banks!
// ⊂  /   ・゜+.
// しーJ   °。+
*((vu8*)(MEM_SRAM + 0x5555)) = 0xAA;
*((vu8*)(MEM_SRAM + 0x2AAA)) = 0x55;
*((vu8*)(MEM_SRAM + 0x5555)) = 0xB0;
*((vu8*)(MEM_SRAM + 0x0000)) = 0x01;
bank_switched = true;
// This also means that we have to start reading from the top
// of MEM_SRAM again
section_num = 0;
}
}
if (switch_back) {
// If we found the party in the first two sections of save game
// B, but the trainer info was found in a later section, that
// means that the pointer to the party section is no longer right.
// ∧_∧
// (。・ω・。)つ━☆・*。 I cast Spell of Switch Banks Again!
// ⊂  /   ・゜+.
// しーJ   °。+
*((vu8*)(MEM_SRAM + 0x5555)) = 0xAA;
*((vu8*)(MEM_SRAM + 0x2AAA)) = 0x55;
*((vu8*)(MEM_SRAM + 0x5555)) = 0xB0;
*((vu8*)(MEM_SRAM + 0x0000)) = 0x00;
}
return section_offsets;
}
void get_cartridge_name(char* cart_name) {
memcpy(cart_name, (char*)(MEM_ROM + 0x00A0), 15);
cart_name[15] = '\0'; // don't think i need to do this but just in case
}
bool valid_cartridge(char* this_cart_name) {
const char* cart_names[5] = {
"POKEMON RUBYAXV",
"POKEMON SAPPAXP",
"POKEMON LEAFBPG",
"POKEMON FIREBPR",
"POKEMON EMERBPE",
};
for (int i=0; i<5; i++) {
if (!strcmp(this_cart_name, cart_names[i])) {
return true;
}
}
return false;
}
void get_stats(std::string* stats) {
*stats = "";
int save_game = get_save_game_offset();
u64 sectionoffsets = get_section_offsets(save_game);
int game_version = sectionoffsets >> 32;
int team_items_section = sectionoffsets & 0xffffffff;
int version_offset;
// RSE and FRLG have almost identical save structures, but some stuff is shifted around
// Importantly for us, we need to know the offset into the Team / Items section that the
// party information is found
// Getting this from the cartridge header would be easier and prevent some nonsense, but
// I've decided to be a sicko who wants to support the case where someone restored an RSE
// save onto a FRLG cart or something
if (game_version == 1) {
// frlg
version_offset = 0x0000;
} else {
// rse
version_offset = 0x0200;
}
int partySize = get_int_from_sram(team_items_section + version_offset + 0x0034);
*stats += " HP ATK DEF SPA SPD SPE\n";
*stats += " --- --- --- --- --- ---\n";
for (int i=0; i<partySize; i++) {
interpret_pokemon(team_items_section + version_offset + 0x0038 + (i * 100), stats);
}
}
int main() {
init();
// double negative here so my ide doesn't grey out the more important code
// vsc isn't smart enough to see defines that are added by the makefile :pensive:
#ifndef NOMULTIBOOT
std::string output = "Insert a gen 3 Pokemon\ncartridge";
log(output);
char cart_names[3][16];
while (true) {
get_cartridge_name(cart_names[0]);
// only read cart if it matches the name that was read last frame, but not the
// frame before (to ensure the cartridge is good and seated, but also new)
if (!strcmp(cart_names[0], cart_names[1]) && strcmp(cart_names[0], cart_names[2]) && valid_cartridge(cart_names[0])) {
get_stats(&output);
log(output);
}
strcpy(cart_names[2], cart_names[1]);
strcpy(cart_names[1], cart_names[0]);
vid_vsync();
}
#else
// if i'm compiling without multiboot, it's for debugging in an emulator
// i want it to run even though the cartridge header is wrong, because the
// save file will be one i've put there manually
std::string output;
get_stats(&output);
log(output);
while (true) {
vid_vsync();
};
#endif
return 0;
}