Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Pokemon Gen 1 Game Wrapper Expansion #281

Open
wants to merge 82 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
82 commits
Select commit Hold shift + click to select a range
3e3fe16
Add Pokemon Red constants
Jun 27, 2020
3e87f78
Add initial Pokemon Red wrapper code
Jun 27, 2020
ba72073
Merge branch 'master' into feature/pokered
Baekalfen Sep 29, 2020
b1ede15
Merge branch 'master' into feature/pokered
Baekalfen Jul 18, 2021
a00f320
Merge remote-tracking branch 'origin/master' into pokemon-red
Nov 3, 2023
b6229b9
Change files from Pokemon Red to PokemonGen1 as both games are functi…
Nov 3, 2023
75aa8be
Remove old pokemon_gen1 PLUGIN AS WELL AS now-renamed GameWrapperPoke…
Nov 3, 2023
6e41541
Add debug.pxi
Nov 3, 2023
a52ec6c
Add functions to read/write multiple consecutive bytes of memory
SnarkAttack Nov 4, 2023
2b591d3
Create Python file with memory addresses so they can be referenced as…
SnarkAttack Nov 4, 2023
b63f66b
Convert get_memory_value and set_memory_value to allow for reading an…
SnarkAttack Nov 4, 2023
c6dc375
Create Gen1MemoryManager, a class to wrap memory read/writes for Poke…
SnarkAttack Nov 4, 2023
1815652
Convert moves to an Enum
SnarkAttack Nov 4, 2023
1f518c0
Convert Pokemon (previously MONSTERS) into Enum
SnarkAttack Nov 4, 2023
c44d925
Convert Items into Enum
SnarkAttack Nov 4, 2023
d3d5398
Convert Sprites into Enum
SnarkAttack Nov 4, 2023
935ac55
Convert Maps into Enum
SnarkAttack Nov 4, 2023
24efb7a
Convert Statuses into Enum
SnarkAttack Nov 4, 2023
be72c8f
Raname Status Enum to Statuses (which was the original intent)
SnarkAttack Nov 4, 2023
1998390
Create Pokemon class to wrap memory loading of all values of a pokemo…
SnarkAttack Nov 5, 2023
4b08aef
Start recreating PokemonPOkedexIndex enum
SnarkAttack Nov 6, 2023
1439fc0
Define pretty print function for Pokemon type
SnarkAttack Nov 6, 2023
2e4a2dc
Convert pokedex entry lookup into a dictionary with a Pokemon Enum key
SnarkAttack Nov 6, 2023
6abad35
Break up constants.py into directory with smaller files so it's a lit…
Nov 6, 2023
84dc3e8
Restructure files to break down constants into smaller parts for read…
Nov 6, 2023
4cba7ba
Move location constants into new directory and create dict for name l…
Nov 6, 2023
6d7e4f7
Remove location constants from constants.py after their move
Nov 6, 2023
5eb582f
Change Pokemon enum to PokemonIds to avoid any confusion with Pokemon…
Nov 6, 2023
2f23ad9
Fix mapping dicts to use newly named PokemonIds
Nov 6, 2023
02815d4
Continue fleshing out Pokemon class
Nov 6, 2023
c93dd7b
Rename Locations enum to LocationIds
Nov 6, 2023
bd36dbd
Add Move class (currently just a wrapper with static methods)
Nov 6, 2023
2a22a16
Restructure data folder
Nov 6, 2023
5a7fec8
Change storage of moves in Pokemon class to list
Nov 6, 2023
c1ccbc4
Add defaults tring to fill in nonexistent move slots in pretty printing
Nov 6, 2023
1bb770e
Restructure stats, evs, and move PP to also be stored in lists in the…
Nov 6, 2023
902c9f4
Finish splitting constants into respective files
Nov 6, 2023
b45490b
Change PokemonOffsets to PokemonMemoryOffsets to make it clearer what…
Nov 7, 2023
024407c
Reset pyboy.py, going to try and avoid chaning it for now
Nov 7, 2023
5bb82da
Add sprite memory addresses
Nov 7, 2023
65389fe
Add function to read multibyte data
Nov 7, 2023
efdb89a
Update pokemon.py to use newly named PokemonMemoryOffets enum
Nov 7, 2023
c7aa8f4
remove Gen1MemoryManager class, was just unnecessary level of encapsu…
Nov 7, 2023
da33d44
Update Pokemon class to include 'hidden' level field and added utilit…
Nov 7, 2023
c8e0563
Add POkedex class
Nov 7, 2023
a402409
Add player money get
Nov 7, 2023
5c6b76e
Memory manager class is back baby
Nov 8, 2023
bb0f626
Define explicit function calls for different type of memory access
Nov 8, 2023
7766dfa
rename function calls to indicate these are memory reads
Nov 8, 2023
5d1a66b
Change all memory access calls to now go through MemoryManager class
Nov 8, 2023
2f295f1
Fix issue where strings shorted than the adresses they could occupy w…
Nov 8, 2023
bf7d90a
Fix bugs in/discovered by Player class
Nov 8, 2023
a573f84
Add specific badge check to Player
Nov 8, 2023
f32025b
REname all enums to singular form for better readability
Nov 8, 2023
488f56c
Fix issue where we needed enum value not enum to get correct list slice
Nov 8, 2023
754ac34
Convert all memory address enums to singular as well
Nov 8, 2023
d196a60
Add write functions to match corresponding reads in memory and implem…
Nov 8, 2023
e2dacc3
Add MemoryAdressEnum, which all memory address data will inherit from…
Nov 8, 2023
403f8c2
Add wrapper functions to write MemoryAddressEnums without having to o…
SnarkAttack Nov 8, 2023
f30bdc3
Fix issues with MemoryAddressEnum working in read/write calls
SnarkAttack Nov 8, 2023
e65acb9
Fix a few write bugs discovered in Player write testing
SnarkAttack Nov 8, 2023
91f1355
Add memory access functions for a variety of common functions
SnarkAttack Dec 4, 2023
d0919b9
Merge branch 'memory_access_functions' into pokemon-gen-1
SnarkAttack Dec 4, 2023
d25ba96
Merge branch 'master' into pokemon-gen-1
Dec 4, 2023
bfba88b
Add function to get player x/y location
SnarkAttack Dec 4, 2023
a6c5359
Add memory access function in game wrapper that uses the memory manag…
Dec 5, 2023
19d1905
Add GameState object to track general game info
Dec 5, 2023
bc2c544
Fix check on game_state.is_in_battle (was returning opposite of desired)
Dec 5, 2023
2486aad
Rewrite memory address enums so they are now a class and also track i…
Dec 5, 2023
759d616
Allow read and write super calls tot ake in straight MemoryAddress ob…
Dec 5, 2023
3027a8e
Convert previous objects that stored representations of data in memor…
Dec 5, 2023
1e16bec
Rename add_address to add_offset
Dec 5, 2023
9623437
Remove print statement that was acidentally committed
SnarkAttack Dec 5, 2023
5cbb27e
Update Player class to use new memory reading approach
SnarkAttack Dec 5, 2023
974d7d2
Merge branch 'rewrite_memory_reading' into pokemon-gen-1
SnarkAttack Dec 5, 2023
0897771
Add map_id to player location result and add access to screen in wrap…
Dec 6, 2023
437a8ea
Add function to return all Pokemon in party
Dec 7, 2023
3510133
Add experience property to Pokemon
Dec 7, 2023
78bb7fd
Merge branch 'master' into pokemon-gen-1
SnarkAttack Dec 15, 2023
2ac86f3
Add function to read ROM, not exposed out of class because we don't w…
Dec 15, 2023
7e4f4a8
Give access to ROM reading memory
Dec 15, 2023
4204412
Merge branch 'pokemon-gen-1' of github.com:SnarkAttack/PyBoy into pok…
Dec 15, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions pyboy/core/debug.pxi
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#
# License: See LICENSE.md file
# GitHub: https://github.com/Baekalfen/PyBoy
#
DEF DEBUG=0
10 changes: 10 additions & 0 deletions pyboy/plugins/game_wrapper_pokemon_gen1/core/battle.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from .memory_object import MemoryObject
from ..data.memory_addrs.battle import BattleAddress, BATTLE_ADDRESS_LOOKUP

class Battle(MemoryObject):

_enum = BattleAddress
_lookup = BATTLE_ADDRESS_LOOKUP

def __init__(self, fields_to_track=None):
super().__init__(fields_to_track)
23 changes: 23 additions & 0 deletions pyboy/plugins/game_wrapper_pokemon_gen1/core/game_state.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from ..data.memory_addrs.game_state import GameStateAddress

class GameState():

def __init__(self,
battle_type
):

self._battle_type = battle_type

def is_in_battle(self):
return self._battle_type != 0

@property
def battle_type(self):
return self._battle_type

@staticmethod
def load_game_state(mem_manager):

battle_type = mem_manager.read_memory_address(GameStateAddress.BATTLE_TYPE)

return GameState(battle_type)
10 changes: 10 additions & 0 deletions pyboy/plugins/game_wrapper_pokemon_gen1/core/mem_manager.pxd
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
#
# License: See LICENSE.md file
# GitHub: https://github.com/Baekalfen/PyBoy
#
from libc.stdint cimport uint8_t
from pyboy.pyboy cimport PyBoy
cimport cython

cdef class MemoryManager:
cdef PyBoy pyboy
205 changes: 205 additions & 0 deletions pyboy/plugins/game_wrapper_pokemon_gen1/core/mem_manager.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
from enum import Enum
from ..data.memory_addrs.base import MemoryAddress, MemoryAddressType

STRING_TERMINATOR = 80
ASCII_DELTA = 63

MAX_STRING_LENGTH = 16

class MemoryManager():

def __init__(self, pyboy):
self.pyboy = pyboy

@staticmethod
def _byte_to_bitfield(n, reverse : bool):
# Force bit extension to 10 chars; 2 for '0x' and 8 for the bits
bitlist = [1 if digit=='1' else 0 for digit in format(n, '#010b')[2:]]

if reverse:
bitlist.reverse()

return bitlist

@staticmethod
def _bitfield_to_byte(bitlist, reverse : bool):

if reverse:
bitlist.reverse()

bit_str = ''.join([str(i) for i in bitlist])

return int(bit_str, 2)

@staticmethod
def get_character_index(character):
if character == ' ':
return 0x7F
if character == '?':
return 0xE6
if character == '!':
return 0xE7
if character == 'é':
return 0xBA

index = ord(character)
if index > 47 and index < 58:
# number
return index + 197
return index + ASCII_DELTA

def get_

def _read_byte(self, addr):
return self.pyboy.get_memory_value(addr)

def _write_byte(self, value, addr):
return self.pyboy.set_memory_value(addr, value)

def read_address_from_memory(self, addr, num_bytes=1):
p_addr = 0
for i in range(num_bytes):
p_addr += self._read_byte(addr+i) << i*8
return p_addr

def read_hex_from_memory(self, addr, num_bytes=1):
bytes = []
for i in range(num_bytes):
bytes.append(self._read_byte(addr + i))
# Do not believe there is ever a case where we would
# need to read this in little endian for Pokemon gen 1
return int.from_bytes(bytes, byteorder='big')

def write_hex_to_memory(self, value, addr, num_bytes=1):
bytes = value.to_bytes(2, byteorder='big')
assert len(bytes) == num_bytes*2
for i, byte in enumerate(bytes):
self._write_byte(addr + i, byte)

def read_bcd_from_memory(self, addr, num_bytes=1):
byte_str = ""
for i in range(num_bytes):
byte_str += "%x"%self._read_byte(addr+i)
return int(byte_str)

def write_bcd_to_memory(self, value, addr, num_bytes=1):
val_str = str(value)
assert len(val_str) <= num_bytes*2

padded_val = val_str.zfill(num_bytes*2)
for i in range(num_bytes):
sub_val = int(padded_val[i*2:(i*2)+2], 16)
self._write_byte(sub_val, addr+i)

def read_bitfield_from_memory(self, addr, num_bytes=1, reverse=False):
bits = []
for i in range(num_bytes):
bits.extend(MemoryManager._byte_to_bitfield(self._read_byte(addr+i), reverse))
return bits

def write_bitlist_to_memory(self, value, addr, num_bytes=1, reverse=False):

# TODO: This check might be too restrictive
assert len(value) % 8 == 0
# TODO: Maybe don't need this check if we trust users to make value fit
assert len(value)/8 == num_bytes

for i in range(num_bytes):
bit_list_start = i*8
sub_bit_list = value[bit_list_start:bit_list_start+8]
byte_val = MemoryManager._bitfield_to_byte(sub_bit_list, reverse)
self._write_byte(byte_val, addr+i)

def read_text_from_memory(self, address, num_bytes):
"""
Retrieves a string from a given address.

Args:
address (int): Address from where to retrieve text from.
cap (int): Maximum expected length of string (default: 16).
"""
# Make sure string to write not too large
assert num_bytes <= MAX_STRING_LENGTH
text = ''
for i in range(num_bytes):
value = self._read_byte(address + i)
if value == STRING_TERMINATOR:
break
text += chr(value - ASCII_DELTA)
return text

def write_text_to_memory(self, text, address, num_bytes=1):
"""Sets text at address.

Will always add a string terminator (80) at the end.
"""

terminated_text = text + '\0'
# TODO: Check that this isn't an off-by-one issue
assert len(terminated_text) <= num_bytes

i = 0
for i, chr in enumerate(text):
self.pyboy.set_memory_value(address + i, MemoryManager.get_character_index(chr))

def read_memory_address(self, mem_addr):
if isinstance(mem_addr, Enum):
mem_addr = mem_addr.value
if mem_addr.memory_type == MemoryAddressType.HEX:
mem_func = self.read_hex_from_memory
elif mem_addr.memory_type == MemoryAddressType.ADDRESS:
mem_func = self.read_address_from_memory
elif mem_addr.memory_type == MemoryAddressType.BCD:
mem_func = self.read_bcd_from_memory
elif mem_addr.memory_type == MemoryAddressType.BITFIELD:
mem_func = self.read_bitfield_from_memory
elif mem_addr.memory_type == MemoryAddressType.TEXT:
mem_func = self.read_text_from_memory
else:
raise ValueError(f"{mem_addr.memory_type} is not a valid memory type")

return mem_func(mem_addr.address, mem_addr.num_bytes)

def write_memory_address(self, mem_addr):
if isinstance(mem_addr, Enum):
mem_addr = mem_addr.value
if mem_addr.memory_type == MemoryAddressType.HEX:
mem_func = self.write_hex_to_memory
elif mem_addr.memory_type == MemoryAddressType.ADDRESS:
#mem_func = self.write_add
pass
elif mem_addr.memory_type == MemoryAddressType.BCD:
mem_func = self.write_bcd_to_memory
elif mem_addr.memory_type == MemoryAddressType.BITFIELD:
mem_func = self.write_bitlist_to_memory
elif mem_addr.memory_type == MemoryAddressType.TEXT:
mem_func = self.write_text_to_memory
else:
raise ValueError(f"{mem_addr.memory_type} is not a valid memory type")

return mem_func(mem_addr.address, mem_addr.num_bytes)

def _load_rombank(self, rombank_selected):
self.pyboy.set_memory_value(0x2000, rombank_selected)

def read_from_rom(self, address, num_bytes=1):
rombank = int(address / 0x4000)
address_in_rom = address % 0x4000

# PyBoy treats the switchable ROM bank as being at address
# 0x4000 to 0x8000 (0x0000 to 0x4000 is always ROM bank 0),
# append 04x4000

address_in_rom += 0x4000

# Switch in correct ROM bank
self._load_rombank(rombank)

rom_values = []

for i in range(num_bytes):
rom_values.append(self.pyboy.get_memory_value(address_in_rom+i))

return rom_values


21 changes: 21 additions & 0 deletions pyboy/plugins/game_wrapper_pokemon_gen1/core/memory_object.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
class MemoryObject():

_enum = {}
_lookup = {}

def __init__(self, fields_to_track):
if fields_to_track is None:
fields_to_track = [e for e in self._enum]
self._fields_to_track = fields_to_track
self._data = {}

def _load_field_from_memory(self, mem_manager, field_enum):
self.data[field_enum] = mem_manager.read_memory_address(self._lookup[field_enum])

def load_from_memory(self, mem_manager):
for field_enum in self._fields_to_track:
self._load_field_from_memory(mem_manager, field_enum)

def get_value(self, k):
return self._data.get(k)

19 changes: 19 additions & 0 deletions pyboy/plugins/game_wrapper_pokemon_gen1/core/move.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from ..data.constants.moves import MoveId

class Move():

NONEXISTENT_MOVE_STR = "---"

def __init__(self, move_id):
pass

# We can cheat with just using the enum name as the move name
# since there is no move that has any special characters in it
@classmethod
def get_name_from_id(cls, move_id, camel_case=False):
if move_id == 0:
return cls.NONEXISTENT_MOVE_STR
move_name = MoveId(move_id).name.replace('_', ' ')
if camel_case:
return move_name.title()
return move_name
71 changes: 71 additions & 0 deletions pyboy/plugins/game_wrapper_pokemon_gen1/core/player.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
from ..data.memory_addrs.player import PlayerAddress, PLAYER_ADDRESS_LOOKUP
from ..data.constants.pokemon import PokemonId
from ..data.constants.misc import Badge
from .memory_object import MemoryObject

class Player(MemoryObject):

_enum = PlayerAddress
_lookup = PLAYER_ADDRESS_LOOKUP

def __init__(self, fields_to_track):
super().__init__(fields_to_track)

'''
Getters and setters
'''
@property
def name(self):
return self._name

@name.setter
def name(self, n):
# Name truncated to 8 characters
self._name = n[:8]

@property
def money(self):
return self._money

@money.setter
def money(self, m):
# Max money game can store is 999999
self._money = min(m, 999999)

@property
def num_pokemon_in_party(self):
return len(self._pokemon_in_party)

@property
def num_badges(self):
return sum(self._badges)

'''
Badge functionality
'''
def has_badge(self, badge : Badge):
return self._badges[badge.value] == 1

def give_badge(self, badge : Badge):
self._badges[badge.value] = 1

def remove_badge(self, badge : Badge):
self._badges[badge.value] = 0

@staticmethod
def load_player(mem_manager, fields_to_track=None):
player = Player(mem_manager, fields_to_track)
player.load_from_memory(mem_manager)
return player

# def save_player(self, mem_manager):

# mem_manager.write_memory_address(self._name, PlayerAddress.NAME)
# mem_manager.write_memory_address(self.num_pokemon_in_party, PlayerAddress.NUM_POKEMON_IN_PARTY)
# # TODO: Figure out the loading and unloading of Pokemon in party, as that should be in tandem with
# # Pokemon class so that data does not get out of sync
# mem_manager.write_memory_address(self._badges, PlayerAddress.BADGES)
# mem_manager.write_memory_address(self._money, PlayerAddress.MONEY)



Loading