#!/usr/bin/env python3
#
# Copyright (C) 2019-2022 Síle Ekaterin Liszka
#
# Permission to use, copy, modify, and/or distribute this software for
# any purpose with or without fee is hereby granted, provided that the
# above copyright notice and this permission notice appear in all
# copies.
#
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
# WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE
# AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR
# CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
# OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
# WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
#
# This project was made possible by:
# * Lenophis, who told me where the 'give this esper' bytes are
# * ff6hacking.com, which hosts the data tables I used to pick from.
# * CanOfWorms, for figuring out the GBA offsets.
#
# ChangeLog:
# 0.3:
# - GBA support! Purportedly works for J, U, *and* E versions.
# 0.2:
# - Preliminary work toward supporting GBA ROMs. This needs data I
# don't have to complete, however, but the cleanup it required was
# useful.
# - Reworked learning rates. Rates should be overall lower now.
# - Majority of output moved to a spoilers file.
import sys
import io
from random import randint, seed, shuffle, random, choice, sample
from enum import IntEnum
from copy import copy
class Bonus(IntEnum):
HP_10 = 0x00
HP_30 = 0x01
HP_50 = 0x02
MP_10 = 0x03
MP_30 = 0x04
MP_50 = 0x05
HP100 = 0x06
LV_30 = 0x07
LV_50 = 0x08
Str_1 = 0x09
Str_2 = 0x0A
Spd_1 = 0x0B
Spd_2 = 0x0C
Vit_1 = 0x0D
Vit_2 = 0x0E
Mag_1 = 0x0F
Mag_2 = 0x10
Nil = 0xFF
bonuses = {
Bonus.HP_10: 'HP +10%',
Bonus.HP_30: 'HP +30%',
Bonus.HP_50: 'HP +50%',
Bonus.MP_10: 'MP +10%',
Bonus.MP_30: 'MP +30%',
Bonus.MP_50: 'MP +50%',
Bonus.HP100: 'HP +100%',
Bonus.Str_1: 'Strength +1',
Bonus.Str_2: 'Strength +2',
Bonus.Spd_1: 'Speed +1',
Bonus.Spd_2: 'Speed +2',
Bonus.Mag_1: 'Magic +1',
Bonus.Mag_2: 'Magic +2',
}
class Spell(IntEnum):
Fire = 0x00
Blizzard = 0x01
Thunder = 0x02
Poison = 0x03
Drain = 0x04
Firera = 0x05
Blizzardra = 0x06
Thunderra = 0x07
Bio = 0x08
Firega = 0x09
Blizzardga = 0x0A
Thunderga = 0x0B
Break = 0x0C
Death = 0x0D
Holy = 0x0E
Flare = 0x0F
Gravity = 0x10
Gravityga = 0x11
Dezone = 0x12
Meteor = 0x13
Ultima = 0x14
Quake = 0x15
Tornado = 0x16
Meltdown = 0x17
Library = 0x18
Slow = 0x19
Rasp = 0x1A
Silence = 0x1B
Protect = 0x1C
Sleep = 0x1D
Confuse = 0x1E
Haste = 0x1F
Stop = 0x20
Berserk = 0x21
Float = 0x22
Kappa = 0x23
Reflect = 0x24
Shell = 0x25
Vanish = 0x26
Hastega = 0x27
Slowga = 0x28
Osmose = 0x29
Warp = 0x2A
Quick = 0x2B
Dispel = 0x2C
Cure = 0x2D
Curera = 0x2E
Curega = 0x2F
Raise = 0x30
Arise = 0x31
NullPoison = 0x32
NullStatus = 0x33
Regen = 0x34
Reraise = 0x35
Flood = 54
Gravija = 55
Valor = 56
SNES_Count = 54
GBA_Count = 57
class Esper(IntEnum):
Ramuh = 0
Ifrit = 1
Shiva = 2
Siren = 3
Jormungand = 4
Catoblepas = 5
Maduin = 6
Bismarck = 7
CatSidhe = 8
Quetzali = 9
Valigarmander = 10
Odin = 11
Raiden = 12
Bahamut = 13
Alexander = 14
Jihad = 15
Ragnarok = 16
QiLin = 17
ZonaSeeker = 18
Carbunkle = 19
Phantom = 20
Seraphim = 21
Golem = 22
Unicorn = 23
Fenrir = 24
Lakshmi = 25
Phoenix = 26
Leviathan = 27
Cactender = 28
Diabolus = 29
Gilgamesh = 30
SNES_Count = 27
GBA_Count = 31
def roll_espers(max):
return [Esper(i) for i in sample(list(range(max)), 4)]
spell_rate = [1, 1, 1, 2, 2, 3, 4, 3, 2, 2, 1, 1, 1]
def roll_spells(count, max, forbidden=[]):
spells = []
spell_base = []
def add_spell(spell):
spell_base.append(spell)
spells.append([spell, choice(spell_rate) * 5])
if randint(1, 100) < 50: add_spell(Spell.Curera)
else: add_spell(Spell.Curega)
num = randint(1, 100)
if num < 34:
add_spell(Spell.Fire)
num2 = randint(1, 100)
if num2 < 50: add_spell(Spell.Blizzardra)
else: add_spell(Spell.Thunderra)
elif num < 67:
add_spell(Spell.Blizzard)
num2 = randint(1, 100)
if num2 < 50: add_spell(Spell.Firera)
else: add_spell(Spell.Thunderra)
else:
add_spell(Spell.Thunder)
num2 = randint(1, 100)
if num2 < 50: add_spell(Spell.Firera)
else: add_spell(Spell.Blizzardra)
add_spell(Spell.Protect)
add_spell(Spell.Shell)
add_spell(Spell.Haste)
num = randint(1, 100)
if num < 34: add_spell(Spell.Raise)
elif num < 67: add_spell(Spell.Arise)
n = count - len(spells)
while True:
if n < 0:
break
spell = Spell(randint(0, max))
if spell not in spell_base and spell not in forbidden:
add_spell(spell)
n -= 1
return spells
def generate(rng_seed=None, mode='snes'):
seed(rng_seed)
if mode == 'snes':
esper_max = Esper.SNES_Count - 1
spell_max = Spell.SNES_Count - 1
elif mode == 'gba':
esper_max = Esper.GBA_Count - 1
spell_max = Spell.GBA_Count - 1
espers = roll_espers(esper_max)
statup = [Bonus(i) for i in sample(list(bonuses.keys()), 4)]
spells = roll_spells(20, spell_max)
shuffle(spells)
return {
'espers': espers,
'level+': statup,
'spells': spells,
'seed': rng_seed
}
remove = [
0xA55DE, 0xB4DE3, 0xB52C9, 0xB5452,
0xA5A2C, 0xB5B77, 0xB9A7C, 0xC1ED1,
0xC1F85, 0xC2044, 0xC316C, 0xC37A8,
0xC4ADA, 0xC5E08, 0xC5E08, 0xC79D9,
0xC79E9, 0xCD78F
]
def write_rom(inname, outname, mode, data):
print('Writing data for seed {} to {}...'.format(data['seed'], realname))
offset = 0
if mode == 'gba':
offset = 0x70000
spello = 0x62A480
remove.extend((0xD2B52, 0xD2BD4, 0xD11ED, 0xD27A5))
else:
spello = 0x186e00
rom = io.BytesIO()
with open(inname, mode='rb') as f:
rom.write(f.read())
# erase all the slots we're removing
# Bahamut
rom.seek(0xA00DE + offset, 0)
rom.write(b'\xFD' * 5)
# sorcerous research facility
rom.seek(0xC7C6D + offset, 0)
rom.write(b'\xFD' * 11)
# everybody else
for i in remove:
rom.seek(i + offset, 0)
rom.write(b'\xFD' * 2)
# these four are the espers given when you reach Tina in Zozo
rom.seek(0xAA824 + offset, 0)
rom.write(bytes([data['espers'][0] + 0x36]))
rom.seek(0xAAC9C + offset, 0)
rom.write(bytes([data['espers'][1] + 0x36]))
rom.seek(0xAACAB + offset, 0)
rom.write(bytes([data['espers'][2] + 0x36]))
rom.seek(0xAACBA + offset, 0)
rom.write(bytes([data['espers'][3] + 0x36]))
# write esper data for the espers we're giving out
for i in range(4):
rom.seek(spello + (data['espers'][i] * 11), 0)
for j in range(5):
spell = data['spells'].pop(0)
rom.write(bytes([spell[1], spell[0]]))
rom.write(bytes([data['level+'][i]]))
rom.seek(0, 0)
with open(outname, mode='wb') as f:
f.write(rom.read())
if __name__ == '__main__':
from sys import argv
from time import time
now = str(time()).split('.')[0]
if len(argv) == 3:
rng_seed = argv[2]
elif len(argv) < 2 or len(argv) > 3:
print('Usage: {} <filename> [seed]'.format(argv[0]))
else:
rng_seed = now
filename = argv[1]
if filename.endswith('.gba'):
mode = 'gba'
else:
mode = 'snes'
print('Mode: {}'.format(mode))
print('Seed: {}'.format(rng_seed))
data = generate(rng_seed, mode)
filefrags = filename.split('.')
ext = filefrags[-1]
filefrags[-1] = str(data['seed'])
filefrags.append(ext)
realname = '.'.join(filefrags)
filefrags[-1] = 'txt'
spoilers = '.'.join(filefrags)
espers = []
for i in range(4):
esper = str(data['espers'][i]).split('.')[1]
bonus = bonuses[data['level+'][i]]
espers.append(esper + '(' + bonus + ')')
spells = data['spells']
with open(spoilers, mode='wt', encoding='utf8') as f:
f.write('Seed: {}\n'.format(rng_seed))
for i in range(4):
f.write('{}\n'.format(espers[i]))
for j in range(5):
spell = data['spells'][(i * 5) + j]
f.write(' {} ×{}\n'.format(str(spell[0]).split('.')[1], spell[1]))
write_rom(filename, realname, mode, data)
Comments
0 B
|👍
/👎