diff options
author | yenatch <yenatch@gmail.com> | 2013-11-18 21:03:31 -0500 |
---|---|---|
committer | yenatch <yenatch@gmail.com> | 2013-11-18 21:03:31 -0500 |
commit | dfc88b9ac0369632bfa93a3859bf26dc2828ae9e (patch) | |
tree | 6243ede1e0a5a0bc2a76abf8c5027e21f1a72471 | |
parent | 9d01c85d3bac2a6a7b5826dc2139f69731a901ab (diff) | |
parent | 3027746bd69db504f8d0e311d9f81593337ff236 (diff) |
Merge branch 'master' of github.com:kanzure/pokemon-reverse-engineering-tools
-rw-r--r-- | README.md | 46 | ||||
-rw-r--r-- | pokemontools/__init__.py | 2 | ||||
-rw-r--r-- | pokemontools/audio.py | 15 | ||||
-rw-r--r-- | pokemontools/crystalparts/old_parsers.py | 2 | ||||
-rw-r--r-- | pokemontools/data/__init__.py | 15 | ||||
-rw-r--r-- | pokemontools/data/pokecrystal/wram.asm | 2293 | ||||
-rw-r--r-- | pokemontools/gfx.py | 7 | ||||
-rw-r--r-- | pokemontools/map_editor.py | 4 | ||||
-rw-r--r-- | pokemontools/vba/autoplayer.py | 820 | ||||
-rw-r--r-- | pokemontools/vba/battle.py | 521 | ||||
-rw-r--r-- | pokemontools/vba/vba.py | 1093 | ||||
-rw-r--r-- | pokemontools/wram.py | 17 | ||||
-rw-r--r-- | setup.py | 2 | ||||
-rw-r--r-- | tests/bootstrapping.py | 54 | ||||
-rw-r--r-- | tests/integration/tests.py | 5 | ||||
-rw-r--r-- | tests/setup_vba.py | 4 | ||||
-rw-r--r-- | tests/test_vba.py | 276 | ||||
-rw-r--r-- | tests/test_vba_battle.py | 117 | ||||
-rw-r--r-- | tests/tests.py | 5 |
19 files changed, 4650 insertions, 648 deletions
@@ -1,3 +1,49 @@ +pokemontools +============================== + +``pokemontools`` is a python module that provides various reverse engineering +components for various Pokémon games. This includes: + +* a utility to disassemble bytes from games into asm +* map editor +* python bindings for Pokémon games running in the vba-linux emulator +* in-game graphics converter (png, lz, 2bpp) +* preprocessor that dumps out rgbds-compatible asm +* stuff that parses and dumps data from ROMs + +# installing + +To install this python library in ``site-packages``: + +``` +pip install --upgrade pokemontools +``` + +And for local development work: + +``` +python setup.py develop +``` + +And of course local installation: + +``` +python setup.py install +``` + +# testing + +Run the tests with: + +``` +nosetests-2.7 +``` + +# see also + +* [Pokémon Crystal source code](https://github.com/kanzure/pokecrystal) +* [Pokémon Red source code](https://github.com/iimarckus/pokered) + Pokémon Crystal utilities and extras ============================== diff --git a/pokemontools/__init__.py b/pokemontools/__init__.py index 293e2f2..dc4346a 100644 --- a/pokemontools/__init__.py +++ b/pokemontools/__init__.py @@ -1,3 +1,5 @@ import configuration as config import crystal import preprocessor + +__version__ = "1.6.0" diff --git a/pokemontools/audio.py b/pokemontools/audio.py index 1cce1fe..38fd65f 100644 --- a/pokemontools/audio.py +++ b/pokemontools/audio.py @@ -8,14 +8,19 @@ from gbz80disasm import get_global_address, get_local_address import crystal from crystal import music_classes as sound_classes -from crystal import Command -from crystal import load_rom +from crystal import ( + Command, + SingleByteParam, + MultiByteParam, + load_rom, +) + rom = load_rom() rom = bytearray(rom) -import config -conf = config.Config() +import configuration +conf = configuration.Config() def sort_asms(asms): @@ -209,7 +214,7 @@ class Channel: for class_ in sound_classes: if class_.id == i: return class_ - if self.channel in [4. 8]: return Noise + if self.channel in [4, 8]: return Noise return Note diff --git a/pokemontools/crystalparts/old_parsers.py b/pokemontools/crystalparts/old_parsers.py index e07082d..2c1d2b2 100644 --- a/pokemontools/crystalparts/old_parsers.py +++ b/pokemontools/crystalparts/old_parsers.py @@ -2,7 +2,7 @@ Some old methods rescued from crystal.py """ -import pointers +import pokemontools.pointers as pointers map_header_byte_size = 9 diff --git a/pokemontools/data/__init__.py b/pokemontools/data/__init__.py new file mode 100644 index 0000000..fcc59e9 --- /dev/null +++ b/pokemontools/data/__init__.py @@ -0,0 +1,15 @@ +""" +Access to data files. +""" + +# hide the os import +import os as _os + +# path to where these files are located +path = _os.path.abspath(_os.path.dirname(__file__)) + +def join(filename, path=path): + """ + Construct the absolute path to the file. + """ + return _os.path.join(path, filename) diff --git a/pokemontools/data/pokecrystal/wram.asm b/pokemontools/data/pokecrystal/wram.asm new file mode 100644 index 0000000..3796969 --- /dev/null +++ b/pokemontools/data/pokecrystal/wram.asm @@ -0,0 +1,2293 @@ +SECTION "tiles0",VRAM[$8000],BANK[0] +VTiles0: +SECTION "tiles1",VRAM[$8800],BANK[0] +VTiles1: +SECTION "tiles2",VRAM[$9000],BANK[0] +VTiles2: +SECTION "bgmap0",VRAM[$9800],BANK[0] +VBGMap0: +SECTION "bgmap1",VRAM[$9C00],BANK[0] +VBGMap1: + + + +SECTION "WRAMBank0",WRAM0[$c000] + +SECTION "stack",WRAM0[$c0ff] +Stack: ; c0ff + ds -$100 + + +SECTION "audio",WRAM0[$c100] +MusicPlaying: ; c100 +; nonzero if playing + ds 1 + +Channels: +Channel1: +Channel1MusicID: ; c101 + ds 2 +Channel1MusicBank: ; c103 + ds 1 +Channel1Flags: ; c104 +; 0: on/off +; 1: subroutine +; 2: +; 3: +; 4: noise sampling on/off +; 5: +; 6: +; 7: + ds 1 +Channel1Flags2: ; c105 +; 0: vibrato on/off +; 1: +; 2: duty cycle on/off +; 3: +; 4: +; 5: +; 6: +; 7: + ds 1 +Channel1Flags3: ; c106 +; 0: vibrato up/down +; 1: +; 2: +; 3: +; 4: +; 5: +; 6: +; 7: + ds 1 +Channel1MusicAddress: ; c107 + ds 2 +Channel1LastMusicAddress: ; c109 + ds 2 +; could have been meant as a third-level address + ds 2 +Channel1NoteFlags: ; c10d +; 0: +; 1: +; 2: +; 3: +; 4: +; 5: rest +; 6: +; 7: + ds 1 +Channel1Condition: ; c10e +; used for conditional jumps + ds 1 +Channel1DutyCycle: ; c10f +; uses top 2 bits only +; 0: 12.5% +; 1: 25% +; 2: 50% +; 3: 75% + ds 1 +Channel1Intensity: ; c110 +; hi: pressure +; lo: velocity + ds 1 +Channel1Frequency: +; 11 bits +Channel1FrequencyLo: ; c111 + ds 1 +Channel1FrequencyHi: ; c112 + ds 1 +Channel1Pitch: ; c113 +; 0: rest +; 1: C +; 2: C# +; 3: D +; 4: D# +; 5: E +; 6: F +; 7: F# +; 8: G +; 9: G# +; a: A +; b: A# +; c: B + ds 1 +Channel1Octave: ; c114 +; 0: highest +; 7: lowest + ds 1 +Channel1StartingOctave: ; c115 +; raises existing octaves by this value +; used for repeating phrases in a higher octave to save space + ds 1 +Channel1NoteDuration: ; c116 +; number of frames remaining in the current note + ds 1 +; c117 + ds 1 +; c118 + ds 1 +Channel1LoopCount: ; c119 + ds 1 +Channel1Tempo: ; c11a + ds 2 +Channel1Tracks: ; c11c +; hi: l +; lo: r + ds 1 +; c11d + ds 1 + +Channel1VibratoDelayCount: ; c11e +; initialized at the value in VibratoDelay +; decrements each frame +; at 0, vibrato starts + ds 1 +Channel1VibratoDelay: ; c11f +; number of frames a note plays until vibrato starts + ds 1 +Channel1VibratoExtent: ; c120 +; difference in + ds 1 +Channel1VibratoRate: ; c121 +; counts down from a max of 15 frames +; over which the pitch is alternated +; hi: init frames +; lo: frame count + ds 1 + +; c122 + ds 1 +; c123 + ds 1 +; c124 + ds 1 +; c125 + ds 1 +; c126 + ds 1 +; c127 + ds 1 +Channel1CryPitch: ; c128 + ds 1 +Channel1CryEcho: ; c129 + ds 1 + ds 4 +Channel1NoteLength: ; c12e +; # frames per 16th note + ds 1 +; c12f + ds 1 +; c130 + ds 1 +; c131 + ds 1 +; c132 + ds 1 +; end + +Channel2: ; c133 + ds 50 +Channel3: ; c165 + ds 50 +Channel4: ; c197 + ds 50 + +SFXChannels: +Channel5: ; c1c9 + ds 50 +Channel6: ; c1fb + ds 50 +Channel7: ; c22d + ds 50 +Channel8: ; c25f + ds 50 + +; c291 + ds 1 +; c292 + ds 1 +; c293 + ds 1 +; c294 + ds 1 +; c295 + ds 1 +; c296 + ds 1 +; c297 + ds 1 + +CurMusicByte: ; c298 + ds 1 +CurChannel: ; c299 + ds 1 +Volume: ; c29a +; corresponds to $ff24 +; Channel control / ON-OFF / Volume (R/W) +; bit 7 - Vin->SO2 ON/OFF +; bit 6-4 - SO2 output level (volume) (# 0-7) +; bit 3 - Vin->SO1 ON/OFF +; bit 2-0 - SO1 output level (volume) (# 0-7) + ds 1 +SoundOutput: ; c29b +; corresponds to $ff25 +; bit 4-7: ch1-4 so2 on/off +; bit 0-3: ch1-4 so1 on/off + ds 1 +SoundInput: ; c29c +; corresponds to $ff26 +; bit 7: global on/off +; bit 0: ch1 on/off +; bit 1: ch2 on/off +; bit 2: ch3 on/off +; bit 3: ch4 on/off + ds 1 + +MusicID: +MusicIDLo: ; c29d + ds 1 +MusicIDHi: ; c29e + ds 1 +MusicBank: ; c29f + ds 1 +NoiseSampleAddress: +NoiseSampleAddressLo: ; c2a0 + ds 1 +NoiseSampleAddressHi: ; c2a1 + ds 1 +; noise delay? ; c2a2 + ds 1 +; c2a3 + ds 1 +MusicNoiseSampleSet: ; c2a4 + ds 1 +SFXNoiseSampleSet: ; c2a5 + ds 1 +Danger: ; c2a6 +; bit 7: on/off +; bit 4: pitch +; bit 0-3: counter + ds 1 +MusicFade: ; c2a7 +; fades volume over x frames +; bit 7: fade in/out +; bit 0-5: number of frames for each volume level +; $00 = none (default) + ds 1 +MusicFadeCount: ; c2a8 + ds 1 +MusicFadeID: +MusicFadeIDLo: ; c2a9 + ds 1 +MusicFadeIDHi: ; c2aa + ds 1 + ds 5 +CryPitch: ; c2b0 + ds 1 +CryEcho: ; c2b1 + ds 1 +CryLength: ; c2b2 + ds 2 +LastVolume: ; c2b4 + ds 1 + ds 1 +SFXPriority: ; c2b6 +; if nonzero, turn off music when playing sfx + ds 1 + ds 6 +CryTracks: ; c2bd +; plays only in left or right track depending on what side the monster is on +; both tracks active outside of battle + ds 1 + ds 1 +CurSFX: ; c2bf +; id of sfx currently playing + ds 1 +CurMusic: ; c2c0 +; id of music currently playing + ds 1 + +SECTION "auto",WRAM0[$c2c7] +InputType: ; c2c7 + ds 1 +AutoInputAddress: ; c2c8 + ds 2 +AutoInputBank: ; c2ca + ds 1 +AutoInputLength: ; c2cb + ds 1 + +SECTION "linkbattle",WRAM0[$c2dc] +InLinkBattle: ; c2dc +; 0 not in link battle +; 1 link battle +; 4 mobile battle + ds 1 + +SECTION "scriptengine",WRAM0[$c2dd] +ScriptVar: ; c2dd + ds 1 + + +SECTION "tiles",WRAM0[$c2fa] +TileDown: ; c2fa + ds 1 +TileUp: ; c2fb + ds 1 +TileLeft: ; c2fc + ds 1 +TileRight: ; c2fd + ds 1 + +TilePermissions: ; c2fe +; set if tile behavior prevents +; you from walking in that direction +; bit 3: down +; bit 2: up +; bit 1: left +; bit 0: right + ds 1 + +SECTION "icons",WRAM0[$c3b6] + +CurIcon: ; c3b6 + ds 1 + +SECTION "gfx",WRAM0[$c400] + +Sprites: ; c400 +; 4 bytes per sprite +; 40 sprites +; struct: +; y in pixels +; x in pixels +; tile id +; attributes: +; bit 7: priority +; bit 6: y flip +; bit 5: x flip +; bit 4: pal # (non-cgb) +; bit 3: vram bank (cgb only) +; bit 2-0: pal # (cgb only) + ds 160 +SpritesEnd: + +TileMap: ; c4a0 +; 20x18 grid of 8x8 tiles + ds 360 + + +SECTION "BattleMons",WRAM0[$c608] + +EnemyMoveStruct: +EnemyMoveAnimation: ; c608 + ds 1 +EnemyMoveEffect: ; c609 + ds 1 +EnemyMovePower: ; c60a + ds 1 +EnemyMoveType: ; c60b + ds 1 +EnemyMoveAccuracy: ; c60c + ds 1 +EnemyMovePP: ; c60d + ds 1 +EnemyMoveEffectChance: ; c60e + ds 1 + +PlayerMoveStruct: +PlayerMoveAnimation: ; c60f + ds 1 +PlayerMoveEffect: ; c610 + ds 1 +PlayerMovePower: ; c611 + ds 1 +PlayerMoveType: ; c612 + ds 1 +PlayerMoveAccuracy: ; c613 + ds 1 +PlayerMovePP: ; c614 + ds 1 +PlayerMoveEffectChance: ; c615 + ds 1 + +EnemyMonNick: ; c616 + ds 11 +BattleMonNick: ; c621 + ds 11 + + +BattleMonSpecies: ; c62c + ds 1 +BattleMonItem: ; c62d + ds 1 + +BattleMonMoves: +BattleMonMove1: ; c62e + ds 1 +BattleMonMove2: ; c62f + ds 1 +BattleMonMove3: ; c630 + ds 1 +BattleMonMove4: ; c631 + ds 1 + +BattleMonDVs: +BattleMonAtkDefDV: ; c632 + ds 1 +BattleMonSpdSpclDV: ; c633 + ds 1 + +BattleMonPP: +BattleMonPPMove1: ; c634 + ds 1 +BattleMonPPMove2: ; c635 + ds 1 +BattleMonPPMove3: ; c636 + ds 1 +BattleMonPPMove4: ; c637 + ds 1 + +BattleMonHappiness: ; c638 + ds 1 +BattleMonLevel: ; c639 + ds 1 + +BattleMonStatus: ; c63a + ds 2 + +BattleMonHP: ; c63c + ds 2 +BattleMonMaxHP: ; c63e + ds 2 + +BattleMonAtk: ; c640 + ds 2 +BattleMonDef: ; c642 + ds 2 +BattleMonSpd: ; c644 + ds 2 +BattleMonSpclAtk: ; c646 + ds 2 +BattleMonSpclDef: ; c648 + ds 2 + +BattleMonType1: ; c64a + ds 1 +BattleMonType2: ; c64b + ds 1 + + ds 10 + +OTName: ; c656 + ds 13 + +CurOTMon: ; c663 + ds 1 + + ds 1 + +TypeModifier: ; c665 +; >10: super-effective +; 10: normal +; <10: not very effective + +; bit 7: stab + ds 1 + +CriticalHit: ; c666 +; nonzero for a critical hit + ds 1 + +AttackMissed: ; c667 +; nonzero for a miss + ds 1 + +PlayerSubStatus1: ; c668 +; bit +; 7 attract +; 6 encore +; 5 endure +; 4 perish song +; 3 identified +; 2 protect +; 1 curse +; 0 nightmare + ds 1 +PlayerSubStatus2: ; c669 +; bit +; 7 +; 6 +; 5 +; 4 +; 3 +; 2 +; 1 +; 0 curled + ds 1 +PlayerSubStatus3: ; c66a +; bit +; 7 confused +; 6 flying +; 5 underground +; 4 charged +; 3 flinch +; 2 +; 1 rollout +; 0 bide + ds 1 +PlayerSubStatus4: ; c66b +; bit +; 7 leech seed +; 6 rage +; 5 recharge +; 4 substitute +; 3 +; 2 focus energy +; 1 mist +; 0 bide: unleashed energy + ds 1 +PlayerSubStatus5: ; c66c +; bit +; 7 cant run +; 6 destiny bond +; 5 lock-on +; 4 +; 3 +; 2 +; 1 +; 0 toxic + ds 1 + +EnemySubStatus1: ; c66d +; see PlayerSubStatus1 + ds 1 +EnemySubStatus2: ; c66e +; see PlayerSubStatus2 + ds 1 +EnemySubStatus3: ; c66f +; see PlayerSubStatus3 + ds 1 +EnemySubStatus4: ; c670 +; see PlayerSubStatus4 + ds 1 +EnemySubStatus5: ; c671 +; see PlayerSubStatus5 + ds 1 + +PlayerRolloutCount: ; c672 + ds 1 +PlayerConfuseCount: ; c673 + ds 1 + ds 1 +PlayerDisableCount: ; c675 + ds 1 +PlayerEncoreCount: ; c676 + ds 1 +PlayerPerishCount: ; c677 + ds 1 +PlayerFuryCutterCount: ; c678 + ds 1 +PlayerProtectCount: ; c679 + ds 1 + +EnemyRolloutCount: ; c67a + ds 1 +EnemyConfuseCount: ; c67b + ds 1 + ds 1 +EnemyDisableCount: ; c67d + ds 1 +EnemyEncoreCount: ; c67e + ds 1 +EnemyPerishCount: ; c67f + ds 1 +EnemyFuryCutterCount: ; c680 + ds 1 +EnemyProtectCount: ; c681 + ds 1 + +PlayerDamageTaken: ; c682 + ds 2 +EnemyDamageTaken: ; c684 + ds 2 + + ds 3 + + ds 1 + +BattleScriptBuffer: ; c68a + ds 40 + +BattleScriptBufferLoc: ; c6b2 + ds 2 + + ds 2 + +PlayerStats: ; c6b6 + ds 10 + ds 1 +EnemyStats: ; c6c1 + ds 10 + ds 1 + +PlayerStatLevels: ; c6cc +; 07 neutral +PlayerAtkLevel: ; c6cc + ds 1 +PlayerDefLevel: ; c6cd + ds 1 +PlayerSpdLevel: ; c6ce + ds 1 +PlayerSAtkLevel: ; c6cf + ds 1 +PlayerSDefLevel: ; c6d0 + ds 1 +PlayerAccLevel: ; c6d1 + ds 1 +PlayerEvaLevel: ; c6d2 + ds 1 +; c6d3 + ds 1 +PlayerStatLevelsEnd: + +EnemyStatLevels: ; c6d4 +; 07 neutral +EnemyAtkLevel: ; c6d4 + ds 1 +EnemyDefLevel: ; c6d5 + ds 1 +EnemySpdLevel: ; c6d6 + ds 1 +EnemySAtkLevel: ; c6d7 + ds 1 +EnemySDefLevel: ; c6d8 + ds 1 +EnemyAccLevel: ; c6d9 + ds 1 +EnemyEvaLevel: ; c6da + ds 1 +; c6db + ds 1 + +EnemyTurnsTaken: ; c6dc + ds 1 +PlayerTurnsTaken: ; c6dd + ds 1 + + ds 5 + +CurPlayerMove: ; c6e3 + ds 1 +CurEnemyMove: ; c6e4 + ds 1 + +LinkBattleRNCount: ; c6e5 +; how far through the prng stream + ds 1 + + ds 3 + +CurEnemyMoveNum: ; c6e9 + ds 1 + + ds 10 + +AlreadyDisobeyed: ; c6f4 + ds 1 + +DisabledMove: ; c6f5 + ds 1 +EnemyDisabledMove: ; c6f6 + ds 1 + ds 1 + +; exists so you can't counter on switch +LastEnemyCounterMove: ; c6f8 + ds 1 +LastPlayerCounterMove: ; c6f9 + ds 1 + + ds 1 + +AlreadyFailed: ; c6fb + ds 1 + + ds 3 + +PlayerScreens: ; c6ff +; bit +; 4 reflect +; 3 light screen +; 2 safeguard +; 0 spikes + ds 1 + +EnemyScreens: ; c700 +; see PlayerScreens + ds 1 + + ds 1 + +PlayerLightScreenCount: ; c702 + ds 1 +PlayerReflectCount: ; c703 + ds 1 + + ds 2 + +EnemyLightScreenCount: ; c706 + ds 1 +EnemyReflectCount: ; c707 + ds 1 + + ds 2 + +Weather: ; c70a +; 00 normal +; 01 rain +; 02 sun +; 03 sandstorm +; 04 rain stopped +; 05 sunliight faded +; 06 sandstorm subsided + ds 1 + +WeatherCount: ; c70b +; # turns remaining + ds 1 + +LoweredStat: ; c70c + ds 1 +EffectFailed: ; c70d + ds 1 +FailedMessage: ; c70e + ds 1 + + ds 3 + +PlayerUsedMoves: ; c712 +; add a move that has been used once by the player +; added in order of use + ds 4 + + ds 5 + +LastPlayerMove: ; c71b + ds 1 +LastEnemyMove: ; c71c + ds 1 + + +SECTION "battle",WRAM0[$c734] +BattleEnded: ; c734 + ds 1 + + +SECTION "overworldmap",WRAM0[$c800] +OverworldMap: ; c800 + ds 1300 +OverworldMapEnd: + + ds 12 + +SECTION "gfx2",WRAM0[$cd20] +CreditsPos: +BGMapBuffer: ; cd20 + ds 2 +CreditsTimer: ; cd22 + ds 1 + ds 37 + +BGMapPalBuffer: ; cd48 + ds 40 + +BGMapBufferPtrs: ; cd70 +; 20 bg map addresses (16x8 tiles) + ds 40 + +SGBPredef: ; cd98 + ds 1 +PlayerHPPal: ; cd99 + ds 1 +EnemyHPPal: ; cd9a + ds 1 + + ds 62 + +AttrMap: ; cdd9 +; 20x18 grid of palettes for 8x8 tiles +; read horizontally from the top row +; bit 3: vram bank +; bit 0-2: palette id + ds 360 + + ds 30 + +MonType: ; cf5f + ds 1 + +CurSpecies: ; cf60 + ds 1 + + ds 6 + +Requested2bpp: ; cf67 + ds 1 +Requested2bppSource: ; cf68 + ds 2 +Requested2bppDest: ; cf6a + ds 2 + +Requested1bpp: ; cf6c + ds 1 +Requested1bppSource: ; cf6d + ds 2 +Requested1bppDest: ; cf6f + ds 2 + + ds 3 + +MenuSelection:; cf74 + ds 1 + + + +SECTION "VBlank",WRAM0[$cfb1] +OverworldDelay: ; cfb1 + ds 1 +TextDelayFrames: ; cfb2 + ds 1 +VBlankOccurred: ; cfb3 + ds 1 + +PredefID: ; cfb4 + ds 1 +PredefTemp: ; cfb5 + ds 2 +PredefAddress: ; cfb7 + ds 2 + + ds 3 + +GameTimerPause: ; cfbc +; bit 0 + ds 1 + +SECTION "Engine",WRAM0[$cfc2] +FXAnimID: +FXAnimIDLo: ; cfc2 + ds 1 +FXAnimIDHi: ; cfc3 + ds 1 + + ds 2 + +TileAnimationTimer: ; cfc6 + ds 1 + + ds 5 + +Options: ; cfcc +; bit 0-2: number of frames to delay when printing text +; fast 1; mid 3; slow 5 +; bit 3: ? +; bit 4: no text delay +; bit 5: stereo off/on +; bit 6: battle style shift/set +; bit 7: battle scene off/on + ds 1 + + ds 1 + +TextBoxFrame: ; cfce +; bits 0-2: textbox frame 0-7 + ds 1 + + ds 1 + +GBPrinter: ; cfd0 +; bit 0-6: brightness +; lightest: $00 +; lighter: $20 +; normal: $40 (default) +; darker: $60 +; darkest: $7F + ds 1 + +Options2: ; cfd1 +; bit 1: menu account off/on + ds 1 + + ds 46 + + +SECTION "WRAMBank1",WRAMX[$d000],BANK[1] + + ds 2 + +DefaultFlypoint: ; d002 + ds 1 +; d003 + ds 1 +; d004 + ds 1 +StartFlypoint: ; d005 + ds 1 +EndFlypoint: ; d006 + ds 1 + +MovementBuffer: ; d007 + + ds 55 + +MenuItemsList: +CurFruitTree: +CurInput: +EngineBuffer1: ; d03e + ds 1 +CurFruit: ; d03f + ds 1 + +MartPointer: ; d040 + ds 2 + +MovementAnimation: ; d042 + ds 1 + +WalkingDirection: ; d043 + ds 1 + +FacingDirection: ; d044 + ds 1 + +WalkingX: ; d045 + ds 1 +WalkingY: ; d046 + ds 1 +WalkingTile: ; d047 + ds 1 + + ds 43 + +StringBuffer1: ; d073 + ds 19 +StringBuffer2: ; d086 + ds 19 +StringBuffer3: ; d099 + ds 19 +StringBuffer4: ; d0ac + ds 19 + + ds 21 + +CurBattleMon: ; d0d4 + ds 1 +CurMoveNum: ; d0d5 + ds 1 + + ds 23 + +VramState: ; d0ed +; bit 0: overworld sprite updating on/off +; bit 6: something to do with text +; bit 7: on when surf initiates +; flickers when climbing waterfall + ds 1 + + ds 2 + +CurMart: ; d0f0 + ds 16 +CurMartEnd: + + ds 6 + +CurItem: ; d106 + ds 1 + + ds 1 + +CurPartySpecies: ; d108 + ds 1 + +CurPartyMon: ; d109 +; contains which monster in a party +; is being dealt with at the moment +; 0-5 + ds 1 + + ds 4 + +TempMon: +TempMonSpecies: ; d10e + ds 1 +TempMonItem: ; d10f + ds 1 +TempMonMoves: ; d110 +TempMonMove1: ; d110 + ds 1 +TempMonMove2: ; d111 + ds 1 +TempMonMove3: ; d112 + ds 1 +TempMonMove4: ; d113 + ds 1 +TempMonID: ; d114 + ds 2 +TempMonExp: ; d116 + ds 3 +TempMonHPExp: ; d119 + ds 2 +TempMonAtkExp: ; d11b + ds 2 +TempMonDefExp: ; d11d + ds 2 +TempMonSpdExp: ; d11f + ds 2 +TempMonSpclExp: ; d121 + ds 2 +TempMonDVs: ; d123 +; hp = %1000 for each dv + ds 1 ; atk/def + ds 1 ; spd/spc +TempMonPP: ; d125 + ds 4 +TempMonHappiness: ; d129 + ds 1 +TempMonPokerusStatus: ; d12a + ds 1 +TempMonCaughtData: ; d12b +TempMonCaughtTime: ; d12b +TempMonCaughtLevel: ; d12b + ds 1 +TempMonCaughtGender: ; d12c +TempMonCaughtLocation: ; d12c + ds 1 +TempMonLevel: ; d12d + ds 1 +TempMonStatus: ; d12e + ds 1 +; d12f + ds 1 +TempMonCurHP: ; d130 + ds 2 +TempMonMaxHP: ; d132 + ds 2 +TempMonAtk: ; d134 + ds 2 +TempMonDef: ; d136 + ds 2 +TempMonSpd: ; d138 + ds 2 +TempMonSpclAtk: ; d13a + ds 2 +TempMonSpclDef: ; d13c + ds 2 +TempMonEnd: ; d13e + + ds 3 + +PartyMenuActionText: ; d141 + ds 1 + ds 1 + +CurPartyLevel: ; d143 + ds 1 + + +SECTION "UsedSprites",WRAMX[$d154],BANK[1] +UsedSprites: ; d154 + ds 32 + +SECTION "map",WRAMX[$d19d],BANK[1] + +; both are in blocks (2x2 walkable tiles, 4x4 graphics tiles) +MapHeader: ; d19d +MapBorderBlock: ; d19d + ds 1 +MapHeight: ; d19e + ds 1 +MapWidth: ; d19f + ds 1 +MapBlockDataBank: ; d1a0 + ds 1 +MapBlockDataPointer: ; d1a1 + ds 2 +MapScriptHeaderBank: ; d1a3 + ds 1 +MapScriptHeaderPointer: ; d1a4 + ds 2 +MapEventHeaderPointer: ; d1a6 + ds 2 +; bit set +MapConnections: ; d1a8 + ds 1 +NorthMapConnection: ; d1a9 +NorthConnectedMapGroup: ; d1a9 + ds 1 +NorthConnectedMapNumber: ; d1aa + ds 1 +NorthConnectionStripPointer: ; d1ab + ds 2 +NorthConnectionStripLocation: ; d1ad + ds 2 +NorthConnectionStripLength: ; d1af + ds 1 +NorthConnectedMapWidth: ; d1b0 + ds 1 +NorthConnectionStripYOffset: ; d1b1 + ds 1 +NorthConnectionStripXOffset: ; d1b2 + ds 1 +NorthConnectionWindow: ; d1b3 + ds 2 + +SouthMapConnection: ; d1b5 +SouthConnectedMapGroup: ; d1b5 + ds 1 +SouthConnectedMapNumber: ; d1b6 + ds 1 +SouthConnectionStripPointer: ; d1b7 + ds 2 +SouthConnectionStripLocation: ; d1b9 + ds 2 +SouthConnectionStripLength: ; d1bb + ds 1 +SouthConnectedMapWidth: ; d1bc + ds 1 +SouthConnectionStripYOffset: ; d1bd + ds 1 +SouthConnectionStripXOffset: ; d1be + ds 1 +SouthConnectionWindow: ; d1bf + ds 2 + +WestMapConnection: ; d1c1 +WestConnectedMapGroup: ; d1c1 + ds 1 +WestConnectedMapNumber: ; d1c2 + ds 1 +WestConnectionStripPointer: ; d1c3 + ds 2 +WestConnectionStripLocation: ; d1c5 + ds 2 +WestConnectionStripLength: ; d1c7 + ds 1 +WestConnectedMapWidth: ; d1c8 + ds 1 +WestConnectionStripYOffset: ; d1c9 + ds 1 +WestConnectionStripXOffset: ; d1ca + ds 1 +WestConnectionWindow: ; d1cb + ds 2 + +EastMapConnection: ; d1cd +EastConnectedMapGroup: ; d1cd + ds 1 +EastConnectedMapNumber: ; d1ce + ds 1 +EastConnectionStripPointer: ; d1cf + ds 2 +EastConnectionStripLocation: ; d1d1 + ds 2 +EastConnectionStripLength: ; d1d3 + ds 1 +EastConnectedMapWidth: ; d1d4 + ds 1 +EastConnectionStripYOffset: ; d1d5 + ds 1 +EastConnectionStripXOffset: ; d1d6 + ds 1 +EastConnectionWindow: ; d1d7 + ds 2 + + +TilesetHeader: +TilesetBank: ; d1d9 + ds 1 +TilesetAddress: ; d1da + ds 2 +TilesetBlocksBank: ; d1dc + ds 1 +TilesetBlocksAddress: ; d1dd + ds 2 +TilesetCollisionBank: ; d1df + ds 1 +TilesetCollisionAddress: ; d1e0 + ds 2 +TilesetAnim: ; d1e2 +; bank 3f + ds 2 +; unused ; d1e4 + ds 2 +TilesetPalettes: ; d1e6 +; bank 3f + ds 2 + +EvolvableFlags: ; d1e8 + ds 1 + + ds 1 + +MagikarpLength: +Buffer1: ; d1ea + ds 1 +MovementType: +Buffer2: ; d1eb + ds 1 + +SECTION "BattleMons2",WRAMX[$d1fa],BANK[1] +LinkBattleRNs: ; d1fa + ds 10 + +TempEnemyMonSpecies: ; d204 + ds 1 +TempBattleMonSpecies: ; d205 + ds 1 + +EnemyMon: +EnemyMonSpecies: ; d206 + ds 1 +EnemyMonItem: ; d207 + ds 1 + +EnemyMonMoves: +EnemyMonMove1: ; d208 + ds 1 +EnemyMonMove2: ; d209 + ds 1 +EnemyMonMove3: ; d20a + ds 1 +EnemyMonMove4: ; d20b + ds 1 +EnemyMonMovesEnd: + +EnemyMonDVs: +EnemyMonAtkDefDV: ; d20c + ds 1 +EnemyMonSpdSpclDV: ; d20d + ds 1 + +EnemyMonPP: +EnemyMonPPMove1: ; d20e + ds 1 +EnemyMonPPMove2: ; d20f + ds 1 +EnemyMonPPMove3: ; d210 + ds 1 +EnemyMonPPMove4: ; d211 + ds 1 + +EnemyMonHappiness: ; d212 + ds 1 +EnemyMonLevel: ; d213 + ds 1 + +EnemyMonStatus: ; d214 + ds 2 + +EnemyMonHP: +EnemyMonHPHi: ; d216 + ds 1 +EnemyMonHPLo: ; d217 + ds 1 + +EnemyMonMaxHP: +EnemyMonMaxHPHi: ; d218 + ds 1 +EnemyMonMaxHPLo: ; d219 + ds 1 + +EnemyMonStats: +EnemyMonAtk: ; d21a + ds 2 +EnemyMonDef: ; d21c + ds 2 +EnemyMonSpd: ; d21e + ds 2 +EnemyMonSpclAtk: ; d220 + ds 2 +EnemyMonSpclDef: ; d222 + ds 2 +EnemyMonStatsEnd: + +EnemyMonType1: ; d224 + ds 1 +EnemyMonType2: ; d225 + ds 1 + +EnemyMonBaseStats: ; d226 + ds 5 + +EnemyMonCatchRate: ; d22b + ds 1 +EnemyMonBaseExp: ; d22c + ds 1 + +EnemyMonEnd: + + +IsInBattle: ; d22d +; 0: overworld +; 1: wild battle +; 2: trainer battle + ds 1 + + ds 1 + +OtherTrainerClass: ; d22f +; class (Youngster, Bug Catcher, etc.) of opposing trainer +; 0 if opponent is a wild Pokémon, not a trainer + ds 1 + +BattleType: ; d230 +; $00 normal +; $01 +; $02 +; $03 dude +; $04 fishing +; $05 roaming +; $06 +; $07 shiny +; $08 headbutt/rock smash +; $09 +; $0a force Item1 +; $0b +; $0c suicune + ds 1 + +OtherTrainerID: ; d231 +; which trainer of the class that you're fighting +; (Joey, Mikey, Albert, etc.) + ds 1 + + ds 1 + +TrainerClass: ; d233 + ds 1 + +UnownLetter: ; d234 + ds 1 + + ds 1 + +CurBaseData: ; d236 +BaseDexNo: ; d236 + ds 1 +BaseStats: ; d237 +BaseHP: ; d237 + ds 1 +BaseAttack: ; d238 + ds 1 +BaseDefense: ; d239 + ds 1 +BaseSpeed: ; d23a + ds 1 +BaseSpecialAttack: ; d23b + ds 1 +BaseSpecialDefense: ; d23c + ds 1 +BaseType: ; d23d +BaseType1: ; d23d + ds 1 +BaseType2: ; d23e + ds 1 +BaseCatchRate: ; d23f + ds 1 +BaseExp: ; d240 + ds 1 +BaseItems: ; d241 + ds 2 +BaseGender: ; d243 + ds 1 +BaseUnknown1: ; d244 + ds 1 +BaseEggSteps: ; d245 + ds 1 +BaseUnknown2: ; d246 + ds 1 +BasePicSize: ; d247 + ds 1 +BasePadding: ; d248 + ds 4 +BaseGrowthRate: ; d24c + ds 1 +BaseEggGroups: ; d24d + ds 1 +BaseTMHM: ; d24e + ds 8 + + +CurDamage: ; d256 + ds 2 + + +SECTION "TimeOfDay",WRAMX[$d269],BANK[1] + +TimeOfDay: ; d269 + ds 1 + +SECTION "OTParty",WRAMX[$d280],BANK[1] + +OTPartyCount: ; d280 + ds 1 ; number of Pokémon in party +OTPartySpecies: ; d281 + ds 6 ; species of each Pokémon in party +; d287 + ds 1 ; any empty slots including the 7th must be FF + ; or the routine will keep going + +OTPartyMon1: +OTPartyMon1Species: ; d288 + ds 1 +OTPartyMon1Item: ; d289 + ds 1 + +OTPartyMon1Moves: ; d28a +OTPartyMon1Move1: ; d28a + ds 1 +OTPartyMon1Move2: ; d28b + ds 1 +OTPartyMon1Move3: ; d28c + ds 1 +OTPartyMon1Move4: ; d28d + ds 1 + +OTPartyMon1ID: ; d28e + ds 2 +OTPartyMon1Exp: ; d290 + ds 3 +OTPartyMon1HPExp: ; d293 + ds 2 +OTPartyMon1AtkExp: ; d295 + ds 2 +OTPartyMon1DefExp: ; d297 + ds 2 +OTPartyMon1SpdExp: ; d299 + ds 2 +OTPartyMon1SpclExp: ; d29b + ds 2 + +OTPartyMon1DVs: ; d29d +OTPartyMon1AtkDefDV: ; d29d + ds 1 +OTPartyMon1SpdSpclDV: ; d29e + ds 1 + +OTPartyMon1PP: ; d29f +OTPartyMon1PPMove1: ; d29f + ds 1 +OTPartyMon1PPMove2: ; d2a0 + ds 1 +OTPartyMon1PPMove3: ; d2a1 + ds 1 +OTPartyMon1PPMove4: ; d2a2 + ds 1 + +OTPartyMon1Happiness: ; d2a3 + ds 1 +OTPartyMon1PokerusStatus: ; d2a4 + ds 1 + +OTPartyMon1CaughtData: ; d2a5 +OTPartyMon1CaughtGender: ; d2a5 +OTPartyMon1CaughtLocation: ; d2a5 + ds 1 +OTPartyMon1CaughtTime: ; d2a6 + ds 1 +OTPartyMon1Level: ; d2a7 + ds 1 + +OTPartyMon1Status: ; d2a8 + ds 1 +OTPartyMon1Unused: ; d2a9 + ds 1 +OTPartyMon1CurHP: ; d2aa + ds 2 +OTPartyMon1MaxHP: ; d2ac + ds 2 +OTPartyMon1Atk: ; d2ae + ds 2 +OTPartyMon1Def: ; d2b0 + ds 2 +OTPartyMon1Spd: ; d2b2 + ds 2 +OTPartyMon1SpclAtk: ; d2b4 + ds 2 +OTPartyMon1SpclDef: ; d2b6 + ds 2 + +OTPartyMon2: ; d2b8 + ds 48 +OTPartyMon3: ; d2e8 + ds 48 +OTPartyMon4: ; d318 + ds 48 +OTPartyMon5: ; d348 + ds 48 +OTPartyMon6: ; d378 + ds 48 + + +OTPartyMonOT: +OTPartyMon1OT: ; d3a8 + ds 11 +OTPartyMon2OT: ; d3b3 + ds 11 +OTPartyMon3OT: ; d3be + ds 11 +OTPartyMon4OT: ; d3c9 + ds 11 +OTPartyMon5OT: ; d3d4 + ds 11 +OTPartyMon6OT: ; d3df + ds 11 + +OTPartyMonNicknames: +OTPartyMon1Nickname: ; d3ea + ds 11 +OTPartyMon2Nickname: ; d3f5 + ds 11 +OTPartyMon3Nickname: ; d400 + ds 11 +OTPartyMon4Nickname: ; d40b + ds 11 +OTPartyMon5Nickname: ; d416 + ds 11 +OTPartyMon6Nickname: ; d421 + ds 11 + +SECTION "Scripting",WRAMX[$d434],BANK[1] +ScriptFlags: ; d434 + ds 1 +ScriptFlags2: ; d435 + ds 1 +ScriptFlags3: ; d436 + ds 1 + +ScriptMode: ; d437 + ds 1 +ScriptRunning: ; d438 + ds 1 +ScriptBank: ; d439 + ds 1 +ScriptPos: ; d43a + ds 2 + + ds 17 + +ScriptDelay: ; d44d + ds 1 + +SECTION "Player",WRAMX[$d472],BANK[1] +PlayerGender: ; d472 +; bit 0: +; 0 male +; 1 female + ds 1 + ds 8 +PlayerID: ; d47b + ds 2 + +PlayerName: ; d47d + ds 11 +MomsName: ; d488 + ds 11 +RivalName: ; d493 + ds 11 +RedsName: ; d49e + ds 11 +GreensName: ; d4a9 + ds 11 + + ds 2 + +; init time set at newgame +StartDay: ; d4b6 + ds 1 +StartHour: ; d4b7 + ds 1 +StartMinute: ; d4b8 + ds 1 +StartSecond: ; d4b9 + ds 1 + + ds 9 + +GameTimeCap: ; d4c3 + ds 1 +GameTimeHours: ; d4c4 + ds 2 +GameTimeMinutes: ; d4c6 + ds 1 +GameTimeSeconds: ; d4c7 + ds 1 +GameTimeFrames: ; d4c8 + ds 1 + + ds 2 + +CurDay: ; d4cb + ds 1 + + ds 10 + + ds 2 + +PlayerSprite: ; d4d8 + ds 1 + + ds 3 + +PlayerPalette: ; d4dc + ds 1 + + ds 1 + +PlayerDirection: ; d4de +; uses bits 2 and 3 / $0c / %00001100 +; %00 down +; %01 up +; %10 left +; $11 right + ds 1 + + ds 2 + +PlayerAction: ; d4e1 +; 1 standing +; 2 walking +; 4 spinning +; 6 fishing + ds 1 + + ds 2 + +StandingTile: ; d4e4 + ds 1 +StandingTile2: ; d4e5 + ds 1 + +; relative to the map struct including borders +MapX: ; d4e6 + ds 1 +MapY: ; d4e7 + ds 1 +MapX2: ; d4e8 + ds 1 +MapY2: ; d4e9 + ds 1 + + ds 3 + +; relative to the bg map, in px +PlayerSpriteX: ; d4ed + ds 1 +PlayerSpriteY: ; d4ee + ds 1 + + +SECTION "Objects",WRAMX[$d71e],BANK[1] +MapObjects: ; d71e + ds OBJECT_LENGTH * NUM_OBJECTS + + +SECTION "VariableSprites",WRAMX[$d82e],BANK[1] +VariableSprites: ; d82e + ds $10 + + +SECTION "Status",WRAMX[$d841],BANK[1] +TimeOfDayPal: ; d841 + ds 1 + ds 4 +; d846 + ds 1 + ds 1 +CurTimeOfDay: ; d848 + ds 1 + + ds 3 + +StatusFlags: ; d84c + ds 1 +StatusFlags2: ; d84d + ds 1 + +Money: ; d84e + ds 3 + + ds 4 + +Coins: ; d855 + ds 2 + +Badges: +JohtoBadges: ; d857 + ds 1 +KantoBadges: ; d858 + ds 1 + +SECTION "Items",WRAMX[$d859],BANK[1] +TMsHMs: ; d859 + ds 57 +TMsHMsEnd: + +NumItems: ; d892 + ds 1 +Items: ; d893 + ds 41 +ItemsEnd: + +NumKeyItems: ; d8bc + ds 1 +KeyItems: ; d8bd + ds 26 +KeyItemsEnd: + +NumBalls: ; d8d7 + ds 1 +Balls: ; d8d8 + ds 25 +BallsEnd: + +PCItems: ; d8f1 + ds 101 +PCItemsEnd: + + +SECTION "overworld",WRAMX[$d95b],BANK[1] +WhichRegisteredItem: ; d95b + ds 1 +RegisteredItem: ; d95c + ds 1 + +PlayerState: ; d95d + ds 1 + +SECTION "scriptram",WRAMX[$d962],BANK[1] +MooMooBerries: ; d962 + ds 1 ; how many berries fed to MooMoo +UndergroundSwitchPositions: ; d963 + ds 1 ; which positions the switches are in +FarfetchdPosition: ; d964 + ds 1 ; which position the ilex farfetch'd is in + +SECTION "Events",WRAMX[$da72],BANK[1] + +EventFlags: ; da72 +;RoomDecorations: ; dac6 +;TeamRocketAzaleaTownAttackEvent: ; db51 +;PoliceAtElmsLabEvent: ; db52 +;SalesmanMahoganyTownEvent: ; db5c +;RedGyaradosEvent: ; db5c + ds 250 +; db6c + +SECTION "BoxNames",WRAMX[$db75],BANK[1] +; 8 chars + $50 +Box1Name: ; db75 + ds 9 +Box2Name: ; db7e + ds 9 +Box3Name: ; db87 + ds 9 +Box4Name: ; db90 + ds 9 +Box5Name: ; db99 + ds 9 +Box6Name: ; dba2 + ds 9 +Box7Name: ; dbab + ds 9 +Box8Name: ; dbb4 + ds 9 +Box9Name: ; dbbd + ds 9 +Box10Name: ; dbc6 + ds 9 +Box11Name: ; dbcf + ds 9 +Box12Name: ; dbd8 + ds 9 +Box13Name: ; dbe1 + ds 9 +Box14Name: ; dbea + ds 9 + +SECTION "bike", WRAMX[$dbf5],BANK[1] +BikeFlags: ; dbf5 +; bit 1: always on bike +; bit 2: downhill + ds 1 + +SECTION "decorations", WRAMX[$dc0f],BANK[1] +; Sprite id of each decoration +Bed: ; dc0f + ds 1 +Carpet: ; dc10 + ds 1 +Plant: ; dc11 + ds 1 +Poster: ; dc12 + ds 1 +Console: ; dc13 + ds 1 +LeftOrnament: ; dc14 + ds 1 +RightOrnament: ; dc15 + ds 1 +BigDoll: ; dc16 + ds 1 + +SECTION "fruittrees", WRAMX[$dc27],BANK[1] +FruitTreeFlags: ; dc27 + ds 1 + +SECTION "steps", WRAMX[$dc73],BANK[1] +StepCount: ; dc73 + ds 1 +PoisonStepCount: ; dc74 + ds 1 + +SECTION "FlypointPermissions", WRAMX[$dca5],BANK[1] +FlypointPerms: ; dca5 + ds 4 + +SECTION "BackupMapInfo", WRAMX[$dcad],BANK[1] + +; used on maps like second floor pokécenter, which are reused, so we know which +; map to return to +BackupMapGroup: ; dcad + ds 1 +BackupMapNumber: ; dcae + ds 1 + +SECTION "PlayerMapInfo", WRAMX[$dcb4],BANK[1] + +WarpNumber: ; dcb4 + ds 1 +MapGroup: ; dcb5 + ds 1 ; map group of current map +MapNumber: ; dcb6 + ds 1 ; map number of current map +YCoord: ; dcb7 + ds 1 ; current y coordinate relative to top-left corner of current map +XCoord: ; dcb8 + ds 1 ; current x coordinate relative to top-left corner of current map + +SECTION "PlayerParty",WRAMX[$dcd7],BANK[1] + +PartyCount: ; dcd7 + ds 1 ; number of Pokémon in party +PartySpecies: ; dcd8 + ds 6 ; species of each Pokémon in party +PartyEnd: ; dcde + ds 1 ; legacy functions don't check PartyCount + +PartyMons: +PartyMon1: +PartyMon1Species: ; dcdf + ds 1 +PartyMon1Item: ; dce0 + ds 1 + +PartyMon1Moves: ; dce1 +PartyMon1Move1: ; dce1 + ds 1 +PartyMon1Move2: ; dce2 + ds 1 +PartyMon1Move3: ; dce3 + ds 1 +PartyMon1Move4: ; dce4 + ds 1 + +PartyMon1ID: ; dce5 + ds 2 +PartyMon1Exp: ; dce7 + ds 3 + +PartyMon1HPExp: ; dcea + ds 2 +PartyMon1AtkExp: ; dcec + ds 2 +PartyMon1DefExp: ; dcee + ds 2 +PartyMon1SpdExp: ; dcf0 + ds 2 +PartyMon1SpclExp: ; dcf2 + ds 2 + +PartyMon1DVs: ; dcf4 +; hp = %1000 for each dv + ds 1 ; atk/def + ds 1 ; spd/spc +PartyMon1PP: ; dcf6 + ds 4 +PartyMon1Happiness: ; dcfa + ds 1 +PartyMon1PokerusStatus: ; dcfb + ds 1 +PartyMon1CaughtData: ; dcfc +PartyMon1CaughtTime: ; dcfc +PartyMon1CaughtLevel: ; dcfc + ds 1 +PartyMon1CaughtGender: ; dcfd +PartyMon1CaughtLocation: ; dcfd + ds 1 +PartyMon1Level: ; dcfe + ds 1 +PartyMon1Status: ; dcff + ds 1 +; dd00 unused + ds 1 +PartyMon1CurHP: ; dd01 + ds 2 +PartyMon1MaxHP: ; dd03 + ds 2 +PartyMon1Atk: ; dd05 + ds 2 +PartyMon1Def: ; dd07 + ds 2 +PartyMon1Spd: ; dd09 + ds 2 +PartyMon1SpclAtk: ; dd0b + ds 2 +PartyMon1SpclDef: ; dd0d + ds 2 + + +PartyMon2: ; dd0f + ds 48 +PartyMon3: ; dd3f + ds 48 +PartyMon4: ; dd6f + ds 48 +PartyMon5: ; dd9f + ds 48 +PartyMon6: ; ddcf + ds 48 + +PartyMonOT: +PartyMon1OT: ; ddff + ds 11 +PartyMon2OT: ; de0a + ds 11 +PartyMon3OT: ; de15 + ds 11 +PartyMon4OT: ; de20 + ds 11 +PartyMon5OT: ; de2b + ds 11 +PartyMon6OT: ; de36 + ds 11 + +PartyMonNicknames: +PartyMon1Nickname: ; de41 + ds 11 +PartyMon2Nickname: ; de4c + ds 11 +PartyMon3Nickname: ; de57 + ds 11 +PartyMon4Nickname: ; de62 + ds 11 +PartyMon5Nickname: ; de6d + ds 11 +PartyMon6Nickname: ; de78 + ds 11 +PartyMonNicknamesEnd: + +SECTION "Pokedex",WRAMX[$de99],BANK[1] +PokedexCaught: ; de99 + ds 32 +EndPokedexCaught: +PokedexSeen: ; deb9 + ds 32 +EndPokedexSeen: +UnownDex: ; ded9 + ds 26 +UnlockedUnowns: ; def3 + ds 1 + +SECTION "Breeding",WRAMX[$def5],BANK[1] +DaycareMan: ; def5 +; bit 7: active +; bit 6: monsters are compatible +; bit 5: egg ready +; bit 0: monster 1 in daycare + ds 1 + +BreedMon1: +BreedMon1Nick: ; def6 + ds 11 +BreedMon1OT: ; df01 + ds 11 +BreedMon1Stats: +BreedMon1Species: ; df0c + ds 1 + ds 31 + +DaycareLady: ; df2c +; bit 7: active +; bit 0: monster 2 in daycare + ds 1 + +StepsToEgg: ; df2d + ds 1 +DittoInDaycare: ; df2e +; z: yes +; nz: no + ds 1 + +BreedMon2: +BreedMon2Nick: ; df2f + ds 11 +BreedMon2OT: ; df3a + ds 11 +BreedMon2Stats: +BreedMon2Species: ; df45 + ds 1 + ds 31 + +EggNick: ; df65 +; EGG@ + ds 11 +EggOT: ; df70 + ds 11 +EggStats: +EggSpecies: ; df7b + ds 1 + ds 31 + +SECTION "RoamMons",WRAMX[$dfcf],BANK[1] +RoamMon1Species: ; dfcf + ds 1 +RoamMon1Level: ; dfd0 + ds 1 +RoamMon1MapGroup: ; dfd1 + ds 1 +RoamMon1MapNumber: ; dfd2 + ds 1 +RoamMon1CurHP: ; dfd3 + ds 1 +RoamMon1DVs: ; dfd4 + ds 2 + +RoamMon2Species: ; dfd6 + ds 1 +RoamMon2Level: ; dfd7 + ds 1 +RoamMon2MapGroup: ; dfd8 + ds 1 +RoamMon2MapNumber: ; dfd9 + ds 1 +RoamMon2CurHP: ; dfda + ds 1 +RoamMon2DVs: ; dfdb + ds 2 + +RoamMon3Species: ; dfdd + ds 1 +RoamMon3Level: ; dfde + ds 1 +RoamMon3MapGroup: ; dfdf + ds 1 +RoamMon3MapNumber: ; dfe0 + ds 1 +RoamMon3CurHP: ; dfe1 + ds 1 +RoamMon3DVs: ; dfe2 + ds 2 + + + +SECTION "WRAMBank5",WRAMX[$d000],BANK[5] + +; 8 4-color palettes +Unkn1Pals: ; d000 + ds $40 +Unkn2Pals: ; d040 + ds $40 +BGPals: ; d080 + ds $40 +OBPals: ; d0c0 + ds $40 + +LYOverrides: ; d100 + ds 144 +LYOverridesEnd: + + +SECTION "SRAMBank1",SRAM,BANK[1] + +SECTION "BoxMons",SRAM[$ad10],BANK[1] +BoxCount: ; ad10 + ds 1 +BoxSpecies: ; ad11 + ds 20 + ds 1 +BoxMons: +BoxMon1: +BoxMon1Species: ; ad26 + ds 1 +BoxMon1Item: ; ad27 + ds 1 +BoxMon1Moves: ; ad28 + ds 4 +BoxMon1ID: ; ad2c + ds 2 +BoxMon1Exp: ; ad2e + ds 3 +BoxMon1HPExp: ; ad31 + ds 2 +BoxMon1AtkExp: ; ad33 + ds 2 +BoxMon1DefExp: ; ad35 + ds 2 +BoxMon1SpdExp: ; ad37 + ds 2 +BoxMon1SpcExp: ; ad39 + ds 2 +BoxMon1DVs: ; ad3b + ds 2 +BoxMon1PP: ; ad3d + ds 4 +BoxMon1Happiness: ; ad41 + ds 1 +BoxMon1PokerusStatus: ; ad42 + ds 1 +BoxMon1CaughtData: +BoxMon1CaughtTime: +BoxMon1CaughtLevel: ; ad43 + ds 1 +BoxMon1CaughtGender: +BoxMon1CaughtLocation: ; ad44 + ds 1 +BoxMon1Level: ; ad45 + ds 1 + +BoxMon2: ; ad46 + ds 32 +BoxMon3: ; ad66 + ds 32 +BoxMon4: ; ad86 + ds 32 +BoxMon5: ; ada6 + ds 32 +BoxMon6: ; adc6 + ds 32 +BoxMon7: ; ade6 + ds 32 +BoxMon8: ; ae06 + ds 32 +BoxMon9: ; ae26 + ds 32 +BoxMon10: ; ae46 + ds 32 +BoxMon11: ; ae66 + ds 32 +BoxMon12: ; ae86 + ds 32 +BoxMon13: ; aea6 + ds 32 +BoxMon14: ; aec6 + ds 32 +BoxMon15: ; aee6 + ds 32 +BoxMon16: ; af06 + ds 32 +BoxMon17: ; af26 + ds 32 +BoxMon18: ; af46 + ds 32 +BoxMon19: ; af66 + ds 32 +BoxMon20: ; af86 + ds 32 + +BoxMonOT: +BoxMon1OT: ; afa6 + ds 11 +BoxMon2OT: ; afb1 + ds 11 +BoxMon3OT: ; afbc + ds 11 +BoxMon4OT: ; afc7 + ds 11 +BoxMon5OT: ; afd2 + ds 11 +BoxMon6OT: ; afdd + ds 11 +BoxMon7OT: ; afe8 + ds 11 +BoxMon8OT: ; aff3 + ds 11 +BoxMon9OT: ; affe + ds 11 +BoxMon10OT: ; b009 + ds 11 +BoxMon11OT: ; b014 + ds 11 +BoxMon12OT: ; b01f + ds 11 +BoxMon13OT: ; b02a + ds 11 +BoxMon14OT: ; b035 + ds 11 +BoxMon15OT: ; b040 + ds 11 +BoxMon16OT: ; b04b + ds 11 +BoxMon17OT: ; b056 + ds 11 +BoxMon18OT: ; b061 + ds 11 +BoxMon19OT: ; b06c + ds 11 +BoxMon20OT: ; b077 + ds 11 + +BoxMonNicknames: +BoxMon1Nickname: ; b082 + ds 11 +BoxMon2Nickname: ; b08d + ds 11 +BoxMon3Nickname: ; b098 + ds 11 +BoxMon4Nickname: ; b0a3 + ds 11 +BoxMon5Nickname: ; b0ae + ds 11 +BoxMon6Nickname: ; b0b9 + ds 11 +BoxMon7Nickname: ; b0c4 + ds 11 +BoxMon8Nickname: ; b0cf + ds 11 +BoxMon9Nickname: ; b0da + ds 11 +BoxMon10Nickname: ; b0e5 + ds 11 +BoxMon11Nickname: ; b0f0 + ds 11 +BoxMon12Nickname: ; b0fb + ds 11 +BoxMon13Nickname: ; b106 + ds 11 +BoxMon14Nickname: ; b111 + ds 11 +BoxMon15Nickname: ; b11c + ds 11 +BoxMon16Nickname: ; b127 + ds 11 +BoxMon17Nickname: ; b132 + ds 11 +BoxMon18Nickname: ; b13d + ds 11 +BoxMon19Nickname: ; b148 + ds 11 +BoxMon20Nickname: ; b153 + ds 11 +BoxMonNicknamesEnd: + diff --git a/pokemontools/gfx.py b/pokemontools/gfx.py index 2ea6b54..37dd1b6 100644 --- a/pokemontools/gfx.py +++ b/pokemontools/gfx.py @@ -5,12 +5,15 @@ import sys import png from math import sqrt, floor, ceil -import crystal +import configuration +config = configuration.Config() + import pokemon_constants import trainers +import romstr if __name__ != "__main__": - rom = crystal.load_rom() + rom = romstr.RomStr(filename=config.rom_path) def split(list_, interval): diff --git a/pokemontools/map_editor.py b/pokemontools/map_editor.py index 4c0bec5..566d422 100644 --- a/pokemontools/map_editor.py +++ b/pokemontools/map_editor.py @@ -6,8 +6,8 @@ from ttk import Frame, Style import PIL from PIL import Image, ImageTk -import config -conf = config.Config() +import configuration +conf = configuration.Config() #version = 'crystal' diff --git a/pokemontools/vba/autoplayer.py b/pokemontools/vba/autoplayer.py index 9aa8f4a..af14d47 100644 --- a/pokemontools/vba/autoplayer.py +++ b/pokemontools/vba/autoplayer.py @@ -4,492 +4,580 @@ Programmatic speedrun of Pokémon Crystal """ import os -# bring in the emulator and basic tools -import vba - -def main(): - """ - Start the game. - """ - vba.load_rom() +import pokemontools.configuration as configuration - # get past the opening sequence - skip_intro() - - # walk to mom and handle her text - handle_mom() - - # walk outside into new bark town - walk_into_new_bark_town() - - # walk to elm and do whatever he wants - handle_elm("totodile") - - new_bark_level_grind(10, skip=False) +# bring in the emulator and basic tools +import vba as _vba def skippable(func): """ Makes a function skippable. - Saves the state before and after the function runs. - Pass "skip=True" to the function to load the previous save - state from when the function finished. + Saves the state before and after the function runs. Pass "skip=True" to the + function to load the previous save state from when the function finished. """ def wrapped_function(*args, **kwargs): + self = args[0] skip = True + override = True if "skip" in kwargs.keys(): skip = kwargs["skip"] del kwargs["skip"] + if "override" in kwargs.keys(): + override = kwargs["override"] + del kwargs["override"] + # override skip if there's no save if skip: full_name = func.__name__ + "-end.sav" - if not os.path.exists(os.path.join(vba.save_state_path, full_name)): + if not os.path.exists(os.path.join(self.config.save_state_path, full_name)): skip = False return_value = None if not skip: - vba.save_state(func.__name__ + "-start", override=True) + if override: + self.cry.save_state(func.__name__ + "-start", override=override) + return_value = func(*args, **kwargs) - vba.save_state(func.__name__ + "-end", override=True) + + if override: + self.cry.save_state(func.__name__ + "-end", override=override) elif skip: - vba.set_state(vba.load_state(func.__name__ + "-end")) + self.cry.vba.state = self.cry.load_state(func.__name__ + "-end") return return_value return wrapped_function -@skippable -def skip_intro(): +class Runner(object): """ - Skip the game boot intro sequence. + ``Runner`` is used to represent a set of functions that control an instance + of the emulator. This allows for automated runs of games. """ + pass - # copyright sequence - vba.nstep(400) +class SpeedRunner(Runner): + def __init__(self, cry=None, config=None): + super(SpeedRunner, self).__init__() - # skip the ditto sequence - vba.press("a") - vba.nstep(100) + self.cry = cry - # skip the start screen - vba.press("start") - vba.nstep(100) + if not config: + config = configuration.Config() - # click "new game" - vba.press("a", holdsteps=50, aftersteps=1) + self.config = config - # skip text up to "Are you a boy? Or are you a girl?" - vba.crystal.text_wait() + def setup(self): + """ + Configure this ``Runner`` instance to contain a reference to an active + emulator session. + """ + if not self.cry: + self.cry = _vba.crystal(config=self.config) - # select "Boy" - vba.press("a", holdsteps=50, aftersteps=1) + def main(self): + """ + Main entry point for complete control of the game as the main player. + """ + # get past the opening sequence + self.skip_intro(skip=True) - # text until "What time is it?" - vba.crystal.text_wait() + # walk to mom and handle her text + self.handle_mom(skip=True) - # select 10 o'clock - vba.press("a", holdsteps=50, aftersteps=1) + # walk outside into new bark town + self.walk_into_new_bark_town(skip=True) - # yes i mean it - vba.press("a", holdsteps=50, aftersteps=1) + # walk to elm and do whatever he wants + self.handle_elm("totodile", skip=True) - # "How many minutes?" 0 min. - vba.press("a", holdsteps=50, aftersteps=1) + self.new_bark_level_grind(17, skip=False) - # "Who! 0 min.?" yes/no select yes - vba.press("a", holdsteps=50, aftersteps=1) + @skippable + def skip_intro(self, stop_at_name_selection=False): + """ + Skip the game boot intro sequence. + """ - # read text until name selection - vba.crystal.text_wait() + # copyright sequence + self.cry.nstep(400) - # select "Chris" - vba.press("d", holdsteps=10, aftersteps=1) - vba.press("a", holdsteps=50, aftersteps=1) + # skip the ditto sequence + self.cry.vba.press("a") + self.cry.nstep(100) - def overworldcheck(): - """ - A basic check for when the game starts. - """ - return vba.get_memory_at(0xcfb1) != 0 + # skip the start screen + self.cry.vba.press("start") + self.cry.nstep(100) - # go until the introduction is done - vba.crystal.text_wait(callback=overworldcheck) + # click "new game" + self.cry.vba.press("a", hold=50, after=1) - return + # skip text up to "Are you a boy? Or are you a girl?" + self.cry.text_wait() -@skippable -def handle_mom(): - """ - Walk to mom. Handle her speech and questions. - """ + # select "Boy" + self.cry.vba.press("a", hold=50, after=1) - vba.crystal.move("r") - vba.crystal.move("r") - vba.crystal.move("r") - vba.crystal.move("r") + # text until "What time is it?" + self.cry.text_wait() - vba.crystal.move("u") - vba.crystal.move("u") - vba.crystal.move("u") + # select 10 o'clock + self.cry.vba.press("a", hold=50, after=1) - vba.crystal.move("d") - vba.crystal.move("d") + # yes i mean it + self.cry.vba.press("a", hold=50, after=1) - # move into mom's line of sight - vba.crystal.move("d") + # "How many minutes?" 0 min. + self.cry.vba.press("a", hold=50, after=1) - # let mom talk until "What day is it?" - vba.crystal.text_wait() + # "Who! 0 min.?" yes/no select yes + self.cry.vba.press("a", hold=50, after=1) - # "What day is it?" Sunday - vba.press("a", holdsteps=10) # Sunday + # read text until name selection + self.cry.text_wait() - vba.crystal.text_wait() + if stop_at_name_selection: + return - # "SUNDAY, is it?" yes/no - vba.press("a", holdsteps=10) # yes + # select "Chris" + self.cry.vba.press("d", hold=10, after=1) + self.cry.vba.press("a", hold=50, after=1) - vba.crystal.text_wait() + def overworldcheck(): + """ + A basic check for when the game starts. + """ + return self.cry.vba.memory[0xcfb1] != 0 - # "Is it Daylight Saving Time now?" yes/no - vba.press("a", holdsteps=10) # yes + # go until the introduction is done + self.cry.text_wait(callback=overworldcheck) - vba.crystal.text_wait() + return - # "AM DST, is that OK?" yes/no - vba.press("a", holdsteps=10) # yes + @skippable + def handle_mom(self): + """ + Walk to mom. Handle her speech and questions. + """ - # text until "know how to use the PHONE?" yes/no - vba.crystal.text_wait() + self.cry.move("r") + self.cry.move("r") + self.cry.move("r") + self.cry.move("r") - # press yes - vba.press("a", holdsteps=10) + self.cry.move("u") + self.cry.move("u") + self.cry.move("u") - # wait until mom is done talking - vba.crystal.text_wait() + self.cry.move("d") + self.cry.move("d") - # wait until the script is done running - vba.crystal.wait_for_script_running() + # move into mom's line of sight + self.cry.move("d") - return + # let mom talk until "What day is it?" + self.cry.text_wait() -@skippable -def walk_into_new_bark_town(): - """ - Walk outside after talking with mom. - """ + # "What day is it?" Sunday + self.cry.vba.press("a", hold=10) # Sunday - vba.crystal.move("d") - vba.crystal.move("d") - vba.crystal.move("d") - vba.crystal.move("l") - vba.crystal.move("l") + self.cry.text_wait() - # walk outside - vba.crystal.move("d") + # "SUNDAY, is it?" yes/no + self.cry.vba.press("a", hold=10) # yes -@skippable -def handle_elm(starter_choice): - """ - Walk to Elm's Lab and get a starter. - """ + self.cry.text_wait() - # walk to the lab - vba.crystal.move("l") - vba.crystal.move("l") - vba.crystal.move("l") - vba.crystal.move("l") - vba.crystal.move("l") - vba.crystal.move("l") - vba.crystal.move("l") - vba.crystal.move("u") - vba.crystal.move("u") + # "Is it Daylight Saving Time now?" yes/no + self.cry.vba.press("a", hold=10) # yes - # walk into the lab - vba.crystal.move("u") + self.cry.text_wait() - # talk to elm - vba.crystal.text_wait() + # "AM DST, is that OK?" yes/no + self.cry.vba.press("a", hold=10) # yes - # "that I recently caught." yes/no - vba.press("a", holdsteps=10) # yes + # text until "know how to use the PHONE?" yes/no + self.cry.text_wait() - # talk to elm some more - vba.crystal.text_wait() + # press yes + self.cry.vba.press("a", hold=10) - # talking isn't done yet.. - vba.crystal.text_wait() - vba.crystal.text_wait() - vba.crystal.text_wait() + # wait until mom is done talking + self.cry.text_wait() - # wait until the script is done running - vba.crystal.wait_for_script_running() + # wait until the script is done running + self.cry.wait_for_script_running() - # move toward the pokeballs - vba.crystal.move("r") + return - # move to cyndaquil - vba.crystal.move("r") + @skippable + def walk_into_new_bark_town(self): + """ + Walk outside after talking with mom. + """ - moves = 0 + self.cry.move("d") + self.cry.move("d") + self.cry.move("d") + self.cry.move("l") + self.cry.move("l") + + # walk outside + self.cry.move("d") + + @skippable + def handle_elm(self, starter_choice): + """ + Walk to Elm's Lab and get a starter. + """ + + # walk to the lab + self.cry.move("l") + self.cry.move("l") + self.cry.move("l") + self.cry.move("l") + self.cry.move("l") + self.cry.move("l") + self.cry.move("l") + self.cry.move("u") + self.cry.move("u") + + # walk into the lab + self.cry.move("u") + + # talk to elm + self.cry.text_wait() + + # "that I recently caught." yes/no + self.cry.vba.press("a", hold=10) # yes + + # talk to elm some more + self.cry.text_wait() + + # talking isn't done yet.. + self.cry.text_wait() + self.cry.text_wait() + self.cry.text_wait() + + # wait until the script is done running + self.cry.wait_for_script_running() + + # move toward the pokeballs + self.cry.move("r") + + # move to cyndaquil + self.cry.move("r") - if starter_choice.lower() == "cyndaquil": moves = 0 - if starter_choice.lower() == "totodile": - moves = 1 - else: - moves = 2 - for each in range(0, moves): - vba.crystal.move("r") + if starter_choice.lower() == "cyndaquil": + moves = 0 + elif starter_choice.lower() == "totodile": + moves = 1 + else: + moves = 2 - # face the pokeball - vba.crystal.move("u") + for each in range(0, moves): + self.cry.move("r") - # select it - vba.press("a", holdsteps=10, aftersteps=0) + # face the pokeball + self.cry.move("u") - # wait for the image to pop up - vba.crystal.text_wait() + # select it + self.cry.vba.press("a", hold=10, after=0) - # wait for the image to close - vba.crystal.text_wait() + # wait for the image to pop up + self.cry.text_wait() - # wait for the yes/no box - vba.crystal.text_wait() + # wait for the image to close + self.cry.text_wait() - # press yes - vba.press("a", holdsteps=10, aftersteps=0) + # wait for the yes/no box + self.cry.text_wait() - # wait for elm to talk a bit - vba.crystal.text_wait() + # press yes + self.cry.vba.press("a", hold=10, after=0) - # TODO: why didn't that finish his talking? - vba.crystal.text_wait() + # wait for elm to talk a bit + self.cry.text_wait() - # give a nickname? yes/no - vba.press("d", holdsteps=10, aftersteps=0) # move to "no" - vba.press("a", holdsteps=10, aftersteps=0) # no + # TODO: why didn't that finish his talking? + self.cry.text_wait() - # TODO: why didn't this wait until he was completely done? - vba.crystal.text_wait() - vba.crystal.text_wait() + # give a nickname? yes/no + self.cry.vba.press("d", hold=10, after=0) # move to "no" + self.cry.vba.press("a", hold=10, after=0) # no - # get the phone number - vba.crystal.text_wait() + # TODO: why didn't this wait until he was completely done? + self.cry.text_wait() + self.cry.text_wait() - # talk with elm a bit more - vba.crystal.text_wait() + # get the phone number + self.cry.text_wait() - # TODO: and again.. wtf? - vba.crystal.text_wait() + # talk with elm a bit more + self.cry.text_wait() - # wait until the script is done running - vba.crystal.wait_for_script_running() + # wait until the script is done running + self.cry.wait_for_script_running() - # move down - vba.crystal.move("d") - vba.crystal.move("d") - vba.crystal.move("d") - vba.crystal.move("d") + # move down + self.cry.move("d") + self.cry.move("d") + self.cry.move("d") + self.cry.move("d") - # move into the researcher's line of sight - vba.crystal.move("d") + # move into the researcher's line of sight + self.cry.move("d") - # get the potion from the person - vba.crystal.text_wait() - vba.crystal.text_wait() + # get the potion from the person + self.cry.text_wait() + self.cry.text_wait() - # wait for the script to end - vba.crystal.wait_for_script_running() + # wait for the script to end + self.cry.wait_for_script_running() - vba.crystal.move("d") - vba.crystal.move("d") - vba.crystal.move("d") + self.cry.move("d") + self.cry.move("d") + self.cry.move("d") - # go outside - vba.crystal.move("d") + # go outside + self.cry.move("d") - return + return -@skippable -def new_bark_level_grind(level): - """ - Do level grinding in New Bark. + @skippable + def new_bark_level_grind(self, level, walk_to_grass=True): + """ + Do level grinding in New Bark. - Starting just outside of Elm's Lab, do some level grinding until the first - partymon level is equal to the given value.. - """ + Starting just outside of Elm's Lab, do some level grinding until the + first partymon level is equal to the given value.. + """ - # walk to the grass area - new_bark_level_grind_walk_to_grass(skip=False) + # walk to the grass area + if walk_to_grass: + self.new_bark_level_grind_walk_to_grass(skip=False) - # TODO: walk around in grass, handle battles - walk = ["d", "d", "u", "d", "u", "d"] - for direction in walk: - vba.crystal.move(direction) + last_direction = "u" - # wait for wild battle to completely start - vba.crystal.text_wait() + # walk around in the grass until a battle happens + while self.cry.vba.memory[0xd22d] == 0: + if last_direction == "u": + direction = "d" + else: + direction = "u" - attacks = 5 + self.cry.move(direction) - while attacks > 0: - # FIGHT - vba.press("a", holdsteps=10, aftersteps=1) + last_direction = direction - # wait to select a move - vba.crystal.text_wait() + # wait for wild battle to completely start + self.cry.text_wait() - # SCRATCH - vba.press("a", holdsteps=10, aftersteps=1) + attacks = 5 - # wait for the move to be over - vba.crystal.text_wait() + while attacks > 0: + # FIGHT + self.cry.vba.press("a", hold=10, after=1) - hp = ((vba.get_memory_at(0xd218) << 8) | vba.get_memory_at(0xd217)) - print "enemy hp is: " + str(hp) + # wait to select a move + self.cry.text_wait() - if hp == 0: - print "enemy hp is zero, exiting" - break - else: + # SCRATCH + self.cry.vba.press("a", hold=10, after=1) + + # wait for the move to be over + self.cry.text_wait() + + hp = self.cry.get_enemy_hp() print "enemy hp is: " + str(hp) - attacks = attacks - 1 - - while vba.get_memory_at(0xd22d) != 0: - vba.press("a", holdsteps=10, aftersteps=1) - - # wait for the map to finish loading - vba.nstep(50) - - print "okay, back in the overworld" - - # move up - vba.crystal.move("u") - vba.crystal.move("u") - vba.crystal.move("u") - vba.crystal.move("u") - - # move into new bark town - vba.crystal.move("r") - vba.crystal.move("r") - vba.crystal.move("r") - vba.crystal.move("r") - vba.crystal.move("r") - vba.crystal.move("r") - vba.crystal.move("r") - vba.crystal.move("r") - vba.crystal.move("r") - vba.crystal.move("r") - - # move up - vba.crystal.move("u") - vba.crystal.move("u") - vba.crystal.move("u") - vba.crystal.move("u") - vba.crystal.move("u") - - # move to the door - vba.crystal.move("r") - vba.crystal.move("r") - vba.crystal.move("r") - - # walk in - vba.crystal.move("u") - - # move up to the healing thing - vba.crystal.move("u") - vba.crystal.move("u") - vba.crystal.move("u") - vba.crystal.move("u") - vba.crystal.move("u") - vba.crystal.move("u") - vba.crystal.move("u") - vba.crystal.move("u") - vba.crystal.move("u") - vba.crystal.move("l") - vba.crystal.move("l") - - # face it - vba.crystal.move("u") - - # interact - vba.press("a", holdsteps=10, aftersteps=1) - - # wait for yes/no box - vba.crystal.text_wait() - - # press yes - vba.press("a", holdsteps=10, aftersteps=1) - - # TODO: when is healing done? - - # wait until the script is done running - vba.crystal.wait_for_script_running() - - # wait for it to be really really done - vba.nstep(50) - - vba.crystal.move("r") - vba.crystal.move("r") - - # move to the door - vba.crystal.move("d") - vba.crystal.move("d") - vba.crystal.move("d") - vba.crystal.move("d") - vba.crystal.move("d") - vba.crystal.move("d") - vba.crystal.move("d") - vba.crystal.move("d") - vba.crystal.move("d") - - # walk out - vba.crystal.move("d") - - # check partymon1 level - if vba.get_memory_at(0xdcfe) < level: - new_bark_level_grind(level, skip=False) - else: - return + if hp == 0: + print "enemy hp is zero, exiting" + break + else: + print "enemy hp is: " + str(hp) + + attacks = attacks - 1 + + while self.cry.vba.memory[0xd22d] != 0: + self.cry.vba.press("a", hold=10, after=1) + + # wait for the map to finish loading + self.cry.vba.step(count=50) + + # This is used to handle any additional textbox that might be up on the + # screen. The debug parameter is set to True so that max_wait is + # enabled. This might be a textbox that is still waiting around because + # of some faint during the battle. I am not completely sure why this + # happens. + self.cry.text_wait(max_wait=30, debug=True) + + print "okay, back in the overworld" + + cur_hp = ((self.cry.vba.memory[0xdd01] << 8) | self.cry.vba.memory[0xdd02]) + move_pp = self.cry.vba.memory[0xdcf6] # move 1 pp + + # if pokemon health is >20, just continue + # if move 1 PP is 0, just continue + if cur_hp > 20 and move_pp > 5 and self.cry.vba.memory[0xdcfe] < level: + self.cry.move("u") + return self.new_bark_level_grind(level, walk_to_grass=False, skip=False) + + # move up + self.cry.move("u") + self.cry.move("u") + self.cry.move("u") + self.cry.move("u") + + # move into new bark town + self.cry.move("r") + self.cry.move("r") + self.cry.move("r") + self.cry.move("r") + self.cry.move("r") + self.cry.move("r") + self.cry.move("r") + self.cry.move("r") + self.cry.move("r") + self.cry.move("r") + + # move up + self.cry.move("u") + self.cry.move("u") + self.cry.move("u") + self.cry.move("u") + self.cry.move("u") + + # move to the door + self.cry.move("r") + self.cry.move("r") + self.cry.move("r") + + # walk in + self.cry.move("u") + + # move up to the healing thing + self.cry.move("u") + self.cry.move("u") + self.cry.move("u") + self.cry.move("u") + self.cry.move("u") + self.cry.move("u") + self.cry.move("u") + self.cry.move("u") + self.cry.move("u") + self.cry.move("l") + self.cry.move("l") + + # face it + self.cry.move("u") + + # interact + self.cry.vba.press("a", hold=10, after=1) + + # wait for yes/no box + self.cry.text_wait() + + # press yes + self.cry.vba.press("a", hold=10, after=1) + + # TODO: when is healing done? + + # wait until the script is done running + self.cry.wait_for_script_running() + + # wait for it to be really really done + self.cry.vba.step(count=50) + + self.cry.move("r") + self.cry.move("r") + + # move to the door + self.cry.move("d") + self.cry.move("d") + self.cry.move("d") + self.cry.move("d") + self.cry.move("d") + self.cry.move("d") + self.cry.move("d") + self.cry.move("d") + self.cry.move("d") + + # walk out + self.cry.move("d") + + # check partymon1 level + if self.cry.vba.memory[0xdcfe] < level: + self.new_bark_level_grind(level, skip=False) + else: + return + + @skippable + def new_bark_level_grind_walk_to_grass(self): + """ + Move to just above the grass from outside Elm's lab. + """ + + self.cry.move("d") + self.cry.move("d") + + self.cry.move("l") + self.cry.move("l") + + self.cry.move("d") + self.cry.move("d") + + self.cry.move("l") + self.cry.move("l") -@skippable -def new_bark_level_grind_walk_to_grass(): + # move to route 29 past the trees + self.cry.move("l") + self.cry.move("l") + self.cry.move("l") + self.cry.move("l") + self.cry.move("l") + self.cry.move("l") + self.cry.move("l") + self.cry.move("l") + self.cry.move("l") + + # move to just above the grass + self.cry.move("d") + self.cry.move("d") + self.cry.move("d") + +def bootstrap(runner=None, cry=None): """ - Move to just above the grass from outside Elm's lab. + Setup the initial game and return the state. This skips the intro and + performs some other actions to get the game to a reasonable starting state. """ + if not runner: + runner = SpeedRunner(cry=cry) + runner.setup() + + # skip=False means always run the skip_intro function regardless of the + # presence of a saved after state. + runner.skip_intro(skip=True) + + # keep a reference of the current state + state = runner.cry.vba.state + + runner.cry.vba.shutdown() - vba.crystal.move("d") - vba.crystal.move("d") - - vba.crystal.move("l") - vba.crystal.move("l") - - vba.crystal.move("d") - vba.crystal.move("d") - - vba.crystal.move("l") - vba.crystal.move("l") - - # move to route 29 past the trees - vba.crystal.move("l") - vba.crystal.move("l") - vba.crystal.move("l") - vba.crystal.move("l") - vba.crystal.move("l") - vba.crystal.move("l") - vba.crystal.move("l") - vba.crystal.move("l") - vba.crystal.move("l") - - # move to just above the grass - vba.crystal.move("d") - vba.crystal.move("d") - vba.crystal.move("d") + return state + +def main(): + """ + Setup a basic ``SpeedRunner`` instance and then run the runner. + """ + runner = SpeedRunner() + runner.setup() + return runner.main() if __name__ == "__main__": main() diff --git a/pokemontools/vba/battle.py b/pokemontools/vba/battle.py new file mode 100644 index 0000000..39d7047 --- /dev/null +++ b/pokemontools/vba/battle.py @@ -0,0 +1,521 @@ +""" +Code that attempts to model a battle. +""" + +from pokemontools.vba.vba import crystal as emulator +import pokemontools.vba.vba as vba + +class BattleException(Exception): + """ + Something went terribly wrong in a battle. + """ + +class EmulatorController(object): + """ + Controls the emulator. I don't have a good reason for this. + """ + +class Battle(EmulatorController): + """ + Wrapper around the battle routine inside of the game. This object controls + the emulator and provides a sanitized interface for interacting with a + battle through python. + """ + + def __init__(self, emulator=None): + """ + Setup the battle. + """ + self.emulator = emulator + + def is_in_battle(self): + """ + @rtype: bool + """ + return self.emulator.is_in_battle() + + def is_input_required(self): + """ + Detects if the battle is waiting for player input. + """ + return self.is_player_turn() or self.is_mandatory_switch() or self.is_switch_prompt() or self.is_levelup_screen() or self.is_make_room_for_move_prompt() + + def is_fight_pack_run_menu(self): + """ + Attempts to detect if the current menu is fight-pack-run. This is only + for whether or not the player needs to choose what to do next. + """ + signs = ["FIGHT", "PACK", "RUN"] + screentext = self.emulator.get_text() + return all([sign in screentext for sign in signs]) + + def select_battle_menu_action(self, action, execute=True): + """ + Moves the cursor to the requested action and selects it. + + :param action: fight, pkmn, pack, run + """ + if not self.is_fight_pack_run_menu(): + raise Exception( + "This isn't the fight-pack-run menu." + ) + + action = action.lower() + + action_map = { + "fight": (1, 1), + "pkmn": (1, 2), + "pack": (2, 1), + "run": (2, 2), + } + + if action not in action_map.keys(): + raise Exception( + "Unexpected requested action {0}".format(action) + ) + + current_row = self.emulator.vba.read_memory_at(0xcfa9) + current_column = self.emulator.vba.read_memory_at(0xcfaa) + + direction = None + if current_row != action_map[action][0]: + if current_row > action_map[action][0]: + direction = "u" + elif current_row < action_map[action][0]: + direction = "d" + + self.emulator.vba.press(direction, hold=5, after=10) + + direction = None + if current_column != action_map[action][1]: + if current_column > action_map[action][1]: + direction = "l" + elif current_column < action_map[action][1]: + direction = "r" + + self.emulator.vba.press(direction, hold=5, after=10) + + # now select the action + if execute: + self.emulator.vba.press("a", hold=5, after=100) + + def select_attack(self, move_number=1, hold=5, after=10): + """ + Moves the cursor to the correct attack in the menu and presses the + button. + + :param move_number: the attack number on the FIGHT menu. Note that this + starts from 1. + :param hold: how many frames to hold each button press + :param after: how many frames to wait after each button press + """ + # TODO: detect fight menu and make sure it's detected here. + + pp_address = 0xc634 + (move_number - 1) + pp = self.emulator.vba.read_memory_at(pp_address) + + # detect zero pp because i don't want to write a way to inform the + # caller that there was no more pp. Just check the pp yourself. + if pp == 0: + raise BattleException( + "Move {num} has no more PP.".format( + num=move_number, + ) + ) + + valid_selection_states = (1, 2, 3, 4) + + selection = self.emulator.vba.read_memory_at(0xcfa9) + + while selection != move_number: + if selection not in valid_selection_states: + raise BattleException( + "The current selected attack is out of bounds: {num}".format( + num=selection, + ) + ) + + direction = None + + if selection > move_number: + direction = "d" + elif selection < move_number: + direction = "u" + else: + # probably never happens + raise BattleException( + "Not sure what to do here." + ) + + # press the arrow button + self.emulator.vba.press(direction, hold=hold, after=after) + + # let's see what the current selection is + selection = self.emulator.vba.read_memory_at(0xcfa9) + + # press to choose the attack + self.emulator.vba.press("a", hold=hold, after=after) + + def fight(self, move_number): + """ + Select FIGHT from the flight-pack-run menu and select the move + identified by move_number. + """ + # make sure the menu is detected + if not self.is_fight_pack_run_menu(): + raise BattleException( + "Wrong menu. Can't press FIGHT here." + ) + + # select FIGHT + self.select_battle_menu_action("fight") + + # select the requested attack + self.select_attack(move_number) + + def is_player_turn(self): + """ + Detects if the battle is waiting for the player to choose an attack. + """ + return self.is_fight_pack_run_menu() + + def is_trainer_switch_prompt(self): + """ + Detects if the battle is waiting for the player to choose whether or + not to switch pokemon. This is the prompt that asks yes/no for whether + to switch pokemon, like if the trainer is switching pokemon at the end + of a turn set. + """ + return self.emulator.is_trainer_switch_prompt() + + def is_wild_switch_prompt(self): + """ + Detects if the battle is waiting for the player to choose whether or + not to continue to fight the wild pokemon. + """ + return self.emulator.is_wild_switch_prompt() + + def is_switch_prompt(self): + """ + Detects both trainer and wild switch prompts (for prompting whether to + switch pokemon). This is a yes/no box and not the actual pokemon + selection menu. + """ + return self.is_trainer_switch_prompt() or self.is_wild_switch_prompt() + + def is_mandatory_switch(self): + """ + Detects if the battle is waiting for the player to choose a next + pokemon. + """ + # TODO: test when "no" fails to escape for wild battles. + # trainer battles: menu asks to select the next mon + # wild battles: yes/no box first + # The following conditions are probably sufficient: + # 1) current pokemon hp is 0 + # 2) game is polling for input + + if "CANCEL Which ?" in self.emulator.get_text(): + return True + else: + return False + + def is_levelup_screen(self): + """ + Detects the levelup stats screen. + """ + # This is implemented as reading some text on the screen instead of + # using get_text() because checking every loop is really slow. + + address = 0xc50f + values = [146, 143, 130, 139] + + for (index, value) in enumerate(values): + if self.emulator.vba.read_memory_at(address + index) != value: + return False + else: + return True + + def is_evolution_screen(self): + """ + What? MEW is evolving! + """ + address = 0xc5e4 + + values = [164, 181, 174, 171, 181, 168, 173, 166, 231] + + for (index, value) in enumerate(values): + if self.emulator.vba.read_memory_at(address + index) != value: + return False + else: + # also check "What?" + what_address = 0xc5b9 + what_values = [150, 167, 160, 179, 230] + for (index, value) in enumerate(what_values): + if self.emulator.vba.read_memory_at(what_address + index) != value: + return False + else: + return True + + def is_evolved_screen(self): + """ + Checks if the screen is the "evolved into METAPOD!" screen. Note that + this only works inside of a battle. This is because there may be other + text boxes that have the same text when outside of battle. But within a + battle, this is probably the only time the text "evolved into ... !" is + seen. + """ + if not self.is_in_battle(): + return False + + address = 0x4bb1 + values = [164, 181, 174, 171, 181, 164, 163, 127, 168, 173, 179, 174, 79] + + for (index, value) in enumerate(values): + if self.emulator.vba.read_memory_at(address + index) != value: + return False + else: + return True + + def is_make_room_for_move_prompt(self): + """ + Detects the prompt that asks whether to make room for a move. + """ + if not self.is_in_battle(): + return False + + address = 0xc5b9 + values = [172, 174, 181, 164, 127, 179, 174, 127, 172, 160, 170, 164, 127, 177, 174, 174, 172] + + for (index, value) in enumerate(values): + if self.emulator.vba.read_memory_at(address + index) != value: + return False + else: + return True + + def skip_start_text(self, max_loops=20): + """ + Skip any initial conversation until the player can select an action. + This includes skipping any text that appears on a map from an NPC as + well as text that appears prior to the first time the action selection + menu appears. + """ + if not self.is_in_battle(): + while not self.is_in_battle() and max_loops > 0: + self.emulator.text_wait() + max_loops -= 1 + + if max_loops <= 0: + raise Exception("Couldn't start the battle.") + else: + self.emulator.text_wait() + + def skip_end_text(self, loops=20): + """ + Skip through any text that appears after the final attack. + """ + if not self.is_in_battle(): + # TODO: keep talking until the character can move? A battle can be + # triggered inside of a script, and after the battle is ver the + # player may not be able to move until the script is done. The + # script might only finish after other player input is given, so + # using "text_wait() until the player can move" is a bad idea here. + self.emulator.text_wait() + else: + while self.is_in_battle() and loops > 0: + self.emulator.text_wait() + loops -= 1 + + if loops <= 0: + raise Exception("Couldn't get out of the battle.") + + def skip_until_input_required(self): + """ + Waits until the battle needs player input. + """ + # callback causes text_wait to exit when the callback returns True + def is_in_battle_checker(): + result = (self.emulator.vba.read_memory_at(0xd22d) == 0) and (self.emulator.vba.read_memory_at(0xc734) != 0) + + # but also, jump out if it's the stats screen + result = result or self.is_levelup_screen() + + # jump out if it's the "make room for a new move" screen + result = result or self.is_make_room_for_move_prompt() + + # stay in text_wait if it's the evolution screen + result = result and not self.is_evolution_screen() + + return result + + while not self.is_input_required() and self.is_in_battle(): + self.emulator.text_wait(callback=is_in_battle_checker) + + # let the text draw so that the state is more obvious + self.emulator.vba.step(count=10) + + def run(self): + """ + Step through the entire battle. + """ + # Advance to the battle from either of these states: + # 1) the player is talking with an npc + # 2) the battle has already started but there's initial text + # xyz wants to battle, a wild foobar appeared + self.skip_start_text() + + # skip a few hundred frames + self.emulator.vba.step(count=100) + + wild = (self.emulator.vba.read_memory_at(0xd22d) == 1) + + while self.is_in_battle(): + self.skip_until_input_required() + + if not self.is_in_battle(): + continue + + if self.is_player_turn(): + # battle hook provides input to handle this situation + self.handle_turn() + elif self.is_trainer_switch_prompt(): + self.handle_trainer_switch_prompt() + elif self.is_wild_switch_prompt(): + self.handle_wild_switch_prompt() + elif self.is_mandatory_switch(): + # battle hook provides input to handle this situation too + self.handle_mandatory_switch() + elif self.is_levelup_screen(): + self.emulator.vba.press("a", hold=5, after=30) + elif self.is_evolved_screen(): + self.emulator.vba.step(count=30) + elif self.is_make_room_for_move_prompt(): + self.handle_make_room_for_move() + else: + raise BattleException("unknown state, aborting") + + # "how did i lose? wah" + # TODO: this doesn't happen for wild battles + if not wild: + self.skip_end_text() + + # TODO: return should indicate win/loss (blackout) + + def handle_mandatory_switch(self): + """ + Something fainted, pick the next mon. + """ + raise NotImplementedError + + def handle_trainer_switch_prompt(self): + """ + The trainer is switching pokemon. The game asks yes/no for whether or + not the player would like to switch. + """ + raise NotImplementedError + + def handle_wild_switch_prompt(self): + """ + The wild pokemon defeated the party pokemon. This is the yes/no box for + whether to switch pokemon or not. + """ + raise NotImplementedError + + def handle_turn(self): + """ + Take actions inside of a battle based on the game state. + """ + raise NotImplementedError + +class BattleStrategy(Battle): + """ + This class shows the relevant methods to make a battle handler. + """ + + def handle_mandatory_switch(self): + """ + Something fainted, pick the next mon. + """ + raise NotImplementedError + + def handle_trainer_switch_prompt(self): + """ + The trainer is switching pokemon. The game asks yes/no for whether or + not the player would like to switch. + """ + raise NotImplementedError + + def handle_wild_switch_prompt(self): + """ + The wild pokemon defeated the party pokemon. This is the yes/no box for + whether to switch pokemon or not. + """ + raise NotImplementedError + + def handle_turn(self): + """ + Take actions inside of a battle based on the game state. + """ + raise NotImplementedError + + def handle_make_room_for_move(self): + """ + Choose yes/no then handle learning the move. + """ + raise NotImplementedError + +class SpamBattleStrategy(BattleStrategy): + """ + A really simple battle strategy that always picks the first move of the + first pokemon to attack the enemy. + """ + + def handle_turn(self): + """ + Always picks the first move of the current pokemon. + """ + self.fight(1) + + def handle_trainer_switch_prompt(self): + """ + The trainer is switching pokemon. The game asks yes/no for whether or + not the player would like to switch. + """ + # decline + self.emulator.vba.press(["b"], hold=5, after=10) + + def handle_wild_switch_prompt(self): + """ + The wild pokemon defeated the party pokemon. This is the yes/no box for + whether to switch pokemon or not. + """ + # why not just make a battle strategy that doesn't lose? + # TODO: Note that the longer "after" value is required here. + self.emulator.vba.press("a", hold=5, after=30) + + self.handle_mandatory_switch() + + def handle_mandatory_switch(self): + """ + Something fainted, pick the next mon. + """ + + # TODO: make a better selector for which pokemon. + + # now scroll down + self.emulator.vba.press("d", hold=5, after=10) + + # select this mon + self.emulator.vba.press("a", hold=5, after=30) + + def handle_make_room_for_move(self): + """ + Choose yes/no then handle learning the move. + """ + # make room? no + self.emulator.vba.press("b", hold=5, after=100) + + # stop learning? yes + self.emulator.vba.press("a", hold=5, after=20) + + self.emulator.text_wait() diff --git a/pokemontools/vba/vba.py b/pokemontools/vba/vba.py index d7fdf1d..204e102 100644 --- a/pokemontools/vba/vba.py +++ b/pokemontools/vba/vba.py @@ -9,18 +9,20 @@ import re import string from copy import copy -import unittest - # for converting bytes to readable text -from pokemontools.chars import chars +from pokemontools.chars import ( + chars, +) -from pokemontools.map_names import map_names +from pokemontools.map_names import ( + map_names, +) import keyboard # just use a default config for now until the globals are removed completely -import pokemontools.config as conf -config = conf.Config() +import pokemontools.configuration as configuration +config = configuration.Config() project_path = config.path save_state_path = config.save_state_path rom_path = config.rom_path @@ -30,94 +32,379 @@ if not os.path.exists(rom_path): import vba_wrapper -vba = vba_wrapper.VBA(rom_path) -registers = vba_wrapper.core.registers.Registers(vba) - button_masks = vba_wrapper.core.VBA.button_masks button_combiner = vba_wrapper.core.VBA.button_combine +def calculate_bank(address): + """ + Which bank does this address exist in? + """ + return address / 0x4000 + +def calculate_address(address): + """ + Gives the relative address once the bank is loaded. + + This is not the same as the calculate_pointer in the + pokemontools.crystal.pointers module. + """ + return (address % 0x4000) + 0x4000 + def translate_chars(charz): + """ + Translate a string from the in-game format to readable form. This is + accomplished through the same lookup table that the preprocessors use. + """ result = "" for each in charz: result += chars[each] return result -def press(buttons, holdsteps=1, aftersteps=1): +def translate_text(text, chars=chars): """ - Press a button. - - Use steplimit to say for how many steps you want to press - the button (try leaving it at the default, 1). - """ - if hasattr(buttons, "__len__"): - number = button_combiner(buttons) - elif isinstance(buttons, int): - number = buttons - else: - number = buttons - for step_counter in range(0, holdsteps): - Gb.step(number) - - # clear the button press - if aftersteps > 0: - for step_counter in range(0, aftersteps): - Gb.step(0) - -def call(bank, address): + Converts text to the in-game byte coding. """ - Jumps into a function at a certain address. + output = [] + for given_char in text: + for (byte, char) in chars.iteritems(): + if char == given_char: + output.append(byte) + break + else: + raise Exception( + "no match for {0}".format(given_char) + ) + return output - Go into the start menu, pause the game and try call(1, 0x1078) to see a - string printed to the screen. +class crystal(object): """ - push = [ - registers.pc, - registers.hl, - registers.de, - registers.bc, - registers.af, - 0x3bb7, - ] - - for value in push: - registers.sp -= 2 - set_memory_at(registers.sp + 1, value >> 8) - set_memory_at(registers.sp, value & 0xFF) - if get_memory_range(registers.sp, 2) != [value & 0xFF, value >> 8]: - print "desired memory values: " + str([value & 0xFF, value >> 8] ) - print "actual memory values: " + str(get_memory_range(registers.sp , 2)) - print "wrong value at " + hex(registers.sp) + " expected " + hex(value) + " but got " + hex(get_memory_at(registers.sp)) - - if bank != 0: - registers["af"] = (bank << 8) | (registers["af"] & 0xFF) - registers["hl"] = address - registers["pc"] = 0x2d63 # FarJump - else: - registers["pc"] = address - -def get_stack(): - """ - Return a list of functions on the stack. + Just a simple namespace to store a bunch of functions for Pokémon Crystal. + There can only be one running instance of the emulator per process because + it's a poorly written shared library. """ - addresses = [] - sp = registers.sp - for x in range(0, 11): - sp = sp - (2 * x) - hi = get_memory_at(sp + 1) - lo = get_memory_at(sp) - address = ((hi << 8) | lo) - addresses.append(address) + def __init__(self, config=None): + """ + Launch the VBA controller. + """ + if not config: + config = configuration.Config() - return addresses + self.config = config -class crystal: - """ - Just a simple namespace to store a bunch of functions for Pokémon Crystal. - """ + self.vba = vba_wrapper.VBA(self.config.rom_path) + self.registers = vba_wrapper.core.registers.Registers(self.vba) + + if not os.path.exists(self.config.rom_path): + raise Exception("rom_path is not configured properly; edit vba_config.py? " + str(rom_path)) + + def shutdown(self): + """ + Reset the emulator. + """ + self.vba.shutdown() + + def save_state(self, name, state=None, override=False): + """ + Saves the given state to save_state_path. + + The file format must be ".sav", and this will be appended to your + string if necessary. + """ + if state == None: + state = self.vba.state + + if len(name) < 4 or name[-4:] != ".sav": + name += ".sav" + + save_path = os.path.join(self.config.save_state_path, name) + + if not override and os.path.exists(save_path): + raise Exception("oops, save state path already exists: {0}".format(save_path)) + + with open(save_path, "wb") as file_handler: + file_handler.write(state) + + def load_state(self, name, loadit=True): + """ + Read a state from file based on the name of the state. + + Looks in save_state_path for a file with this name (".sav" is + optional). + + @param loadit: whether or not to set the emulator to this state + """ + save_path = os.path.join(self.config.save_state_path, name) + + if not os.path.exists(save_path): + if len(name) < 4 or name[-4:] != ".sav": + name += ".sav" + save_path = os.path.join(self.config.save_state_path, name) + + with open(save_path, "rb") as file_handler: + state = file_handler.read() + + if loadit: + self.vba.state = state + + return state + + def call(self, address, bank=None): + """ + Jumps into a function at a certain address. + + Go into the start menu, pause the game and try call(1, 0x1078) to see a + string printed to the screen. + """ + if bank is None: + bank = calculate_bank(address) + + push = [ + self.registers.pc, + self.registers.hl, + self.registers.de, + self.registers.bc, + self.registers.af, + 0x3bb7, + ] + + self.push_stack(push) + + if bank != 0: + self.registers["af"] = (bank << 8) | (self.registers["af"] & 0xFF) + self.registers["hl"] = address + self.registers["pc"] = 0x2d63 # FarJump + else: + self.registers["pc"] = address + + def push_stack(self, push): + for value in push: + self.registers["sp"] -= 2 + self.vba.write_memory_at(self.registers.sp + 1, value >> 8) + self.vba.write_memory_at(self.registers.sp, value & 0xFF) + if list(self.vba.memory[self.registers.sp : self.registers.sp + 2]) != [value & 0xFF, value >> 8]: + print "desired memory values: " + str([value & 0xFF, value >> 8] ) + print "actual memory values: " + str(list(self.vba.memory[self.registers.sp : self.registers.sp + 2])) + print "wrong value at " + hex(self.registers.sp) + " expected " + hex(value) + " but got " + hex(self.vba.read_memory_at(self.registers.sp)) + + def get_stack(self): + """ + Return a list of functions on the stack. + """ + addresses = [] + sp = self.registers.sp + + for x in range(0, 11): + sp = sp - (2 * x) + hi = self.vba.read_memory_at(sp + 1) + lo = self.vba.read_memory_at(sp) + address = ((hi << 8) | lo) + addresses.append(address) + + return addresses + + def inject_asm_into_rom(self, asm=[], address=0x75 * 0x4000, has_finished_address=0xdb75): + """ + Writes asm to the loaded ROM. Calls the asm. + + :param address: ROM address for where to store the injected asm script. + The default value is an address in pokecrystal that isn't used for + anything. - @staticmethod - def text_wait(step_size=1, max_wait=200, sfx_limit=0, debug=False, callback=None): + :param has_finished_address: address for where to store whether the + script executed or not. This value is restored when the script has been + confirmed to work. It's conceivable that some injected asm might need + to change that address if the asm needs to access the original wram + value itself. + """ + if len(asm) > 0x4000: + raise Exception("too much asm") + + # temporarily use wram + cached_wram_value = self.vba.memory[has_finished_address] + + # set the value at has_finished_address to 0 + reset_wram_mem = list(self.vba.memory) + reset_wram_mem[has_finished_address] = 0 + self.vba.memory = reset_wram_mem + + # set a value to indicate that the script has executed + set_has_finished = [ + # push af + 0xf5, + + # ld a, 1 + 0x3e, 1, + + # ld [has_finished], a + 0xea, has_finished_address & 0xff, has_finished_address >> 8, + + # pop af + 0xf1, + + # ret + 0xc9, + ] + + # TODO: check if asm ends with a byte that causes a return or call or + # other "ender". Raise an exception if it already returns on its own. + + # combine the given asm with the setter bytes + total_asm = asm + set_has_finished + + # get a copy of the current rom + rom = list(self.vba.rom) + + # inject the asm + rom[address : address + len(total_asm)] = total_asm + + # set the rom with the injected asm + self.vba.rom = rom + + # call the injected asm + self.call(calculate_address(address), bank=calculate_bank(address)) + + # make the emulator step forward + self.vba.step(count=20) + + # check if the script has executed (see below) + current_mem = self.vba.memory + + # reset the wram value to its original value + another_mem = list(self.vba.memory) + another_mem[has_finished_address] = cached_wram_value + self.vba.memory = another_mem + + # check if the script has actually executed + # TODO: should this raise an exception if the script didn't finish? + if current_mem[has_finished_address] == 0: + return False + elif current_mem[has_finished_address] == 1: + return True + else: + raise Exception( + "has_finished_address at {has_finished_address} was overwritten with an unexpected value {value}".format( + has_finished_address=hex(has_finished_address), + value=current_mem[has_finished_address], + ) + ) + + def inject_asm_into_wram(self, asm=[], address=0xdfcf): + """ + Writes asm to memory. Makes the emulator run the asm. + + This function will append "ret" to the list of bytes. Before returning, + it updates the value at the first byte to indicate that the function + has executed. + + The first byte at the given address is reserved for whether the asm has + finished executing. + """ + memory = list(self.vba.memory) + + # the first byte is reserved for whether the script has finished + has_finished = address + memory[has_finished] = 0 + + # the second byte is where the script will be stored + script_address = address + 1 + + # TODO: error checking; make sure the last byte doesn't already return. + # Use some functions from gbz80disasm to perform this check. + + # set a value to indicate that the script has executed + set_has_finished = [ + # push af + 0xf5, + + # ld a, 1 + 0x3e, 1, + + # ld [has_finished], a + 0xea, has_finished & 0xff, has_finished >> 8, + + # pop af + 0xf1, + + # ret + 0xc9, + ] + + # append the last opcodes to the script + asm = bytearray(asm) + bytearray(set_has_finished) + + memory[script_address : script_address + len(asm)] = asm + self.vba.memory = memory + + # make the emulator call the script + self.call(script_address, bank=0) + + # make the emulator step forward + self.vba.step(count=50) + + # check if the script has executed + # TODO: should this raise an exception if the script didn't finish? + if self.vba.memory[has_finished] == 0: + return False + elif self.vba.memory[has_finished] == 1: + return True + else: + raise Exception( + "has_finished at {has_finished} was overwritten with an unexpected value {value}".format( + has_finished=hex(has_finished), + value=self.vba.memory[has_finished], + ) + ) + + def call_script(self, address, bank=None, wram=False, force=False): + """ + Sets wram values so that the engine plays a script. + + :param address: address of the map script + :param bank: override for bank calculation (based on address) + :param wram: force bank to 0 + :param force: override an already-running script + """ + + ScriptFlags = 0xd434 + ScriptMode = 0xd437 + ScriptRunning = 0xd438 + ScriptBank = 0xd439 + ScriptPos = 0xd43a + NumScriptParents = 0xd43c + ScriptParents = 0xd43d + + num_possible_parents = 4 + len_parent = 3 + + mem = list(self.vba.memory) + + if mem[ScriptRunning] == 0xff: + if force: + # wipe the parent routine array + mem[NumScriptParents] = 0 + for i in xrange(num_possible_parents * len_parent): + mem[ScriptParents + i] = 0 + else: + raise Exception("a script is already running, use force=True") + + if wram: + bank = 0 + elif not bank: + bank = calculate_bank(address) + address = address % 0x4000 + 0x4000 * bool(bank) + + mem[ScriptFlags] |= 4 + mem[ScriptMode] = 1 + mem[ScriptRunning] = 0xff + + mem[ScriptBank] = bank + mem[ScriptPos] = address % 0x100 + mem[ScriptPos+1] = address / 0x100 + + self.vba.memory = mem + + def text_wait(self, step_size=1, max_wait=200, sfx_limit=0, debug=False, callback=None): """ Presses the "A" button when text is done being drawn to screen. @@ -134,22 +421,27 @@ class crystal: :param max_wait: number of wait loops to perform """ while max_wait > 0: - hi = get_memory_at(registers.sp + 1) - lo = get_memory_at(registers.sp) + hi = self.vba.read_memory_at(self.registers.sp + 1) + lo = self.vba.read_memory_at(self.registers.sp) address = ((hi << 8) | lo) if address in range(0xa1b, 0xa46) + range(0xaaf, 0xaf5): # 0xaef: print "pressing, then breaking.. address is: " + str(hex(address)) # set CurSFX - set_memory_at(0xc2bf, 0) + self.vba.write_memory_at(0xc2bf, 0) - press("a", holdsteps=10, aftersteps=1) + self.vba.press("a", hold=10, after=50) # check if CurSFX is SFX_READ_TEXT_2 - if get_memory_at(0xc2bf) == 0x8: - print "cursfx is set to SFX_READ_TEXT_2, looping.." - return crystal.text_wait(step_size=step_size, max_wait=max_wait, debug=debug, callback=callback, sfx_limit=sfx_limit) + if self.vba.read_memory_at(0xc2bf) == 0x8: + if "CANCEL Which" in self.get_text(): + print "probably the 'switch pokemon' menu" + return + else: + print "cursfx is set to SFX_READ_TEXT_2, looping.." + print self.get_text() + return self.text_wait(step_size=step_size, max_wait=max_wait, debug=debug, callback=callback, sfx_limit=sfx_limit) else: if sfx_limit > 0: sfx_limit = sfx_limit - 1 @@ -160,7 +452,7 @@ class crystal: break else: - stack = get_stack() + stack = self.get_stack() # yes/no box or the name selection box if address in range(0xa46, 0xaaf): @@ -170,23 +462,23 @@ class crystal: # date/time box (day choice) # 0x47ab is the one from the intro, 0x49ab is the one from mom. elif 0x47ab in stack or 0x49ab in stack: # was any([x in stack for x in range(0x46EE, 0x47AB)]) - print "probably at a date/time box ? exiting." - break + if not self.is_in_battle(): + print "probably at a date/time box ? exiting." + break # "How many minutes?" selection box elif 0x4826 in stack: print "probably at a \"How many minutes?\" box ? exiting." break - else: - nstep(step_size) + self.vba.step(count=step_size) # if there is a callback, then call the callback and exit when the # callback returns True. This is especially useful during the # OakSpeech intro where textboxes are running constantly, and then # suddenly the player can move around. One way to detect that is to # set callback to a function that returns - # "vba.get_memory_at(0xcfb1) != 0". + # "vba.read_memory_at(0xcfb1) != 0". if callback != None: result = callback() if result == True: @@ -202,17 +494,15 @@ class crystal: if max_wait == 0: print "max_wait was hit" - @staticmethod - def walk_through_walls_slow(): - memory = get_memory() + def walk_through_walls_slow(self): + memory = self.vba.memory memory[0xC2FA] = 0 memory[0xC2FB] = 0 memory[0xC2FC] = 0 memory[0xC2FD] = 0 - set_memory(memory) + self.vba.memory = memory - @staticmethod - def walk_through_walls(): + def walk_through_walls(self): """ Lets the player walk all over the map. @@ -221,73 +511,75 @@ class crystal: to be executed each step/tick if continuous walk-through-walls is desired. """ - set_memory_at(0xC2FA, 0) - set_memory_at(0xC2FB, 0) - set_memory_at(0xC2FC, 0) - set_memory_at(0xC2FD, 0) + self.vba.write_memory_at(0xC2FA, 0) + self.vba.write_memory_at(0xC2FB, 0) + self.vba.write_memory_at(0xC2FC, 0) + self.vba.write_memory_at(0xC2FD, 0) - #@staticmethod - #def set_enemy_level(level): - # set_memory_at(0xd213, level) + def lower_enemy_hp(self): + """ + Dramatically lower the enemy's HP. + """ + self.vba.write_memory_at(0xd216, 0) + self.vba.write_memory_at(0xd217, 1) - @staticmethod - def nstep(steplimit=500): + def set_battle_mon_hp(self, hp): + """ + Set the BattleMonHP variable to the given hp. + """ + self.vba.write_memory_at(0xc63c, hp / 0x100) + self.vba.write_memory_at(0xc63c + 1, hp % 0x100) + + def nstep(self, steplimit=500): """ Steps the CPU forward and calls some functions in between each step. (For example, to manipulate memory.) This is pretty slow. """ for step_counter in range(0, steplimit): - crystal.walk_through_walls() - #call(0x1, 0x1078) - step() + self.walk_through_walls() + #call(0x1078) + self.vba.step() - @staticmethod - def disable_triggers(): - set_memory_at(0x23c4, 0xAF) - set_memory_at(0x23d0, 0xAF); + def disable_triggers(self): + self.vba.write_memory_at(0x23c4, 0xAF) + self.vba.write_memory_at(0x23d0, 0xAF); - @staticmethod - def disable_callbacks(): - set_memory_at(0x23f2, 0xAF) - set_memory_at(0x23fe, 0xAF) + def disable_callbacks(self): + self.vba.write_memory_at(0x23f2, 0xAF) + self.vba.write_memory_at(0x23fe, 0xAF) - @staticmethod - def get_map_group_id(): + def get_map_group_id(self): """ Returns the current map group. """ - return get_memory_at(0xdcb5) + return self.vba.read_memory_at(0xdcb5) - @staticmethod - def get_map_id(): + def get_map_id(self): """ Returns the map number of the current map. """ - return get_memory_at(0xdcb6) + return self.vba.read_memory_at(0xdcb6) - @staticmethod - def get_map_name(): + def get_map_name(self, map_names=map_names): """ Figures out the current map name. """ - map_group_id = crystal.get_map_group_id() - map_id = crystal.get_map_id() + map_group_id = self.get_map_group_id() + map_id = self.get_map_id() return map_names[map_group_id][map_id]["name"] - @staticmethod - def get_xy(): + def get_xy(self): """ (x, y) coordinates of player on map. Relative to top-left corner of map. """ - x = get_memory_at(0xdcb8) - y = get_memory_at(0xdcb7) + x = self.vba.read_memory_at(0xdcb8) + y = self.vba.read_memory_at(0xdcb7) return (x, y) - @staticmethod - def menu_select(id=1): + def menu_select(self, id=1): """ Sets the cursor to the given pokemon in the player's party. @@ -296,38 +588,84 @@ class crystal: This probably works on other menus. """ - set_memory_at(0xcfa9, id) + self.vba.write_memory_at(0xcfa9, id) - @staticmethod - def is_in_battle(): + def is_in_battle(self): """ Checks whether or not we're in a battle. """ - return (get_memory_at(0xd22d) != 0) or crystal.is_in_link_battle() + return (self.vba.read_memory_at(0xd22d) != 0) or self.is_in_link_battle() + + def is_in_link_battle(self): + return self.vba.read_memory_at(0xc2dc) != 0 + + def is_trainer_switch_prompt(self): + """ + Checks if the game is currently displaying the yes/no prompt for + whether or not to switch pokemon. This happens when the trainer is + switching pokemon out. + """ + # TODO: this method should return False if the game options have been + # set to not use the battle switching style. + + # get on-screen text + text = self.get_text() + + requirements = [ + "YES", + "NO", + "Will ", + "change POKMON?", + ] - @staticmethod - def is_in_link_battle(): - return get_memory_at(0xc2dc) != 0 + return all([requirement in text for requirement in requirements]) - @staticmethod - def unlock_flypoints(): + def is_wild_switch_prompt(self): + """ + Detects if the battle is waiting for the player to choose whether or + not to continue to fight the wild pokemon. + """ + # get on-screen text + screen_text = self.get_text() + + requirements = [ + "YES", + "NO", + "Use next POKMON?", + ] + + return all([requirement in screen_text for requirement in requirements]) + + def is_switch_prompt(self): + """ + Detects both the trainer-style switch prompt and the wild-style switch + prompt. This is the yes/no prompt for whether to switch pokemon. + """ + return self.is_trainer_switch_prompt() or self.is_wild_switch_prompt() + + def unlock_flypoints(self): """ Unlocks different destinations for flying. Note: this might start at 0xDCA4 (minus one on all addresses), but not sure. """ - set_memory_at(0xDCA5, 0xFF) - set_memory_at(0xDCA6, 0xFF) - set_memory_at(0xDCA7, 0xFF) - set_memory_at(0xDCA8, 0xFF) + self.vba.write_memory_at(0xDCA5, 0xFF) + self.vba.write_memory_at(0xDCA6, 0xFF) + self.vba.write_memory_at(0xDCA7, 0xFF) + self.vba.write_memory_at(0xDCA8, 0xFF) - @staticmethod - def get_gender(): + def set_battle_type(self, battle_type): + """ + Changes the battle type value. + """ + self.vba.write_memory_at(0xd230, battle_type) + + def get_gender(self): """ Returns 'male' or 'female'. """ - gender = get_memory_at(0xD472) + gender = self.vba.read_memory_at(0xD472) if gender == 0: return "male" elif gender == 1: @@ -335,54 +673,59 @@ class crystal: else: return gender - @staticmethod - def get_player_name(): + def get_player_name(self): """ Returns the 7 characters making up the player's name. """ - bytez = get_memory_range(0xD47D, 7) + bytez = self.vba.memory[0xD47D:0xD47D + 7] name = translate_chars(bytez) return name - @staticmethod - def warp(map_group_id, map_id, x, y): - set_memory_at(0xdcb5, map_group_id) - set_memory_at(0xdcb6, map_id) - set_memory_at(0xdcb7, y) - set_memory_at(0xdcb8, x) - set_memory_at(0xd001, 0xFF) - set_memory_at(0xff9f, 0xF1) - set_memory_at(0xd432, 1) - set_memory_at(0xd434, 0 & 251) - - @staticmethod - def warp_pokecenter(): - crystal.warp(1, 1, 3, 3) - crystal.nstep(200) - - @staticmethod - def masterballs(): + def warp(self, map_group_id, map_id, x, y): + """ + Warp into another map. + """ + self.vba.write_memory_at(0xdcb5, map_group_id) + self.vba.write_memory_at(0xdcb6, map_id) + self.vba.write_memory_at(0xdcb7, y) + self.vba.write_memory_at(0xdcb8, x) + self.vba.write_memory_at(0xd001, 0xFF) + self.vba.write_memory_at(0xff9f, 0xF1) + self.vba.write_memory_at(0xd432, 1) + self.vba.write_memory_at(0xd434, 0 & 251) + + def warp_pokecenter(self): + """ + Warp straight into a pokecenter. + """ + self.warp(1, 1, 3, 3) + self.nstep(200) + + def masterballs(self): + """ + Deposit some pokeballs into the first few slots of the pack. This + overrides whatever items were previously there. + """ # masterball - set_memory_at(0xd8d8, 1) - set_memory_at(0xd8d9, 99) + self.vba.write_memory_at(0xd8d8, 1) + self.vba.write_memory_at(0xd8d9, 99) # ultraball - set_memory_at(0xd8da, 2) - set_memory_at(0xd8db, 99) + self.vba.write_memory_at(0xd8da, 2) + self.vba.write_memory_at(0xd8db, 99) # pokeballs - set_memory_at(0xd8dc, 5) - set_memory_at(0xd8dd, 99) + self.vba.write_memory_at(0xd8dc, 5) + self.vba.write_memory_at(0xd8dd, 99) - @staticmethod - def get_text(): + def get_text(self, chars=chars, offset=0, bounds=1000): """ Returns alphanumeric text on the screen. Other characters will not be shown. """ output = "" - tiles = get_memory_range(0xc4a0, 1000) + tiles = self.vba.memory[0xc4a0 + offset:0xc4a0 + offset + bounds] for each in tiles: if each in chars.keys(): thing = chars[each] @@ -405,32 +748,69 @@ class crystal: return output - @staticmethod - def keyboard_apply(button_sequence): + def is_showing_stats_screen(self): + """ + This is meant to detect whether or not the stats screen is showing. + This is the menu that pops up after leveling up. + """ + # These words must be on the screen if the stats screen is currently + # displayed. + parts = [ + "ATTACK", + "DEFENSE", + "SPCL.ATK", + "SPCL.DEF", + "SPEED", + ] + + # get the current text on the screen + text = self.get_text() + + if all([part in text for part in parts]): + return True + else: + return False + + def handle_stats_screen(self, force=False): + """ + Attempts to bypass a stats screen. Set force=True if you want to make + the attempt regardless of whether or not the system thinks a stats + screen is showing. + """ + if self.is_showing_stats_screen() or force: + self.vba.press("a") + self.vba.step(count=20) + + def keyboard_apply(self, button_sequence): """ Applies a sequence of buttons to the on-screen keyboard. """ for buttons in button_sequence: - press(buttons) - nstep(2) - press([]) + self.vba.press(buttons) + + if buttons == "select": + self.vba.step(count=5) + else: + self.vba.step(count=2) + + self.vba.press([]) - @staticmethod - def write(something="TrAiNeR"): + def write(self, something="TrAiNeR"): """ Types out a word. Uses a planning algorithm to do this in the most efficient way possible. """ button_sequence = keyboard.plan_typing(something) - crystal.keyboard_apply([[x] for x in button_sequence]) + self.vba.step(count=10) + self.keyboard_apply([[x] for x in button_sequence]) + return button_sequence - @staticmethod - def set_partymon2(): + def set_partymon2(self): """ This causes corruption, so it's not working yet. """ - memory = get_memory() + memory = self.vba.memory memory[0xdcd7] = 2 memory[0xdcd9] = 0x7 @@ -464,19 +844,18 @@ class crystal: memory[0xdd33] = 0x10 memory[0xdd34] = 0x40 - set_memory(memory) + self.vba.memory = memory - @staticmethod - def wait_for_script_running(debug=False, limit=1000): + def wait_for_script_running(self, debug=False, limit=1000): """ Wait until ScriptRunning isn't -1. """ while limit > 0: - if get_memory_at(0xd438) != 255: + if self.vba.read_memory_at(0xd438) != 255: print "script is done executing" return else: - step() + self.vba.step() if debug: limit = limit - 1 @@ -484,44 +863,316 @@ class crystal: if limit == 0: print "limit ran out" - @staticmethod - def move(cmd): + def move(self, cmd): """ Attempt to move the player. """ - press(cmd, holdsteps=10, aftersteps=0) - press([]) + if isinstance(cmd, list): + for command in cmd: + self.move(command) + else: + self.vba.press(cmd, hold=10, after=0) + self.vba.press([]) + + memory = self.vba.memory + #while memory[0xd4e1] == 2 and memory[0xd042] != 0x3e: + while memory[0xd043] in [0, 1, 2, 3]: + #while memory[0xd043] in [0, 1, 2, 3] or memory[0xd042] != 0x3e: + self.vba.step(count=10) + memory = self.vba.memory + + def get_enemy_hp(self): + """ + Returns the HP of the current enemy. + """ + hp = ((self.vba.memory[0xd218] << 8) | self.vba.memory[0xd217]) + return hp - memory = get_memory() - #while memory[0xd4e1] == 2 and memory[0xd042] != 0x3e: - while memory[0xd043] in [0, 1, 2, 3]: - #while memory[0xd043] in [0, 1, 2, 3] or memory[0xd042] != 0x3e: - nstep(10) - memory = get_memory() + def start_trainer_battle_lamely(self, map_group=0x1, map_id=0xc, x=6, y=8, direction="l", loop_limit=10): + """ + Starts a trainer battle by warping into a map at the designated + coordinates, pressing the direction button for a full walk step (which + ideally should be blocked, this is mainly to establish direction), and + then pressing "a" to initiate the trainer battle. + + Consider using start_trainer_battle instead. + """ + self.warp(map_group, map_id, x, y) -class TestEmulator(unittest.TestCase): - def test_PlaceString(self): - call(0, 0x1078) + # finish loading the map, might not be necessary? + self.nstep(100) - # where to draw the text - registers["hl"] = 0xc4a0 + # face towards the trainer (or whatever direction was specified). If + # this direction is blocked, then this will only change which direction + # the character is facing. However, if this direction is not blocked by + # the map or by an npc, then this will cause an entire step to be + # taken. + self.vba.press([direction]) + + # talk to the trainer, don't assume line of sight will be triggered + self.vba.press(["a"]) + self.vba.press([]) + + # trainer might talk, skip any text until the player can choose moves + while not self.is_in_battle() and loop_limit > 0: + self.text_wait() + loop_limit -= 1 + + def start_trainer_battle(self, trainer_group=0x1, trainer_id=0x1, text_win="YOU WIN", text_address=0xdb90): + """ + Start a trainer battle with the trainer located by trainer_group and + trainer_id. + + :param trainer_group: trainer group id + :param trainer_id: trainer id within the group + :param text_win: text to show if player wins + :param text_address: where to store text_win in wram + """ + # where the script will be written + rom_address = 0x75 * 0x4000 - # what text to read from - registers["de"] = 0x1276 + # battle win message + translated_text = translate_text(text_win) - nstep(10) + # also include the first and last bytes needed for text + translated_text = [0] + translated_text + [0x57] - text = crystal.get_text() + mem = self.vba.memory - self.assertTrue("TRAINER" in text) + # create a backup of the current data + wram_backup = mem[text_address : text_address + len(translated_text)] -class TestWriter(unittest.TestCase): - def test_very_basic(self): - button_sequence = keyboard.plan_typing("an") - expected_result = ["select", "a", "d", "r", "r", "r", "r", "a"] + # manipulate the memory + mem[text_address : text_address + len(translated_text)] = translated_text + self.vba.memory = mem - self.assertEqual(len(expected_result), len(button_sequence)) - self.assertEqual(expected_result, button_sequence) + text_pointer_hi = text_address / 0x100 + text_pointer_lo = text_address % 0x100 -if __name__ == "__main__": - unittest.main() + script = [ + # loadfont + #0x47, + + # winlosstext address, address + 0x64, text_pointer_lo, text_pointer_hi, 0, 0, + + # loadtrainer group, id + 0x5e, trainer_group, trainer_id, + + # startbattle + 0x5f, + + # returnafterbattle + 0x60, + + # reloadmapmusic + 0x83, + + # reloadmap + 0x7B, + ] + + # Now make the script restore wram at the end (after the text has been + # used). The assumption here is that this particular subset of wram + # data would not be needed during the bulk of the script. + address = text_address + for byte in wram_backup: + address_hi = address / 0x100 + address_lo = address % 0x100 + + script += [ + # loadvar + 0x1b, address_lo, address_hi, byte, + ] + + address += 1 + + script += [ + # end + 0x91, + ] + + # Use a different wram address because the default is something related + # to trainers. + # use a higher loop limit because otherwise it doesn't start fast enough? + self.inject_script_into_rom(asm=script, rom_address=rom_address, wram_address=0xdb75, limit=1000) + + def set_script(self, address): + """ + Sets the current script in wram to whatever address. + """ + ScriptBank = 0xd439 + ScriptPos = 0xd43a + + memory = self.vba.memory + memory[ScriptBank] = calculate_bank(address) + pointer = calculate_address(address) + memory[ScriptPos] = (calculate_address(address) & 0xff00) >> 8 + memory[ScriptPos] = calculate_address(address) & 0xff + + # TODO: determine if this is necessary + #memory[ScriptRunning] = 0xff + + self.vba.memory = memory + + def inject_script_into_rom(self, asm=[0x91], rom_address=0x75 * 0x4000, wram_address=0xd280, limit=50): + """ + Writes a script to the ROM in a blank location. Calls call_script to + make the game engine aware of the script. Then executes the script and + looks for confirmation thta the script has started to run. + + The script must end itself. + + :param asm: scripting command bytes + :param rom_address: rom location to write asm to + :param wram_address: temporary storage for indicating if the script has + started yet + :param limit: number of frames to emulate before giving up on the start + script + """ + execution_pending = 0 + execution_started = 1 + valid_execution_states = (execution_pending, execution_started) + + # location for byte for whether script has started executing + execution_indicator_address = wram_address + + # backup whatever exists at the current wram location + backup_wram = self.vba.read_memory_at(execution_indicator_address) + + # .. and set it to "pending" + self.vba.write_memory_at(execution_indicator_address, execution_pending) + + # initial script that runs first to tell python that execution started + execution_indicator_script = [ + # loadvar address, value + 0x1b, execution_indicator_address & 0xff, execution_indicator_address >> 8, execution_started, + ] + + # make the indicator script run before the user script + full_script = execution_indicator_script + asm + + # inject the asm + rom = list(self.vba.rom) + rom[rom_address : rom_address + len(full_script)] = full_script + + # set the rom with the injected bytes + self.vba.rom = rom + + # setup the script for execution + self.call_script(rom_address) + + status = execution_pending + while status != execution_started and limit > 0: + # emulator time travel + self.vba.step(count=1) + + # get latest wram + status = self.vba.read_memory_at(execution_indicator_address) + if status not in valid_execution_states: + raise Exception( + "The execution indicator at {addr} has invalid state {value}".format( + addr=hex(execution_indicator_address), + value=status, + ) + ) + elif status == execution_started: + break # hooray + + limit -= 1 + + if status == execution_pending and limit == 0: + raise Exception( + "Emulation timeout while waiting for script to start." + ) + + # The script has started so it's okay to reset wram back to whatever it + # was. + self.vba.write_memory_at(execution_indicator_address, backup_wram) + + return True + + def givepoke(self, pokemon_id, level, nickname=None, wram=False): + """ + Give the player a pokemon. + """ + if isinstance(nickname, str): + if len(nickname) == 0: + raise Exception("invalid nickname") + elif len(nickname) > 11: + raise Exception("nickname too long") + else: + if not nickname: + nickname = False + else: + raise Exception("nickname needs to be a string, False or None") + + # script to inject into wram + script = [ + 0x47, # loadfont + #0x55, # keeptextopen + + # givepoke pokemon_id, level, 0, 0 + 0x2d, pokemon_id, level, 0, 0, + + #0x54, # closetext + 0x49, # loadmovesprites + 0x91, # end + ] + + # picked this region of wram because it looks like it's probably unused + # in situations where givepoke will work. + #address = 0xd073 + #address = 0xc000 + #address = 0xd8f1 + address = 0xd280 + + if not wram: + self.inject_script_into_rom(asm=script, wram_address=address) + else: + # TODO: move this into a separate function. Maybe use a context + # manager to restore wram at the end. + mem = list(self.vba.memory) + backup_wram = mem[address : address + len(script)] + mem[address : address + len(script)] = script + self.vba.memory = mem + + self.call_script(address, wram=True) + + # "would you like to give it a nickname?" + self.text_wait() + + if nickname: + # yes + self.vba.press("a", hold=10) + + # wait for the keyboard to appear + # TODO: this wait should be moved into write() + self.vba.step(count=20) + + # type the requested nicknameb + self.write(nickname) + + self.vba.press("start", hold=5, after=10) + self.vba.press("a", hold=5, after=50) + else: + # no nickname + self.vba.press("b", hold=10, after=20) + + if wram: + # Wait for the script to end in the engine before copying the original + # wram values back in. + self.vba.step(count=100) + + # reset whatever was in wram before this script was called + mem = list(self.vba.memory) + mem[address : address + len(script)] = backup_wram + self.vba.memory = mem + + def start_random_battle_by_rocksmash_battle_script(self): + """ + Initiates a wild battle using the same function that using rocksmash + would call. + """ + RockSmashBattleScript_address = 0x97cf9 + self.call_script(RockSmashBattleScript_address) diff --git a/pokemontools/wram.py b/pokemontools/wram.py index 60001aa..e1b9212 100644 --- a/pokemontools/wram.py +++ b/pokemontools/wram.py @@ -5,6 +5,10 @@ RGBDS BSS section and constant parsing. import os +# TODO: parse these constants from constants.asm +NUM_OBJECTS = 0x10 +OBJECT_LENGTH = 0x10 + def make_wram_labels(wram_sections): wram_labels = {} for section in wram_sections: @@ -108,6 +112,8 @@ class WRAMProcessor(object): self.setup_hram_constants() self.setup_gbhw_constants() + self.reformat_wram_labels() + def read_wram_sections(self): """ Opens the wram file and calls read_bss_sections. @@ -162,3 +168,14 @@ class WRAMProcessor(object): """ self.gbhw_constants = self.read_gbhw_constants() return self.gbhw_constants + + def reformat_wram_labels(self): + """ + Flips the wram_labels dictionary the other way around to access + addresses by label. + """ + self.wram = {} + + for (address, labels) in self.wram_labels.iteritems(): + for label in labels: + self.wram[label] = address @@ -24,7 +24,7 @@ requires = [ setup( name="pokemontools", - version="1.4.1", + version="1.6.0", description="Tools for compiling and disassembling Pokémon Red and Pokémon Crystal.", long_description=open("README.md", "r").read(), license="BSD", diff --git a/tests/bootstrapping.py b/tests/bootstrapping.py new file mode 100644 index 0000000..b71c19a --- /dev/null +++ b/tests/bootstrapping.py @@ -0,0 +1,54 @@ +""" +Functions to bootstrap the emulator state +""" + +from setup_vba import ( + vba, + autoplayer, +) + +def bootstrap(): + """ + Every test needs to be run against a certain minimum context. That context + is constructed by this function. + """ + + cry = vba.crystal(config=None) + runner = autoplayer.SpeedRunner(cry=cry) + + # skip=False means run the skip_intro function instead of just skipping to + # a saved state. + runner.skip_intro(skip=True) + + state = cry.vba.state + + # clean everything up again + cry.vba.shutdown() + + return state + +def bootstrap_trainer_battle(): + """ + Start a trainer battle. + """ + # setup + cry = vba.crystal(config=None) + runner = autoplayer.SpeedRunner(cry=cry) + + runner.skip_intro(skip=True) + runner.handle_mom(skip=True) + runner.walk_into_new_bark_town(skip=True) + runner.handle_elm("totodile", skip=True) + + # levelgrind a pokemon + # TODO: make new_bark_level_grind able to figure out how to construct its + # initial state if none is provided. + runner.new_bark_level_grind(17, skip=True) + + cry.givepoke(64, 31, "kAdAbRa") + cry.givepoke(224, 60, "OcTiLlErY") + cry.givepoke(126, 87, "magmar") + + cry.start_trainer_battle() + + return runner.cry.vba.state diff --git a/tests/integration/tests.py b/tests/integration/tests.py index 40933e5..4f96699 100644 --- a/tests/integration/tests.py +++ b/tests/integration/tests.py @@ -42,6 +42,10 @@ from pokemontools.helpers import ( index, ) +from pokemontools.crystalparts.old_parsers import ( + old_parse_map_header_at, +) + from pokemontools.crystal import ( rom, load_rom, @@ -65,7 +69,6 @@ from pokemontools.crystal import ( all_labels, write_all_labels, parse_map_header_at, - old_parse_map_header_at, process_00_subcommands, parse_all_map_headers, translate_command_byte, diff --git a/tests/setup_vba.py b/tests/setup_vba.py new file mode 100644 index 0000000..6e615e2 --- /dev/null +++ b/tests/setup_vba.py @@ -0,0 +1,4 @@ +import pokemontools.vba.vba as vba +import pokemontools.vba.keyboard as keyboard +import pokemontools.vba.autoplayer as autoplayer +autoplayer.vba = vba diff --git a/tests/test_vba.py b/tests/test_vba.py index 56a71e3..caa1867 100644 --- a/tests/test_vba.py +++ b/tests/test_vba.py @@ -4,81 +4,96 @@ Tests for VBA automation tools import unittest -import pokemontools.vba.vba as vba +from setup_vba import ( + vba, + autoplayer, + keyboard, +) -try: - import pokemontools.vba.vba_autoplayer -except ImportError: - import pokemontools.vba.autoplayer as vba_autoplayer - -vba_autoplayer.vba = vba +from bootstrapping import ( + bootstrap, + bootstrap_trainer_battle, +) def setup_wram(): """ Loads up some default addresses. Should eventually be replaced with the actual wram parser. """ + # TODO: this should just be parsed straight out of wram.asm wram = {} wram["PlayerDirection"] = 0xd4de wram["PlayerAction"] = 0xd4e1 wram["MapX"] = 0xd4e6 wram["MapY"] = 0xd4e7 + + wram["WarpNumber"] = 0xdcb4 + wram["MapGroup"] = 0xdcb5 + wram["MapNumber"] = 0xdcb6 + wram["YCoord"] = 0xdcb7 + wram["XCoord"] = 0xdcb8 + return wram -def bootstrap(): - """ - Every test needs to be run against a certain minimum context. That context - is constructed by this function. - """ +class OtherVbaTests(unittest.TestCase): + def test_keyboard_planner(self): + button_sequence = keyboard.plan_typing("an") + expected_result = ["select", "a", "d", "r", "r", "r", "r", "a"] - # reset the rom - vba.shutdown() - vba.load_rom() + self.assertEqual(len(expected_result), len(button_sequence)) + self.assertEqual(expected_result, button_sequence) - # skip=False means run the skip_intro function instead of just skipping to - # a saved state. - vba_autoplayer.skip_intro() +class VbaTests(unittest.TestCase): + cry = None + wram = None - state = vba.get_state() + @classmethod + def setUpClass(cls): + cls.bootstrap_state = bootstrap() - # clean everything up again - vba.shutdown() + cls.wram = setup_wram() - return state + cls.cry = vba.crystal() + cls.vba = cls.cry.vba -class VbaTests(unittest.TestCase): - # unittest in jython2.5 doesn't seem to have setUpClass ?? Man, why am I on - # jython2.5? This is ancient. - #@classmethod - #def setUpClass(cls): - # # get a good game state - # cls.state = bootstrap() - # - # # figure out addresses - # cls.wram = setup_wram() - - # FIXME: work around jython2.5 unittest - state = bootstrap() - wram = setup_wram() + cls.vba.state = cls.bootstrap_state - def get_wram_value(self, name): - return vba.get_memory_at(self.wram[name]) + @classmethod + def tearDownClass(cls): + cls.vba.shutdown() def setUp(self): - # clean the state - vba.shutdown() - vba.load_rom() - # reset to whatever the bootstrapper created - vba.set_state(self.state) + self.vba.state = self.bootstrap_state + + def get_wram_value(self, name): + return self.vba.memory[self.wram[name]] + + def check_movement(self, direction="d"): + """ + Check if (y, x) before attempting to move and (y, x) after attempting + to move are the same. + """ + start = (self.get_wram_value("MapY"), self.get_wram_value("MapX")) + self.cry.move(direction) + end = (self.get_wram_value("MapY"), self.get_wram_value("MapX")) + return start != end + + def bootstrap_name_prompt(self): + runner = autoplayer.SpeedRunner(cry=None) + runner.setup() + runner.skip_intro(stop_at_name_selection=True, skip=False, override=False) + + self.cry.vba.press("a", hold=20) - def tearDown(self): - vba.shutdown() + # wait for "Your name?" to show up + while "YOUR NAME?" not in self.cry.get_text(): + self.cry.step(count=50) def test_movement_changes_player_direction(self): player_direction = self.get_wram_value("PlayerDirection") - vba.crystal.move("u") + self.cry.move("u") # direction should have changed self.assertNotEqual(player_direction, self.get_wram_value("PlayerDirection")) @@ -86,7 +101,7 @@ class VbaTests(unittest.TestCase): def test_movement_changes_y_coord(self): first_map_y = self.get_wram_value("MapY") - vba.crystal.move("u") + self.cry.move("u") # y location should be different second_map_y = self.get_wram_value("MapY") @@ -96,11 +111,176 @@ class VbaTests(unittest.TestCase): # should start with standing self.assertEqual(self.get_wram_value("PlayerAction"), 1) - vba.crystal.move("l") + self.cry.move("l") # should be standing player_action = self.get_wram_value("PlayerAction") self.assertEqual(player_action, 1) # 1 = standing + def test_PlaceString(self): + self.cry.call(0, 0x1078) + + # where to draw the text + self.cry.registers["hl"] = 0xc4a0 + + # what text to read from + self.cry.registers["de"] = 0x1276 + + self.cry.vba.step(count=10) + + text = self.cry.get_text() + + self.assertTrue("TRAINER" in text) + + def test_speedrunner_constructor(self): + runner = autoplayer.SpeedRunner(cry=self.cry) + + def test_speedrunner_handle_mom(self): + # TODO: why can't i pass in the current state of the emulator? + runner = autoplayer.SpeedRunner(cry=None) + runner.setup() + runner.skip_intro(skip=True) + runner.handle_mom(skip=False) + + # confirm that handle_mom is done by attempting to move on the map + self.assertTrue(self.check_movement("d")) + + def test_speedrunner_walk_into_new_bark_town(self): + runner = autoplayer.SpeedRunner(cry=None) + runner.setup() + runner.skip_intro(skip=True) + runner.handle_mom(skip=True) + runner.walk_into_new_bark_town(skip=False) + + # test that the game is in a state such that the player can walk + self.assertTrue(self.check_movement("d")) + + # check that the map is correct + self.assertEqual(self.get_wram_value("MapGroup"), 24) + self.assertEqual(self.get_wram_value("MapNumber"), 4) + + def test_speedrunner_handle_elm(self): + runner = autoplayer.SpeedRunner(cry=None) + runner.setup() + runner.skip_intro(skip=True) + runner.handle_mom(skip=True) + runner.walk_into_new_bark_town(skip=False) + + # go through the Elm's Lab sequence + runner.handle_elm("cyndaquil", skip=False) + + # test again if the game is in a state where the player can walk + self.assertTrue(self.check_movement("u")) + + # check that the map is correct + self.assertEqual(self.get_wram_value("MapGroup"), 24) + self.assertEqual(self.get_wram_value("MapNumber"), 5) + + def test_moving_back_and_forth(self): + runner = autoplayer.SpeedRunner(cry=None) + runner.setup() + runner.skip_intro(skip=True) + runner.handle_mom(skip=True) + runner.walk_into_new_bark_town(skip=False) + + # must be in New Bark Town + self.assertEqual(self.get_wram_value("MapGroup"), 24) + self.assertEqual(self.get_wram_value("MapNumber"), 4) + + runner.cry.move("l") + runner.cry.move("l") + runner.cry.move("l") + runner.cry.move("d") + runner.cry.move("d") + + for x in range(0, 10): + runner.cry.move("l") + runner.cry.move("d") + runner.cry.move("r") + runner.cry.move("u") + + # must still be in New Bark Town + self.assertEqual(self.get_wram_value("MapGroup"), 24) + self.assertEqual(self.get_wram_value("MapNumber"), 4) + + def test_crystal_move_list(self): + runner = autoplayer.SpeedRunner(cry=None) + runner.setup() + runner.skip_intro(skip=True) + runner.handle_mom(skip=True) + runner.walk_into_new_bark_town(skip=False) + + # must be in New Bark Town + self.assertEqual(self.get_wram_value("MapGroup"), 24) + self.assertEqual(self.get_wram_value("MapNumber"), 4) + + first_map_x = self.get_wram_value("MapX") + + runner.cry.move(["l", "l", "l"]) + + # x location should be different + second_map_x = self.get_wram_value("MapX") + self.assertNotEqual(first_map_x, second_map_x) + + # must still be in New Bark Town + self.assertEqual(self.get_wram_value("MapGroup"), 24) + self.assertEqual(self.get_wram_value("MapNumber"), 4) + + def test_keyboard_typing_dumb_name(self): + self.bootstrap_name_prompt() + + name = "tRaInEr" + self.cry.write(name) + + # save this selection + self.cry.vba.press("a", hold=20) + + self.assertEqual(name, self.cry.get_player_name()) + + def test_keyboard_typing_cap_name(self): + names = [ + "trainer", + "TRAINER", + "TrAiNeR", + "tRaInEr", + "ExAmPlE", + "Chris", + "Kris", + "beepaaa", + "chris", + "CHRIS", + "Python", + "pYthon", + "pyThon", + "pytHon", + "pythOn", + "pythoN", + "python", + "PyThOn", + "Zot", + "Death", + "Hiro", + "HIRO", + ] + + self.bootstrap_name_prompt() + start_state = self.cry.vba.state + + for name in names: + print "Writing name: " + name + + self.cry.vba.state = start_state + + sequence = self.cry.write(name) + + print "sequence is: " + str(sequence) + + # save this selection + self.cry.vba.press("start", hold=20) + self.cry.vba.press("a", hold=20) + + pname = self.cry.get_player_name().replace("@", "") + self.assertEqual(name, pname) + if __name__ == "__main__": unittest.main() diff --git a/tests/test_vba_battle.py b/tests/test_vba_battle.py new file mode 100644 index 0000000..c6debc3 --- /dev/null +++ b/tests/test_vba_battle.py @@ -0,0 +1,117 @@ +""" +Tests for the battle controller +""" + +import unittest + +from setup_vba import ( + vba, + autoplayer, +) + +from pokemontools.vba.battle import ( + Battle, + BattleException, +) + +from bootstrapping import ( + bootstrap, + bootstrap_trainer_battle, +) + +class BattleTests(unittest.TestCase): + cry = None + vba = None + bootstrap_state = None + + @classmethod + def setUpClass(cls): + cls.cry = vba.crystal() + cls.vba = cls.cry.vba + + cls.bootstrap_state = bootstrap_trainer_battle() + cls.vba.state = cls.bootstrap_state + + @classmethod + def tearDownClass(cls): + cls.vba.shutdown() + + def setUp(self): + # reset to whatever the bootstrapper created + self.vba.state = self.bootstrap_state + self.battle = Battle(emulator=self.cry) + self.battle.skip_start_text() + + def test_is_in_battle(self): + self.assertTrue(self.battle.is_in_battle()) + + def test_is_player_turn(self): + self.battle.skip_start_text() + self.battle.skip_until_input_required() + + # the initial state should be the player's turn + self.assertTrue(self.battle.is_player_turn()) + + def test_is_mandatory_switch_initial(self): + # should not be asking for a switch so soon in the battle + self.assertFalse(self.battle.is_mandatory_switch()) + + def test_is_mandatory_switch(self): + self.battle.skip_start_text() + self.battle.skip_until_input_required() + + # press "FIGHT" + self.vba.press(["a"], after=20) + + # press the first move ("SCRATCH") + self.vba.press(["a"], after=20) + + # set partymon1 hp to very low + self.cry.set_battle_mon_hp(1) + + # let the enemy attack and kill the pokemon + self.battle.skip_until_input_required() + + self.assertTrue(self.battle.is_mandatory_switch()) + + def test_attack_loop(self): + self.battle.skip_start_text() + self.battle.skip_until_input_required() + + # press "FIGHT" + self.vba.press(["a"], after=20) + + # press the first move ("SCRATCH") + self.vba.press(["a"], after=20) + + self.battle.skip_until_input_required() + + self.assertTrue(self.battle.is_player_turn()) + + def test_is_battle_switch_prompt(self): + self.battle.skip_start_text() + self.battle.skip_until_input_required() + + # press "FIGHT" + self.vba.press(["a"], after=20) + + # press the first move ("SCRATCH") + self.vba.press(["a"], after=20) + + # set enemy hp to very low + self.cry.lower_enemy_hp() + + # attack the enemy and kill it + self.battle.skip_until_input_required() + + # yes/no menu is present, should be detected + self.assertTrue(self.battle.is_trainer_switch_prompt()) + + # and input should be required + self.assertTrue(self.battle.is_input_required()) + + # but it's not mandatory + self.assertFalse(self.battle.is_mandatory_switch()) + +if __name__ == "__main__": + unittest.main() diff --git a/tests/tests.py b/tests/tests.py index 7919a66..4398f03 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -38,6 +38,10 @@ from pokemontools.labels import ( find_labels_without_addresses, ) +from pokemontools.crystalparts.old_parsers import ( + old_parse_map_header_at, +) + from pokemontools.helpers import ( grouper, index, @@ -66,7 +70,6 @@ from pokemontools.crystal import ( all_labels, write_all_labels, parse_map_header_at, - old_parse_map_header_at, process_00_subcommands, parse_all_map_headers, translate_command_byte, |