[Pokemon Red and Blue] Initial implementation (#1016)

This commit is contained in:
Alchav 2022-10-13 01:45:52 -04:00 committed by GitHub
parent 0afb7096de
commit 30a4bcbbbe
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 7078 additions and 2 deletions

3
.gitignore vendored
View File

@ -13,6 +13,9 @@
*.z64
*.n64
*.nes
*.gb
*.gbc
*.gba
*.wixobj
*.lck
*.db3

View File

@ -145,6 +145,8 @@ components: Iterable[Component] = (
Component('OoT Adjuster', 'OoTAdjuster'),
# FF1
Component('FF1 Client', 'FF1Client'),
# Pokémon
Component('Pokemon Client', 'PokemonClient', file_identifier=SuffixIdentifier('.apred', '.apblue')),
# ChecksFinder
Component('ChecksFinder Client', 'ChecksFinderClient'),
# Starcraft 2

319
PokemonClient.py Normal file
View File

@ -0,0 +1,319 @@
import asyncio
import json
import time
import os
import bsdiff4
import subprocess
import zipfile
import hashlib
from asyncio import StreamReader, StreamWriter
from typing import List
import Utils
from CommonClient import CommonContext, server_loop, gui_enabled, ClientCommandProcessor, logger, \
get_base_parser
from worlds.pokemon_rb.locations import location_data
location_map = {"Rod": {}, "EventFlag": {}, "Missable": {}, "Hidden": {}, "list": {}}
location_bytes_bits = {}
for location in location_data:
if location.ram_address is not None:
if type(location.ram_address) == list:
location_map[type(location.ram_address).__name__][(location.ram_address[0].flag, location.ram_address[1].flag)] = location.address
location_bytes_bits[location.address] = [{'byte': location.ram_address[0].byte, 'bit': location.ram_address[0].bit},
{'byte': location.ram_address[1].byte, 'bit': location.ram_address[1].bit}]
else:
location_map[type(location.ram_address).__name__][location.ram_address.flag] = location.address
location_bytes_bits[location.address] = {'byte': location.ram_address.byte, 'bit': location.ram_address.bit}
SYSTEM_MESSAGE_ID = 0
CONNECTION_TIMING_OUT_STATUS = "Connection timing out. Please restart your emulator, then restart pkmn_rb.lua"
CONNECTION_REFUSED_STATUS = "Connection Refused. Please start your emulator and make sure pkmn_rb.lua is running"
CONNECTION_RESET_STATUS = "Connection was reset. Please restart your emulator, then restart pkmn_rb.lua"
CONNECTION_TENTATIVE_STATUS = "Initial Connection Made"
CONNECTION_CONNECTED_STATUS = "Connected"
CONNECTION_INITIAL_STATUS = "Connection has not been initiated"
DISPLAY_MSGS = True
class GBCommandProcessor(ClientCommandProcessor):
def __init__(self, ctx: CommonContext):
super().__init__(ctx)
def _cmd_gb(self):
"""Check Gameboy Connection State"""
if isinstance(self.ctx, GBContext):
logger.info(f"Gameboy Status: {self.ctx.gb_status}")
class GBContext(CommonContext):
command_processor = GBCommandProcessor
game = 'Pokemon Red and Blue'
items_handling = 0b101
def __init__(self, server_address, password):
super().__init__(server_address, password)
self.gb_streams: (StreamReader, StreamWriter) = None
self.gb_sync_task = None
self.messages = {}
self.locations_array = None
self.gb_status = CONNECTION_INITIAL_STATUS
self.awaiting_rom = False
self.display_msgs = True
async def server_auth(self, password_requested: bool = False):
if password_requested and not self.password:
await super(GBContext, self).server_auth(password_requested)
if not self.auth:
self.awaiting_rom = True
logger.info('Awaiting connection to Bizhawk to get Player information')
return
await self.send_connect()
def _set_message(self, msg: str, msg_id: int):
if DISPLAY_MSGS:
self.messages[(time.time(), msg_id)] = msg
def on_package(self, cmd: str, args: dict):
if cmd == 'Connected':
self.locations_array = None
elif cmd == "RoomInfo":
self.seed_name = args['seed_name']
elif cmd == 'Print':
msg = args['text']
if ': !' not in msg:
self._set_message(msg, SYSTEM_MESSAGE_ID)
elif cmd == "ReceivedItems":
msg = f"Received {', '.join([self.item_names[item.item] for item in args['items']])}"
self._set_message(msg, SYSTEM_MESSAGE_ID)
def run_gui(self):
from kvui import GameManager
class GBManager(GameManager):
logging_pairs = [
("Client", "Archipelago")
]
base_title = "Archipelago Pokémon Client"
self.ui = GBManager(self)
self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI")
def get_payload(ctx: GBContext):
current_time = time.time()
return json.dumps(
{
"items": [item.item for item in ctx.items_received],
"messages": {f'{key[0]}:{key[1]}': value for key, value in ctx.messages.items()
if key[0] > current_time - 10}
}
)
async def parse_locations(data: List, ctx: GBContext):
locations = []
flags = {"EventFlag": data[:0x140], "Missable": data[0x140:0x140 + 0x20],
"Hidden": data[0x140 + 0x20: 0x140 + 0x20 + 0x0E], "Rod": data[0x140 + 0x20 + 0x0E:]}
# Check for clear problems
if len(flags['Rod']) > 1:
return
if flags["EventFlag"][1] + flags["EventFlag"][8] + flags["EventFlag"][9] + flags["EventFlag"][12] \
+ flags["EventFlag"][61] + flags["EventFlag"][62] + flags["EventFlag"][63] + flags["EventFlag"][64] \
+ flags["EventFlag"][65] + flags["EventFlag"][66] + flags["EventFlag"][67] + flags["EventFlag"][68] \
+ flags["EventFlag"][69] + flags["EventFlag"][70] != 0:
return
for flag_type, loc_map in location_map.items():
for flag, loc_id in loc_map.items():
if flag_type == "list":
if (flags["EventFlag"][location_bytes_bits[loc_id][0]['byte']] & 1 << location_bytes_bits[loc_id][0]['bit']
and flags["Missable"][location_bytes_bits[loc_id][1]['byte']] & 1 << location_bytes_bits[loc_id][1]['bit']):
locations.append(loc_id)
elif flags[flag_type][location_bytes_bits[loc_id]['byte']] & 1 << location_bytes_bits[loc_id]['bit']:
locations.append(loc_id)
if flags["EventFlag"][280] & 1 and not ctx.finished_game:
await ctx.send_msgs([
{"cmd": "StatusUpdate",
"status": 30}
])
ctx.finished_game = True
if locations == ctx.locations_array:
return
ctx.locations_array = locations
if locations is not None:
await ctx.send_msgs([{"cmd": "LocationChecks", "locations": locations}])
async def gb_sync_task(ctx: GBContext):
logger.info("Starting GB connector. Use /gb for status information")
while not ctx.exit_event.is_set():
error_status = None
if ctx.gb_streams:
(reader, writer) = ctx.gb_streams
msg = get_payload(ctx).encode()
writer.write(msg)
writer.write(b'\n')
try:
await asyncio.wait_for(writer.drain(), timeout=1.5)
try:
# Data will return a dict with up to two fields:
# 1. A keepalive response of the Players Name (always)
# 2. An array representing the memory values of the locations area (if in game)
data = await asyncio.wait_for(reader.readline(), timeout=5)
data_decoded = json.loads(data.decode())
#print(data_decoded)
if ctx.seed_name and ctx.seed_name != bytes(data_decoded['seedName']).decode():
msg = "The server is running a different multiworld than your client is. (invalid seed_name)"
logger.info(msg, extra={'compact_gui': True})
ctx.gui_error('Error', msg)
error_status = CONNECTION_RESET_STATUS
ctx.seed_name = bytes(data_decoded['seedName']).decode()
if not ctx.auth:
ctx.auth = ''.join([chr(i) for i in data_decoded['playerName'] if i != 0])
if ctx.auth == '':
logger.info("Invalid ROM detected. No player name built into the ROM.")
if ctx.awaiting_rom:
await ctx.server_auth(False)
if 'locations' in data_decoded and ctx.game and ctx.gb_status == CONNECTION_CONNECTED_STATUS \
and not error_status and ctx.auth:
# Not just a keep alive ping, parse
asyncio.create_task(parse_locations(data_decoded['locations'], ctx))
except asyncio.TimeoutError:
logger.debug("Read Timed Out, Reconnecting")
error_status = CONNECTION_TIMING_OUT_STATUS
writer.close()
ctx.gb_streams = None
except ConnectionResetError as e:
logger.debug("Read failed due to Connection Lost, Reconnecting")
error_status = CONNECTION_RESET_STATUS
writer.close()
ctx.gb_streams = None
except TimeoutError:
logger.debug("Connection Timed Out, Reconnecting")
error_status = CONNECTION_TIMING_OUT_STATUS
writer.close()
ctx.gb_streams = None
except ConnectionResetError:
logger.debug("Connection Lost, Reconnecting")
error_status = CONNECTION_RESET_STATUS
writer.close()
ctx.gb_streams = None
if ctx.gb_status == CONNECTION_TENTATIVE_STATUS:
if not error_status:
logger.info("Successfully Connected to Gameboy")
ctx.gb_status = CONNECTION_CONNECTED_STATUS
else:
ctx.gb_status = f"Was tentatively connected but error occured: {error_status}"
elif error_status:
ctx.gb_status = error_status
logger.info("Lost connection to Gameboy and attempting to reconnect. Use /gb for status updates")
else:
try:
logger.debug("Attempting to connect to Gameboy")
ctx.gb_streams = await asyncio.wait_for(asyncio.open_connection("localhost", 17242), timeout=10)
ctx.gb_status = CONNECTION_TENTATIVE_STATUS
except TimeoutError:
logger.debug("Connection Timed Out, Trying Again")
ctx.gb_status = CONNECTION_TIMING_OUT_STATUS
continue
except ConnectionRefusedError:
logger.debug("Connection Refused, Trying Again")
ctx.gb_status = CONNECTION_REFUSED_STATUS
continue
async def run_game(romfile):
auto_start = Utils.get_options()["pokemon_rb_options"].get("rom_start", True)
if auto_start is True:
import webbrowser
webbrowser.open(romfile)
elif os.path.isfile(auto_start):
subprocess.Popen([auto_start, romfile],
stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
async def patch_and_run_game(game_version, patch_file, ctx):
base_name = os.path.splitext(patch_file)[0]
comp_path = base_name + '.gb'
with open(Utils.local_path(Utils.get_options()["pokemon_rb_options"][f"{game_version}_rom_file"]), "rb") as stream:
base_rom = bytes(stream.read())
try:
with open(Utils.local_path('lib', 'worlds', 'pokemon_rb', f'basepatch_{game_version}.bsdiff4'), 'rb') as stream:
base_patch = bytes(stream.read())
except FileNotFoundError:
with open(Utils.local_path('worlds', 'pokemon_rb', f'basepatch_{game_version}.bsdiff4'), 'rb') as stream:
base_patch = bytes(stream.read())
base_patched_rom_data = bsdiff4.patch(base_rom, base_patch)
basemd5 = hashlib.md5()
basemd5.update(base_patched_rom_data)
with zipfile.ZipFile(patch_file, 'r') as patch_archive:
with patch_archive.open('delta.bsdiff4', 'r') as stream:
patch = stream.read()
patched_rom_data = bsdiff4.patch(base_patched_rom_data, patch)
written_hash = patched_rom_data[0xFFCC:0xFFDC]
if written_hash == basemd5.digest():
with open(comp_path, "wb") as patched_rom_file:
patched_rom_file.write(patched_rom_data)
asyncio.create_task(run_game(comp_path))
else:
msg = "Patch supplied was not generated with the same base patch version as this client. Patching failed."
logger.warning(msg)
ctx.gui_error('Error', msg)
if __name__ == '__main__':
Utils.init_logging("PokemonClient")
options = Utils.get_options()
async def main():
parser = get_base_parser()
parser.add_argument('patch_file', default="", type=str, nargs="?",
help='Path to an APRED or APBLUE patch file')
args = parser.parse_args()
ctx = GBContext(args.connect, args.password)
ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop")
if gui_enabled:
ctx.run_gui()
ctx.run_cli()
ctx.gb_sync_task = asyncio.create_task(gb_sync_task(ctx), name="GB Sync")
if args.patch_file:
ext = args.patch_file.split(".")[len(args.patch_file.split(".")) - 1].lower()
if ext == "apred":
logger.info("APRED file supplied, beginning patching process...")
asyncio.create_task(patch_and_run_game("red", args.patch_file, ctx))
elif ext == "apblue":
logger.info("APBLUE file supplied, beginning patching process...")
asyncio.create_task(patch_and_run_game("blue", args.patch_file, ctx))
else:
logger.warning(f"Unknown patch file extension {ext}")
await ctx.exit_event.wait()
ctx.server_address = None
await ctx.shutdown()
if ctx.gb_sync_task:
await ctx.gb_sync_task
import colorama
colorama.init()
asyncio.run(main())
colorama.deinit()

View File

@ -29,6 +29,7 @@ Currently, the following games are supported:
* Donkey Kong Country 3
* Dark Souls 3
* Super Mario World
* Pokémon Red and Blue
For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial/).
Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled

View File

@ -295,6 +295,11 @@ def get_default_options() -> OptionsType:
"sni": "SNI",
"rom_start": True,
},
"pokemon_rb_options": {
"red_rom_file": "Pokemon Red (UE) [S][!].gb",
"blue_rom_file": "Pokemon Blue (UE) [S][!].gb",
"rom_start": True
}
}
return options

BIN
data/lua/PKMN_RB/core.dll Normal file

Binary file not shown.

389
data/lua/PKMN_RB/json.lua Normal file
View File

@ -0,0 +1,389 @@
--
-- json.lua
--
-- Copyright (c) 2015 rxi
--
-- This library is free software; you can redistribute it and/or modify it
-- under the terms of the MIT license. See LICENSE for details.
--
local json = { _version = "0.1.0" }
-------------------------------------------------------------------------------
-- Encode
-------------------------------------------------------------------------------
local encode
function error(err)
print(err)
end
local escape_char_map = {
[ "\\" ] = "\\\\",
[ "\"" ] = "\\\"",
[ "\b" ] = "\\b",
[ "\f" ] = "\\f",
[ "\n" ] = "\\n",
[ "\r" ] = "\\r",
[ "\t" ] = "\\t",
}
local escape_char_map_inv = { [ "\\/" ] = "/" }
for k, v in pairs(escape_char_map) do
escape_char_map_inv[v] = k
end
local function escape_char(c)
return escape_char_map[c] or string.format("\\u%04x", c:byte())
end
local function encode_nil(val)
return "null"
end
local function encode_table(val, stack)
local res = {}
stack = stack or {}
-- Circular reference?
if stack[val] then error("circular reference") end
stack[val] = true
if val[1] ~= nil or next(val) == nil then
-- Treat as array -- check keys are valid and it is not sparse
local n = 0
for k in pairs(val) do
if type(k) ~= "number" then
error("invalid table: mixed or invalid key types")
end
n = n + 1
end
if n ~= #val then
print("invalid table: sparse array")
print(n)
print("VAL:")
print(val)
print("STACK:")
print(stack)
end
-- Encode
for i, v in ipairs(val) do
table.insert(res, encode(v, stack))
end
stack[val] = nil
return "[" .. table.concat(res, ",") .. "]"
else
-- Treat as an object
for k, v in pairs(val) do
if type(k) ~= "string" then
error("invalid table: mixed or invalid key types")
end
table.insert(res, encode(k, stack) .. ":" .. encode(v, stack))
end
stack[val] = nil
return "{" .. table.concat(res, ",") .. "}"
end
end
local function encode_string(val)
return '"' .. val:gsub('[%z\1-\31\\"]', escape_char) .. '"'
end
local function encode_number(val)
-- Check for NaN, -inf and inf
if val ~= val or val <= -math.huge or val >= math.huge then
error("unexpected number value '" .. tostring(val) .. "'")
end
return string.format("%.14g", val)
end
local type_func_map = {
[ "nil" ] = encode_nil,
[ "table" ] = encode_table,
[ "string" ] = encode_string,
[ "number" ] = encode_number,
[ "boolean" ] = tostring,
}
encode = function(val, stack)
local t = type(val)
local f = type_func_map[t]
if f then
return f(val, stack)
end
error("unexpected type '" .. t .. "'")
end
function json.encode(val)
return ( encode(val) )
end
-------------------------------------------------------------------------------
-- Decode
-------------------------------------------------------------------------------
local parse
local function create_set(...)
local res = {}
for i = 1, select("#", ...) do
res[ select(i, ...) ] = true
end
return res
end
local space_chars = create_set(" ", "\t", "\r", "\n")
local delim_chars = create_set(" ", "\t", "\r", "\n", "]", "}", ",")
local escape_chars = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u")
local literals = create_set("true", "false", "null")
local literal_map = {
[ "true" ] = true,
[ "false" ] = false,
[ "null" ] = nil,
}
local function next_char(str, idx, set, negate)
for i = idx, #str do
if set[str:sub(i, i)] ~= negate then
return i
end
end
return #str + 1
end
local function decode_error(str, idx, msg)
--local line_count = 1
--local col_count = 1
--for i = 1, idx - 1 do
-- col_count = col_count + 1
-- if str:sub(i, i) == "\n" then
-- line_count = line_count + 1
-- col_count = 1
-- end
-- end
-- emu.message( string.format("%s at line %d col %d", msg, line_count, col_count) )
end
local function codepoint_to_utf8(n)
-- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa
local f = math.floor
if n <= 0x7f then
return string.char(n)
elseif n <= 0x7ff then
return string.char(f(n / 64) + 192, n % 64 + 128)
elseif n <= 0xffff then
return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128)
elseif n <= 0x10ffff then
return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128,
f(n % 4096 / 64) + 128, n % 64 + 128)
end
error( string.format("invalid unicode codepoint '%x'", n) )
end
local function parse_unicode_escape(s)
local n1 = tonumber( s:sub(3, 6), 16 )
local n2 = tonumber( s:sub(9, 12), 16 )
-- Surrogate pair?
if n2 then
return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000)
else
return codepoint_to_utf8(n1)
end
end
local function parse_string(str, i)
local has_unicode_escape = false
local has_surrogate_escape = false
local has_escape = false
local last
for j = i + 1, #str do
local x = str:byte(j)
if x < 32 then
decode_error(str, j, "control character in string")
end
if last == 92 then -- "\\" (escape char)
if x == 117 then -- "u" (unicode escape sequence)
local hex = str:sub(j + 1, j + 5)
if not hex:find("%x%x%x%x") then
decode_error(str, j, "invalid unicode escape in string")
end
if hex:find("^[dD][89aAbB]") then
has_surrogate_escape = true
else
has_unicode_escape = true
end
else
local c = string.char(x)
if not escape_chars[c] then
decode_error(str, j, "invalid escape char '" .. c .. "' in string")
end
has_escape = true
end
last = nil
elseif x == 34 then -- '"' (end of string)
local s = str:sub(i + 1, j - 1)
if has_surrogate_escape then
s = s:gsub("\\u[dD][89aAbB]..\\u....", parse_unicode_escape)
end
if has_unicode_escape then
s = s:gsub("\\u....", parse_unicode_escape)
end
if has_escape then
s = s:gsub("\\.", escape_char_map_inv)
end
return s, j + 1
else
last = x
end
end
decode_error(str, i, "expected closing quote for string")
end
local function parse_number(str, i)
local x = next_char(str, i, delim_chars)
local s = str:sub(i, x - 1)
local n = tonumber(s)
if not n then
decode_error(str, i, "invalid number '" .. s .. "'")
end
return n, x
end
local function parse_literal(str, i)
local x = next_char(str, i, delim_chars)
local word = str:sub(i, x - 1)
if not literals[word] then
decode_error(str, i, "invalid literal '" .. word .. "'")
end
return literal_map[word], x
end
local function parse_array(str, i)
local res = {}
local n = 1
i = i + 1
while 1 do
local x
i = next_char(str, i, space_chars, true)
-- Empty / end of array?
if str:sub(i, i) == "]" then
i = i + 1
break
end
-- Read token
x, i = parse(str, i)
res[n] = x
n = n + 1
-- Next token
i = next_char(str, i, space_chars, true)
local chr = str:sub(i, i)
i = i + 1
if chr == "]" then break end
if chr ~= "," then decode_error(str, i, "expected ']' or ','") end
end
return res, i
end
local function parse_object(str, i)
local res = {}
i = i + 1
while 1 do
local key, val
i = next_char(str, i, space_chars, true)
-- Empty / end of object?
if str:sub(i, i) == "}" then
i = i + 1
break
end
-- Read key
if str:sub(i, i) ~= '"' then
decode_error(str, i, "expected string for key")
end
key, i = parse(str, i)
-- Read ':' delimiter
i = next_char(str, i, space_chars, true)
if str:sub(i, i) ~= ":" then
decode_error(str, i, "expected ':' after key")
end
i = next_char(str, i + 1, space_chars, true)
-- Read value
val, i = parse(str, i)
-- Set
res[key] = val
-- Next token
i = next_char(str, i, space_chars, true)
local chr = str:sub(i, i)
i = i + 1
if chr == "}" then break end
if chr ~= "," then decode_error(str, i, "expected '}' or ','") end
end
return res, i
end
local char_func_map = {
[ '"' ] = parse_string,
[ "0" ] = parse_number,
[ "1" ] = parse_number,
[ "2" ] = parse_number,
[ "3" ] = parse_number,
[ "4" ] = parse_number,
[ "5" ] = parse_number,
[ "6" ] = parse_number,
[ "7" ] = parse_number,
[ "8" ] = parse_number,
[ "9" ] = parse_number,
[ "-" ] = parse_number,
[ "t" ] = parse_literal,
[ "f" ] = parse_literal,
[ "n" ] = parse_literal,
[ "[" ] = parse_array,
[ "{" ] = parse_object,
}
parse = function(str, idx)
local chr = str:sub(idx, idx)
local f = char_func_map[chr]
if f then
return f(str, idx)
end
decode_error(str, idx, "unexpected character '" .. chr .. "'")
end
function json.decode(str)
if type(str) ~= "string" then
error("expected argument of type string, got " .. type(str))
end
return ( parse(str, next_char(str, 1, space_chars, true)) )
end
return json

View File

@ -0,0 +1,238 @@
local socket = require("socket")
local json = require('json')
local math = require('math')
local STATE_OK = "Ok"
local STATE_TENTATIVELY_CONNECTED = "Tentatively Connected"
local STATE_INITIAL_CONNECTION_MADE = "Initial Connection Made"
local STATE_UNINITIALIZED = "Uninitialized"
local APIndex = 0x1A6E
local APItemAddress = 0x00FF
local EventFlagAddress = 0x1735
local MissableAddress = 0x161A
local HiddenItemsAddress = 0x16DE
local RodAddress = 0x1716
local InGame = 0x1A71
local ItemsReceived = nil
local playerName = nil
local seedName = nil
local prevstate = ""
local curstate = STATE_UNINITIALIZED
local gbSocket = nil
local frame = 0
local u8 = nil
local wU8 = nil
local u16
--Sets correct memory access functions based on whether NesHawk or QuickNES is loaded
local function defineMemoryFunctions()
local memDomain = {}
local domains = memory.getmemorydomainlist()
--if domains[1] == "System Bus" then
-- --NesHawk
-- isNesHawk = true
-- memDomain["systembus"] = function() memory.usememorydomain("System Bus") end
-- memDomain["saveram"] = function() memory.usememorydomain("Battery RAM") end
-- memDomain["rom"] = function() memory.usememorydomain("PRG ROM") end
--elseif domains[1] == "WRAM" then
-- --QuickNES
-- memDomain["systembus"] = function() memory.usememorydomain("System Bus") end
-- memDomain["saveram"] = function() memory.usememorydomain("WRAM") end
-- memDomain["rom"] = function() memory.usememorydomain("PRG ROM") end
--end
memDomain["rom"] = function() memory.usememorydomain("ROM") end
memDomain["wram"] = function() memory.usememorydomain("WRAM") end
return memDomain
end
local memDomain = defineMemoryFunctions()
u8 = memory.read_u8
wU8 = memory.write_u8
u16 = memory.read_u16_le
function uRange(address, bytes)
data = memory.readbyterange(address - 1, bytes + 1)
data[0] = nil
return data
end
function table.empty (self)
for _, _ in pairs(self) do
return false
end
return true
end
function slice (tbl, s, e)
local pos, new = 1, {}
for i = s + 1, e do
new[pos] = tbl[i]
pos = pos + 1
end
return new
end
function processBlock(block)
if block == nil then
return
end
local itemsBlock = block["items"]
memDomain.wram()
if itemsBlock ~= nil then-- and u8(0x116B) ~= 0x00 then
-- print(itemsBlock)
ItemsReceived = itemsBlock
end
end
function difference(a, b)
local aa = {}
for k,v in pairs(a) do aa[v]=true end
for k,v in pairs(b) do aa[v]=nil end
local ret = {}
local n = 0
for k,v in pairs(a) do
if aa[v] then n=n+1 ret[n]=v end
end
return ret
end
function generateLocationsChecked()
memDomain.wram()
events = uRange(EventFlagAddress, 0x140)
missables = uRange(MissableAddress, 0x20)
hiddenitems = uRange(HiddenItemsAddress, 0x0E)
rod = u8(RodAddress)
data = {}
table.foreach(events, function(k, v) table.insert(data, v) end)
table.foreach(missables, function(k, v) table.insert(data, v) end)
table.foreach(hiddenitems, function(k, v) table.insert(data, v) end)
table.insert(data, rod)
return data
end
function generateSerialData()
memDomain.wram()
status = u8(0x1A73)
if status == 0 then
return nil
end
return uRange(0x1A76, u8(0x1A74))
end
local function arrayEqual(a1, a2)
if #a1 ~= #a2 then
return false
end
for i, v in ipairs(a1) do
if v ~= a2[i] then
return false
end
end
return true
end
function receive()
l, e = gbSocket:receive()
if e == 'closed' then
if curstate == STATE_OK then
print("Connection closed")
end
curstate = STATE_UNINITIALIZED
return
elseif e == 'timeout' then
--print("timeout") -- this keeps happening for some reason? just hide it
return
elseif e ~= nil then
print(e)
curstate = STATE_UNINITIALIZED
return
end
if l ~= nil then
processBlock(json.decode(l))
end
-- Determine Message to send back
memDomain.rom()
newPlayerName = uRange(0xFFF0, 0x10)
newSeedName = uRange(0xFFDC, 20)
if (playerName ~= nil and not arrayEqual(playerName, newPlayerName)) or (seedName ~= nil and not arrayEqual(seedName, newSeedName)) then
print("ROM changed, quitting")
curstate = STATE_UNINITIALIZED
return
end
playerName = newPlayerName
seedName = newSeedName
local retTable = {}
retTable["playerName"] = playerName
retTable["seedName"] = seedName
memDomain.wram()
if u8(InGame) == 0xAC then
retTable["locations"] = generateLocationsChecked()
serialData = generateSerialData()
if serialData ~= nil then
retTable["serial"] = serialData
end
end
msg = json.encode(retTable).."\n"
local ret, error = gbSocket:send(msg)
if ret == nil then
print(error)
elseif curstate == STATE_INITIAL_CONNECTION_MADE then
curstate = STATE_TENTATIVELY_CONNECTED
elseif curstate == STATE_TENTATIVELY_CONNECTED then
print("Connected!")
curstate = STATE_OK
end
end
function main()
if (is23Or24Or25 or is26To28) == false then
print("Must use a version of bizhawk 2.3.1 or higher")
return
end
server, error = socket.bind('localhost', 17242)
while true do
if not (curstate == prevstate) then
print("Current state: "..curstate)
prevstate = curstate
end
if (curstate == STATE_OK) or (curstate == STATE_INITIAL_CONNECTION_MADE) or (curstate == STATE_TENTATIVELY_CONNECTED) then
if (frame % 60 == 0) then
receive()
if u8(InGame) == 0xAC then
ItemIndex = u16(APIndex)
if ItemsReceived[ItemIndex + 1] ~= nil then
wU8(APItemAddress, ItemsReceived[ItemIndex + 1] - 172000000)
end
end
end
elseif (curstate == STATE_UNINITIALIZED) then
if (frame % 60 == 0) then
print("Waiting for client.")
emu.frameadvance()
server:settimeout(2)
print("Attempting to connect")
local client, timeout = server:accept()
if timeout == nil then
-- print('Initial Connection Made')
curstate = STATE_INITIAL_CONNECTION_MADE
gbSocket = client
gbSocket:settimeout(0)
end
end
end
emu.frameadvance()
end
end
main()

132
data/lua/PKMN_RB/socket.lua Normal file
View File

@ -0,0 +1,132 @@
-----------------------------------------------------------------------------
-- LuaSocket helper module
-- Author: Diego Nehab
-- RCS ID: $Id: socket.lua,v 1.22 2005/11/22 08:33:29 diego Exp $
-----------------------------------------------------------------------------
-----------------------------------------------------------------------------
-- Declare module and import dependencies
-----------------------------------------------------------------------------
local base = _G
local string = require("string")
local math = require("math")
local socket = require("socket.core")
module("socket")
-----------------------------------------------------------------------------
-- Exported auxiliar functions
-----------------------------------------------------------------------------
function connect(address, port, laddress, lport)
local sock, err = socket.tcp()
if not sock then return nil, err end
if laddress then
local res, err = sock:bind(laddress, lport, -1)
if not res then return nil, err end
end
local res, err = sock:connect(address, port)
if not res then return nil, err end
return sock
end
function bind(host, port, backlog)
local sock, err = socket.tcp()
if not sock then return nil, err end
sock:setoption("reuseaddr", true)
local res, err = sock:bind(host, port)
if not res then return nil, err end
res, err = sock:listen(backlog)
if not res then return nil, err end
return sock
end
try = newtry()
function choose(table)
return function(name, opt1, opt2)
if base.type(name) ~= "string" then
name, opt1, opt2 = "default", name, opt1
end
local f = table[name or "nil"]
if not f then base.error("unknown key (".. base.tostring(name) ..")", 3)
else return f(opt1, opt2) end
end
end
-----------------------------------------------------------------------------
-- Socket sources and sinks, conforming to LTN12
-----------------------------------------------------------------------------
-- create namespaces inside LuaSocket namespace
sourcet = {}
sinkt = {}
BLOCKSIZE = 2048
sinkt["close-when-done"] = function(sock)
return base.setmetatable({
getfd = function() return sock:getfd() end,
dirty = function() return sock:dirty() end
}, {
__call = function(self, chunk, err)
if not chunk then
sock:close()
return 1
else return sock:send(chunk) end
end
})
end
sinkt["keep-open"] = function(sock)
return base.setmetatable({
getfd = function() return sock:getfd() end,
dirty = function() return sock:dirty() end
}, {
__call = function(self, chunk, err)
if chunk then return sock:send(chunk)
else return 1 end
end
})
end
sinkt["default"] = sinkt["keep-open"]
sink = choose(sinkt)
sourcet["by-length"] = function(sock, length)
return base.setmetatable({
getfd = function() return sock:getfd() end,
dirty = function() return sock:dirty() end
}, {
__call = function()
if length <= 0 then return nil end
local size = math.min(socket.BLOCKSIZE, length)
local chunk, err = sock:receive(size)
if err then return nil, err end
length = length - string.len(chunk)
return chunk
end
})
end
sourcet["until-closed"] = function(sock)
local done
return base.setmetatable({
getfd = function() return sock:getfd() end,
dirty = function() return sock:dirty() end
}, {
__call = function()
if done then return nil end
local chunk, err, partial = sock:receive(socket.BLOCKSIZE)
if not err then return chunk
elseif err == "closed" then
sock:close()
done = 1
return partial
else return nil, err end
end
})
end
sourcet["default"] = sourcet["until-closed"]
source = choose(sourcet)

View File

@ -138,6 +138,14 @@ dkc3_options:
# True for operating system default program
# Alternatively, a path to a program to open the .sfc file with
rom_start: true
pokemon_rb_options:
# File names of the Pokemon Red and Blue roms
red_rom_file: "Pokemon Red (UE) [S][!].gb"
blue_rom_file: "Pokemon Blue (UE) [S][!].gb"
# Set this to false to never autostart a rom (such as after patching)
# True for operating system default program
# Alternatively, a path to a program to open the .gb file with
rom_start: true
smw_options:
# File name of the SMW US rom
rom_file: "Super Mario World (USA).sfc"

View File

@ -59,6 +59,8 @@ Name: "generator/smw"; Description: "Super Mario World ROM Setup"; Types: ful
Name: "generator/soe"; Description: "Secret of Evermore ROM Setup"; Types: full hosting; ExtraDiskSpaceRequired: 3145728; Flags: disablenouninstallwarning
Name: "generator/lttp"; Description: "A Link to the Past ROM Setup and Enemizer"; Types: full hosting; ExtraDiskSpaceRequired: 5191680
Name: "generator/oot"; Description: "Ocarina of Time ROM Setup"; Types: full hosting; ExtraDiskSpaceRequired: 100663296; Flags: disablenouninstallwarning
Name: "generator/pkmn_r"; Description: "Pokemon Red ROM Setup"; Types: full hosting
Name: "generator/pkmn_b"; Description: "Pokemon Blue ROM Setup"; Types: full hosting
Name: "server"; Description: "Server"; Types: full hosting
Name: "client"; Description: "Clients"; Types: full playing
Name: "client/sni"; Description: "SNI Client"; Types: full playing
@ -70,6 +72,9 @@ Name: "client/factorio"; Description: "Factorio"; Types: full playing
Name: "client/minecraft"; Description: "Minecraft"; Types: full playing; ExtraDiskSpaceRequired: 226894278
Name: "client/oot"; Description: "Ocarina of Time"; Types: full playing
Name: "client/ff1"; Description: "Final Fantasy 1"; Types: full playing
Name: "client/pkmn"; Description: "Pokemon Client"
Name: "client/pkmn/red"; Description: "Pokemon Client - Pokemon Red Setup"; Types: full playing; ExtraDiskSpaceRequired: 1048576
Name: "client/pkmn/blue"; Description: "Pokemon Client - Pokemon Blue Setup"; Types: full playing; ExtraDiskSpaceRequired: 1048576
Name: "client/cf"; Description: "ChecksFinder"; Types: full playing
Name: "client/sc2"; Description: "Starcraft 2"; Types: full playing
Name: "client/text"; Description: "Text, to !command and chat"; Types: full playing
@ -84,6 +89,8 @@ Source: "{code:GetDKC3ROMPath}"; DestDir: "{app}"; DestName: "Donkey Kong Countr
Source: "{code:GetSMWROMPath}"; DestDir: "{app}"; DestName: "Super Mario World (USA).sfc"; Flags: external; Components: client/sni/smw or generator/smw
Source: "{code:GetSoEROMPath}"; DestDir: "{app}"; DestName: "Secret of Evermore (USA).sfc"; Flags: external; Components: generator/soe
Source: "{code:GetOoTROMPath}"; DestDir: "{app}"; DestName: "The Legend of Zelda - Ocarina of Time.z64"; Flags: external; Components: client/oot or generator/oot
Source: "{code:GetRedROMPath}"; DestDir: "{app}"; DestName: "Pokemon Red (UE) [S][!].gb"; Flags: external; Components: client/pkmn/red or generator/pkmn_r
Source: "{code:GetBlueROMPath}"; DestDir: "{app}"; DestName: "Pokemon Blue (UE) [S][!].gb"; Flags: external; Components: client/pkmn/blue or generator/pkmn_b
Source: "{#source_path}\*"; Excludes: "*.sfc, *.log, data\sprites\alttpr, SNI, EnemizerCLI, Archipelago*.exe"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
Source: "{#source_path}\SNI\*"; Excludes: "*.sfc, *.log"; DestDir: "{app}\SNI"; Flags: ignoreversion recursesubdirs createallsubdirs; Components: client/sni
Source: "{#source_path}\EnemizerCLI\*"; Excludes: "*.sfc, *.log"; DestDir: "{app}\EnemizerCLI"; Flags: ignoreversion recursesubdirs createallsubdirs; Components: generator/lttp
@ -98,6 +105,7 @@ Source: "{#source_path}\ArchipelagoMinecraftClient.exe"; DestDir: "{app}"; Flags
Source: "{#source_path}\ArchipelagoOoTClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/oot
Source: "{#source_path}\ArchipelagoOoTAdjuster.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/oot
Source: "{#source_path}\ArchipelagoFF1Client.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/ff1
Source: "{#source_path}\ArchipelagoPokemonClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/pkmn
Source: "{#source_path}\ArchipelagoChecksFinderClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/cf
Source: "{#source_path}\ArchipelagoStarcraft2Client.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/sc2
Source: "vc_redist.x64.exe"; DestDir: {tmp}; Flags: deleteafterinstall
@ -111,6 +119,7 @@ Name: "{group}\{#MyAppName} Factorio Client"; Filename: "{app}\ArchipelagoFactor
Name: "{group}\{#MyAppName} Minecraft Client"; Filename: "{app}\ArchipelagoMinecraftClient.exe"; Components: client/minecraft
Name: "{group}\{#MyAppName} Ocarina of Time Client"; Filename: "{app}\ArchipelagoOoTClient.exe"; Components: client/oot
Name: "{group}\{#MyAppName} Final Fantasy 1 Client"; Filename: "{app}\ArchipelagoFF1Client.exe"; Components: client/ff1
Name: "{group}\{#MyAppName} Pokemon Client"; Filename: "{app}\ArchipelagoPokemonClient.exe"; Components: client/pkmn
Name: "{group}\{#MyAppName} ChecksFinder Client"; Filename: "{app}\ArchipelagoChecksFinderClient.exe"; Components: client/cf
Name: "{group}\{#MyAppName} Starcraft 2 Client"; Filename: "{app}\ArchipelagoStarcraft2Client.exe"; Components: client/sc2
@ -121,6 +130,7 @@ Name: "{commondesktop}\{#MyAppName} Factorio Client"; Filename: "{app}\Archipela
Name: "{commondesktop}\{#MyAppName} Minecraft Client"; Filename: "{app}\ArchipelagoMinecraftClient.exe"; Tasks: desktopicon; Components: client/minecraft
Name: "{commondesktop}\{#MyAppName} Ocarina of Time Client"; Filename: "{app}\ArchipelagoOoTClient.exe"; Tasks: desktopicon; Components: client/oot
Name: "{commondesktop}\{#MyAppName} Final Fantasy 1 Client"; Filename: "{app}\ArchipelagoFF1Client.exe"; Tasks: desktopicon; Components: client/ff1
Name: "{commondesktop}\{#MyAppName} Pokemon Client"; Filename: "{app}\ArchipelagoPokemonClient.exe"; Tasks: desktopicon; Components: client/pkmn
Name: "{commondesktop}\{#MyAppName} ChecksFinder Client"; Filename: "{app}\ArchipelagoChecksFinderClient.exe"; Tasks: desktopicon; Components: client/cf
Name: "{commondesktop}\{#MyAppName} Starcraft 2 Client"; Filename: "{app}\ArchipelagoStarcraft2Client.exe"; Tasks: desktopicon; Components: client/sc2
@ -179,6 +189,16 @@ Root: HKCR; Subkey: "{#MyAppName}n64zpf"; ValueData: "Archip
Root: HKCR; Subkey: "{#MyAppName}n64zpf\DefaultIcon"; ValueData: "{app}\ArchipelagoOoTClient.exe,0"; ValueType: string; ValueName: ""; Components: client/oot
Root: HKCR; Subkey: "{#MyAppName}n64zpf\shell\open\command"; ValueData: """{app}\ArchipelagoOoTClient.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/oot
Root: HKCR; Subkey: ".apred"; ValueData: "{#MyAppName}pkmnrpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/pkmn/red
Root: HKCR; Subkey: "{#MyAppName}pkmnrpatch"; ValueData: "Archipelago Pokemon Red Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/pkmn/red
Root: HKCR; Subkey: "{#MyAppName}pkmnrpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoPokemonClient.exe,0"; ValueType: string; ValueName: ""; Components: client/pkmn/red
Root: HKCR; Subkey: "{#MyAppName}pkmnrpatch\shell\open\command"; ValueData: """{app}\ArchipelagoPokemonClient.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/pkmn/red
Root: HKCR; Subkey: ".apblue"; ValueData: "{#MyAppName}pkmnbpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/pkmn/blue
Root: HKCR; Subkey: "{#MyAppName}pkmnbpatch"; ValueData: "Archipelago Pokemon Blue Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/pkmn/blue
Root: HKCR; Subkey: "{#MyAppName}pkmnbpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoPokemonClient.exe,0"; ValueType: string; ValueName: ""; Components: client/pkmn/blue
Root: HKCR; Subkey: "{#MyAppName}pkmnbpatch\shell\open\command"; ValueData: """{app}\ArchipelagoPokemonClient.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/pkmn/blue
Root: HKCR; Subkey: ".archipelago"; ValueData: "{#MyAppName}multidata"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: server
Root: HKCR; Subkey: "{#MyAppName}multidata"; ValueData: "Archipelago Server Data"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: server
Root: HKCR; Subkey: "{#MyAppName}multidata\DefaultIcon"; ValueData: "{app}\ArchipelagoServer.exe,0"; ValueType: string; ValueName: ""; Components: server
@ -234,6 +254,12 @@ var SoERomFilePage: TInputFileWizardPage;
var ootrom: string;
var OoTROMFilePage: TInputFileWizardPage;
var redrom: string;
var RedROMFilePage: TInputFileWizardPage;
var bluerom: string;
var BlueROMFilePage: TInputFileWizardPage;
function GetSNESMD5OfFile(const rom: string): string;
var data: AnsiString;
begin
@ -281,6 +307,21 @@ begin
'.sfc');
end;
function AddGBRomPage(name: string): TInputFileWizardPage;
begin
Result :=
CreateInputFilePage(
wpSelectComponents,
'Select ROM File',
'Where is your ' + name + ' located?',
'Select the file, then click Next.');
Result.Add(
'Location of ROM file:',
'GB ROM files|*.gb;*.gbc|All files|*.*',
'.gb');
end;
procedure AddOoTRomPage();
begin
ootrom := FileSearch('The Legend of Zelda - Ocarina of Time.z64', WizardDirValue());
@ -425,6 +466,38 @@ begin
Result := '';
end;
function GetRedROMPath(Param: string): string;
begin
if Length(redrom) > 0 then
Result := redrom
else if Assigned(RedRomFilePage) then
begin
R := CompareStr(GetMD5OfFile(RedROMFilePage.Values[0]), '3d45c1ee9abd5738df46d2bdda8b57dc')
if R <> 0 then
MsgBox('Pokemon Red ROM validation failed. Very likely wrong file.', mbInformation, MB_OK);
Result := RedROMFilePage.Values[0]
end
else
Result := '';
end;
function GetBlueROMPath(Param: string): string;
begin
if Length(bluerom) > 0 then
Result := bluerom
else if Assigned(BlueRomFilePage) then
begin
R := CompareStr(GetMD5OfFile(BlueROMFilePage.Values[0]), '50927e843568814f7ed45ec4f944bd8b')
if R <> 0 then
MsgBox('Pokemon Blue ROM validation failed. Very likely wrong file.', mbInformation, MB_OK);
Result := BlueROMFilePage.Values[0]
end
else
Result := '';
end;
procedure InitializeWizard();
begin
AddOoTRomPage();
@ -448,6 +521,14 @@ begin
soerom := CheckRom('Secret of Evermore (USA).sfc', '6e9c94511d04fac6e0a1e582c170be3a');
if Length(soerom) = 0 then
SoEROMFilePage:= AddRomPage('Secret of Evermore (USA).sfc');
redrom := CheckRom('Pokemon Red (UE) [S][!].gb','3d45c1ee9abd5738df46d2bdda8b57dc');
if Length(redrom) = 0 then
RedROMFilePage:= AddGBRomPage('Pokemon Red (UE) [S][!].gb');
bluerom := CheckRom('Pokemon Blue (UE) [S][!].gb','50927e843568814f7ed45ec4f944bd8b');
if Length(redrom) = 0 then
BlueROMFilePage:= AddGBRomPage('Pokemon Blue (UE) [S][!].gb');
end;
@ -466,4 +547,8 @@ begin
Result := not (WizardIsComponentSelected('generator/soe'));
if (assigned(OoTROMFilePage)) and (PageID = OoTROMFilePage.ID) then
Result := not (WizardIsComponentSelected('generator/oot') or WizardIsComponentSelected('client/oot'));
end;
if (assigned(RedROMFilePage)) and (PageID = RedROMFilePage.ID) then
Result := not (WizardIsComponentSelected('generator/pkmn_r') or WizardIsComponentSelected('client/pkmn/red'));
if (assigned(BlueROMFilePage)) and (PageID = BlueROMFilePage.ID) then
Result := not (WizardIsComponentSelected('generator/pkmn_b') or WizardIsComponentSelected('client/pkmn/blue'));
end;

View File

@ -99,7 +99,7 @@ class APContainer:
"player_name": self.player_name,
"game": self.game,
# minimum version of patch system expected for patching to be successful
"compatible_version": 4,
"compatible_version": 5,
"version": current_patch_version,
}

21
worlds/pokemon_rb/LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 Alex "Alchav" Avery
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,254 @@
from typing import TextIO
import os
import logging
from BaseClasses import Item, MultiWorld, Tutorial, ItemClassification
from Fill import fill_restrictive, FillError, sweep_from_pool
from ..AutoWorld import World, WebWorld
from ..generic.Rules import add_item_rule
from .items import item_table, item_groups
from .locations import location_data, PokemonRBLocation
from .regions import create_regions
from .logic import PokemonLogic
from .options import pokemon_rb_options
from .rom_addresses import rom_addresses
from .text import encode_text
from .rom import generate_output, get_base_rom_bytes, get_base_rom_path, process_pokemon_data, process_wild_pokemon,\
process_static_pokemon
from .rules import set_rules
import worlds.pokemon_rb.poke_data as poke_data
class PokemonWebWorld(WebWorld):
tutorials = [Tutorial(
"Multiworld Setup Guide",
"A guide to playing Pokemon Red and Blue with Archipelago.",
"English",
"setup_en.md",
"setup/en",
["Alchav"]
)]
class PokemonRedBlueWorld(World):
"""Pokémon Red and Pokémon Blue are the original monster-collecting turn-based RPGs. Explore the Kanto region with
your Pokémon, catch more than 150 unique creatures, earn badges from the region's Gym Leaders, and challenge the
Elite Four to become the champion!"""
# -MuffinJets#4559
game = "Pokemon Red and Blue"
option_definitions = pokemon_rb_options
remote_items = False
data_version = 1
topology_present = False
item_name_to_id = {name: data.id for name, data in item_table.items()}
location_name_to_id = {location.name: location.address for location in location_data if location.type == "Item"}
item_name_groups = item_groups
web = PokemonWebWorld()
def __init__(self, world: MultiWorld, player: int):
super().__init__(world, player)
self.fly_map = None
self.fly_map_code = None
self.extra_badges = {}
self.type_chart = None
self.local_poke_data = None
self.learnsets = None
self.trainer_name = None
self.rival_name = None
@classmethod
def stage_assert_generate(cls, world):
versions = set()
for player in world.player_ids:
if world.worlds[player].game == "Pokemon Red and Blue":
versions.add(world.game_version[player].current_key)
for version in versions:
if not os.path.exists(get_base_rom_path(version)):
raise FileNotFoundError(get_base_rom_path(version))
def generate_early(self):
def encode_name(name, t):
try:
if len(encode_text(name)) > 7:
raise IndexError(f"{t} name too long for player {self.world.player_name[self.player]}. Must be 7 characters or fewer.")
return encode_text(name, length=8, whitespace="@", safety=True)
except KeyError as e:
raise KeyError(f"Invalid character(s) in {t} name for player {self.world.player_name[self.player]}") from e
self.trainer_name = encode_name(self.world.trainer_name[self.player].value, "Player")
self.rival_name = encode_name(self.world.rival_name[self.player].value, "Rival")
if self.world.badges_needed_for_hm_moves[self.player].value >= 2:
badges_to_add = ["Marsh Badge", "Volcano Badge", "Earth Badge"]
if self.world.badges_needed_for_hm_moves[self.player].value == 3:
badges = ["Boulder Badge", "Cascade Badge", "Thunder Badge", "Rainbow Badge", "Marsh Badge",
"Soul Badge", "Volcano Badge", "Earth Badge"]
self.world.random.shuffle(badges)
badges_to_add += [badges.pop(), badges.pop()]
hm_moves = ["Cut", "Fly", "Surf", "Strength", "Flash"]
self.world.random.shuffle(hm_moves)
self.extra_badges = {}
for badge in badges_to_add:
self.extra_badges[hm_moves.pop()] = badge
process_pokemon_data(self)
def create_items(self) -> None:
locations = [location for location in location_data if location.type == "Item"]
item_pool = []
for location in locations:
if "Hidden" in location.name and not self.world.randomize_hidden_items[self.player].value:
continue
if "Rock Tunnel B1F" in location.region and not self.world.extra_key_items[self.player].value:
continue
if location.name == "Celadon City - Mansion Lady" and not self.world.tea[self.player].value:
continue
item = self.create_item(location.original_item)
if location.event:
self.world.get_location(location.name, self.player).place_locked_item(item)
elif ("Badge" not in item.name or self.world.badgesanity[self.player].value) and \
(item.name != "Oak's Parcel" or self.world.old_man[self.player].value != 1):
item_pool.append(item)
self.world.random.shuffle(item_pool)
self.world.itempool += item_pool
def pre_fill(self):
process_wild_pokemon(self)
process_static_pokemon(self)
if self.world.old_man[self.player].value == 1:
item = self.create_item("Oak's Parcel")
locations = []
for location in self.world.get_locations():
if location.player == self.player and location.item is None and location.can_reach(self.world.state) \
and location.item_rule(item):
locations.append(location)
self.world.random.choice(locations).place_locked_item(item)
if not self.world.badgesanity[self.player].value:
self.world.non_local_items[self.player].value -= self.item_name_groups["Badges"]
for i in range(5):
try:
badges = []
badgelocs = []
for badge in ["Boulder Badge", "Cascade Badge", "Thunder Badge", "Rainbow Badge", "Soul Badge",
"Marsh Badge", "Volcano Badge", "Earth Badge"]:
badges.append(self.create_item(badge))
for loc in ["Pewter Gym - Brock 1", "Cerulean Gym - Misty 1", "Vermilion Gym - Lt. Surge 1",
"Celadon Gym - Erika 1", "Fuchsia Gym - Koga 1", "Saffron Gym - Sabrina 1",
"Cinnabar Gym - Blaine 1", "Viridian Gym - Giovanni 1"]:
badgelocs.append(self.world.get_location(loc, self.player))
state = self.world.get_all_state(False)
self.world.random.shuffle(badges)
self.world.random.shuffle(badgelocs)
fill_restrictive(self.world, state, badgelocs.copy(), badges, True, True)
except FillError:
for location in badgelocs:
location.item = None
continue
break
else:
raise FillError(f"Failed to place badges for player {self.player}")
locs = [self.world.get_location("Fossil - Choice A", self.player),
self.world.get_location("Fossil - Choice B", self.player)]
for loc in locs:
add_item_rule(loc, lambda i: i.advancement or i.name in self.item_name_groups["Unique"]
or i.name == "Master Ball")
loc = self.world.get_location("Pallet Town - Player's PC", self.player)
if loc.item is None:
locs.append(loc)
for loc in locs:
unplaced_items = []
if loc.name in self.world.priority_locations[self.player].value:
add_item_rule(loc, lambda i: i.advancement)
for item in self.world.itempool:
if item.player == self.player and loc.item_rule(item):
self.world.itempool.remove(item)
state = sweep_from_pool(self.world.state, self.world.itempool + unplaced_items)
if state.can_reach(loc, "Location", self.player):
loc.place_locked_item(item)
break
else:
unplaced_items.append(item)
self.world.itempool += unplaced_items
intervene = False
test_state = self.world.get_all_state(False)
if not test_state.pokemon_rb_can_surf(self.player) or not test_state.pokemon_rb_can_strength(self.player):
intervene = True
elif self.world.accessibility[self.player].current_key != "minimal":
if not test_state.pokemon_rb_can_cut(self.player) or not test_state.pokemon_rb_can_flash(self.player):
intervene = True
if intervene:
# the way this is handled will be improved significantly in the future when I add options to
# let you choose the exact weights for HM compatibility
logging.warning(
f"HM-compatible Pokémon possibly missing, placing Mew on Route 1 for player {self.player}")
loc = self.world.get_location("Route 1 - Wild Pokemon - 1", self.player)
loc.item = self.create_item("Mew")
def create_regions(self):
if self.world.free_fly_location[self.player].value:
fly_map_code = self.world.random.randint(5, 9)
if fly_map_code == 9:
fly_map_code = 10
if fly_map_code == 5:
fly_map_code = 4
else:
fly_map_code = 0
self.fly_map = ["Pallet Town", "Viridian City", "Pewter City", "Cerulean City", "Lavender Town",
"Vermilion City", "Celadon City", "Fuchsia City", "Cinnabar Island", "Indigo Plateau",
"Saffron City"][fly_map_code]
self.fly_map_code = fly_map_code
create_regions(self.world, self.player)
self.world.completion_condition[self.player] = lambda state, player=self.player: state.has("Become Champion", player=player)
def set_rules(self):
set_rules(self.world, self.player)
def create_item(self, name: str) -> Item:
return PokemonRBItem(name, self.player)
def generate_output(self, output_directory: str):
generate_output(self, output_directory)
def write_spoiler_header(self, spoiler_handle: TextIO):
if self.world.free_fly_location[self.player].value:
spoiler_handle.write('Fly unlocks: %s\n' % self.fly_map)
if self.extra_badges:
for hm_move, badge in self.extra_badges.items():
spoiler_handle.write(hm_move + " enabled by: " + (" " * 20)[:20 - len(hm_move)] + badge + "\n")
def write_spoiler(self, spoiler_handle):
if self.world.randomize_type_matchup_types[self.player].value or \
self.world.randomize_type_matchup_type_effectiveness[self.player].value:
spoiler_handle.write(f"\n\nType matchups ({self.world.player_name[self.player]}):\n\n")
for matchup in self.type_chart:
spoiler_handle.write(f"{matchup[0]} deals {matchup[2] * 10}% damage to {matchup[1]}\n")
def get_filler_item_name(self) -> str:
return self.world.random.choice([item for item in item_table if item_table[item].classification in
[ItemClassification.filler, ItemClassification.trap]])
class PokemonRBItem(Item):
game = "Pokemon Red and Blue"
type = None
def __init__(self, name, player: int = None):
item_data = item_table[name]
super(PokemonRBItem, self).__init__(
name,
item_data.classification,
item_data.id, player
)

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,55 @@
# Pokémon Red and Blue
## Where is the settings page?
The [player settings page for this game](../player-settings) contains all the options you need to configure and export a
config file.
## What does randomization do to this game?
Items which the player would normally acquire throughout the game have been moved around. Logic remains, so the game is
always able to be completed, but because of the item shuffle the player may need to access certain areas before they
would in the vanilla game.
A great many things besides item placement can be randomized, such as the location of Pokémon, their stats, types, etc., depending on your yaml settings.
Many baseline changes are made to the game, including:
* Bag item space increased to 128 slots (up from 20)
* PC item storage increased to 64 slots (up from 50)
* You can hold B to run (or bike extra fast!).
* You can hold select while talking to a trainer to re-battle them.
* You can return to route 2 from Diglett's Cave without the use of Cut.
* Mew can be encountered at the S.S. Anne dock truck. This can be randomized depending on your settings.
* The S.S. Anne will never depart.
* Seafoam Islands entrances are swapped. This means you need Strength to travel through from Cinnabar Island to Fuchsia
City
* After obtaining one of the fossil item checks in Mt Moon, the remaining item can be received from the Cinnabar Lab
fossil scientist. This may require reviving a number of fossils, depending on your settings.
* Obedience depends on the total number of badges you have obtained instead of depending on specific badges.
* Pokémon that evolve by trading can also evolve by reaching level 35.
* Evolution stones are reusable.
* Much of the dialogue throughout the game has been removed or shortened.
* If the Old Man is blocking your way through Viridian City, you do not have Oak's Parcel in your inventory, and you've
exhausted your money and Poké Balls, you can get a free Poké Ball from your mom.
## What items and locations get shuffled?
All items that go into your bags given by NPCs or found on the ground, as well as gym badges.
Optionally, hidden items (those located with the Item Finder) can be shuffled as well.
## Which items can be in another player's world?
Any of the items which can be shuffled may also be placed into another player's world.
By default, gym badges are shuffled across only the 8 gyms, but you can turn on Badgesanity in your yaml to shuffle them
into the general item pool.
## What does another world's item look like in Pokémon Red and Blue?
All items for other games will display simply as "AP ITEM," including those for other Pokémon Red and Blue games.
## When the player receives an item, what happens?
A "received item" sound effect will play. Currently, there is no in-game message informing you of what the item is.
If you are in battle, have menus or text boxes opened, or scripted events are occurring, the items will not be given to
you until these have ended.

View File

@ -0,0 +1,84 @@
# Setup Guide for Pokémon Red and Blue: Archipelago
## Important
As we are using Bizhawk, this guide is only applicable to Windows and Linux systems.
## Required Software
- Bizhawk: [Bizhawk Releases from TASVideos](https://tasvideos.org/BizHawk/ReleaseHistory)
- Version 2.3.1 and later are supported. Version 2.7 is recommended for stability.
- Detailed installation instructions for Bizhawk can be found at the above link.
- Windows users must run the prereq installer first, which can also be found at the above link.
- The built-in Archipelago client, which can be installed [here](https://github.com/ArchipelagoMW/Archipelago/releases)
(select `Pokemon Client` during installation).
- Pokémon Red and/or Blue ROM files. The Archipelago community cannot provide these.
## Configuring Bizhawk
Once Bizhawk has been installed, open Bizhawk and change the following settings:
- Under Config > Customize > Advanced, make sure the box for AutoSaveRAM is checked, and click the 5s button.
This reduces the possibility of losing save data in emulator crashes.
- Under Config > Customize, check the "Run in background" and "Accept background input" boxes. This will allow you to
continue playing in the background, even if another window is selected.
It is strongly recommended to associate GB rom extensions (\*.gb) to the Bizhawk we've just installed.
To do so, we simply have to search any Gameboy rom we happened to own, right click and select "Open with...", unfold
the list that appears and select the bottom option "Look for another application", then browse to the Bizhawk folder
and select EmuHawk.exe.
## Configuring your YAML file
### What is a YAML file and why do I need one?
Your YAML file contains a set of configuration options which provide the generator with information about how it should
generate your game. Each player of a multiworld will provide their own YAML file. This setup allows each player to enjoy
an experience customized for their taste, and different players in the same multiworld can all have different options.
### Where do I get a YAML file?
You can generate a yaml or download a template by visiting the [Pokemon Red and Blue Player Settings Page](/games/Pokemon Red and Blue/player-settings)
It is important to note that the `game_version` option determines the ROM file that will be patched.
Both the player and the person generating (if they are generating locally) will need the corresponding ROM file.
For `trainer_name` and `rival_name` the following regular characters are allowed:
* `‘’“”·… ABCDEFGHIJKLMNOPQRSTUVWXYZ():;[]abcdefghijklmnopqrstuvwxyzé'-?!.♂$×/,♀0123456789`
And the following special characters (these each take up one character):
* `<'d>`
* `<'l>`
* `<'t>`
* `<'v>`
* `<'r>`
* `<'m>`
* `<PK>`
* `<MN>`
* `<MALE>` alias for `♂`
* `<FEMALE>` alias for `♀`
## Joining a MultiWorld Game
### Obtain your Pokémon patch file
When you join a multiworld game, you will be asked to provide your YAML file to whoever is hosting. Once that is done,
the host will provide you with either a link to download your data file, or with a zip file containing everyone's data
files. Your data file should have a `.apred` or `.apblue` extension.
Double-click on your patch file to start your client and start the ROM patch process. Once the process is finished
(this can take a while), the client and the emulator will be started automatically (if you associated the extension
to the emulator as recommended).
### Connect to the Multiserver
Once both the client and the emulator are started, you must connect them. Within the emulator click on the "Tools"
menu and select "Lua Console". Click the folder button or press Ctrl+O to open a Lua script.
Navigate to your Archipelago install folder and open `data/lua/PKMN_RB/pkmr_rb.lua`.
To connect the client to the multiserver simply put `<address>:<port>` on the textfield on top and press enter (if the
server uses password, type in the bottom textfield `/connect <address>:<port> [password]`)
Now you are ready to start your adventure in Kanto.

176
worlds/pokemon_rb/items.py Normal file
View File

@ -0,0 +1,176 @@
from BaseClasses import ItemClassification
from .poke_data import pokemon_data
class ItemData:
def __init__(self, id, classification, groups):
self.groups = groups
self.classification = classification
self.id = None if id is None else id + 172000000
item_table = {
"Master Ball": ItemData(1, ItemClassification.useful, ["Consumables", "Poke Balls"]),
"Ultra Ball": ItemData(2, ItemClassification.filler, ["Consumables", "Poke Balls"]),
"Great Ball": ItemData(3, ItemClassification.filler, ["Consumables", "Poke Balls"]),
"Poke Ball": ItemData(4, ItemClassification.filler, ["Consumables", "Poke Balls"]),
"Town Map": ItemData(5, ItemClassification.progression_skip_balancing, ["Unique", "Key Items"]),
"Bicycle": ItemData(6, ItemClassification.progression, ["Unique", "Key Items"]),
# "Flippers": ItemData(7, ItemClassification.progression),
#"Safari Ball": ItemData(8, ItemClassification.filler),
#"Pokedex": ItemData(9, ItemClassification.filler),
"Moon Stone": ItemData(10, ItemClassification.useful, ["Unique", "Evolution Stones"]),
"Antidote": ItemData(11, ItemClassification.filler, ["Consumables"]),
"Burn Heal": ItemData(12, ItemClassification.filler, ["Consumables"]),
"Ice Heal": ItemData(13, ItemClassification.filler, ["Consumables"]),
"Awakening": ItemData(14, ItemClassification.filler, ["Consumables"]),
"Paralyze Heal": ItemData(15, ItemClassification.filler, ["Consumables"]),
"Full Restore": ItemData(16, ItemClassification.filler, ["Consumables"]),
"Max Potion": ItemData(17, ItemClassification.filler, ["Consumables"]),
"Hyper Potion": ItemData(18, ItemClassification.filler, ["Consumables"]),
"Super Potion": ItemData(19, ItemClassification.filler, ["Consumables"]),
"Potion": ItemData(20, ItemClassification.filler, ["Consumables"]),
"Boulder Badge": ItemData(21, ItemClassification.progression, ["Unique", "Key Items", "Badges"]),
"Cascade Badge": ItemData(22, ItemClassification.progression, ["Unique", "Key Items", "Badges"]),
"Thunder Badge": ItemData(23, ItemClassification.progression, ["Unique", "Key Items", "Badges"]),
"Rainbow Badge": ItemData(24, ItemClassification.progression, ["Unique", "Key Items", "Badges"]),
"Soul Badge": ItemData(25, ItemClassification.progression, ["Unique", "Key Items", "Badges"]),
"Marsh Badge": ItemData(26, ItemClassification.progression, ["Unique", "Key Items", "Badges"]),
"Volcano Badge": ItemData(27, ItemClassification.progression, ["Unique", "Key Items", "Badges"]),
"Earth Badge": ItemData(28, ItemClassification.progression, ["Unique", "Key Items", "Badges"]),
"Escape Rope": ItemData(29, ItemClassification.filler, ["Consumables"]),
"Repel": ItemData(30, ItemClassification.filler, ["Consumables"]),
"Old Amber": ItemData(31, ItemClassification.progression_skip_balancing, ["Unique", "Fossils"]),
"Fire Stone": ItemData(32, ItemClassification.useful, ["Unique", "Evolution Stones"]),
"Thunder Stone": ItemData(33, ItemClassification.useful, ["Unique", "Evolution Stones"]),
"Water Stone": ItemData(34, ItemClassification.useful, ["Unique", "Evolution Stones"]),
"HP Up": ItemData(35, ItemClassification.filler, ["Consumables", "Vitamins"]),
"Protein": ItemData(36, ItemClassification.filler, ["Consumables", "Vitamins"]),
"Iron": ItemData(37, ItemClassification.filler, ["Consumables", "Vitamins"]),
"Carbos": ItemData(38, ItemClassification.filler, ["Consumables", "Vitamins"]),
"Calcium": ItemData(39, ItemClassification.filler, ["Consumables", "Vitamins"]),
"Rare Candy": ItemData(40, ItemClassification.useful, ["Consumables"]),
"Dome Fossil": ItemData(41, ItemClassification.progression_skip_balancing, ["Unique", "Fossils"]),
"Helix Fossil": ItemData(42, ItemClassification.progression_skip_balancing, ["Unique", "Fossils"]),
"Secret Key": ItemData(43, ItemClassification.progression, ["Unique", "Key Items"]),
"Bike Voucher": ItemData(45, ItemClassification.progression, ["Unique", "Key Items"]),
"X Accuracy": ItemData(46, ItemClassification.filler, ["Consumables", "Battle Items"]),
"Leaf Stone": ItemData(47, ItemClassification.useful, ["Unique", "Evolution Stones"]),
"Card Key": ItemData(48, ItemClassification.progression, ["Unique", "Key Items"]),
"Nugget": ItemData(49, ItemClassification.filler, []),
#"Laptop": ItemData(50, ItemClassification.useful, ["Unique"]),
"Poke Doll": ItemData(51, ItemClassification.filler, ["Consumables"]),
"Full Heal": ItemData(52, ItemClassification.filler, ["Consumables"]),
"Revive": ItemData(53, ItemClassification.filler, ["Consumables"]),
"Max Revive": ItemData(54, ItemClassification.filler, ["Consumables"]),
"Guard Spec": ItemData(55, ItemClassification.filler, ["Consumables", "Battle Items"]),
"Super Repel": ItemData(56, ItemClassification.filler, ["Consumables"]),
"Max Repel": ItemData(57, ItemClassification.filler, ["Consumables"]),
"Dire Hit": ItemData(58, ItemClassification.filler, ["Consumables", "Battle Items"]),
#"Coin": ItemData(59, ItemClassification.filler),
"Fresh Water": ItemData(60, ItemClassification.filler, ["Consumables"]),
"Soda Pop": ItemData(61, ItemClassification.filler, ["Consumables"]),
"Lemonade": ItemData(62, ItemClassification.filler, ["Consumables"]),
"S.S. Ticket": ItemData(63, ItemClassification.progression, ["Unique", "Key Items"]),
"Gold Teeth": ItemData(64, ItemClassification.progression, ["Unique", "Key Items"]),
"X Attack": ItemData(65, ItemClassification.filler, ["Consumables", "Battle Items"]),
"X Defend": ItemData(66, ItemClassification.filler, ["Consumables", "Battle Items"]),
"X Speed": ItemData(67, ItemClassification.filler, ["Consumables", "Battle Items"]),
"X Special": ItemData(68, ItemClassification.filler, ["Consumables", "Battle Items"]),
"Coin Case": ItemData(69, ItemClassification.progression_skip_balancing, ["Unique", "Key Items"]),
"Oak's Parcel": ItemData(70, ItemClassification.progression, ["Unique", "Key Items"]),
"Item Finder": ItemData(71, ItemClassification.progression, ["Unique", "Key Items"]),
"Silph Scope": ItemData(72, ItemClassification.progression, ["Unique", "Key Items"]),
"Poke Flute": ItemData(73, ItemClassification.progression, ["Unique", "Key Items"]),
"Lift Key": ItemData(74, ItemClassification.progression, ["Unique", "Key Items"]),
"Exp. All": ItemData(75, ItemClassification.useful, ["Unique"]),
"Old Rod": ItemData(76, ItemClassification.progression_skip_balancing, ["Unique", "Key Items", "Rods"]),
"Good Rod": ItemData(77, ItemClassification.progression_skip_balancing, ["Unique", "Key Items", "Rods"]),
"Super Rod": ItemData(78, ItemClassification.progression_skip_balancing, ["Unique", "Key Items", "Rods"]),
"PP Up": ItemData(79, ItemClassification.filler, ["Consumables"]),
"Ether": ItemData(80, ItemClassification.filler, ["Consumables"]),
"Max Ether": ItemData(81, ItemClassification.filler, ["Consumables"]),
"Elixir": ItemData(82, ItemClassification.filler, ["Consumables"]),
"Max Elixir": ItemData(83, ItemClassification.filler, ["Consumables"]),
"Tea": ItemData(84, ItemClassification.progression, ["Unique", "Key Items"]),
# "Master Sword": ItemData(85, ItemClassification.progression),
# "Flute": ItemData(86, ItemClassification.progression),
# "Titan's Mitt": ItemData(87, ItemClassification.progression),
# "Lamp": ItemData(88, ItemClassification.progression),
"Plant Key": ItemData(89, ItemClassification.progression, ["Unique", "Key Items"]),
"Mansion Key": ItemData(90, ItemClassification.progression, ["Unique", "Key Items"]),
"Hideout Key": ItemData(91, ItemClassification.progression, ["Unique", "Key Items"]),
"Safari Pass": ItemData(93, ItemClassification.progression, ["Unique", "Key Items"]),
"HM01 Cut": ItemData(196, ItemClassification.progression, ["Unique", "HMs"]),
"HM02 Fly": ItemData(197, ItemClassification.progression, ["Unique", "HMs"]),
"HM03 Surf": ItemData(198, ItemClassification.progression, ["Unique", "HMs"]),
"HM04 Strength": ItemData(199, ItemClassification.progression, ["Unique", "HMs"]),
"HM05 Flash": ItemData(200, ItemClassification.progression, ["Unique", "HMs"]),
"TM01 Mega Punch": ItemData(201, ItemClassification.useful, ["Unique", "TMs"]),
"TM02 Razor Wind": ItemData(202, ItemClassification.useful, ["Unique", "TMs"]),
"TM03 Swords Dance": ItemData(203, ItemClassification.useful, ["Unique", "TMs"]),
"TM04 Whirlwind": ItemData(204, ItemClassification.filler, ["Unique", "TMs"]),
"TM05 Mega Kick": ItemData(205, ItemClassification.useful, ["Unique", "TMs"]),
"TM06 Toxic": ItemData(206, ItemClassification.useful, ["Unique", "TMs"]),
"TM07 Horn Drill": ItemData(207, ItemClassification.useful, ["Unique", "TMs"]),
"TM08 Body Slam": ItemData(208, ItemClassification.useful, ["Unique", "TMs"]),
"TM09 Take Down": ItemData(209, ItemClassification.useful, ["Unique", "TMs"]),
"TM10 Double Edge": ItemData(210, ItemClassification.useful, ["Unique", "TMs"]),
"TM11 Bubble Beam": ItemData(211, ItemClassification.useful, ["Unique", "TMs"]),
"TM12 Water Gun": ItemData(212, ItemClassification.useful, ["Unique", "TMs"]),
"TM13 Ice Beam": ItemData(213, ItemClassification.useful, ["Unique", "TMs"]),
"TM14 Blizzard": ItemData(214, ItemClassification.useful, ["Unique", "TMs"]),
"TM15 Hyper Beam": ItemData(215, ItemClassification.useful, ["Unique", "TMs"]),
"TM16 Pay Day": ItemData(216, ItemClassification.useful, ["Unique", "TMs"]),
"TM17 Submission": ItemData(217, ItemClassification.useful, ["Unique", "TMs"]),
"TM18 Counter": ItemData(218, ItemClassification.filler, ["Unique", "TMs"]),
"TM19 Seismic Toss": ItemData(219, ItemClassification.useful, ["Unique", "TMs"]),
"TM20 Rage": ItemData(220, ItemClassification.useful, ["Unique", "TMs"]),
"TM21 Mega Drain": ItemData(221, ItemClassification.useful, ["Unique", "TMs"]),
"TM22 Solar Beam": ItemData(222, ItemClassification.useful, ["Unique", "TMs"]),
"TM23 Dragon Rage": ItemData(223, ItemClassification.useful, ["Unique", "TMs"]),
"TM24 Thunderbolt": ItemData(224, ItemClassification.useful, ["Unique", "TMs"]),
"TM25 Thunder": ItemData(225, ItemClassification.useful, ["Unique", "TMs"]),
"TM26 Earthquake": ItemData(226, ItemClassification.useful, ["Unique", "TMs"]),
"TM27 Fissure": ItemData(227, ItemClassification.useful, ["Unique", "TMs"]),
"TM28 Dig": ItemData(228, ItemClassification.useful, ["Unique", "TMs"]),
"TM29 Psychic": ItemData(229, ItemClassification.useful, ["Unique", "TMs"]),
"TM30 Teleport": ItemData(230, ItemClassification.filler, ["Unique", "TMs"]),
"TM31 Mimic": ItemData(231, ItemClassification.useful, ["Unique", "TMs"]),
"TM32 Double Team": ItemData(232, ItemClassification.useful, ["Unique", "TMs"]),
"TM33 Reflect": ItemData(233, ItemClassification.useful, ["Unique", "TMs"]),
"TM34 Bide": ItemData(234, ItemClassification.filler, ["Unique", "TMs"]),
"TM35 Metronome": ItemData(235, ItemClassification.useful, ["Unique", "TMs"]),
"TM36 Self Destruct": ItemData(236, ItemClassification.useful, ["Unique", "TMs"]),
"TM37 Egg Bomb": ItemData(237, ItemClassification.useful, ["Unique", "TMs"]),
"TM38 Fire Blast": ItemData(238, ItemClassification.useful, ["Unique", "TMs"]),
"TM39 Swift": ItemData(239, ItemClassification.useful, ["Unique", "TMs"]),
"TM40 Skull Bash": ItemData(240, ItemClassification.filler, ["Unique", "TMs"]),
"TM41 Soft Boiled": ItemData(241, ItemClassification.useful, ["Unique", "TMs"]),
"TM42 Dream Eater": ItemData(242, ItemClassification.useful, ["Unique", "TMs"]),
"TM43 Sky Attack": ItemData(243, ItemClassification.filler, ["Unique", "TMs"]),
"TM44 Rest": ItemData(244, ItemClassification.useful, ["Unique", "TMs"]),
"TM45 Thunder Wave": ItemData(245, ItemClassification.useful, ["Unique", "TMs"]),
"TM46 Psywave": ItemData(246, ItemClassification.filler, ["Unique", "TMs"]),
"TM47 Explosion": ItemData(247, ItemClassification.useful, ["Unique", "TMs"]),
"TM48 Rock Slide": ItemData(248, ItemClassification.useful, ["Unique", "TMs"]),
"TM49 Tri Attack": ItemData(249, ItemClassification.useful, ["Unique", "TMs"]),
"TM50 Substitute": ItemData(250, ItemClassification.useful, ["Unique", "TMs"]),
"Fuji Saved": ItemData(None, ItemClassification.progression, []),
"Silph Co Liberated": ItemData(None, ItemClassification.progression, []),
"Become Champion": ItemData(None, ItemClassification.progression, [])
}
item_table.update(
{pokemon: ItemData(None, ItemClassification.progression, []) for pokemon in pokemon_data.keys()}
)
item_table.update(
{f"Missable {pokemon}": ItemData(None, ItemClassification.useful, []) for pokemon in pokemon_data.keys()}
)
item_table.update(
{f"Static {pokemon}": ItemData(None, ItemClassification.progression, []) for pokemon in pokemon_data.keys()}
)
item_groups = {}
for item, data in item_table.items():
for group in data.groups:
item_groups[group] = item_groups.get(group, []) + [item]

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,73 @@
from ..AutoWorld import LogicMixin
import worlds.pokemon_rb.poke_data as poke_data
class PokemonLogic(LogicMixin):
def pokemon_rb_can_surf(self, player):
return (((self.has("HM03 Surf", player) and self.can_learn_hm("10000", player))
or self.has("Flippers", player)) and (self.has("Soul Badge", player) or
self.has(self.world.worlds[player].extra_badges.get("Surf"), player)
or self.world.badges_needed_for_hm_moves[player].value == 0))
def pokemon_rb_can_cut(self, player):
return ((self.has("HM01 Cut", player) and self.can_learn_hm("100", player) or self.has("Master Sword", player))
and (self.has("Cascade Badge", player) or
self.has(self.world.worlds[player].extra_badges.get("Cut"), player) or
self.world.badges_needed_for_hm_moves[player].value == 0))
def pokemon_rb_can_fly(self, player):
return (((self.has("HM02 Fly", player) and self.can_learn_hm("1000", player)) or self.has("Flute", player)) and
(self.has("Thunder Badge", player) or self.has(self.world.worlds[player].extra_badges.get("Fly"), player)
or self.world.badges_needed_for_hm_moves[player].value == 0))
def pokemon_rb_can_strength(self, player):
return ((self.has("HM04 Strength", player) and self.can_learn_hm("100000", player)) or
self.has("Titan's Mitt", player)) and (self.has("Rainbow Badge", player) or
self.has(self.world.worlds[player].extra_badges.get("Strength"), player)
or self.world.badges_needed_for_hm_moves[player].value == 0)
def pokemon_rb_can_flash(self, player):
return (((self.has("HM05 Flash", player) and self.can_learn_hm("1000000", player)) or self.has("Lamp", player))
and (self.has("Boulder Badge", player) or self.has(self.world.worlds[player].extra_badges.get("Flash"),
player) or self.world.badges_needed_for_hm_moves[player].value == 0))
def can_learn_hm(self, move, player):
for pokemon, data in self.world.worlds[player].local_poke_data.items():
if self.has(pokemon, player) and data["tms"][6] & int(move, 2):
return True
return False
def pokemon_rb_can_get_hidden_items(self, player):
return self.has("Item Finder", player) or not self.world.require_item_finder[player].value
def pokemon_rb_cerulean_cave(self, count, player):
return len([item for item in
["Boulder Badge", "Cascade Badge", "Thunder Badge", "Rainbow Badge", "Soul Badge", "Marsh Badge",
"Volcano Badge", "Earth Badge", "Bicycle", "Silph Scope", "Item Finder", "Super Rod", "Good Rod",
"Old Rod", "Lift Key", "Card Key", "Town Map", "Coin Case", "S.S. Ticket", "Secret Key",
"Mansion Key", "Safari Pass", "Plant Key", "Hideout Key", "HM01 Cut", "HM02 Fly", "HM03 Surf",
"HM04 Strength", "HM05 Flash"] if self.has(item, player)]) >= count
def pokemon_rb_can_pass_guards(self, player):
if self.world.tea[player].value:
return self.has("Tea", player)
else:
# this could just be "True", but you never know what weird options I might introduce later ;)
return self.can_reach("Celadon City - Counter Man", "Location", player)
def pokemon_rb_has_badges(self, count, player):
return len([item for item in ["Boulder Badge", "Cascade Badge", "Thunder Badge", "Rainbow Badge", "Marsh Badge",
"Soul Badge", "Volcano Badge", "Earth Badge"] if self.has(item, player)]) >= count
def pokemon_rb_has_pokemon(self, count, player):
obtained_pokemon = set()
for pokemon in poke_data.pokemon_data.keys():
if self.has(pokemon, player) or self.has(f"Static {pokemon}", player):
obtained_pokemon.add(pokemon)
return len(obtained_pokemon) >= count
def pokemon_rb_fossil_checks(self, count, player):
return (self.can_reach('Mt Moon 1F - Southwest Item', 'Location', player) and
self.can_reach('Cinnabar Island - Lab Scientist', 'Location', player) and len(
[item for item in ["Dome Fossil", "Helix Fossil", "Old Amber"] if self.has(item, player)]) >= count)

View File

@ -0,0 +1,481 @@
from Options import Toggle, Choice, Range, SpecialRange, FreeText, TextChoice
class GameVersion(Choice):
"""Select Red or Blue version."""
display_name = "Game Version"
option_red = 1
option_blue = 0
default = "random"
class TrainerName(FreeText):
"""Your trainer name. Cannot exceed 7 characters.
See the setup guide on archipelago.gg for a list of allowed characters."""
display_name = "Trainer Name"
default = "ASH"
class RivalName(FreeText):
"""Your rival's name. Cannot exceed 7 characters.
See the setup guide on archipelago.gg for a list of allowed characters."""
display_name = "Rival's Name"
default = "GARY"
class Goal(Choice):
"""If Professor Oak is selected, your victory condition will require challenging and defeating Oak after becoming"""
"""Champion and defeating or capturing the Pokemon at the end of Cerulean Cave."""
display_name = "Goal"
option_pokemon_league = 0
option_professor_oak = 1
default = 0
class EliteFourCondition(Range):
"""Number of badges required to challenge the Elite Four once the Indigo Plateau has been reached.
Your rival will reveal the amount needed on the first Route 22 battle (after turning in Oak's Parcel)."""
display_name = "Elite Four Condition"
range_start = 0
range_end = 8
default = 8
class VictoryRoadCondition(Range):
"""Number of badges required to reach Victory Road."""
display_name = "Victory Road Condition"
range_start = 0
range_end = 8
default = 8
class ViridianGymCondition(Range):
"""Number of badges required to enter Viridian Gym."""
display_name = "Viridian Gym Condition"
range_start = 0
range_end = 7
default = 7
class CeruleanCaveCondition(Range):
"""Number of badges, HMs, and key items (not counting items you can lose) required to access Cerulean Cave."""
"""If extra_key_items is turned on, the number chosen will be increased by 4."""
display_name = "Cerulean Cave Condition"
range_start = 0
range_end = 25
default = 20
class SecondFossilCheckCondition(Range):
"""After choosing one of the fossil location items, you can obtain the remaining item from the Cinnabar Lab
Scientist after reviving this number of fossils."""
display_name = "Second Fossil Check Condition"
range_start = 0
range_end = 3
default = 3
class BadgeSanity(Toggle):
"""Shuffle gym badges into the general item pool. If turned off, badges will be shuffled across the 8 gyms."""
display_name = "Badgesanity"
default = 0
class BadgesNeededForHMMoves(Choice):
"""Off will remove the requirement for badges to use HM moves. Extra will give the Marsh, Volcano, and Earth
Badges a random HM move to enable. Extra Plus will additionally pick two random badges to enable a second HM move.
A man in Cerulean City will reveal the moves enabled by each Badge."""
display_name = "Badges Needed For HM Moves"
default = 1
option_on = 1
alias_true = 1
option_off = 0
alias_false = 0
option_extra = 2
option_extra_plus = 3
class OldMan(Choice):
"""With Open Viridian City, the Old Man will let you through without needing to turn in Oak's Parcel."""
"""Early Parcel will ensure Oak's Parcel is available at the beginning of your game."""
display_name = "Old Man"
option_vanilla = 0
option_early_parcel = 1
option_open_viridian_city = 2
default = 1
class Tea(Toggle):
"""Adds a Tea item to the item pool which the Saffron guards require instead of the vending machine drinks.
Adds a location check to the Celadon Mansion 1F, where Tea is acquired in FireRed and LeafGreen."""
display_name = "Tea"
default = 0
class ExtraKeyItems(Toggle):
"""Adds key items that are required to access the Rocket Hideout, Cinnabar Mansion, Safari Zone, and Power Plant.
Adds four item pickups to Rock Tunnel B1F."""
display_name = "Extra Key Items"
default = 0
class ExtraStrengthBoulders(Toggle):
"""Adds Strength Boulders blocking the Route 11 gate, and in Route 13 (can be bypassed with Surf).
This potentially increases the usefulness of Strength as well as the Bicycle."""
display_name = "Extra Strength Boulders"
default = 0
class RequireItemFinder(Toggle):
"""Require Item Finder to pick up hidden items."""
display_name = "Require Item Finder"
default = 0
class RandomizeHiddenItems(Choice):
"""Randomize hidden items. If you choose exclude, they will be randomized but will be guaranteed junk items."""
display_name = "Randomize Hidden Items"
option_on = 1
option_off = 0
alias_true = 1
alias_false = 0
option_exclude = 2
default = 0
class FreeFlyLocation(Toggle):
"""One random fly destination will be unlocked by default."""
display_name = "Free Fly Location"
default = 1
class OaksAidRt2(Range):
"""Number of Pokemon registered in the Pokedex required to receive the item from Oak's Aide on Route 2"""
display_name = "Oak's Aide Route 2"
range_start = 0
range_end = 80
default = 10
class OaksAidRt11(Range):
"""Number of Pokemon registered in the Pokedex required to receive the item from Oak's Aide on Route 11"""
display_name = "Oak's Aide Route 11"
range_start = 0
range_end = 80
default = 30
class OaksAidRt15(Range):
"""Number of Pokemon registered in the Pokedex required to receive the item from Oak's Aide on Route 15"""
display_name = "Oak's Aide Route 15"
range_start = 0
range_end = 80
default = 50
class ExpModifier(SpecialRange):
"""Modifier for EXP gained. When specifying a number, exp is multiplied by this amount and divided by 16."""
display_name = "Exp Modifier"
range_start = 0
range_end = 255
default = 16
special_range_names = {
"half": default / 2,
"normal": default,
"double": default * 2,
"triple": default * 3,
"quadruple": default * 4,
"quintuple": default * 5,
"sextuple": default * 6,
"septuple": default * 7,
"octuple": default * 8,
}
class RandomizeWildPokemon(Choice):
"""Randomize all wild Pokemon and game corner prize Pokemon. match_types will select a Pokemon with at least one
type matching the original type of the original Pokemon. match_base_stats will prefer Pokemon with closer base stat
totals. match_types_and_base_stats will match types and will weight towards similar base stats, but there may not be
many to choose from."""
display_name = "Randomize Wild Pokemon"
default = 0
option_vanilla = 0
option_match_types = 1
option_match_base_stats = 2
option_match_types_and_base_stats = 3
option_completely_random = 4
class RandomizeStarterPokemon(Choice):
"""Randomize the starter Pokemon choices."""
display_name = "Randomize Starter Pokemon"
default = 0
option_vanilla = 0
option_match_types = 1
option_match_base_stats = 2
option_match_types_and_base_stats = 3
option_completely_random = 4
class RandomizeStaticPokemon(Choice):
"""Randomize all one-time gift and encountered Pokemon, except legendaries.
These will always be first evolution stage Pokemon."""
display_name = "Randomize Static Pokemon"
default = 0
option_vanilla = 0
option_match_types = 1
option_match_base_stats = 2
option_match_types_and_base_stats = 3
option_completely_random = 4
class RandomizeLegendaryPokemon(Choice):
"""Randomize Legendaries. Mew has been added as an encounter at the Vermilion dock truck.
Shuffle will shuffle the legendaries with each other. Static will shuffle them into other static Pokemon locations.
'Any' will allow legendaries to appear anywhere based on wild and static randomization options, and their locations
will be randomized according to static Pokemon randomization options."""
display_name = "Randomize Legendary Pokemon"
default = 0
option_vanilla = 0
option_shuffle = 1
option_static = 2
option_any = 3
class CatchEmAll(Choice):
"""Guarantee all first evolution stage Pokemon are available, or all Pokemon of all stages.
Currently only has an effect if wild Pokemon are randomized."""
display_name = "Catch 'Em All"
default = 0
option_off = 0
alias_false = 0
option_first_stage = 1
option_all_pokemon = 2
class RandomizeTrainerParties(Choice):
"""Randomize enemy Pokemon encountered in trainer battles."""
display_name = "Randomize Trainer Parties"
default = 0
option_vanilla = 0
option_match_types = 1
option_match_base_stats = 2
option_match_types_and_base_stats = 3
option_completely_random = 4
class TrainerLegendaries(Toggle):
"""Allow legendary Pokemon in randomized trainer parties."""
display_name = "Trainer Legendaries"
default = 0
class BlindTrainers(Range):
"""Chance each frame that you are standing on a tile in a trainer's line of sight that they will fail to initiate a
battle. If you move into and out of their line of sight without stopping, this chance will only trigger once."""
display_name = "Blind Trainers"
range_start = 0
range_end = 100
default = 0
class MinimumStepsBetweenEncounters(Range):
"""Minimum number of steps between wild Pokemon encounters."""
display_name = "Minimum Steps Between Encounters"
default = 3
range_start = 0
range_end = 255
class RandomizePokemonStats(Choice):
"""Randomize base stats for each Pokemon. Shuffle will shuffle the 5 base stat values amongst each other. Randomize
will completely randomize each stat, but will still add up to the same base stat total."""
display_name = "Randomize Pokemon Stats"
default = 0
option_vanilla = 0
option_shuffle = 1
option_randomize = 2
class RandomizePokemonCatchRates(Toggle):
"""Randomize the catch rate for each Pokemon."""
display_name = "Randomize Catch Rates"
default = 0
class MinimumCatchRate(Range):
"""Minimum catch rate for each Pokemon. If randomize_catch_rates is on, this will be the minimum value that can be
chosen. Otherwise, it will raise any Pokemon's catch rate up to this value if its normal catch rate is lower."""
display_name = "Minimum Catch Rate"
range_start = 1
range_end = 255
default = 3
class RandomizePokemonMovesets(Choice):
"""Randomize the moves learned by Pokemon. prefer_types will prefer moves that match the type of the Pokemon."""
display_name = "Randomize Pokemon Movesets"
option_vanilla = 0
option_prefer_types = 1
option_completely_random = 2
default = 0
class StartWithFourMoves(Toggle):
"""If movesets are randomized, this will give all Pokemon 4 starting moves."""
display_name = "Start With Four Moves"
default = 0
class TMCompatibility(Choice):
"""Randomize which Pokemon can learn each TM. prefer_types: 90% chance if Pokemon's type matches the move,
50% chance if move is Normal type and the Pokemon is not, and 25% chance otherwise. Pokemon will retain the same
TM compatibility when they evolve if the evolved form has the same type(s). Mew will always be able to learn
every TM."""
display_name = "TM Compatibility"
default = 0
option_vanilla = 0
option_prefer_types = 1
option_completely_random = 2
option_full_compatibility = 3
class HMCompatibility(Choice):
"""Randomize which Pokemon can learn each HM. prefer_types: 100% chance if Pokemon's type matches the move,
75% chance if move is Normal type and the Pokemon is not, and 25% chance otherwise. Pokemon will retain the same
HM compatibility when they evolve if the evolved form has the same type(s). Mew will always be able to learn
every HM."""
display_name = "HM Compatibility"
default = 0
option_vanilla = 0
option_prefer_types = 1
option_completely_random = 2
option_full_compatibility = 3
class RandomizePokemonTypes(Choice):
"""Randomize the types of each Pokemon. Follow Evolutions will ensure Pokemon's types remain the same when evolving
(except possibly gaining a type)."""
display_name = "Pokemon Types"
option_vanilla = 0
option_follow_evolutions = 1
option_randomize = 2
default = 0
class SecondaryTypeChance(SpecialRange):
"""If randomize_pokemon_types is on, this is the chance each Pokemon will have a secondary type. If follow_evolutions
is selected, it is the chance a second type will be added at each evolution stage. vanilla will give secondary types
to Pokemon that normally have a secondary type."""
display_name = "Secondary Type Chance"
range_start = -1
range_end = 100
default = -1
special_range_names = {
"vanilla": -1
}
class RandomizeTypeChartTypes(Choice):
"""The game's type chart consists of 3 columns: attacking type, defending type, and type effectiveness.
Matchups that have regular type effectiveness are not in the chart. Shuffle will shuffle the attacking types
across the attacking type column and the defending types across the defending type column (so for example Normal
type will still have exactly 2 types that it receives non-regular damage from, and 2 types it deals non-regular
damage to). Randomize will randomize each type in both columns to any random type."""
display_name = "Randomize Type Chart Types"
option_vanilla = 0
option_shuffle = 1
option_randomize = 2
default = 0
class RandomizeTypeChartTypeEffectiveness(Choice):
"""The game's type chart consists of 3 columns: attacking type, defending type, and type effectiveness.
Matchups that have regular type effectiveness are not in the chart. Shuffle will shuffle the type effectiveness
across the type effectiveness column (so for example there will always be 6 type immunities). Randomize will
randomize each entry in the table to no effect, not very effective, or super effective; with no effect occurring
at a low chance. Chaos will randomize the values to anywhere between 0% and 200% damage, in 10% increments."""
display_name = "Randomize Type Chart Type Effectiveness"
option_vanilla = 0
option_shuffle = 1
option_randomize = 2
option_chaos = 3
default = 0
class SafariZoneNormalBattles(Toggle):
"""Change the Safari Zone to have standard wild pokemon battles."""
display_name = "Safari Zone Normal Battles"
default = 0
class NormalizeEncounterChances(Toggle):
"""Each wild encounter table has 10 slots for Pokemon. Normally the chance for each being chosen ranges from
19.9% to 1.2%. Turn this on to normalize them all to 10% each."""
display_name = "Normalize Encounter Chances"
default = 0
class ReusableTMs(Toggle):
"""Makes TMs reusable, so they will not be consumed upon use."""
display_name = "Reusable TMs"
default = 0
class StartingMoney(Range):
"""The amount of money you start with."""
display_name = "Starting Money"
default = 3000
range_start = 0
range_end = 999999
pokemon_rb_options = {
"game_version": GameVersion,
"trainer_name": TrainerName,
"rival_name": RivalName,
#"goal": Goal,
"elite_four_condition": EliteFourCondition,
"victory_road_condition": VictoryRoadCondition,
"viridian_gym_condition": ViridianGymCondition,
"cerulean_cave_condition": CeruleanCaveCondition,
"second_fossil_check_condition": SecondFossilCheckCondition,
"badgesanity": BadgeSanity,
"old_man": OldMan,
"tea": Tea,
"extra_key_items": ExtraKeyItems,
"extra_strength_boulders": ExtraStrengthBoulders,
"require_item_finder": RequireItemFinder,
"randomize_hidden_items": RandomizeHiddenItems,
"badges_needed_for_hm_moves": BadgesNeededForHMMoves,
"free_fly_location": FreeFlyLocation,
"oaks_aide_rt_2": OaksAidRt2,
"oaks_aide_rt_11": OaksAidRt11,
"oaks_aide_rt_15": OaksAidRt15,
"blind_trainers": BlindTrainers,
"minimum_steps_between_encounters": MinimumStepsBetweenEncounters,
"exp_modifier": ExpModifier,
"randomize_wild_pokemon": RandomizeWildPokemon,
"randomize_starter_pokemon": RandomizeStarterPokemon,
"randomize_static_pokemon": RandomizeStaticPokemon,
"randomize_legendary_pokemon": RandomizeLegendaryPokemon,
"catch_em_all": CatchEmAll,
"randomize_pokemon_stats": RandomizePokemonStats,
"randomize_pokemon_catch_rates": RandomizePokemonCatchRates,
"minimum_catch_rate": MinimumCatchRate,
"randomize_trainer_parties": RandomizeTrainerParties,
"trainer_legendaries": TrainerLegendaries,
"randomize_pokemon_movesets": RandomizePokemonMovesets,
"start_with_four_moves": StartWithFourMoves,
"tm_compatibility": TMCompatibility,
"hm_compatibility": HMCompatibility,
"randomize_pokemon_types": RandomizePokemonTypes,
"secondary_type_chance": SecondaryTypeChance,
"randomize_type_matchup_types": RandomizeTypeChartTypes,
"randomize_type_matchup_type_effectiveness": RandomizeTypeChartTypeEffectiveness,
"safari_zone_normal_battles": SafariZoneNormalBattles,
"normalize_encounter_chances": NormalizeEncounterChances,
"reusable_tms": ReusableTMs,
"starting_money": StartingMoney,
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,305 @@
from BaseClasses import MultiWorld, Region, Entrance, RegionType, LocationProgressType
from worlds.generic.Rules import add_item_rule
from .locations import location_data, PokemonRBLocation
def create_region(world: MultiWorld, player: int, name: str, locations_per_region=None, exits=None):
ret = Region(name, RegionType.Generic, name, player, world)
for location in locations_per_region.get(name, []):
if (world.randomize_hidden_items[player].value or "Hidden" not in location.name) and \
(world.extra_key_items[player].value or name != "Rock Tunnel B1F" or "Item" not in location.name) and \
(world.tea[player].value or location.name != "Celadon City - Mansion Lady"):
location.parent_region = ret
ret.locations.append(location)
if world.randomize_hidden_items[player].value == 2 and "Hidden" in location.name:
location.progress_type = LocationProgressType.EXCLUDED
add_item_rule(location, lambda i: not (i.advancement or i.useful))
if exits:
for exit in exits:
ret.exits.append(Entrance(player, exit, ret))
locations_per_region[name] = []
return ret
def create_regions(world: MultiWorld, player: int):
locations_per_region = {}
for location in location_data:
locations_per_region.setdefault(location.region, [])
locations_per_region[location.region].append(PokemonRBLocation(player, location.name, location.address,
location.rom_address))
regions = [
create_region(world, player, "Menu", locations_per_region),
create_region(world, player, "Anywhere", locations_per_region),
create_region(world, player, "Fossil", locations_per_region),
create_region(world, player, "Pallet Town", locations_per_region),
create_region(world, player, "Route 1", locations_per_region),
create_region(world, player, "Viridian City", locations_per_region),
create_region(world, player, "Viridian City North", locations_per_region),
create_region(world, player, "Viridian Gym", locations_per_region),
create_region(world, player, "Route 2", locations_per_region),
create_region(world, player, "Route 2 East", locations_per_region),
create_region(world, player, "Diglett's Cave", locations_per_region),
create_region(world, player, "Route 22", locations_per_region),
create_region(world, player, "Route 23 South", locations_per_region),
create_region(world, player, "Route 23 North", locations_per_region),
create_region(world, player, "Viridian Forest", locations_per_region),
create_region(world, player, "Pewter City", locations_per_region),
create_region(world, player, "Pewter Gym", locations_per_region),
create_region(world, player, "Route 3", locations_per_region),
create_region(world, player, "Mt Moon 1F", locations_per_region),
create_region(world, player, "Mt Moon B1F", locations_per_region),
create_region(world, player, "Mt Moon B2F", locations_per_region),
create_region(world, player, "Route 4", locations_per_region),
create_region(world, player, "Cerulean City", locations_per_region),
create_region(world, player, "Cerulean Gym", locations_per_region),
create_region(world, player, "Route 24", locations_per_region),
create_region(world, player, "Route 25", locations_per_region),
create_region(world, player, "Route 9", locations_per_region),
create_region(world, player, "Route 10 North", locations_per_region),
create_region(world, player, "Rock Tunnel 1F", locations_per_region),
create_region(world, player, "Rock Tunnel B1F", locations_per_region),
create_region(world, player, "Power Plant", locations_per_region),
create_region(world, player, "Route 10 South", locations_per_region),
create_region(world, player, "Lavender Town", locations_per_region),
create_region(world, player, "Pokemon Tower 1F", locations_per_region),
create_region(world, player, "Pokemon Tower 2F", locations_per_region),
create_region(world, player, "Pokemon Tower 3F", locations_per_region),
create_region(world, player, "Pokemon Tower 4F", locations_per_region),
create_region(world, player, "Pokemon Tower 5F", locations_per_region),
create_region(world, player, "Pokemon Tower 6F", locations_per_region),
create_region(world, player, "Pokemon Tower 7F", locations_per_region),
create_region(world, player, "Route 5", locations_per_region),
create_region(world, player, "Saffron City", locations_per_region),
create_region(world, player, "Saffron Gym", locations_per_region),
create_region(world, player, "Copycat's House", locations_per_region),
create_region(world, player, "Underground Tunnel North-South", locations_per_region),
create_region(world, player, "Route 6", locations_per_region),
create_region(world, player, "Vermilion City", locations_per_region),
create_region(world, player, "Vermilion Gym", locations_per_region),
create_region(world, player, "S.S. Anne 1F", locations_per_region),
create_region(world, player, "S.S. Anne B1F", locations_per_region),
create_region(world, player, "S.S. Anne 2F", locations_per_region),
create_region(world, player, "Route 11", locations_per_region),
create_region(world, player, "Route 11 East", locations_per_region),
create_region(world, player, "Route 12 North", locations_per_region),
create_region(world, player, "Route 12 South", locations_per_region),
create_region(world, player, "Route 12 Grass", locations_per_region),
create_region(world, player, "Route 12 West", locations_per_region),
create_region(world, player, "Route 7", locations_per_region),
create_region(world, player, "Underground Tunnel West-East", locations_per_region),
create_region(world, player, "Route 8", locations_per_region),
create_region(world, player, "Route 8 Grass", locations_per_region),
create_region(world, player, "Celadon City", locations_per_region),
create_region(world, player, "Celadon Prize Corner", locations_per_region),
create_region(world, player, "Celadon Gym", locations_per_region),
create_region(world, player, "Route 16", locations_per_region),
create_region(world, player, "Route 16 North", locations_per_region),
create_region(world, player, "Route 17", locations_per_region),
create_region(world, player, "Route 18", locations_per_region),
create_region(world, player, "Fuchsia City", locations_per_region),
create_region(world, player, "Fuchsia Gym", locations_per_region),
create_region(world, player, "Safari Zone Gate", locations_per_region),
create_region(world, player, "Safari Zone Center", locations_per_region),
create_region(world, player, "Safari Zone East", locations_per_region),
create_region(world, player, "Safari Zone North", locations_per_region),
create_region(world, player, "Safari Zone West", locations_per_region),
create_region(world, player, "Route 15", locations_per_region),
create_region(world, player, "Route 14", locations_per_region),
create_region(world, player, "Route 13", locations_per_region),
create_region(world, player, "Route 19", locations_per_region),
create_region(world, player, "Route 20 East", locations_per_region),
create_region(world, player, "Route 20 West", locations_per_region),
create_region(world, player, "Seafoam Islands 1F", locations_per_region),
create_region(world, player, "Seafoam Islands B1F", locations_per_region),
create_region(world, player, "Seafoam Islands B2F", locations_per_region),
create_region(world, player, "Seafoam Islands B3F", locations_per_region),
create_region(world, player, "Seafoam Islands B4F", locations_per_region),
create_region(world, player, "Cinnabar Island", locations_per_region),
create_region(world, player, "Cinnabar Gym", locations_per_region),
create_region(world, player, "Route 21", locations_per_region),
create_region(world, player, "Silph Co 1F", locations_per_region),
create_region(world, player, "Silph Co 2F", locations_per_region),
create_region(world, player, "Silph Co 3F", locations_per_region),
create_region(world, player, "Silph Co 4F", locations_per_region),
create_region(world, player, "Silph Co 5F", locations_per_region),
create_region(world, player, "Silph Co 6F", locations_per_region),
create_region(world, player, "Silph Co 7F", locations_per_region),
create_region(world, player, "Silph Co 8F", locations_per_region),
create_region(world, player, "Silph Co 9F", locations_per_region),
create_region(world, player, "Silph Co 10F", locations_per_region),
create_region(world, player, "Silph Co 11F", locations_per_region),
create_region(world, player, "Rocket Hideout B1F", locations_per_region),
create_region(world, player, "Rocket Hideout B2F", locations_per_region),
create_region(world, player, "Rocket Hideout B3F", locations_per_region),
create_region(world, player, "Rocket Hideout B4F", locations_per_region),
create_region(world, player, "Pokemon Mansion 1F", locations_per_region),
create_region(world, player, "Pokemon Mansion 2F", locations_per_region),
create_region(world, player, "Pokemon Mansion 3F", locations_per_region),
create_region(world, player, "Pokemon Mansion B1F", locations_per_region),
create_region(world, player, "Victory Road 1F", locations_per_region),
create_region(world, player, "Victory Road 2F", locations_per_region),
create_region(world, player, "Victory Road 3F", locations_per_region),
create_region(world, player, "Indigo Plateau", locations_per_region),
create_region(world, player, "Cerulean Cave 1F", locations_per_region),
create_region(world, player, "Cerulean Cave 2F", locations_per_region),
create_region(world, player, "Cerulean Cave B1F", locations_per_region),
create_region(world, player, "Evolution", locations_per_region),
]
world.regions += regions
connect(world, player, "Menu", "Anywhere", one_way=True)
connect(world, player, "Menu", "Pallet Town", one_way=True)
connect(world, player, "Menu", "Fossil", lambda state: state.pokemon_rb_fossil_checks(
state.world.second_fossil_check_condition[player].value, player), one_way=True)
connect(world, player, "Pallet Town", "Route 1")
connect(world, player, "Route 1", "Viridian City")
connect(world, player, "Viridian City", "Route 22")
connect(world, player, "Route 22", "Route 23 South",
lambda state: state.pokemon_rb_has_badges(state.world.victory_road_condition[player].value, player))
connect(world, player, "Route 23 South", "Route 23 North", lambda state: state.pokemon_rb_can_surf(player))
connect(world, player, "Viridian City North", "Viridian Gym", lambda state:
state.pokemon_rb_has_badges(state.world.viridian_gym_condition[player].value, player), one_way=True)
connect(world, player, "Route 2", "Route 2 East", lambda state: state.pokemon_rb_can_cut(player))
connect(world, player, "Route 2 East", "Diglett's Cave", lambda state: state.pokemon_rb_can_cut(player))
connect(world, player, "Route 2", "Viridian City North")
connect(world, player, "Route 2", "Viridian Forest")
connect(world, player, "Route 2", "Pewter City")
connect(world, player, "Pewter City", "Pewter Gym", one_way=True)
connect(world, player, "Pewter City", "Route 3")
connect(world, player, "Route 4", "Route 3", one_way=True)
connect(world, player, "Mt Moon 1F", "Mt Moon B1F", one_way=True)
connect(world, player, "Mt Moon B1F", "Mt Moon B2F", one_way=True)
connect(world, player, "Mt Moon B1F", "Route 4", one_way=True)
connect(world, player, "Route 4", "Cerulean City")
connect(world, player, "Cerulean City", "Cerulean Gym", one_way=True)
connect(world, player, "Cerulean City", "Route 24", one_way=True)
connect(world, player, "Route 24", "Route 25", one_way=True)
connect(world, player, "Cerulean City", "Route 9", lambda state: state.pokemon_rb_can_cut(player))
connect(world, player, "Route 9", "Route 10 North")
connect(world, player, "Route 10 North", "Rock Tunnel 1F", lambda state: state.pokemon_rb_can_flash(player))
connect(world, player, "Route 10 North", "Power Plant", lambda state: state.pokemon_rb_can_surf(player) and
(state.has("Plant Key", player) or not state.world.extra_key_items[player].value), one_way=True)
connect(world, player, "Rock Tunnel 1F", "Route 10 South", lambda state: state.pokemon_rb_can_flash(player))
connect(world, player, "Rock Tunnel 1F", "Rock Tunnel B1F")
connect(world, player, "Lavender Town", "Pokemon Tower 1F", one_way=True)
connect(world, player, "Lavender Town", "Pokemon Tower 1F", one_way=True)
connect(world, player, "Pokemon Tower 1F", "Pokemon Tower 2F", one_way=True)
connect(world, player, "Pokemon Tower 2F", "Pokemon Tower 3F", one_way=True)
connect(world, player, "Pokemon Tower 3F", "Pokemon Tower 4F", one_way=True)
connect(world, player, "Pokemon Tower 4F", "Pokemon Tower 5F", one_way=True)
connect(world, player, "Pokemon Tower 5F", "Pokemon Tower 6F", one_way=True)
connect(world, player, "Pokemon Tower 6F", "Pokemon Tower 7F", lambda state: state.has("Silph Scope", player))
connect(world, player, "Cerulean City", "Route 5")
connect(world, player, "Route 5", "Saffron City", lambda state: state.pokemon_rb_can_pass_guards(player))
connect(world, player, "Route 5", "Underground Tunnel North-South")
connect(world, player, "Route 6", "Underground Tunnel North-South")
connect(world, player, "Route 6", "Saffron City", lambda state: state.pokemon_rb_can_pass_guards(player))
connect(world, player, "Route 7", "Saffron City", lambda state: state.pokemon_rb_can_pass_guards(player))
connect(world, player, "Route 8", "Saffron City", lambda state: state.pokemon_rb_can_pass_guards(player))
connect(world, player, "Saffron City", "Copycat's House", lambda state: state.has("Silph Co Liberated", player), one_way=True)
connect(world, player, "Saffron City", "Saffron Gym", lambda state: state.has("Silph Co Liberated", player), one_way=True)
connect(world, player, "Route 6", "Vermilion City")
connect(world, player, "Vermilion City", "Vermilion Gym", lambda state: state.pokemon_rb_can_surf(player) or state.pokemon_rb_can_cut(player), one_way=True)
connect(world, player, "Vermilion City", "S.S. Anne 1F", lambda state: state.has("S.S. Ticket", player), one_way=True)
connect(world, player, "S.S. Anne 1F", "S.S. Anne 2F", one_way=True)
connect(world, player, "S.S. Anne 1F", "S.S. Anne B1F", one_way=True)
connect(world, player, "Vermilion City", "Route 11")
connect(world, player, "Vermilion City", "Diglett's Cave")
connect(world, player, "Route 12 West", "Route 11 East", lambda state: state.pokemon_rb_can_strength(player) or not state.world.extra_strength_boulders[player].value)
connect(world, player, "Route 12 North", "Route 12 South", lambda state: state.has("Poke Flute", player) or state.pokemon_rb_can_surf( player))
connect(world, player, "Route 12 West", "Route 12 North", lambda state: state.has("Poke Flute", player))
connect(world, player, "Route 12 West", "Route 12 South", lambda state: state.has("Poke Flute", player))
connect(world, player, "Route 12 South", "Route 12 Grass", lambda state: state.pokemon_rb_can_cut(player))
connect(world, player, "Route 12 North", "Lavender Town")
connect(world, player, "Route 7", "Lavender Town")
connect(world, player, "Route 10 South", "Lavender Town")
connect(world, player, "Route 7", "Underground Tunnel West-East")
connect(world, player, "Route 8", "Underground Tunnel West-East")
connect(world, player, "Route 8", "Celadon City")
connect(world, player, "Route 8", "Route 8 Grass", lambda state: state.pokemon_rb_can_cut(player), one_way=True)
connect(world, player, "Route 7", "Celadon City")
connect(world, player, "Celadon City", "Celadon Gym", lambda state: state.pokemon_rb_can_cut(player), one_way=True)
connect(world, player, "Celadon City", "Celadon Prize Corner")
connect(world, player, "Celadon City", "Route 16")
connect(world, player, "Route 16", "Route 16 North", lambda state: state.pokemon_rb_can_cut(player), one_way=True)
connect(world, player, "Route 16", "Route 17", lambda state: state.has("Poke Flute", player) and state.has("Bicycle", player))
connect(world, player, "Route 17", "Route 18", lambda state: state.has("Bicycle", player))
connect(world, player, "Fuchsia City", "Fuchsia Gym", one_way=True)
connect(world, player, "Fuchsia City", "Route 18")
connect(world, player, "Fuchsia City", "Safari Zone Gate", one_way=True)
connect(world, player, "Safari Zone Gate", "Safari Zone Center", lambda state: state.has("Safari Pass", player) or not state.world.extra_key_items[player].value, one_way=True)
connect(world, player, "Safari Zone Center", "Safari Zone East", one_way=True)
connect(world, player, "Safari Zone Center", "Safari Zone West", one_way=True)
connect(world, player, "Safari Zone Center", "Safari Zone North", one_way=True)
connect(world, player, "Fuchsia City", "Route 15")
connect(world, player, "Route 15", "Route 14")
connect(world, player, "Route 14", "Route 13")
connect(world, player, "Route 13", "Route 12 South", lambda state: state.pokemon_rb_can_strength(player) or state.pokemon_rb_can_surf(player) or not state.world.extra_strength_boulders[player].value)
connect(world, player, "Fuchsia City", "Route 19", lambda state: state.pokemon_rb_can_surf(player))
connect(world, player, "Route 20 East", "Route 19")
connect(world, player, "Route 20 West", "Cinnabar Island", lambda state: state.pokemon_rb_can_surf(player))
connect(world, player, "Route 20 West", "Seafoam Islands 1F")
connect(world, player, "Route 20 East", "Seafoam Islands 1F", one_way=True)
connect(world, player, "Seafoam Islands 1F", "Route 20 East", lambda state: state.pokemon_rb_can_strength(player), one_way=True)
connect(world, player, "Viridian City", "Viridian City North", lambda state: state.has("Oak's Parcel", player) or state.world.old_man[player].value == 2 or state.pokemon_rb_can_cut(player))
connect(world, player, "Route 3", "Mt Moon 1F", one_way=True)
connect(world, player, "Route 11", "Route 11 East", lambda state: state.pokemon_rb_can_strength(player))
connect(world, player, "Cinnabar Island", "Cinnabar Gym", lambda state: state.has("Secret Key", player), one_way=True)
connect(world, player, "Cinnabar Island", "Pokemon Mansion 1F", lambda state: state.has("Mansion Key", player) or not state.world.extra_key_items[player].value, one_way=True)
connect(world, player, "Seafoam Islands 1F", "Seafoam Islands B1F", one_way=True)
connect(world, player, "Seafoam Islands B1F", "Seafoam Islands B2F", one_way=True)
connect(world, player, "Seafoam Islands B2F", "Seafoam Islands B3F", one_way=True)
connect(world, player, "Seafoam Islands B3F", "Seafoam Islands B4F", one_way=True)
connect(world, player, "Route 21", "Cinnabar Island", lambda state: state.pokemon_rb_can_surf(player))
connect(world, player, "Pallet Town", "Route 21", lambda state: state.pokemon_rb_can_surf(player))
connect(world, player, "Saffron City", "Silph Co 1F", lambda state: state.has("Fuji Saved", player), one_way=True)
connect(world, player, "Silph Co 1F", "Silph Co 2F", one_way=True)
connect(world, player, "Silph Co 2F", "Silph Co 3F", one_way=True)
connect(world, player, "Silph Co 3F", "Silph Co 4F", one_way=True)
connect(world, player, "Silph Co 4F", "Silph Co 5F", one_way=True)
connect(world, player, "Silph Co 5F", "Silph Co 6F", one_way=True)
connect(world, player, "Silph Co 6F", "Silph Co 7F", one_way=True)
connect(world, player, "Silph Co 7F", "Silph Co 8F", one_way=True)
connect(world, player, "Silph Co 8F", "Silph Co 9F", one_way=True)
connect(world, player, "Silph Co 9F", "Silph Co 10F", one_way=True)
connect(world, player, "Silph Co 10F", "Silph Co 11F", one_way=True)
connect(world, player, "Celadon City", "Rocket Hideout B1F", lambda state: state.has("Hideout Key", player) or not state.world.extra_key_items[player].value, one_way=True)
connect(world, player, "Rocket Hideout B1F", "Rocket Hideout B2F", one_way=True)
connect(world, player, "Rocket Hideout B2F", "Rocket Hideout B3F", one_way=True)
connect(world, player, "Rocket Hideout B3F", "Rocket Hideout B4F", one_way=True)
connect(world, player, "Pokemon Mansion 1F", "Pokemon Mansion 2F", one_way=True)
connect(world, player, "Pokemon Mansion 2F", "Pokemon Mansion 3F", one_way=True)
connect(world, player, "Pokemon Mansion 1F", "Pokemon Mansion B1F", one_way=True)
connect(world, player, "Route 23 North", "Victory Road 1F", lambda state: state.pokemon_rb_can_strength(player), one_way=True)
connect(world, player, "Victory Road 1F", "Victory Road 2F", one_way=True)
connect(world, player, "Victory Road 2F", "Victory Road 3F", one_way=True)
connect(world, player, "Victory Road 2F", "Indigo Plateau", lambda state: state.pokemon_rb_has_badges(state.world.elite_four_condition[player], player), one_way=True)
connect(world, player, "Cerulean City", "Cerulean Cave 1F", lambda state:
state.pokemon_rb_cerulean_cave(state.world.cerulean_cave_condition[player].value + (state.world.extra_key_items[player].value * 4), player) and
state.pokemon_rb_can_surf(player), one_way=True)
connect(world, player, "Cerulean Cave 1F", "Cerulean Cave 2F", one_way=True)
connect(world, player, "Cerulean Cave 1F", "Cerulean Cave B1F", lambda state: state.pokemon_rb_can_surf(player), one_way=True)
if world.worlds[player].fly_map != "Pallet Town":
connect(world, player, "Menu", world.worlds[player].fly_map, lambda state: state.pokemon_rb_can_fly(player), one_way=True,
name="Fly to " + world.worlds[player].fly_map)
def connect(world: MultiWorld, player: int, source: str, target: str, rule: callable = lambda state: True, one_way=False, name=None):
source_region = world.get_region(source, player)
target_region = world.get_region(target, player)
if name is None:
name = source + " to " + target
connection = Entrance(
player,
name,
source_region
)
connection.access_rule = rule
source_region.exits.append(connection)
connection.connect(target_region)
if not one_way:
connect(world, player, target, source, rule, True)

614
worlds/pokemon_rb/rom.py Normal file
View File

@ -0,0 +1,614 @@
import os
import hashlib
import Utils
import bsdiff4
from copy import deepcopy
from Patch import APDeltaPatch
from .text import encode_text
from .rom_addresses import rom_addresses
from .locations import location_data
import worlds.pokemon_rb.poke_data as poke_data
def choose_forced_type(chances, random):
n = random.randint(1, 100)
for chance in chances:
if chance[0] >= n:
return chance[1]
return None
def filter_moves(moves, type, random):
ret = []
for move in moves:
if poke_data.moves[move]["type"] == type or type is None:
ret.append(move)
random.shuffle(ret)
return ret
def get_move(moves, chances, random, starting_move=False):
type = choose_forced_type(chances, random)
filtered_moves = filter_moves(moves, type, random)
for move in filtered_moves:
if poke_data.moves[move]["accuracy"] > 80 and poke_data.moves[move]["power"] > 0 or not starting_move:
moves.remove(move)
return move
else:
return get_move(moves, [], random, starting_move)
def get_encounter_slots(self):
encounter_slots = [location for location in location_data if location.type == "Wild Encounter"]
for location in encounter_slots:
if isinstance(location.original_item, list):
location.original_item = location.original_item[not self.world.game_version[self.player].value]
return encounter_slots
def get_base_stat_total(mon):
return (poke_data.pokemon_data[mon]["atk"] + poke_data.pokemon_data[mon]["def"]
+ poke_data.pokemon_data[mon]["hp"] + poke_data.pokemon_data[mon]["spd"]
+ poke_data.pokemon_data[mon]["spc"])
def randomize_pokemon(self, mon, mons_list, randomize_type):
if randomize_type in [1, 3]:
type_mons = [pokemon for pokemon in mons_list if any([poke_data.pokemon_data[mon][
"type1"] in [self.local_poke_data[pokemon]["type1"], self.local_poke_data[pokemon]["type2"]],
poke_data.pokemon_data[mon]["type2"] in [self.local_poke_data[pokemon]["type1"],
self.local_poke_data[pokemon]["type2"]]])]
if not type_mons:
type_mons = mons_list.copy()
if randomize_type == 3:
stat_base = get_base_stat_total(mon)
type_mons.sort(key=lambda mon: abs(get_base_stat_total(mon) - stat_base))
mon = type_mons[round(self.world.random.triangular(0, len(type_mons) - 1, 0))]
if randomize_type == 2:
stat_base = get_base_stat_total(mon)
mons_list.sort(key=lambda mon: abs(get_base_stat_total(mon) - stat_base))
mon = mons_list[round(self.world.random.triangular(0, 50, 0))]
elif randomize_type == 4:
mon = self.world.random.choice(mons_list)
return mon
def process_trainer_data(self, data):
mons_list = [pokemon for pokemon in poke_data.pokemon_data.keys() if pokemon not in poke_data.legendary_pokemon
or self.world.trainer_legendaries[self.player].value]
address = rom_addresses["Trainer_Data"]
while address < rom_addresses["Trainer_Data_End"]:
if data[address] == 255:
mode = 1
else:
mode = 0
while True:
address += 1
if data[address] == 0:
address += 1
break
address += mode
mon = None
for i in range(1, 4):
for l in ["A", "B", "C", "D", "E", "F", "G", "H"]:
if rom_addresses[f"Rival_Starter{i}_{l}"] == address:
mon = " ".join(self.world.get_location(f"Pallet Town - Starter {i}", self.player).item.name.split()[1:])
if l in ["D", "E", "F", "G", "H"] and mon in poke_data.evolves_to:
mon = poke_data.evolves_to[mon]
if l in ["F", "G", "H"] and mon in poke_data.evolves_to:
mon = poke_data.evolves_to[mon]
if mon is None and self.world.randomize_trainer_parties[self.player].value:
mon = poke_data.id_to_mon[data[address]]
mon = randomize_pokemon(self, mon, mons_list, self.world.randomize_trainer_parties[self.player].value)
if mon is not None:
data[address] = poke_data.pokemon_data[mon]["id"]
def process_static_pokemon(self):
starter_slots = [location for location in location_data if location.type == "Starter Pokemon"]
legendary_slots = [location for location in location_data if location.type == "Legendary Pokemon"]
static_slots = [location for location in location_data if location.type in
["Static Pokemon", "Missable Pokemon"]]
legendary_mons = [slot.original_item for slot in legendary_slots]
mons_list = [pokemon for pokemon in poke_data.first_stage_pokemon if pokemon not in poke_data.legendary_pokemon
or self.world.randomize_legendary_pokemon[self.player].value == 3]
if self.world.randomize_legendary_pokemon[self.player].value == 0:
for slot in legendary_slots:
location = self.world.get_location(slot.name, self.player)
location.place_locked_item(self.create_item("Missable " + slot.original_item))
elif self.world.randomize_legendary_pokemon[self.player].value == 1:
self.world.random.shuffle(legendary_mons)
for slot in legendary_slots:
location = self.world.get_location(slot.name, self.player)
location.place_locked_item(self.create_item("Missable " + legendary_mons.pop()))
elif self.world.randomize_legendary_pokemon[self.player].value == 2:
static_slots = static_slots + legendary_slots
self.world.random.shuffle(static_slots)
while legendary_slots:
swap_slot = legendary_slots.pop()
slot = static_slots.pop()
slot_type = slot.type.split()[0]
if slot_type == "Legendary":
slot_type = "Missable"
location = self.world.get_location(slot.name, self.player)
location.place_locked_item(self.create_item(slot_type + " " + swap_slot.original_item))
swap_slot.original_item = slot.original_item
elif self.world.randomize_legendary_pokemon[self.player].value == 3:
static_slots = static_slots + legendary_slots
for slot in static_slots:
location = self.world.get_location(slot.name, self.player)
randomize_type = self.world.randomize_static_pokemon[self.player].value
slot_type = slot.type.split()[0]
if slot_type == "Legendary":
slot_type = "Missable"
if not randomize_type:
location.place_locked_item(self.create_item(slot_type + " " + slot.original_item))
else:
location.place_locked_item(self.create_item(slot_type + " " +
randomize_pokemon(self, slot.original_item, mons_list, randomize_type)))
for slot in starter_slots:
location = self.world.get_location(slot.name, self.player)
randomize_type = self.world.randomize_starter_pokemon[self.player].value
slot_type = "Missable"
if not randomize_type:
location.place_locked_item(self.create_item(slot_type + " " + slot.original_item))
else:
location.place_locked_item(self.create_item(slot_type + " " +
randomize_pokemon(self, slot.original_item, mons_list, randomize_type)))
def process_wild_pokemon(self):
encounter_slots = get_encounter_slots(self)
placed_mons = {pokemon: 0 for pokemon in poke_data.pokemon_data.keys()}
if self.world.randomize_wild_pokemon[self.player].value:
mons_list = [pokemon for pokemon in poke_data.pokemon_data.keys() if pokemon not in poke_data.legendary_pokemon
or self.world.randomize_legendary_pokemon[self.player].value == 3]
self.world.random.shuffle(encounter_slots)
locations = []
for slot in encounter_slots:
mon = randomize_pokemon(self, slot.original_item, mons_list, self.world.randomize_wild_pokemon[self.player].value)
placed_mons[mon] += 1
location = self.world.get_location(slot.name, self.player)
location.item = self.create_item(mon)
location.event = True
location.locked = True
location.item.location = location
locations.append(location)
mons_to_add = []
remaining_pokemon = [pokemon for pokemon in poke_data.pokemon_data.keys() if placed_mons[pokemon] == 0 and
(pokemon not in poke_data.legendary_pokemon or self.world.randomize_legendary_pokemon[self.player].value == 3)]
if self.world.catch_em_all[self.player].value == 1:
mons_to_add = [pokemon for pokemon in poke_data.first_stage_pokemon if placed_mons[pokemon] == 0 and
(pokemon not in poke_data.legendary_pokemon or self.world.randomize_legendary_pokemon[self.player].value == 3)]
elif self.world.catch_em_all[self.player].value == 2:
mons_to_add = remaining_pokemon.copy()
logic_needed_mons = max(self.world.oaks_aide_rt_2[self.player].value,
self.world.oaks_aide_rt_11[self.player].value,
self.world.oaks_aide_rt_15[self.player].value)
if self.world.accessibility[self.player] == "minimal":
logic_needed_mons = 0
self.world.random.shuffle(remaining_pokemon)
while (len([pokemon for pokemon in placed_mons if placed_mons[pokemon] > 0])
+ len(mons_to_add) < logic_needed_mons):
mons_to_add.append(remaining_pokemon.pop())
for mon in mons_to_add:
stat_base = get_base_stat_total(mon)
candidate_locations = get_encounter_slots(self)
if self.world.randomize_wild_pokemon[self.player].value in [1, 3]:
candidate_locations = [slot for slot in candidate_locations if any([poke_data.pokemon_data[slot.original_item][
"type1"] in [self.local_poke_data[mon]["type1"], self.local_poke_data[mon]["type2"]],
poke_data.pokemon_data[slot.original_item]["type2"] in [self.local_poke_data[mon]["type1"],
self.local_poke_data[mon]["type2"]]])]
if not candidate_locations:
candidate_locations = location_data
candidate_locations = [self.world.get_location(location.name, self.player) for location in candidate_locations]
candidate_locations.sort(key=lambda slot: abs(get_base_stat_total(slot.item.name) - stat_base))
for location in candidate_locations:
if placed_mons[location.item.name] > 1 or location.item.name not in poke_data.first_stage_pokemon:
placed_mons[location.item.name] -= 1
location.item = self.create_item(mon)
location.item.location = location
placed_mons[mon] += 1
break
else:
for slot in encounter_slots:
location = self.world.get_location(slot.name, self.player)
location.item = self.create_item(slot.original_item)
location.event = True
location.locked = True
location.item.location = location
placed_mons[location.item.name] += 1
def process_pokemon_data(self):
local_poke_data = deepcopy(poke_data.pokemon_data)
learnsets = deepcopy(poke_data.learnsets)
for mon, mon_data in local_poke_data.items():
if self.world.randomize_pokemon_stats[self.player].value == 1:
stats = [mon_data["hp"], mon_data["atk"], mon_data["def"], mon_data["spd"], mon_data["spc"]]
self.world.random.shuffle(stats)
mon_data["hp"] = stats[0]
mon_data["atk"] = stats[1]
mon_data["def"] = stats[2]
mon_data["spd"] = stats[3]
mon_data["spc"] = stats[4]
elif self.world.randomize_pokemon_stats[self.player].value == 2:
old_stats = mon_data["hp"] + mon_data["atk"] + mon_data["def"] + mon_data["spd"] + mon_data["spc"] - 5
stats = [1, 1, 1, 1, 1]
while old_stats > 0:
stat = self.world.random.randint(0, 4)
if stats[stat] < 255:
old_stats -= 1
stats[stat] += 1
mon_data["hp"] = stats[0]
mon_data["atk"] = stats[1]
mon_data["def"] = stats[2]
mon_data["spd"] = stats[3]
mon_data["spc"] = stats[4]
if self.world.randomize_pokemon_types[self.player].value:
if self.world.randomize_pokemon_types[self.player].value == 1 and mon in poke_data.evolves_from:
type1 = local_poke_data[poke_data.evolves_from[mon]]["type1"]
type2 = local_poke_data[poke_data.evolves_from[mon]]["type2"]
if type1 == type2:
if self.world.secondary_type_chance[self.player].value == -1:
if mon_data["type1"] != mon_data["type2"]:
while type2 == type1:
type2 = self.world.random.choice(list(poke_data.type_names.values()))
elif self.world.random.randint(1, 100) <= self.world.secondary_type_chance[self.player].value:
type2 = self.world.random.choice(list(poke_data.type_names.values()))
else:
type1 = self.world.random.choice(list(poke_data.type_names.values()))
type2 = type1
if ((self.world.secondary_type_chance[self.player].value == -1 and mon_data["type1"]
!= mon_data["type2"]) or self.world.random.randint(1, 100)
<= self.world.secondary_type_chance[self.player].value):
while type2 == type1:
type2 = self.world.random.choice(list(poke_data.type_names.values()))
mon_data["type1"] = type1
mon_data["type2"] = type2
if self.world.randomize_pokemon_movesets[self.player].value:
if self.world.randomize_pokemon_movesets[self.player].value == 1:
if mon_data["type1"] == "Normal" and mon_data["type2"] == "Normal":
chances = [[75, "Normal"]]
elif mon_data["type1"] == "Normal" or mon_data["type2"] == "Normal":
if mon_data["type1"] == "Normal":
second_type = mon_data["type2"]
else:
second_type = mon_data["type1"]
chances = [[30, "Normal"], [85, second_type]]
elif mon_data["type1"] == mon_data["type2"]:
chances = [[60, mon_data["type1"]], [80, "Normal"]]
else:
chances = [[50, mon_data["type1"]], [80, mon_data["type2"]], [85, "Normal"]]
else:
chances = []
moves = set(poke_data.moves.keys())
moves -= set(["No Move"] + poke_data.hm_moves)
mon_data["start move 1"] = get_move(moves, chances, self.world.random, True)
for i in range(2, 5):
if mon_data[f"start move {i}"] != "No Move" or self.world.start_with_four_moves[
self.player].value == 1:
mon_data[f"start move {i}"] = get_move(moves, chances, self.world.random)
if mon in learnsets:
for move_num in range(0, len(learnsets[mon])):
learnsets[mon][move_num] = get_move(moves, chances, self.world.random)
if self.world.randomize_pokemon_catch_rates[self.player].value:
mon_data["catch rate"] = self.world.random.randint(self.world.minimum_catch_rate[self.player], 255)
else:
mon_data["catch rate"] = max(self.world.minimum_catch_rate[self.player], mon_data["catch rate"])
if mon in poke_data.evolves_from.keys() and mon_data["type1"] == local_poke_data[poke_data.evolves_from[mon]]["type1"] and mon_data["type2"] == local_poke_data[poke_data.evolves_from[mon]]["type2"]:
mon_data["tms"] = local_poke_data[poke_data.evolves_from[mon]]["tms"]
elif mon != "Mew":
tms_hms = poke_data.tm_moves + poke_data.hm_moves
for flag, tm_move in enumerate(tms_hms):
if (flag < 50 and self.world.tm_compatibility[self.player].value == 1) or (flag >= 50 and self.world.hm_compatibility[self.player].value == 1):
type_match = poke_data.moves[tm_move]["type"] in [mon_data["type1"], mon_data["type2"]]
bit = int(self.world.random.randint(1, 100) < [[90, 50, 25], [100, 75, 25]][flag >= 50][0 if type_match else 1 if poke_data.moves[tm_move]["type"] == "Normal" else 2])
elif (flag < 50 and self.world.tm_compatibility[self.player].value == 2) or (flag >= 50 and self.world.hm_compatibility[self.player].value == 2):
bit = [0, 1][self.world.random.randint(0, 1)]
elif (flag < 50 and self.world.tm_compatibility[self.player].value == 3) or (flag >= 50 and self.world.hm_compatibility[self.player].value == 3):
bit = 1
else:
continue
if bit:
mon_data["tms"][int(flag / 8)] |= 1 << (flag % 8)
else:
mon_data["tms"][int(flag / 8)] &= ~(1 << (flag % 8))
self.local_poke_data = local_poke_data
self.learnsets = learnsets
def generate_output(self, output_directory: str):
random = self.world.slot_seeds[self.player]
game_version = self.world.game_version[self.player].current_key
data = bytearray(get_base_rom_bytes(game_version))
basemd5 = hashlib.md5()
basemd5.update(data)
for location in self.world.get_locations():
if location.player != self.player or location.rom_address is None:
continue
if location.item and location.item.player == self.player:
if location.rom_address:
rom_address = location.rom_address
if not isinstance(rom_address, list):
rom_address = [rom_address]
for address in rom_address:
if location.item.name in poke_data.pokemon_data.keys():
data[address] = poke_data.pokemon_data[location.item.name]["id"]
elif " ".join(location.item.name.split()[1:]) in poke_data.pokemon_data.keys():
data[address] = poke_data.pokemon_data[" ".join(location.item.name.split()[1:])]["id"]
else:
data[address] = self.item_name_to_id[location.item.name] - 172000000
else:
data[location.rom_address] = 0x2C # AP Item
data[rom_addresses['Fly_Location']] = self.fly_map_code
if self.world.tea[self.player].value:
data[rom_addresses["Option_Tea"]] = 1
data[rom_addresses["Guard_Drink_List"]] = 0x54
data[rom_addresses["Guard_Drink_List"] + 1] = 0
data[rom_addresses["Guard_Drink_List"] + 2] = 0
if self.world.extra_key_items[self.player].value:
data[rom_addresses['Options']] |= 4
data[rom_addresses["Option_Blind_Trainers"]] = round(self.world.blind_trainers[self.player].value * 2.55)
data[rom_addresses['Option_Cerulean_Cave_Condition']] = self.world.cerulean_cave_condition[self.player].value
data[rom_addresses['Option_Encounter_Minimum_Steps']] = self.world.minimum_steps_between_encounters[self.player].value
data[rom_addresses['Option_Victory_Road_Badges']] = self.world.victory_road_condition[self.player].value
data[rom_addresses['Option_Pokemon_League_Badges']] = self.world.elite_four_condition[self.player].value
data[rom_addresses['Option_Viridian_Gym_Badges']] = self.world.viridian_gym_condition[self.player].value
data[rom_addresses['Option_EXP_Modifier']] = self.world.exp_modifier[self.player].value
if not self.world.require_item_finder[self.player].value:
data[rom_addresses['Option_Itemfinder']] = 0
if self.world.extra_strength_boulders[self.player].value:
for i in range(0, 3):
data[rom_addresses['Option_Boulders'] + (i * 3)] = 0x15
if self.world.extra_key_items[self.player].value:
for i in range(0, 4):
data[rom_addresses['Option_Rock_Tunnel_Extra_Items'] + (i * 3)] = 0x15
if self.world.old_man[self.player].value == 2:
data[rom_addresses['Option_Old_Man']] = 0x11
data[rom_addresses['Option_Old_Man_Lying']] = 0x15
money = str(self.world.starting_money[self.player].value)
while len(money) < 6:
money = "0" + money
data[rom_addresses["Starting_Money_High"]] = int(money[:2], 16)
data[rom_addresses["Starting_Money_Middle"]] = int(money[2:4], 16)
data[rom_addresses["Starting_Money_Low"]] = int(money[4:], 16)
data[rom_addresses["Text_Badges_Needed"]] = encode_text(
str(max(self.world.victory_road_condition[self.player].value,
self.world.elite_four_condition[self.player].value)))[0]
if self.world.badges_needed_for_hm_moves[self.player].value == 0:
for hm_move in poke_data.hm_moves:
write_bytes(data, bytearray([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]),
rom_addresses["HM_" + hm_move + "_Badge_a"])
elif self.extra_badges:
written_badges = {}
for hm_move, badge in self.extra_badges.items():
data[rom_addresses["HM_" + hm_move + "_Badge_b"]] = {"Boulder Badge": 0x47, "Cascade Badge": 0x4F,
"Thunder Badge": 0x57, "Rainbow Badge": 0x5F,
"Soul Badge": 0x67, "Marsh Badge": 0x6F,
"Volcano Badge": 0x77, "Earth Badge": 0x7F}[badge]
move_text = hm_move
if badge not in ["Marsh Badge", "Volcano Badge", "Earth Badge"]:
move_text = ", " + move_text
rom_address = rom_addresses["Badge_Text_" + badge.replace(" ", "_")]
if badge in written_badges:
rom_address += len(written_badges[badge])
move_text = ", " + move_text
write_bytes(data, encode_text(move_text.upper()), rom_address)
written_badges[badge] = move_text
for badge in ["Marsh Badge", "Volcano Badge", "Earth Badge"]:
if badge not in written_badges:
write_bytes(data, encode_text("Nothing"), rom_addresses["Badge_Text_" + badge.replace(" ", "_")])
chart = deepcopy(poke_data.type_chart)
if self.world.randomize_type_matchup_types[self.player].value == 1:
attacking_types = []
defending_types = []
for matchup in chart:
attacking_types.append(matchup[0])
defending_types.append(matchup[1])
random.shuffle(attacking_types)
random.shuffle(defending_types)
matchups = []
while len(attacking_types) > 0:
if [attacking_types[0], defending_types[0]] not in matchups:
matchups.append([attacking_types.pop(0), defending_types.pop(0)])
else:
matchup = matchups.pop(0)
attacking_types.append(matchup[0])
defending_types.append(matchup[1])
random.shuffle(attacking_types)
random.shuffle(defending_types)
for matchup, chart_row in zip(matchups, chart):
chart_row[0] = matchup[0]
chart_row[1] = matchup[1]
elif self.world.randomize_type_matchup_types[self.player].value == 2:
used_matchups = []
for matchup in chart:
matchup[0] = random.choice(list(poke_data.type_names.values()))
matchup[1] = random.choice(list(poke_data.type_names.values()))
while [matchup[0], matchup[1]] in used_matchups:
matchup[0] = random.choice(list(poke_data.type_names.values()))
matchup[1] = random.choice(list(poke_data.type_names.values()))
used_matchups.append([matchup[0], matchup[1]])
if self.world.randomize_type_matchup_type_effectiveness[self.player].value == 1:
effectiveness_list = []
for matchup in chart:
effectiveness_list.append(matchup[2])
random.shuffle(effectiveness_list)
for (matchup, effectiveness) in zip(chart, effectiveness_list):
matchup[2] = effectiveness
elif self.world.randomize_type_matchup_type_effectiveness[self.player].value == 2:
for matchup in chart:
matchup[2] = random.choice([0] + ([5, 20] * 5))
elif self.world.randomize_type_matchup_type_effectiveness[self.player].value == 3:
for matchup in chart:
matchup[2] = self.world.random.choice([i for i in range(0, 21) if i != 10])
type_loc = rom_addresses["Type_Chart"]
for matchup in chart:
data[type_loc] = poke_data.type_ids[matchup[0]]
data[type_loc + 1] = poke_data.type_ids[matchup[1]]
data[type_loc + 2] = matchup[2]
type_loc += 3
# sort so that super-effective matchups occur first, to prevent dual "not very effective" / "super effective"
# matchups from leading to damage being ultimately divided by 2 and then multiplied by 2, which can lead to
# damage being reduced by 1 which leads to a "not very effective" message appearing due to my changes
# to the way effectiveness messages are generated.
self.type_chart = sorted(chart, key=lambda matchup: 0 - matchup[2])
if self.world.normalize_encounter_chances[self.player].value:
chances = [25, 51, 77, 103, 129, 155, 180, 205, 230, 255]
for i, chance in enumerate(chances):
data[rom_addresses['Encounter_Chances'] + (i * 2)] = chance
for mon, mon_data in self.local_poke_data.items():
if mon == "Mew":
address = rom_addresses["Base_Stats_Mew"]
else:
address = rom_addresses["Base_Stats"] + (28 * (mon_data["dex"] - 1))
data[address + 1] = self.local_poke_data[mon]["hp"]
data[address + 2] = self.local_poke_data[mon]["atk"]
data[address + 3] = self.local_poke_data[mon]["def"]
data[address + 4] = self.local_poke_data[mon]["spd"]
data[address + 5] = self.local_poke_data[mon]["spc"]
data[address + 6] = poke_data.type_ids[self.local_poke_data[mon]["type1"]]
data[address + 7] = poke_data.type_ids[self.local_poke_data[mon]["type2"]]
data[address + 8] = self.local_poke_data[mon]["catch rate"]
data[address + 15] = poke_data.moves[self.local_poke_data[mon]["start move 1"]]["id"]
data[address + 16] = poke_data.moves[self.local_poke_data[mon]["start move 2"]]["id"]
data[address + 17] = poke_data.moves[self.local_poke_data[mon]["start move 3"]]["id"]
data[address + 18] = poke_data.moves[self.local_poke_data[mon]["start move 4"]]["id"]
write_bytes(data, self.local_poke_data[mon]["tms"], address + 20)
if mon in self.learnsets:
address = rom_addresses["Learnset_" + mon.replace(" ", "")]
for i, move in enumerate(self.learnsets[mon]):
data[(address + 1) + i * 2] = poke_data.moves[move]["id"]
data[rom_addresses["Option_Aide_Rt2"]] = self.world.oaks_aide_rt_2[self.player]
data[rom_addresses["Option_Aide_Rt11"]] = self.world.oaks_aide_rt_11[self.player]
data[rom_addresses["Option_Aide_Rt15"]] = self.world.oaks_aide_rt_15[self.player]
if self.world.safari_zone_normal_battles[self.player].value == 1:
data[rom_addresses["Option_Safari_Zone_Battle_Type"]] = 255
if self.world.reusable_tms[self.player].value:
data[rom_addresses["Option_Reusable_TMs"]] = 0xC9
process_trainer_data(self, data)
mons = [mon["id"] for mon in poke_data.pokemon_data.values()]
random.shuffle(mons)
data[rom_addresses['Title_Mon_First']] = mons.pop()
for mon in range(0, 16):
data[rom_addresses['Title_Mons'] + mon] = mons.pop()
if self.world.game_version[self.player].value:
mons.sort(key=lambda mon: 0 if mon == self.world.get_location("Pallet Town - Starter 1", self.player).item.name
else 1 if mon == self.world.get_location("Pallet Town - Starter 2", self.player).item.name else
2 if mon == self.world.get_location("Pallet Town - Starter 3", self.player).item.name else 3)
else:
mons.sort(key=lambda mon: 0 if mon == self.world.get_location("Pallet Town - Starter 2", self.player).item.name
else 1 if mon == self.world.get_location("Pallet Town - Starter 1", self.player).item.name else
2 if mon == self.world.get_location("Pallet Town - Starter 3", self.player).item.name else 3)
write_bytes(data, encode_text(self.world.seed_name, 20, True), rom_addresses['Title_Seed'])
slot_name = self.world.player_name[self.player]
slot_name.replace("@", " ")
slot_name.replace("<", " ")
slot_name.replace(">", " ")
write_bytes(data, encode_text(slot_name, 16, True, True), rom_addresses['Title_Slot_Name'])
write_bytes(data, self.trainer_name, rom_addresses['Player_Name'])
write_bytes(data, self.rival_name, rom_addresses['Rival_Name'])
write_bytes(data, basemd5.digest(), 0xFFCC)
write_bytes(data, self.world.seed_name.encode(), 0xFFDC)
write_bytes(data, self.world.player_name[self.player].encode(), 0xFFF0)
outfilepname = f'_P{self.player}'
outfilepname += f"_{self.world.get_file_safe_player_name(self.player).replace(' ', '_')}" \
if self.world.player_name[self.player] != 'Player%d' % self.player else ''
rompath = os.path.join(output_directory, f'AP_{self.world.seed_name}{outfilepname}.gb')
with open(rompath, 'wb') as outfile:
outfile.write(data)
if self.world.game_version[self.player].current_key == "red":
patch = RedDeltaPatch(os.path.splitext(rompath)[0] + RedDeltaPatch.patch_file_ending, player=self.player,
player_name=self.world.player_name[self.player], patched_path=rompath)
else:
patch = BlueDeltaPatch(os.path.splitext(rompath)[0] + BlueDeltaPatch.patch_file_ending, player=self.player,
player_name=self.world.player_name[self.player], patched_path=rompath)
patch.write()
os.unlink(rompath)
def write_bytes(data, byte_array, address):
for byte in byte_array:
data[address] = byte
address += 1
def get_base_rom_bytes(game_version: str, hash: str="") -> bytes:
file_name = get_base_rom_path(game_version)
with open(file_name, "rb") as file:
base_rom_bytes = bytes(file.read())
if hash:
basemd5 = hashlib.md5()
basemd5.update(base_rom_bytes)
if hash != basemd5.hexdigest():
raise Exception('Supplied Base Rom does not match known MD5 for US(1.0) release. '
'Get the correct game and version, then dump it')
with open(os.path.join(os.path.dirname(__file__), f'basepatch_{game_version}.bsdiff4'), 'rb') as stream:
base_patch = bytes(stream.read())
base_rom_bytes = bsdiff4.patch(base_rom_bytes, base_patch)
return base_rom_bytes
def get_base_rom_path(game_version: str) -> str:
options = Utils.get_options()
file_name = options["pokemon_rb_options"][f"{game_version}_rom_file"]
if not os.path.exists(file_name):
file_name = Utils.local_path(file_name)
return file_name
class BlueDeltaPatch(APDeltaPatch):
patch_file_ending = ".apblue"
hash = "50927e843568814f7ed45ec4f944bd8b"
game_version = "blue"
game = "Pokemon Red and Blue"
result_file_ending = ".gb"
@classmethod
def get_source_data(cls) -> bytes:
return get_base_rom_bytes(cls.game_version, cls.hash)
class RedDeltaPatch(APDeltaPatch):
patch_file_ending = ".apred"
hash = "3d45c1ee9abd5738df46d2bdda8b57dc"
game_version = "red"
game = "Pokemon Red and Blue"
result_file_ending = ".gb"
@classmethod
def get_source_data(cls) -> bytes:
return get_base_rom_bytes(cls.game_version, cls.hash)

View File

@ -0,0 +1,588 @@
rom_addresses = {
"Option_Encounter_Minimum_Steps": 0x3c3,
"Option_Blind_Trainers": 0x317e,
"Base_Stats_Mew": 0x425b,
"Title_Mon_First": 0x436e,
"Title_Mons": 0x4547,
"Player_Name": 0x4569,
"Rival_Name": 0x4571,
"Title_Seed": 0x5dfe,
"Title_Slot_Name": 0x5e1e,
"PC_Item": 0x61ec,
"PC_Item_Quantity": 0x61f1,
"Options": 0x61f9,
"Fly_Location": 0x61fe,
"Option_Old_Man": 0xcaef,
"Option_Old_Man_Lying": 0xcaf2,
"Option_Boulders": 0xcd98,
"Option_Rock_Tunnel_Extra_Items": 0xcda1,
"Wild_Route1": 0xd0fb,
"Wild_Route2": 0xd111,
"Wild_Route22": 0xd127,
"Wild_ViridianForest": 0xd13d,
"Wild_Route3": 0xd153,
"Wild_MtMoon1F": 0xd169,
"Wild_MtMoonB1F": 0xd17f,
"Wild_MtMoonB2F": 0xd195,
"Wild_Route4": 0xd1ab,
"Wild_Route24": 0xd1c1,
"Wild_Route25": 0xd1d7,
"Wild_Route9": 0xd1ed,
"Wild_Route5": 0xd203,
"Wild_Route6": 0xd219,
"Wild_Route11": 0xd22f,
"Wild_RockTunnel1F": 0xd245,
"Wild_RockTunnelB1F": 0xd25b,
"Wild_Route10": 0xd271,
"Wild_Route12": 0xd287,
"Wild_Route8": 0xd29d,
"Wild_Route7": 0xd2b3,
"Wild_PokemonTower3F": 0xd2cd,
"Wild_PokemonTower4F": 0xd2e3,
"Wild_PokemonTower5F": 0xd2f9,
"Wild_PokemonTower6F": 0xd30f,
"Wild_PokemonTower7F": 0xd325,
"Wild_Route13": 0xd33b,
"Wild_Route14": 0xd351,
"Wild_Route15": 0xd367,
"Wild_Route16": 0xd37d,
"Wild_Route17": 0xd393,
"Wild_Route18": 0xd3a9,
"Wild_SafariZoneCenter": 0xd3bf,
"Wild_SafariZoneEast": 0xd3d5,
"Wild_SafariZoneNorth": 0xd3eb,
"Wild_SafariZoneWest": 0xd401,
"Wild_SeaRoutes": 0xd418,
"Wild_SeafoamIslands1F": 0xd42d,
"Wild_SeafoamIslandsB1F": 0xd443,
"Wild_SeafoamIslandsB2F": 0xd459,
"Wild_SeafoamIslandsB3F": 0xd46f,
"Wild_SeafoamIslandsB4F": 0xd485,
"Wild_PokemonMansion1F": 0xd49b,
"Wild_PokemonMansion2F": 0xd4b1,
"Wild_PokemonMansion3F": 0xd4c7,
"Wild_PokemonMansionB1F": 0xd4dd,
"Wild_Route21": 0xd4f3,
"Wild_Surf_Route21": 0xd508,
"Wild_CeruleanCave1F": 0xd51d,
"Wild_CeruleanCave2F": 0xd533,
"Wild_CeruleanCaveB1F": 0xd549,
"Wild_PowerPlant": 0xd55f,
"Wild_Route23": 0xd575,
"Wild_VictoryRoad2F": 0xd58b,
"Wild_VictoryRoad3F": 0xd5a1,
"Wild_VictoryRoad1F": 0xd5b7,
"Wild_DiglettsCave": 0xd5cd,
"Ghost_Battle5": 0xd723,
"HM_Surf_Badge_a": 0xda11,
"HM_Surf_Badge_b": 0xda16,
"Wild_Old_Rod": 0xe313,
"Wild_Good_Rod": 0xe340,
"Option_Reusable_TMs": 0xe60c,
"Wild_Super_Rod_A": 0xea40,
"Wild_Super_Rod_B": 0xea45,
"Wild_Super_Rod_C": 0xea4a,
"Wild_Super_Rod_D": 0xea51,
"Wild_Super_Rod_E": 0xea56,
"Wild_Super_Rod_F": 0xea5b,
"Wild_Super_Rod_G": 0xea64,
"Wild_Super_Rod_H": 0xea6d,
"Wild_Super_Rod_I": 0xea76,
"Wild_Super_Rod_J": 0xea7f,
"Starting_Money_High": 0xf949,
"Starting_Money_Middle": 0xf94c,
"Starting_Money_Low": 0xf94f,
"HM_Fly_Badge_a": 0x1318e,
"HM_Fly_Badge_b": 0x13193,
"HM_Cut_Badge_a": 0x131c4,
"HM_Cut_Badge_b": 0x131c9,
"HM_Strength_Badge_a": 0x131f4,
"HM_Strength_Badge_b": 0x131f9,
"HM_Flash_Badge_a": 0x13208,
"HM_Flash_Badge_b": 0x1320d,
"Encounter_Chances": 0x13911,
"Option_Viridian_Gym_Badges": 0x1901d,
"Event_Sleepy_Guy": 0x191bc,
"Starter2_K": 0x195a8,
"Starter3_K": 0x195b0,
"Event_Rocket_Thief": 0x196cc,
"Option_Cerulean_Cave_Condition": 0x1986c,
"Event_Stranded_Man": 0x19b2b,
"Event_Rivals_Sister": 0x19cf9,
"Option_Pokemon_League_Badges": 0x19e16,
"Missable_Silph_Co_4F_Item_1": 0x1a0d7,
"Missable_Silph_Co_4F_Item_2": 0x1a0de,
"Missable_Silph_Co_4F_Item_3": 0x1a0e5,
"Missable_Silph_Co_5F_Item_1": 0x1a337,
"Missable_Silph_Co_5F_Item_2": 0x1a33e,
"Missable_Silph_Co_5F_Item_3": 0x1a345,
"Missable_Silph_Co_6F_Item_1": 0x1a5ad,
"Missable_Silph_Co_6F_Item_2": 0x1a5b4,
"Event_Free_Sample": 0x1cade,
"Starter1_F": 0x1cca5,
"Starter2_F": 0x1cca9,
"Starter2_G": 0x1cde2,
"Starter3_G": 0x1cdea,
"Starter2_H": 0x1d0e5,
"Starter1_H": 0x1d0ef,
"Starter3_I": 0x1d0f6,
"Starter2_I": 0x1d100,
"Starter1_D": 0x1d107,
"Starter3_D": 0x1d111,
"Starter2_E": 0x1d2eb,
"Starter3_E": 0x1d2f3,
"Event_Oaks_Gift": 0x1d373,
"Event_Pokemart_Quest": 0x1d566,
"Event_Bicycle_Shop": 0x1d805,
"Text_Bicycle": 0x1d898,
"Event_Fuji": 0x1d9cd,
"Static_Encounter_Mew": 0x1dc4e,
"Gift_Eevee": 0x1dcc7,
"Event_Mr_Psychic": 0x1ddcf,
"Static_Encounter_Voltorb_A": 0x1e397,
"Static_Encounter_Voltorb_B": 0x1e39f,
"Static_Encounter_Voltorb_C": 0x1e3a7,
"Static_Encounter_Electrode_A": 0x1e3af,
"Static_Encounter_Voltorb_D": 0x1e3b7,
"Static_Encounter_Voltorb_E": 0x1e3bf,
"Static_Encounter_Electrode_B": 0x1e3c7,
"Static_Encounter_Voltorb_F": 0x1e3cf,
"Static_Encounter_Zapdos": 0x1e3d7,
"Missable_Power_Plant_Item_1": 0x1e3df,
"Missable_Power_Plant_Item_2": 0x1e3e6,
"Missable_Power_Plant_Item_3": 0x1e3ed,
"Missable_Power_Plant_Item_4": 0x1e3f4,
"Missable_Power_Plant_Item_5": 0x1e3fb,
"Event_Rt16_House_Woman": 0x1e5d4,
"Option_Victory_Road_Badges": 0x1e6a5,
"Event_Bill": 0x1e8d6,
"Starter1_O": 0x372b0,
"Starter2_O": 0x372b4,
"Starter3_O": 0x372b8,
"Base_Stats": 0x383de,
"Starter3_C": 0x39cf2,
"Starter1_C": 0x39cf8,
"Trainer_Data": 0x39d99,
"Rival_Starter2_A": 0x3a1e5,
"Rival_Starter3_A": 0x3a1e8,
"Rival_Starter1_A": 0x3a1eb,
"Rival_Starter2_B": 0x3a1f1,
"Rival_Starter3_B": 0x3a1f7,
"Rival_Starter1_B": 0x3a1fd,
"Rival_Starter2_C": 0x3a207,
"Rival_Starter3_C": 0x3a211,
"Rival_Starter1_C": 0x3a21b,
"Rival_Starter2_D": 0x3a409,
"Rival_Starter3_D": 0x3a413,
"Rival_Starter1_D": 0x3a41d,
"Rival_Starter2_E": 0x3a429,
"Rival_Starter3_E": 0x3a435,
"Rival_Starter1_E": 0x3a441,
"Rival_Starter2_F": 0x3a44d,
"Rival_Starter3_F": 0x3a459,
"Rival_Starter1_F": 0x3a465,
"Rival_Starter2_G": 0x3a473,
"Rival_Starter3_G": 0x3a481,
"Rival_Starter1_G": 0x3a48f,
"Rival_Starter2_H": 0x3a49d,
"Rival_Starter3_H": 0x3a4ab,
"Rival_Starter1_H": 0x3a4b9,
"Trainer_Data_End": 0x3a52e,
"Learnset_Rhydon": 0x3b1d9,
"Learnset_Kangaskhan": 0x3b1e7,
"Learnset_NidoranM": 0x3b1f6,
"Learnset_Clefairy": 0x3b208,
"Learnset_Spearow": 0x3b219,
"Learnset_Voltorb": 0x3b228,
"Learnset_Nidoking": 0x3b234,
"Learnset_Slowbro": 0x3b23c,
"Learnset_Ivysaur": 0x3b24f,
"Learnset_Exeggutor": 0x3b25f,
"Learnset_Lickitung": 0x3b263,
"Learnset_Exeggcute": 0x3b273,
"Learnset_Grimer": 0x3b284,
"Learnset_Gengar": 0x3b292,
"Learnset_NidoranF": 0x3b29b,
"Learnset_Nidoqueen": 0x3b2a9,
"Learnset_Cubone": 0x3b2b4,
"Learnset_Rhyhorn": 0x3b2c3,
"Learnset_Lapras": 0x3b2d1,
"Learnset_Mew": 0x3b2e1,
"Learnset_Gyarados": 0x3b2eb,
"Learnset_Shellder": 0x3b2fb,
"Learnset_Tentacool": 0x3b30a,
"Learnset_Gastly": 0x3b31f,
"Learnset_Scyther": 0x3b325,
"Learnset_Staryu": 0x3b337,
"Learnset_Blastoise": 0x3b347,
"Learnset_Pinsir": 0x3b355,
"Learnset_Tangela": 0x3b363,
"Learnset_Growlithe": 0x3b379,
"Learnset_Onix": 0x3b385,
"Learnset_Fearow": 0x3b391,
"Learnset_Pidgey": 0x3b3a0,
"Learnset_Slowpoke": 0x3b3b1,
"Learnset_Kadabra": 0x3b3c9,
"Learnset_Graveler": 0x3b3e1,
"Learnset_Chansey": 0x3b3ef,
"Learnset_Machoke": 0x3b407,
"Learnset_MrMime": 0x3b413,
"Learnset_Hitmonlee": 0x3b41f,
"Learnset_Hitmonchan": 0x3b42b,
"Learnset_Arbok": 0x3b437,
"Learnset_Parasect": 0x3b443,
"Learnset_Psyduck": 0x3b452,
"Learnset_Drowzee": 0x3b461,
"Learnset_Golem": 0x3b46f,
"Learnset_Magmar": 0x3b47f,
"Learnset_Electabuzz": 0x3b48f,
"Learnset_Magneton": 0x3b49b,
"Learnset_Koffing": 0x3b4ac,
"Learnset_Mankey": 0x3b4bd,
"Learnset_Seel": 0x3b4cc,
"Learnset_Diglett": 0x3b4db,
"Learnset_Tauros": 0x3b4e7,
"Learnset_Farfetchd": 0x3b4f9,
"Learnset_Venonat": 0x3b508,
"Learnset_Dragonite": 0x3b516,
"Learnset_Doduo": 0x3b52b,
"Learnset_Poliwag": 0x3b53c,
"Learnset_Jynx": 0x3b54a,
"Learnset_Moltres": 0x3b558,
"Learnset_Articuno": 0x3b560,
"Learnset_Zapdos": 0x3b568,
"Learnset_Meowth": 0x3b575,
"Learnset_Krabby": 0x3b584,
"Learnset_Vulpix": 0x3b59a,
"Learnset_Pikachu": 0x3b5ac,
"Learnset_Dratini": 0x3b5c1,
"Learnset_Dragonair": 0x3b5d0,
"Learnset_Kabuto": 0x3b5df,
"Learnset_Kabutops": 0x3b5e9,
"Learnset_Horsea": 0x3b5f6,
"Learnset_Seadra": 0x3b602,
"Learnset_Sandshrew": 0x3b615,
"Learnset_Sandslash": 0x3b621,
"Learnset_Omanyte": 0x3b630,
"Learnset_Omastar": 0x3b63a,
"Learnset_Jigglypuff": 0x3b648,
"Learnset_Eevee": 0x3b666,
"Learnset_Flareon": 0x3b670,
"Learnset_Jolteon": 0x3b682,
"Learnset_Vaporeon": 0x3b694,
"Learnset_Machop": 0x3b6a9,
"Learnset_Zubat": 0x3b6b8,
"Learnset_Ekans": 0x3b6c7,
"Learnset_Paras": 0x3b6d6,
"Learnset_Poliwhirl": 0x3b6e6,
"Learnset_Poliwrath": 0x3b6f4,
"Learnset_Beedrill": 0x3b704,
"Learnset_Dodrio": 0x3b714,
"Learnset_Primeape": 0x3b722,
"Learnset_Dugtrio": 0x3b72e,
"Learnset_Venomoth": 0x3b73a,
"Learnset_Dewgong": 0x3b748,
"Learnset_Butterfree": 0x3b762,
"Learnset_Machamp": 0x3b772,
"Learnset_Golduck": 0x3b780,
"Learnset_Hypno": 0x3b78c,
"Learnset_Golbat": 0x3b79a,
"Learnset_Mewtwo": 0x3b7a6,
"Learnset_Snorlax": 0x3b7b2,
"Learnset_Magikarp": 0x3b7bf,
"Learnset_Muk": 0x3b7c7,
"Learnset_Kingler": 0x3b7d7,
"Learnset_Cloyster": 0x3b7e3,
"Learnset_Electrode": 0x3b7e9,
"Learnset_Weezing": 0x3b7f7,
"Learnset_Persian": 0x3b803,
"Learnset_Marowak": 0x3b80f,
"Learnset_Haunter": 0x3b827,
"Learnset_Alakazam": 0x3b832,
"Learnset_Pidgeotto": 0x3b843,
"Learnset_Pidgeot": 0x3b851,
"Learnset_Bulbasaur": 0x3b864,
"Learnset_Venusaur": 0x3b874,
"Learnset_Tentacruel": 0x3b884,
"Learnset_Goldeen": 0x3b89b,
"Learnset_Seaking": 0x3b8a9,
"Learnset_Ponyta": 0x3b8c2,
"Learnset_Rapidash": 0x3b8d0,
"Learnset_Rattata": 0x3b8e1,
"Learnset_Raticate": 0x3b8eb,
"Learnset_Nidorino": 0x3b8f9,
"Learnset_Nidorina": 0x3b90b,
"Learnset_Geodude": 0x3b91c,
"Learnset_Porygon": 0x3b92a,
"Learnset_Aerodactyl": 0x3b934,
"Learnset_Magnemite": 0x3b942,
"Learnset_Charmander": 0x3b957,
"Learnset_Squirtle": 0x3b968,
"Learnset_Charmeleon": 0x3b979,
"Learnset_Wartortle": 0x3b98a,
"Learnset_Charizard": 0x3b998,
"Learnset_Oddish": 0x3b9b1,
"Learnset_Gloom": 0x3b9c3,
"Learnset_Vileplume": 0x3b9d1,
"Learnset_Bellsprout": 0x3b9dc,
"Learnset_Weepinbell": 0x3b9f0,
"Learnset_Victreebel": 0x3ba00,
"Type_Chart": 0x3e4b6,
"Type_Chart_Divider": 0x3e5ac,
"Ghost_Battle3": 0x3efd9,
"Missable_Pokemon_Mansion_1F_Item_1": 0x443d6,
"Missable_Pokemon_Mansion_1F_Item_2": 0x443dd,
"Map_Rock_TunnelF": 0x44676,
"Missable_Victory_Road_3F_Item_1": 0x44b07,
"Missable_Victory_Road_3F_Item_2": 0x44b0e,
"Missable_Rocket_Hideout_B1F_Item_1": 0x44d2d,
"Missable_Rocket_Hideout_B1F_Item_2": 0x44d34,
"Missable_Rocket_Hideout_B2F_Item_1": 0x4511d,
"Missable_Rocket_Hideout_B2F_Item_2": 0x45124,
"Missable_Rocket_Hideout_B2F_Item_3": 0x4512b,
"Missable_Rocket_Hideout_B2F_Item_4": 0x45132,
"Missable_Rocket_Hideout_B3F_Item_1": 0x4536f,
"Missable_Rocket_Hideout_B3F_Item_2": 0x45376,
"Missable_Rocket_Hideout_B4F_Item_1": 0x45627,
"Missable_Rocket_Hideout_B4F_Item_2": 0x4562e,
"Missable_Rocket_Hideout_B4F_Item_3": 0x45635,
"Missable_Rocket_Hideout_B4F_Item_4": 0x4563c,
"Missable_Rocket_Hideout_B4F_Item_5": 0x45643,
"Missable_Safari_Zone_East_Item_1": 0x458b2,
"Missable_Safari_Zone_East_Item_2": 0x458b9,
"Missable_Safari_Zone_East_Item_3": 0x458c0,
"Missable_Safari_Zone_East_Item_4": 0x458c7,
"Missable_Safari_Zone_North_Item_1": 0x45a12,
"Missable_Safari_Zone_North_Item_2": 0x45a19,
"Missable_Safari_Zone_Center_Item": 0x45bf9,
"Missable_Cerulean_Cave_2F_Item_1": 0x45e36,
"Missable_Cerulean_Cave_2F_Item_2": 0x45e3d,
"Missable_Cerulean_Cave_2F_Item_3": 0x45e44,
"Static_Encounter_Mewtwo": 0x45f44,
"Missable_Cerulean_Cave_B1F_Item_1": 0x45f4c,
"Missable_Cerulean_Cave_B1F_Item_2": 0x45f53,
"Missable_Rock_Tunnel_B1F_Item_1": 0x4619f,
"Missable_Rock_Tunnel_B1F_Item_2": 0x461a6,
"Missable_Rock_Tunnel_B1F_Item_3": 0x461ad,
"Missable_Rock_Tunnel_B1F_Item_4": 0x461b4,
"Static_Encounter_Articuno": 0x4690c,
"Hidden_Item_Viridian_Forest_1": 0x46e6d,
"Hidden_Item_Viridian_Forest_2": 0x46e73,
"Hidden_Item_MtMoonB2F_1": 0x46e7a,
"Hidden_Item_MtMoonB2F_2": 0x46e80,
"Hidden_Item_Route_25_1": 0x46e94,
"Hidden_Item_Route_25_2": 0x46e9a,
"Hidden_Item_Route_9": 0x46ea1,
"Hidden_Item_SS_Anne_Kitchen": 0x46eb4,
"Hidden_Item_SS_Anne_B1F": 0x46ebb,
"Hidden_Item_Route_10_1": 0x46ec2,
"Hidden_Item_Route_10_2": 0x46ec8,
"Hidden_Item_Rocket_Hideout_B1F": 0x46ecf,
"Hidden_Item_Rocket_Hideout_B3F": 0x46ed6,
"Hidden_Item_Rocket_Hideout_B4F": 0x46edd,
"Hidden_Item_Pokemon_Tower_5F": 0x46ef1,
"Hidden_Item_Route_13_1": 0x46ef8,
"Hidden_Item_Route_13_2": 0x46efe,
"Hidden_Item_Safari_Zone_West": 0x46f0c,
"Hidden_Item_Silph_Co_5F": 0x46f13,
"Hidden_Item_Silph_Co_9F": 0x46f1a,
"Hidden_Item_Copycats_House": 0x46f21,
"Hidden_Item_Cerulean_Cave_1F": 0x46f28,
"Hidden_Item_Cerulean_Cave_B1F": 0x46f2f,
"Hidden_Item_Power_Plant_1": 0x46f36,
"Hidden_Item_Power_Plant_2": 0x46f3c,
"Hidden_Item_Seafoam_Islands_B2F": 0x46f43,
"Hidden_Item_Seafoam_Islands_B4F": 0x46f4a,
"Hidden_Item_Pokemon_Mansion_1F": 0x46f51,
"Hidden_Item_Pokemon_Mansion_3F": 0x46f65,
"Hidden_Item_Pokemon_Mansion_B1F": 0x46f72,
"Hidden_Item_Route_23_1": 0x46f85,
"Hidden_Item_Route_23_2": 0x46f8b,
"Hidden_Item_Route_23_3": 0x46f91,
"Hidden_Item_Victory_Road_2F_1": 0x46f98,
"Hidden_Item_Victory_Road_2F_2": 0x46f9e,
"Hidden_Item_Unused_6F": 0x46fa5,
"Hidden_Item_Viridian_City": 0x46fb3,
"Hidden_Item_Route_11": 0x47060,
"Hidden_Item_Route_12": 0x47067,
"Hidden_Item_Route_17_1": 0x47075,
"Hidden_Item_Route_17_2": 0x4707b,
"Hidden_Item_Route_17_3": 0x47081,
"Hidden_Item_Route_17_4": 0x47087,
"Hidden_Item_Route_17_5": 0x4708d,
"Hidden_Item_Underground_Path_NS_1": 0x47094,
"Hidden_Item_Underground_Path_NS_2": 0x4709a,
"Hidden_Item_Underground_Path_WE_1": 0x470a1,
"Hidden_Item_Underground_Path_WE_2": 0x470a7,
"Hidden_Item_Celadon_City": 0x470ae,
"Hidden_Item_Seafoam_Islands_B3F": 0x470b5,
"Hidden_Item_Vermilion_City": 0x470bc,
"Hidden_Item_Cerulean_City": 0x470c3,
"Hidden_Item_Route_4": 0x470ca,
"Event_Counter": 0x482d3,
"Event_Thirsty_Girl_Lemonade": 0x484f9,
"Event_Thirsty_Girl_Soda": 0x4851d,
"Event_Thirsty_Girl_Water": 0x48541,
"Option_Tea": 0x4871d,
"Event_Mansion_Lady": 0x4872a,
"Badge_Celadon_Gym": 0x48a1b,
"Event_Celadon_Gym": 0x48a2f,
"Event_Gambling_Addict": 0x49293,
"Gift_Magikarp": 0x49430,
"Option_Aide_Rt11": 0x4958d,
"Event_Rt11_Oaks_Aide": 0x49591,
"Event_Mourning_Girl": 0x4968b,
"Option_Aide_Rt15": 0x49776,
"Event_Rt_15_Oaks_Aide": 0x4977a,
"Missable_Mt_Moon_1F_Item_1": 0x49c75,
"Missable_Mt_Moon_1F_Item_2": 0x49c7c,
"Missable_Mt_Moon_1F_Item_3": 0x49c83,
"Missable_Mt_Moon_1F_Item_4": 0x49c8a,
"Missable_Mt_Moon_1F_Item_5": 0x49c91,
"Missable_Mt_Moon_1F_Item_6": 0x49c98,
"Dome_Fossil_Text": 0x4a001,
"Event_Dome_Fossil": 0x4a021,
"Helix_Fossil_Text": 0x4a05d,
"Event_Helix_Fossil": 0x4a07d,
"Missable_Mt_Moon_B2F_Item_1": 0x4a166,
"Missable_Mt_Moon_B2F_Item_2": 0x4a16d,
"Missable_Safari_Zone_West_Item_1": 0x4a34f,
"Missable_Safari_Zone_West_Item_2": 0x4a356,
"Missable_Safari_Zone_West_Item_3": 0x4a35d,
"Missable_Safari_Zone_West_Item_4": 0x4a364,
"Event_Safari_Zone_Secret_House": 0x4a469,
"Missable_Route_24_Item": 0x506e6,
"Missable_Route_25_Item": 0x5080b,
"Starter2_B": 0x50fce,
"Starter3_B": 0x50fd0,
"Starter1_B": 0x50fd2,
"Starter2_A": 0x510f1,
"Starter3_A": 0x510f3,
"Starter1_A": 0x510f5,
"Option_Badge_Goal": 0x51317,
"Event_Nugget_Bridge": 0x5148f,
"Static_Encounter_Moltres": 0x51939,
"Missable_Victory_Road_2F_Item_1": 0x51941,
"Missable_Victory_Road_2F_Item_2": 0x51948,
"Missable_Victory_Road_2F_Item_3": 0x5194f,
"Missable_Victory_Road_2F_Item_4": 0x51956,
"Starter2_L": 0x51c85,
"Starter3_L": 0x51c8d,
"Gift_Lapras": 0x51d83,
"Missable_Silph_Co_7F_Item_1": 0x51f0d,
"Missable_Silph_Co_7F_Item_2": 0x51f14,
"Missable_Pokemon_Mansion_2F_Item": 0x520c9,
"Missable_Pokemon_Mansion_3F_Item_1": 0x522e2,
"Missable_Pokemon_Mansion_3F_Item_2": 0x522e9,
"Missable_Pokemon_Mansion_B1F_Item_1": 0x5248c,
"Missable_Pokemon_Mansion_B1F_Item_2": 0x52493,
"Missable_Pokemon_Mansion_B1F_Item_3": 0x5249a,
"Missable_Pokemon_Mansion_B1F_Item_4": 0x524a1,
"Missable_Pokemon_Mansion_B1F_Item_5": 0x524ae,
"Option_Safari_Zone_Battle_Type": 0x525c3,
"Prize_Mon_A2": 0x5282f,
"Prize_Mon_B2": 0x52830,
"Prize_Mon_C2": 0x52831,
"Prize_Mon_D2": 0x5283a,
"Prize_Mon_E2": 0x5283b,
"Prize_Mon_F2": 0x5283c,
"Prize_Mon_A": 0x52960,
"Prize_Mon_B": 0x52962,
"Prize_Mon_C": 0x52964,
"Prize_Mon_D": 0x52966,
"Prize_Mon_E": 0x52968,
"Prize_Mon_F": 0x5296a,
"Missable_Route_2_Item_1": 0x5404a,
"Missable_Route_2_Item_2": 0x54051,
"Missable_Route_4_Item": 0x543df,
"Missable_Route_9_Item": 0x546fd,
"Option_EXP_Modifier": 0x552c5,
"Rod_Vermilion_City_Fishing_Guru": 0x560df,
"Rod_Fuchsia_City_Fishing_Brother": 0x561eb,
"Rod_Route12_Fishing_Brother": 0x564ee,
"Missable_Route_12_Item_1": 0x58704,
"Missable_Route_12_Item_2": 0x5870b,
"Missable_Route_15_Item": 0x589c7,
"Ghost_Battle6": 0x58df0,
"Static_Encounter_Snorlax_A": 0x5969b,
"Static_Encounter_Snorlax_B": 0x599db,
"Event_Pokemon_Fan_Club": 0x59c8b,
"Event_Scared_Woman": 0x59e1f,
"Missable_Silph_Co_3F_Item": 0x5a0cb,
"Missable_Silph_Co_10F_Item_1": 0x5a281,
"Missable_Silph_Co_10F_Item_2": 0x5a288,
"Missable_Silph_Co_10F_Item_3": 0x5a28f,
"Guard_Drink_List": 0x5a600,
"Event_Museum": 0x5c266,
"Badge_Pewter_Gym": 0x5c3ed,
"Event_Pewter_Gym": 0x5c401,
"Badge_Cerulean_Gym": 0x5c716,
"Event_Cerulean_Gym": 0x5c72a,
"Badge_Vermilion_Gym": 0x5caba,
"Event_Vermillion_Gym": 0x5cace,
"Event_Copycat": 0x5cca9,
"Gift_Hitmonlee": 0x5cf1a,
"Gift_Hitmonchan": 0x5cf62,
"Badge_Saffron_Gym": 0x5d079,
"Event_Saffron_Gym": 0x5d08d,
"Option_Aide_Rt2": 0x5d5f2,
"Event_Route_2_Oaks_Aide": 0x5d5f6,
"Missable_Victory_Road_1F_Item_1": 0x5dae6,
"Missable_Victory_Road_1F_Item_2": 0x5daed,
"Starter2_J": 0x6060e,
"Starter3_J": 0x60616,
"Missable_Pokemon_Tower_3F_Item": 0x60787,
"Missable_Pokemon_Tower_4F_Item_1": 0x608b5,
"Missable_Pokemon_Tower_4F_Item_2": 0x608bc,
"Missable_Pokemon_Tower_4F_Item_3": 0x608c3,
"Missable_Pokemon_Tower_5F_Item": 0x60a80,
"Ghost_Battle1": 0x60b33,
"Ghost_Battle2": 0x60c0a,
"Missable_Pokemon_Tower_6F_Item_1": 0x60c85,
"Missable_Pokemon_Tower_6F_Item_2": 0x60c8c,
"Gift_Aerodactyl": 0x61064,
"Gift_Omanyte": 0x61068,
"Gift_Kabuto": 0x6106c,
"Missable_Viridian_Forest_Item_1": 0x6122c,
"Missable_Viridian_Forest_Item_2": 0x61233,
"Missable_Viridian_Forest_Item_3": 0x6123a,
"Starter2_M": 0x61450,
"Starter3_M": 0x61458,
"Event_SS_Anne_Captain": 0x618c3,
"Missable_SS_Anne_1F_Item": 0x61ac0,
"Missable_SS_Anne_2F_Item_1": 0x61ced,
"Missable_SS_Anne_2F_Item_2": 0x61d00,
"Missable_SS_Anne_B1F_Item_1": 0x61ee3,
"Missable_SS_Anne_B1F_Item_2": 0x61eea,
"Missable_SS_Anne_B1F_Item_3": 0x61ef1,
"Event_Silph_Co_President": 0x622ed,
"Ghost_Battle4": 0x708e1,
"Badge_Viridian_Gym": 0x749ca,
"Event_Viridian_Gym": 0x749de,
"Missable_Viridian_Gym_Item": 0x74c63,
"Missable_Cerulean_Cave_1F_Item_1": 0x74d68,
"Missable_Cerulean_Cave_1F_Item_2": 0x74d6f,
"Missable_Cerulean_Cave_1F_Item_3": 0x74d76,
"Event_Warden": 0x7512a,
"Missable_Wardens_House_Item": 0x751b7,
"Badge_Fuchsia_Gym": 0x755cd,
"Event_Fuschia_Gym": 0x755e1,
"Badge_Cinnabar_Gym": 0x75995,
"Event_Cinnabar_Gym": 0x759a9,
"Event_Lab_Scientist": 0x75dd6,
"Fossils_Needed_For_Second_Item": 0x75ea3,
"Event_Dome_Fossil_B": 0x75f20,
"Event_Helix_Fossil_B": 0x75f40,
"Starter2_N": 0x76169,
"Starter3_N": 0x76171,
"Option_Itemfinder": 0x76864,
"Text_Badges_Needed": 0x92304,
"Badge_Text_Boulder_Badge": 0x990b3,
"Badge_Text_Cascade_Badge": 0x990cb,
"Badge_Text_Thunder_Badge": 0x99111,
"Badge_Text_Rainbow_Badge": 0x9912e,
"Badge_Text_Soul_Badge": 0x99177,
"Badge_Text_Marsh_Badge": 0x9918c,
"Badge_Text_Volcano_Badge": 0x991d6,
"Badge_Text_Earth_Badge": 0x991f3,
}

165
worlds/pokemon_rb/rules.py Normal file
View File

@ -0,0 +1,165 @@
from ..generic.Rules import add_item_rule, add_rule
def set_rules(world, player):
add_item_rule(world.get_location("Pallet Town - Player's PC", player),
lambda i: i.player == player and "Badge" not in i.name)
access_rules = {
"Pallet Town - Rival's Sister": lambda state: state.has("Oak's Parcel", player),
"Pallet Town - Oak's Post-Route-22-Rival Gift": lambda state: state.has("Oak's Parcel", player),
"Viridian City - Sleepy Guy": lambda state: state.pokemon_rb_can_cut(player) or state.pokemon_rb_can_surf(player),
"Route 2 - Oak's Aide": lambda state: state.pokemon_rb_has_pokemon(state.world.oaks_aide_rt_2[player].value + 5, player),
"Pewter City - Museum": lambda state: state.pokemon_rb_can_cut(player),
"Cerulean City - Bicycle Shop": lambda state: state.has("Bike Voucher", player),
"Lavender Town - Mr. Fuji": lambda state: state.has("Fuji Saved", player),
"Vermilion Gym - Lt. Surge 1": lambda state: state.pokemon_rb_can_cut(player or state.pokemon_rb_can_surf(player)),
"Vermilion Gym - Lt. Surge 2": lambda state: state.pokemon_rb_can_cut(player or state.pokemon_rb_can_surf(player)),
"Route 11 - Oak's Aide": lambda state: state.pokemon_rb_has_pokemon(state.world.oaks_aide_rt_11[player].value + 5, player),
"Celadon City - Stranded Man": lambda state: state.pokemon_rb_can_surf(player),
"Silph Co 11F - Silph Co President": lambda state: state.has("Card Key", player),
"Fuchsia City - Safari Zone Warden": lambda state: state.has("Gold Teeth", player),
"Route 12 - Island Item": lambda state: state.pokemon_rb_can_surf(player),
"Route 12 - Item Behind Cuttable Tree": lambda state: state.pokemon_rb_can_cut(player),
"Route 15 - Item": lambda state: state.pokemon_rb_can_cut(player),
"Route 25 - Item": lambda state: state.pokemon_rb_can_cut(player),
"Fuchsia City - Warden's House Item": lambda state: state.pokemon_rb_can_strength(player),
"Rocket Hideout B4F - Southwest Item (Lift Key)": lambda state: state.has("Lift Key", player),
"Rocket Hideout B4F - Giovanni Item (Lift Key)": lambda state: state.has("Lift Key", player),
"Silph Co 3F - Item (Card Key)": lambda state: state.has("Card Key", player),
"Silph Co 4F - Left Item (Card Key)": lambda state: state.has("Card Key", player),
"Silph Co 4F - Middle Item (Card Key)": lambda state: state.has("Card Key", player),
"Silph Co 4F - Right Item (Card Key)": lambda state: state.has("Card Key", player),
"Silph Co 5F - Northwest Item (Card Key)": lambda state: state.has("Card Key", player),
"Silph Co 6F - West Item (Card Key)": lambda state: state.has("Card Key", player),
"Silph Co 6F - Southwest Item (Card Key)": lambda state: state.has("Card Key", player),
"Silph Co 7F - East Item (Card Key)": lambda state: state.has("Card Key", player),
"Safari Zone Center - Island Item": lambda state: state.pokemon_rb_can_surf(player),
"Silph Co 11F - Silph Co Liberated": lambda state: state.has("Card Key", player),
"Pallet Town - Super Rod Pokemon - 1": lambda state: state.has("Super Rod", player),
"Pallet Town - Super Rod Pokemon - 2": lambda state: state.has("Super Rod", player),
"Route 22 - Super Rod Pokemon - 1": lambda state: state.has("Super Rod", player),
"Route 22 - Super Rod Pokemon - 2": lambda state: state.has("Super Rod", player),
"Route 24 - Super Rod Pokemon - 1": lambda state: state.has("Super Rod", player),
"Route 24 - Super Rod Pokemon - 2": lambda state: state.has("Super Rod", player),
"Route 24 - Super Rod Pokemon - 3": lambda state: state.has("Super Rod", player),
"Route 6 - Super Rod Pokemon - 1": lambda state: state.has("Super Rod", player),
"Route 6 - Super Rod Pokemon - 2": lambda state: state.has("Super Rod", player),
"Route 10 - Super Rod Pokemon - 1": lambda state: state.has("Super Rod", player),
"Route 10 - Super Rod Pokemon - 2": lambda state: state.has("Super Rod", player),
"Safari Zone Center - Super Rod Pokemon - 1": lambda state: state.has("Super Rod", player),
"Safari Zone Center - Super Rod Pokemon - 2": lambda state: state.has("Super Rod", player),
"Safari Zone Center - Super Rod Pokemon - 3": lambda state: state.has("Super Rod", player),
"Safari Zone Center - Super Rod Pokemon - 4": lambda state: state.has("Super Rod", player),
"Route 12 - Super Rod Pokemon - 1": lambda state: state.has("Super Rod", player),
"Route 12 - Super Rod Pokemon - 2": lambda state: state.has("Super Rod", player),
"Route 12 - Super Rod Pokemon - 3": lambda state: state.has("Super Rod", player),
"Route 12 - Super Rod Pokemon - 4": lambda state: state.has("Super Rod", player),
"Route 19 - Super Rod Pokemon - 1": lambda state: state.has("Super Rod", player),
"Route 19 - Super Rod Pokemon - 2": lambda state: state.has("Super Rod", player),
"Route 19 - Super Rod Pokemon - 3": lambda state: state.has("Super Rod", player),
"Route 19 - Super Rod Pokemon - 4": lambda state: state.has("Super Rod", player),
"Route 23 - Super Rod Pokemon - 1": lambda state: state.has("Super Rod", player),
"Route 23 - Super Rod Pokemon - 2": lambda state: state.has("Super Rod", player),
"Route 23 - Super Rod Pokemon - 3": lambda state: state.has("Super Rod", player),
"Route 23 - Super Rod Pokemon - 4": lambda state: state.has("Super Rod", player),
"Fuchsia City - Super Rod Pokemon - 1": lambda state: state.has("Super Rod", player),
"Fuchsia City - Super Rod Pokemon - 2": lambda state: state.has("Super Rod", player),
"Fuchsia City - Super Rod Pokemon - 3": lambda state: state.has("Super Rod", player),
"Fuchsia City - Super Rod Pokemon - 4": lambda state: state.has("Super Rod", player),
"Anywhere - Good Rod Pokemon - 1": lambda state: state.has("Good Rod", player),
"Anywhere - Good Rod Pokemon - 2": lambda state: state.has("Good Rod", player),
"Anywhere - Old Rod Pokemon": lambda state: state.has("Old Rod", player),
"Celadon Prize Corner - Pokemon Prize - 1": lambda state: state.has("Coin Case", player),
"Celadon Prize Corner - Pokemon Prize - 2": lambda state: state.has("Coin Case", player),
"Celadon Prize Corner - Pokemon Prize - 3": lambda state: state.has("Coin Case", player),
"Celadon Prize Corner - Pokemon Prize - 4": lambda state: state.has("Coin Case", player),
"Celadon Prize Corner - Pokemon Prize - 5": lambda state: state.has("Coin Case", player),
"Celadon Prize Corner - Pokemon Prize - 6": lambda state: state.has("Coin Case", player),
"Cinnabar Island - Old Amber Pokemon": lambda state: state.has("Old Amber", player),
"Cinnabar Island - Helix Fossil Pokemon": lambda state: state.has("Helix Fossil", player),
"Cinnabar Island - Dome Fossil Pokemon": lambda state: state.has("Dome Fossil", player),
"Route 12 - Sleeping Pokemon": lambda state: state.has("Poke Flute", player),
"Route 16 - Sleeping Pokemon": lambda state: state.has("Poke Flute", player),
"Seafoam Islands B4F - Legendary Pokemon": lambda state: state.pokemon_rb_can_strength(player),
"Vermilion City - Legendary Pokemon": lambda state: state.pokemon_rb_can_surf(player) and state.has("S.S. Ticket", player)
}
hidden_item_access_rules = {
"Viridian Forest - Hidden Item Northwest by Trainer": lambda state: state.pokemon_rb_can_get_hidden_items(
player),
"Viridian Forest - Hidden Item Entrance Tree": lambda state: state.pokemon_rb_can_get_hidden_items(player),
"Mt Moon B2F - Hidden Item Dead End Before Fossils": lambda state: state.pokemon_rb_can_get_hidden_items(
player),
"Route 25 - Hidden Item Fence Outside Bill's House": lambda state: state.pokemon_rb_can_get_hidden_items(
player),
"Route 9 - Hidden Item Rock By Grass": lambda state: state.pokemon_rb_can_get_hidden_items(player),
"S.S. Anne 1F - Hidden Item Kitchen Trash": lambda state: state.pokemon_rb_can_get_hidden_items(player),
"S.S. Anne B1F - Hidden Item Under Pillow": lambda state: state.pokemon_rb_can_get_hidden_items(player),
"Route 10 - Hidden Item Behind Rock Tunnel Entrance Tree": lambda
state: state.pokemon_rb_can_get_hidden_items(player),
"Route 10 - Hidden Item Rock": lambda state: state.pokemon_rb_can_get_hidden_items(player),
"Rocket Hideout B1F - Hidden Item Pot Plant": lambda state: state.pokemon_rb_can_get_hidden_items(player),
"Rocket Hideout B3F - Hidden Item Near East Item": lambda state: state.pokemon_rb_can_get_hidden_items(player),
"Rocket Hideout B4F - Hidden Item Behind Giovanni": lambda state: state.pokemon_rb_can_get_hidden_items(player),
"Pokemon Tower 5F - Hidden Item Near West Staircase": lambda state: state.pokemon_rb_can_get_hidden_items(
player),
"Route 13 - Hidden Item Dead End Boulder": lambda state: state.pokemon_rb_can_get_hidden_items(player),
"Route 13 - Hidden Item Dead End By Water Corner": lambda state: state.pokemon_rb_can_get_hidden_items(player),
"Pokemon Mansion B1F - Hidden Item Secret Key Room Corner": lambda state: state.pokemon_rb_can_get_hidden_items(
player),
"Safari Zone West - Hidden Item Secret House Statue": lambda state: state.pokemon_rb_can_get_hidden_items(
player),
"Silph Co 5F - Hidden Item Pot Plant": lambda state: state.pokemon_rb_can_get_hidden_items(player),
"Silph Co 9F - Hidden Item Nurse Bed": lambda state: state.pokemon_rb_can_get_hidden_items(player),
"Copycat's House - Hidden Item Desk": lambda state: state.pokemon_rb_can_get_hidden_items(player),
"Cerulean Cave 1F - Hidden Item Center Rocks": lambda state: state.pokemon_rb_can_get_hidden_items(player),
"Cerulean Cave B1F - Hidden Item Northeast Rocks": lambda state: state.pokemon_rb_can_get_hidden_items(player),
"Power Plant - Hidden Item Central Dead End": lambda state: state.pokemon_rb_can_get_hidden_items(player),
"Power Plant - Hidden Item Before Zapdos": lambda state: state.pokemon_rb_can_get_hidden_items(player),
"Seafoam Islands B2F - Hidden Item Rock": lambda state: state.pokemon_rb_can_get_hidden_items(player),
"Seafoam Islands B4F - Hidden Item Corner Island": lambda state: state.pokemon_rb_can_get_hidden_items(player),
"Pokemon Mansion 1F - Hidden Item Block Near Entrance Carpet": lambda
state: state.pokemon_rb_can_get_hidden_items(player),
"Pokemon Mansion 3F - Hidden Item Behind Burglar": lambda state: state.pokemon_rb_can_get_hidden_items(player),
"Route 23 - Hidden Item Rocks Before Final Guard": lambda state: state.pokemon_rb_can_get_hidden_items(
player),
"Route 23 - Hidden Item East Tree After Water": lambda state: state.pokemon_rb_can_get_hidden_items(
player),
"Route 23 - Hidden Item On Island": lambda state: state.pokemon_rb_can_get_hidden_items(player),
"Victory Road 2F - Hidden Item Rock Before Moltres": lambda state: state.pokemon_rb_can_get_hidden_items(
player),
"Victory Road 2F - Hidden Item Rock In Final Room": lambda state: state.pokemon_rb_can_get_hidden_items(player),
"Viridian City - Hidden Item Cuttable Tree": lambda state: state.pokemon_rb_can_get_hidden_items(player),
"Route 11 - Hidden Item Isolated Tree Near Gate": lambda state: state.pokemon_rb_can_get_hidden_items(player),
"Route 12 - Hidden Item Tree Near Gate": lambda state: state.pokemon_rb_can_get_hidden_items(player),
"Route 17 - Hidden Item In Grass": lambda state: state.pokemon_rb_can_get_hidden_items(player),
"Route 17 - Hidden Item Near Northernmost Sign": lambda state: state.pokemon_rb_can_get_hidden_items(player),
"Route 17 - Hidden Item East Center": lambda state: state.pokemon_rb_can_get_hidden_items(player),
"Route 17 - Hidden Item West Center": lambda state: state.pokemon_rb_can_get_hidden_items(player),
"Route 17 - Hidden Item Before Final Bridge": lambda state: state.pokemon_rb_can_get_hidden_items(player),
"Underground Tunnel North-South - Hidden Item Near Northern Stairs": lambda
state: state.pokemon_rb_can_get_hidden_items(player),
"Underground Tunnel North-South - Hidden Item Near Southern Stairs": lambda
state: state.pokemon_rb_can_get_hidden_items(player),
"Underground Tunnel West-East - Hidden Item West": lambda state: state.pokemon_rb_can_get_hidden_items(player),
"Underground Tunnel West-East - Hidden Item East": lambda state: state.pokemon_rb_can_get_hidden_items(player),
"Celadon City - Hidden Item Dead End Near Cuttable Tree": lambda state: state.pokemon_rb_can_get_hidden_items(
player),
"Route 25 - Hidden Item Northeast Of Grass": lambda state: state.pokemon_rb_can_get_hidden_items(player),
"Mt Moon B2F - Hidden Item Lone Rock": lambda state: state.pokemon_rb_can_get_hidden_items(player),
"Seafoam Islands B3F - Hidden Item Rock": lambda state: state.pokemon_rb_can_get_hidden_items(player),
"Vermilion City - Hidden Item In Water Near Fan Club": lambda state: state.pokemon_rb_can_get_hidden_items(
player),
"Cerulean City - Hidden Item Gym Badge Guy's Backyard": lambda state: state.pokemon_rb_can_get_hidden_items(
player),
"Route 4 - Hidden Item Plateau East Of Mt Moon": lambda state: state.pokemon_rb_can_get_hidden_items(player),
}
for loc, rule in access_rules.items():
add_rule(world.get_location(loc, player), rule)
if world.randomize_hidden_items[player].value != 0:
for loc, rule in hidden_item_access_rules.items():
add_rule(world.get_location(loc, player), rule)

147
worlds/pokemon_rb/text.py Normal file
View File

@ -0,0 +1,147 @@
special_chars = {
"PKMN": 0x4A,
"'d": 0xBB,
"'l": 0xBC,
"'t": 0xBE,
"'v": 0xBF,
"PK": 0xE1,
"MN": 0xE2,
"'r": 0xE4,
"'m": 0xE5,
"MALE": 0xEF,
"FEMALE": 0xF5,
}
char_map = {
"@": 0x50, # String terminator
"#": 0x54, # Poké
"": 0x70,
"": 0x71,
"": 0x72,
"": 0x73,
"·": 0x74,
"": 0x75,
" ": 0x7F,
"A": 0x80,
"B": 0x81,
"C": 0x82,
"D": 0x83,
"E": 0x84,
"F": 0x85,
"G": 0x86,
"H": 0x87,
"I": 0x88,
"J": 0x89,
"K": 0x8A,
"L": 0x8B,
"M": 0x8C,
"N": 0x8D,
"O": 0x8E,
"P": 0x8F,
"Q": 0x90,
"R": 0x91,
"S": 0x92,
"T": 0x93,
"U": 0x94,
"V": 0x95,
"W": 0x96,
"X": 0x97,
"Y": 0x98,
"Z": 0x99,
"(": 0x9A,
")": 0x9B,
":": 0x9C,
";": 0x9D,
"[": 0x9E,
"]": 0x9F,
"a": 0xA0,
"b": 0xA1,
"c": 0xA2,
"d": 0xA3,
"e": 0xA4,
"f": 0xA5,
"g": 0xA6,
"h": 0xA7,
"i": 0xA8,
"j": 0xA9,
"k": 0xAA,
"l": 0xAB,
"m": 0xAC,
"n": 0xAD,
"o": 0xAE,
"p": 0xAF,
"q": 0xB0,
"r": 0xB1,
"s": 0xB2,
"t": 0xB3,
"u": 0xB4,
"v": 0xB5,
"w": 0xB6,
"x": 0xB7,
"y": 0xB8,
"z": 0xB9,
"é": 0xBA,
"'": 0xE0,
"-": 0xE3,
"?": 0xE6,
"!": 0xE7,
".": 0xE8,
"": 0xEF,
"¥": 0xF0,
"$": 0xF0,
"×": 0xF1,
"/": 0xF3,
",": 0xF4,
"": 0xF5,
"0": 0xF6,
"1": 0xF7,
"2": 0xF8,
"3": 0xF9,
"4": 0xFA,
"5": 0xFB,
"6": 0xFC,
"7": 0xFD,
"8": 0xFE,
"9": 0xFF,
}
unsafe_chars = ["@", "#", "PKMN"]
def encode_text(text: str, length: int=0, whitespace=False, force=False, safety=False):
encoded_text = bytearray()
spec_char = ""
special = False
for char in text:
if char == ">":
if spec_char in unsafe_chars and safety:
raise KeyError(f"Disallowed Pokemon text special character '<{spec_char}>'")
try:
encoded_text.append(special_chars[spec_char])
except KeyError:
if force:
encoded_text.append(char_map[" "])
else:
raise KeyError(f"Invalid Pokemon text special character '<{spec_char}>'")
spec_char = ""
special = False
elif char == "<":
spec_char = ""
special = True
elif special is True:
spec_char += char
else:
if char in unsafe_chars and safety:
raise KeyError(f"Disallowed Pokemon text character '{char}'")
try:
encoded_text.append(char_map[char])
except KeyError:
if force:
encoded_text.append(char_map[" "])
else:
raise KeyError(f"Invalid Pokemon text character '{char}'")
if length > 0:
encoded_text = encoded_text[:length]
while whitespace and len(encoded_text) < length:
encoded_text.append(char_map[" " if whitespace is True else whitespace])
return encoded_text