mirror of
https://github.com/MarioSpore/Grinch-AP.git
synced 2025-10-21 12:11:33 -06:00
FF1: Bizhawk Client and APWorld Support (#4448)
Co-authored-by: beauxq <beauxq@yahoo.com> Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
This commit is contained in:
267
FF1Client.py
267
FF1Client.py
@@ -1,267 +0,0 @@
|
||||
import asyncio
|
||||
import copy
|
||||
import json
|
||||
import time
|
||||
from asyncio import StreamReader, StreamWriter
|
||||
from typing import List
|
||||
|
||||
|
||||
import Utils
|
||||
from Utils import async_start
|
||||
from CommonClient import CommonContext, server_loop, gui_enabled, ClientCommandProcessor, logger, \
|
||||
get_base_parser
|
||||
|
||||
SYSTEM_MESSAGE_ID = 0
|
||||
|
||||
CONNECTION_TIMING_OUT_STATUS = "Connection timing out. Please restart your emulator, then restart connector_ff1.lua"
|
||||
CONNECTION_REFUSED_STATUS = "Connection Refused. Please start your emulator and make sure connector_ff1.lua is running"
|
||||
CONNECTION_RESET_STATUS = "Connection was reset. Please restart your emulator, then restart connector_ff1.lua"
|
||||
CONNECTION_TENTATIVE_STATUS = "Initial Connection Made"
|
||||
CONNECTION_CONNECTED_STATUS = "Connected"
|
||||
CONNECTION_INITIAL_STATUS = "Connection has not been initiated"
|
||||
|
||||
DISPLAY_MSGS = True
|
||||
|
||||
|
||||
class FF1CommandProcessor(ClientCommandProcessor):
|
||||
def __init__(self, ctx: CommonContext):
|
||||
super().__init__(ctx)
|
||||
|
||||
def _cmd_nes(self):
|
||||
"""Check NES Connection State"""
|
||||
if isinstance(self.ctx, FF1Context):
|
||||
logger.info(f"NES Status: {self.ctx.nes_status}")
|
||||
|
||||
def _cmd_toggle_msgs(self):
|
||||
"""Toggle displaying messages in EmuHawk"""
|
||||
global DISPLAY_MSGS
|
||||
DISPLAY_MSGS = not DISPLAY_MSGS
|
||||
logger.info(f"Messages are now {'enabled' if DISPLAY_MSGS else 'disabled'}")
|
||||
|
||||
|
||||
class FF1Context(CommonContext):
|
||||
command_processor = FF1CommandProcessor
|
||||
game = 'Final Fantasy'
|
||||
items_handling = 0b111 # full remote
|
||||
|
||||
def __init__(self, server_address, password):
|
||||
super().__init__(server_address, password)
|
||||
self.nes_streams: (StreamReader, StreamWriter) = None
|
||||
self.nes_sync_task = None
|
||||
self.messages = {}
|
||||
self.locations_array = None
|
||||
self.nes_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(FF1Context, self).server_auth(password_requested)
|
||||
if not self.auth:
|
||||
self.awaiting_rom = True
|
||||
logger.info('Awaiting connection to NES 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':
|
||||
async_start(parse_locations(self.locations_array, self, True))
|
||||
elif cmd == 'Print':
|
||||
msg = args['text']
|
||||
if ': !' not in msg:
|
||||
self._set_message(msg, SYSTEM_MESSAGE_ID)
|
||||
|
||||
def on_print_json(self, args: dict):
|
||||
if self.ui:
|
||||
self.ui.print_json(copy.deepcopy(args["data"]))
|
||||
else:
|
||||
text = self.jsontotextparser(copy.deepcopy(args["data"]))
|
||||
logger.info(text)
|
||||
relevant = args.get("type", None) in {"Hint", "ItemSend"}
|
||||
if relevant:
|
||||
item = args["item"]
|
||||
# goes to this world
|
||||
if self.slot_concerns_self(args["receiving"]):
|
||||
relevant = True
|
||||
# found in this world
|
||||
elif self.slot_concerns_self(item.player):
|
||||
relevant = True
|
||||
# not related
|
||||
else:
|
||||
relevant = False
|
||||
if relevant:
|
||||
item = args["item"]
|
||||
msg = self.raw_text_parser(copy.deepcopy(args["data"]))
|
||||
self._set_message(msg, item.item)
|
||||
|
||||
def run_gui(self):
|
||||
from kvui import GameManager
|
||||
|
||||
class FF1Manager(GameManager):
|
||||
logging_pairs = [
|
||||
("Client", "Archipelago")
|
||||
]
|
||||
base_title = "Archipelago Final Fantasy 1 Client"
|
||||
|
||||
self.ui = FF1Manager(self)
|
||||
self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI")
|
||||
|
||||
|
||||
def get_payload(ctx: FF1Context):
|
||||
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(locations_array: List[int], ctx: FF1Context, force: bool):
|
||||
if locations_array == ctx.locations_array and not force:
|
||||
return
|
||||
else:
|
||||
# print("New values")
|
||||
ctx.locations_array = locations_array
|
||||
locations_checked = []
|
||||
if len(locations_array) > 0xFE and locations_array[0xFE] & 0x02 != 0 and not ctx.finished_game:
|
||||
await ctx.send_msgs([
|
||||
{"cmd": "StatusUpdate",
|
||||
"status": 30}
|
||||
])
|
||||
ctx.finished_game = True
|
||||
for location in ctx.missing_locations:
|
||||
# index will be - 0x100 or 0x200
|
||||
index = location
|
||||
if location < 0x200:
|
||||
# Location is a chest
|
||||
index -= 0x100
|
||||
flag = 0x04
|
||||
else:
|
||||
# Location is an NPC
|
||||
index -= 0x200
|
||||
flag = 0x02
|
||||
|
||||
# print(f"Location: {ctx.location_names[location]}")
|
||||
# print(f"Index: {str(hex(index))}")
|
||||
# print(f"value: {locations_array[index] & flag != 0}")
|
||||
if locations_array[index] & flag != 0:
|
||||
locations_checked.append(location)
|
||||
if locations_checked:
|
||||
# print([ctx.location_names[location] for location in locations_checked])
|
||||
await ctx.send_msgs([
|
||||
{"cmd": "LocationChecks",
|
||||
"locations": locations_checked}
|
||||
])
|
||||
|
||||
|
||||
async def nes_sync_task(ctx: FF1Context):
|
||||
logger.info("Starting nes connector. Use /nes for status information")
|
||||
while not ctx.exit_event.is_set():
|
||||
error_status = None
|
||||
if ctx.nes_streams:
|
||||
(reader, writer) = ctx.nes_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.game is not None and 'locations' in data_decoded:
|
||||
# Not just a keep alive ping, parse
|
||||
async_start(parse_locations(data_decoded['locations'], ctx, False))
|
||||
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. Please regenerate"
|
||||
"the ROM using the same link but adding your slot name")
|
||||
if ctx.awaiting_rom:
|
||||
await ctx.server_auth(False)
|
||||
except asyncio.TimeoutError:
|
||||
logger.debug("Read Timed Out, Reconnecting")
|
||||
error_status = CONNECTION_TIMING_OUT_STATUS
|
||||
writer.close()
|
||||
ctx.nes_streams = None
|
||||
except ConnectionResetError as e:
|
||||
logger.debug("Read failed due to Connection Lost, Reconnecting")
|
||||
error_status = CONNECTION_RESET_STATUS
|
||||
writer.close()
|
||||
ctx.nes_streams = None
|
||||
except TimeoutError:
|
||||
logger.debug("Connection Timed Out, Reconnecting")
|
||||
error_status = CONNECTION_TIMING_OUT_STATUS
|
||||
writer.close()
|
||||
ctx.nes_streams = None
|
||||
except ConnectionResetError:
|
||||
logger.debug("Connection Lost, Reconnecting")
|
||||
error_status = CONNECTION_RESET_STATUS
|
||||
writer.close()
|
||||
ctx.nes_streams = None
|
||||
if ctx.nes_status == CONNECTION_TENTATIVE_STATUS:
|
||||
if not error_status:
|
||||
logger.info("Successfully Connected to NES")
|
||||
ctx.nes_status = CONNECTION_CONNECTED_STATUS
|
||||
else:
|
||||
ctx.nes_status = f"Was tentatively connected but error occured: {error_status}"
|
||||
elif error_status:
|
||||
ctx.nes_status = error_status
|
||||
logger.info("Lost connection to nes and attempting to reconnect. Use /nes for status updates")
|
||||
else:
|
||||
try:
|
||||
logger.debug("Attempting to connect to NES")
|
||||
ctx.nes_streams = await asyncio.wait_for(asyncio.open_connection("localhost", 52980), timeout=10)
|
||||
ctx.nes_status = CONNECTION_TENTATIVE_STATUS
|
||||
except TimeoutError:
|
||||
logger.debug("Connection Timed Out, Trying Again")
|
||||
ctx.nes_status = CONNECTION_TIMING_OUT_STATUS
|
||||
continue
|
||||
except ConnectionRefusedError:
|
||||
logger.debug("Connection Refused, Trying Again")
|
||||
ctx.nes_status = CONNECTION_REFUSED_STATUS
|
||||
continue
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# Text Mode to use !hint and such with games that have no text entry
|
||||
Utils.init_logging("FF1Client")
|
||||
|
||||
options = Utils.get_options()
|
||||
DISPLAY_MSGS = options["ffr_options"]["display_msgs"]
|
||||
|
||||
async def main(args):
|
||||
ctx = FF1Context(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.nes_sync_task = asyncio.create_task(nes_sync_task(ctx), name="NES Sync")
|
||||
|
||||
await ctx.exit_event.wait()
|
||||
ctx.server_address = None
|
||||
|
||||
await ctx.shutdown()
|
||||
|
||||
if ctx.nes_sync_task:
|
||||
await ctx.nes_sync_task
|
||||
|
||||
|
||||
import colorama
|
||||
|
||||
parser = get_base_parser()
|
||||
args = parser.parse_args()
|
||||
colorama.just_fix_windows_console()
|
||||
|
||||
asyncio.run(main(args))
|
||||
colorama.deinit()
|
@@ -1,462 +0,0 @@
|
||||
local socket = require("socket")
|
||||
local json = require('json')
|
||||
local math = require('math')
|
||||
require("common")
|
||||
|
||||
local STATE_OK = "Ok"
|
||||
local STATE_TENTATIVELY_CONNECTED = "Tentatively Connected"
|
||||
local STATE_INITIAL_CONNECTION_MADE = "Initial Connection Made"
|
||||
local STATE_UNINITIALIZED = "Uninitialized"
|
||||
|
||||
local ITEM_INDEX = 0x03
|
||||
local WEAPON_INDEX = 0x07
|
||||
local ARMOR_INDEX = 0x0B
|
||||
|
||||
local goldLookup = {
|
||||
[0x16C] = 10,
|
||||
[0x16D] = 20,
|
||||
[0x16E] = 25,
|
||||
[0x16F] = 30,
|
||||
[0x170] = 55,
|
||||
[0x171] = 70,
|
||||
[0x172] = 85,
|
||||
[0x173] = 110,
|
||||
[0x174] = 135,
|
||||
[0x175] = 155,
|
||||
[0x176] = 160,
|
||||
[0x177] = 180,
|
||||
[0x178] = 240,
|
||||
[0x179] = 255,
|
||||
[0x17A] = 260,
|
||||
[0x17B] = 295,
|
||||
[0x17C] = 300,
|
||||
[0x17D] = 315,
|
||||
[0x17E] = 330,
|
||||
[0x17F] = 350,
|
||||
[0x180] = 385,
|
||||
[0x181] = 400,
|
||||
[0x182] = 450,
|
||||
[0x183] = 500,
|
||||
[0x184] = 530,
|
||||
[0x185] = 575,
|
||||
[0x186] = 620,
|
||||
[0x187] = 680,
|
||||
[0x188] = 750,
|
||||
[0x189] = 795,
|
||||
[0x18A] = 880,
|
||||
[0x18B] = 1020,
|
||||
[0x18C] = 1250,
|
||||
[0x18D] = 1455,
|
||||
[0x18E] = 1520,
|
||||
[0x18F] = 1760,
|
||||
[0x190] = 1975,
|
||||
[0x191] = 2000,
|
||||
[0x192] = 2750,
|
||||
[0x193] = 3400,
|
||||
[0x194] = 4150,
|
||||
[0x195] = 5000,
|
||||
[0x196] = 5450,
|
||||
[0x197] = 6400,
|
||||
[0x198] = 6720,
|
||||
[0x199] = 7340,
|
||||
[0x19A] = 7690,
|
||||
[0x19B] = 7900,
|
||||
[0x19C] = 8135,
|
||||
[0x19D] = 9000,
|
||||
[0x19E] = 9300,
|
||||
[0x19F] = 9500,
|
||||
[0x1A0] = 9900,
|
||||
[0x1A1] = 10000,
|
||||
[0x1A2] = 12350,
|
||||
[0x1A3] = 13000,
|
||||
[0x1A4] = 13450,
|
||||
[0x1A5] = 14050,
|
||||
[0x1A6] = 14720,
|
||||
[0x1A7] = 15000,
|
||||
[0x1A8] = 17490,
|
||||
[0x1A9] = 18010,
|
||||
[0x1AA] = 19990,
|
||||
[0x1AB] = 20000,
|
||||
[0x1AC] = 20010,
|
||||
[0x1AD] = 26000,
|
||||
[0x1AE] = 45000,
|
||||
[0x1AF] = 65000
|
||||
}
|
||||
|
||||
local extensionConsumableLookup = {
|
||||
[432] = 0x3C,
|
||||
[436] = 0x3C,
|
||||
[440] = 0x3C,
|
||||
[433] = 0x3D,
|
||||
[437] = 0x3D,
|
||||
[441] = 0x3D,
|
||||
[434] = 0x3E,
|
||||
[438] = 0x3E,
|
||||
[442] = 0x3E,
|
||||
[435] = 0x3F,
|
||||
[439] = 0x3F,
|
||||
[443] = 0x3F
|
||||
}
|
||||
|
||||
local noOverworldItemsLookup = {
|
||||
[499] = 0x2B,
|
||||
[500] = 0x12,
|
||||
}
|
||||
|
||||
local consumableStacks = nil
|
||||
local prevstate = ""
|
||||
local curstate = STATE_UNINITIALIZED
|
||||
local ff1Socket = nil
|
||||
local frame = 0
|
||||
|
||||
local isNesHawk = false
|
||||
|
||||
|
||||
--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
|
||||
return memDomain
|
||||
end
|
||||
|
||||
local memDomain = defineMemoryFunctions()
|
||||
|
||||
local function StateOKForMainLoop()
|
||||
memDomain.saveram()
|
||||
local A = u8(0x102) -- Party Made
|
||||
local B = u8(0x0FC)
|
||||
local C = u8(0x0A3)
|
||||
return A ~= 0x00 and not (A== 0xF2 and B == 0xF2 and C == 0xF2)
|
||||
end
|
||||
|
||||
function generateLocationChecked()
|
||||
memDomain.saveram()
|
||||
data = uRange(0x01FF, 0x101)
|
||||
data[0] = nil
|
||||
return data
|
||||
end
|
||||
|
||||
function setConsumableStacks()
|
||||
memDomain.rom()
|
||||
consumableStacks = {}
|
||||
-- In order shards, tent, cabin, house, heal, pure, soft, ext1, ext2, ext3, ex4
|
||||
consumableStacks[0x35] = 1
|
||||
consumableStacks[0x36] = u8(0x47400) + 1
|
||||
consumableStacks[0x37] = u8(0x47401) + 1
|
||||
consumableStacks[0x38] = u8(0x47402) + 1
|
||||
consumableStacks[0x39] = u8(0x47403) + 1
|
||||
consumableStacks[0x3A] = u8(0x47404) + 1
|
||||
consumableStacks[0x3B] = u8(0x47405) + 1
|
||||
consumableStacks[0x3C] = u8(0x47406) + 1
|
||||
consumableStacks[0x3D] = u8(0x47407) + 1
|
||||
consumableStacks[0x3E] = u8(0x47408) + 1
|
||||
consumableStacks[0x3F] = u8(0x47409) + 1
|
||||
end
|
||||
|
||||
function getEmptyWeaponSlots()
|
||||
memDomain.saveram()
|
||||
ret = {}
|
||||
count = 1
|
||||
slot1 = uRange(0x118, 0x4)
|
||||
slot2 = uRange(0x158, 0x4)
|
||||
slot3 = uRange(0x198, 0x4)
|
||||
slot4 = uRange(0x1D8, 0x4)
|
||||
for i,v in pairs(slot1) do
|
||||
if v == 0 then
|
||||
ret[count] = 0x118 + i
|
||||
count = count + 1
|
||||
end
|
||||
end
|
||||
for i,v in pairs(slot2) do
|
||||
if v == 0 then
|
||||
ret[count] = 0x158 + i
|
||||
count = count + 1
|
||||
end
|
||||
end
|
||||
for i,v in pairs(slot3) do
|
||||
if v == 0 then
|
||||
ret[count] = 0x198 + i
|
||||
count = count + 1
|
||||
end
|
||||
end
|
||||
for i,v in pairs(slot4) do
|
||||
if v == 0 then
|
||||
ret[count] = 0x1D8 + i
|
||||
count = count + 1
|
||||
end
|
||||
end
|
||||
return ret
|
||||
end
|
||||
|
||||
function getEmptyArmorSlots()
|
||||
memDomain.saveram()
|
||||
ret = {}
|
||||
count = 1
|
||||
slot1 = uRange(0x11C, 0x4)
|
||||
slot2 = uRange(0x15C, 0x4)
|
||||
slot3 = uRange(0x19C, 0x4)
|
||||
slot4 = uRange(0x1DC, 0x4)
|
||||
for i,v in pairs(slot1) do
|
||||
if v == 0 then
|
||||
ret[count] = 0x11C + i
|
||||
count = count + 1
|
||||
end
|
||||
end
|
||||
for i,v in pairs(slot2) do
|
||||
if v == 0 then
|
||||
ret[count] = 0x15C + i
|
||||
count = count + 1
|
||||
end
|
||||
end
|
||||
for i,v in pairs(slot3) do
|
||||
if v == 0 then
|
||||
ret[count] = 0x19C + i
|
||||
count = count + 1
|
||||
end
|
||||
end
|
||||
for i,v in pairs(slot4) do
|
||||
if v == 0 then
|
||||
ret[count] = 0x1DC + i
|
||||
count = count + 1
|
||||
end
|
||||
end
|
||||
return ret
|
||||
end
|
||||
local 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)
|
||||
local msgBlock = block['messages']
|
||||
if msgBlock ~= nil then
|
||||
for i, v in pairs(msgBlock) do
|
||||
if itemMessages[i] == nil then
|
||||
local msg = {TTL=450, message=v, color=0xFFFF0000}
|
||||
itemMessages[i] = msg
|
||||
end
|
||||
end
|
||||
end
|
||||
local itemsBlock = block["items"]
|
||||
memDomain.saveram()
|
||||
isInGame = u8(0x102)
|
||||
if itemsBlock ~= nil and isInGame ~= 0x00 then
|
||||
if consumableStacks == nil then
|
||||
setConsumableStacks()
|
||||
end
|
||||
memDomain.saveram()
|
||||
-- print('ITEMBLOCK: ')
|
||||
-- print(itemsBlock)
|
||||
itemIndex = u8(ITEM_INDEX)
|
||||
-- print('ITEMINDEX: '..itemIndex)
|
||||
for i, v in pairs(slice(itemsBlock, itemIndex, #itemsBlock)) do
|
||||
-- Minus the offset and add to the correct domain
|
||||
local memoryLocation = v
|
||||
if v >= 0x100 and v <= 0x114 then
|
||||
-- This is a key item
|
||||
memoryLocation = memoryLocation - 0x0E0
|
||||
wU8(memoryLocation, 0x01)
|
||||
elseif v >= 0x1E0 and v <= 0x1F2 then
|
||||
-- This is a movement item
|
||||
-- Minus Offset (0x100) - movement offset (0xE0)
|
||||
memoryLocation = memoryLocation - 0x1E0
|
||||
-- Canal is a flipped bit
|
||||
if memoryLocation == 0x0C then
|
||||
wU8(memoryLocation, 0x00)
|
||||
else
|
||||
wU8(memoryLocation, 0x01)
|
||||
end
|
||||
elseif v >= 0x1F3 and v <= 0x1F4 then
|
||||
-- NoOverworld special items
|
||||
memoryLocation = noOverworldItemsLookup[v]
|
||||
wU8(memoryLocation, 0x01)
|
||||
elseif v >= 0x16C and v <= 0x1AF then
|
||||
-- This is a gold item
|
||||
amountToAdd = goldLookup[v]
|
||||
biggest = u8(0x01E)
|
||||
medium = u8(0x01D)
|
||||
smallest = u8(0x01C)
|
||||
currentValue = 0x10000 * biggest + 0x100 * medium + smallest
|
||||
newValue = currentValue + amountToAdd
|
||||
newBiggest = math.floor(newValue / 0x10000)
|
||||
newMedium = math.floor(math.fmod(newValue, 0x10000) / 0x100)
|
||||
newSmallest = math.floor(math.fmod(newValue, 0x100))
|
||||
wU8(0x01E, newBiggest)
|
||||
wU8(0x01D, newMedium)
|
||||
wU8(0x01C, newSmallest)
|
||||
elseif v >= 0x115 and v <= 0x11B then
|
||||
-- This is a regular consumable OR a shard
|
||||
-- Minus Offset (0x100) + item offset (0x20)
|
||||
memoryLocation = memoryLocation - 0x0E0
|
||||
currentValue = u8(memoryLocation)
|
||||
amountToAdd = consumableStacks[memoryLocation]
|
||||
if currentValue < 99 then
|
||||
wU8(memoryLocation, currentValue + amountToAdd)
|
||||
end
|
||||
elseif v >= 0x1B0 and v <= 0x1BB then
|
||||
-- This is an extension consumable
|
||||
memoryLocation = extensionConsumableLookup[v]
|
||||
currentValue = u8(memoryLocation)
|
||||
amountToAdd = consumableStacks[memoryLocation]
|
||||
if currentValue < 99 then
|
||||
value = currentValue + amountToAdd
|
||||
if value > 99 then
|
||||
value = 99
|
||||
end
|
||||
wU8(memoryLocation, value)
|
||||
end
|
||||
end
|
||||
end
|
||||
if #itemsBlock > itemIndex then
|
||||
wU8(ITEM_INDEX, #itemsBlock)
|
||||
end
|
||||
|
||||
memDomain.saveram()
|
||||
weaponIndex = u8(WEAPON_INDEX)
|
||||
emptyWeaponSlots = getEmptyWeaponSlots()
|
||||
lastUsedWeaponIndex = weaponIndex
|
||||
-- print('WEAPON_INDEX: '.. weaponIndex)
|
||||
memDomain.saveram()
|
||||
for i, v in pairs(slice(itemsBlock, weaponIndex, #itemsBlock)) do
|
||||
if v >= 0x11C and v <= 0x143 then
|
||||
-- Minus the offset and add to the correct domain
|
||||
local itemValue = v - 0x11B
|
||||
if #emptyWeaponSlots > 0 then
|
||||
slot = table.remove(emptyWeaponSlots, 1)
|
||||
wU8(slot, itemValue)
|
||||
lastUsedWeaponIndex = weaponIndex + i
|
||||
else
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
if lastUsedWeaponIndex ~= weaponIndex then
|
||||
wU8(WEAPON_INDEX, lastUsedWeaponIndex)
|
||||
end
|
||||
memDomain.saveram()
|
||||
armorIndex = u8(ARMOR_INDEX)
|
||||
emptyArmorSlots = getEmptyArmorSlots()
|
||||
lastUsedArmorIndex = armorIndex
|
||||
-- print('ARMOR_INDEX: '.. armorIndex)
|
||||
memDomain.saveram()
|
||||
for i, v in pairs(slice(itemsBlock, armorIndex, #itemsBlock)) do
|
||||
if v >= 0x144 and v <= 0x16B then
|
||||
-- Minus the offset and add to the correct domain
|
||||
local itemValue = v - 0x143
|
||||
if #emptyArmorSlots > 0 then
|
||||
slot = table.remove(emptyArmorSlots, 1)
|
||||
wU8(slot, itemValue)
|
||||
lastUsedArmorIndex = armorIndex + i
|
||||
else
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
if lastUsedArmorIndex ~= armorIndex then
|
||||
wU8(ARMOR_INDEX, lastUsedArmorIndex)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function receive()
|
||||
l, e = ff1Socket:receive()
|
||||
if e == 'closed' then
|
||||
if curstate == STATE_OK then
|
||||
print("Connection closed")
|
||||
end
|
||||
curstate = STATE_UNINITIALIZED
|
||||
return
|
||||
elseif e == 'timeout' then
|
||||
print("timeout")
|
||||
return
|
||||
elseif e ~= nil then
|
||||
print(e)
|
||||
curstate = STATE_UNINITIALIZED
|
||||
return
|
||||
end
|
||||
processBlock(json.decode(l))
|
||||
|
||||
-- Determine Message to send back
|
||||
memDomain.rom()
|
||||
local playerName = uRange(0x7BCBF, 0x41)
|
||||
playerName[0] = nil
|
||||
local retTable = {}
|
||||
retTable["playerName"] = playerName
|
||||
if StateOKForMainLoop() then
|
||||
retTable["locations"] = generateLocationChecked()
|
||||
end
|
||||
msg = json.encode(retTable).."\n"
|
||||
local ret, error = ff1Socket: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!")
|
||||
itemMessages["(0,0)"] = {TTL=240, message="Connected", color="green"}
|
||||
curstate = STATE_OK
|
||||
end
|
||||
end
|
||||
|
||||
function main()
|
||||
if not checkBizHawkVersion() then
|
||||
return
|
||||
end
|
||||
server, error = socket.bind('localhost', 52980)
|
||||
|
||||
while true do
|
||||
gui.drawEllipse(248, 9, 6, 6, "Black", "Yellow")
|
||||
frame = frame + 1
|
||||
drawMessages()
|
||||
if not (curstate == prevstate) then
|
||||
-- console.log("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
|
||||
gui.drawEllipse(248, 9, 6, 6, "Black", "Blue")
|
||||
receive()
|
||||
else
|
||||
gui.drawEllipse(248, 9, 6, 6, "Black", "Green")
|
||||
end
|
||||
elseif (curstate == STATE_UNINITIALIZED) then
|
||||
gui.drawEllipse(248, 9, 6, 6, "Black", "White")
|
||||
if (frame % 60 == 0) then
|
||||
gui.drawEllipse(248, 9, 6, 6, "Black", "Yellow")
|
||||
|
||||
drawText(5, 8, "Waiting for client", 0xFFFF0000)
|
||||
drawText(5, 32, "Please start FF1Client.exe", 0xFFFF0000)
|
||||
|
||||
-- Advance so the messages are drawn
|
||||
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
|
||||
ff1Socket = client
|
||||
ff1Socket:settimeout(0)
|
||||
end
|
||||
end
|
||||
end
|
||||
emu.frameadvance()
|
||||
end
|
||||
end
|
||||
|
||||
main()
|
@@ -86,6 +86,7 @@ Type: dirifempty; Name: "{app}"
|
||||
[InstallDelete]
|
||||
Type: files; Name: "{app}\*.exe"
|
||||
Type: files; Name: "{app}\data\lua\connector_pkmn_rb.lua"
|
||||
Type: files; Name: "{app}\data\lua\connector_ff1.lua"
|
||||
Type: filesandordirs; Name: "{app}\SNI\lua*"
|
||||
Type: filesandordirs; Name: "{app}\EnemizerCLI*"
|
||||
#include "installdelete.iss"
|
||||
|
1
setup.py
1
setup.py
@@ -64,7 +64,6 @@ non_apworlds: set[str] = {
|
||||
"ArchipIDLE",
|
||||
"Archipelago",
|
||||
"Clique",
|
||||
"Final Fantasy",
|
||||
"Lufia II Ancient Cave",
|
||||
"Meritous",
|
||||
"Ocarina of Time",
|
||||
|
@@ -224,8 +224,6 @@ components: List[Component] = [
|
||||
Component('OoT Client', 'OoTClient',
|
||||
file_identifier=SuffixIdentifier('.apz5')),
|
||||
Component('OoT Adjuster', 'OoTAdjuster'),
|
||||
# FF1
|
||||
Component('FF1 Client', 'FF1Client'),
|
||||
# TLoZ
|
||||
Component('Zelda 1 Client', 'Zelda1Client', file_identifier=SuffixIdentifier('.aptloz')),
|
||||
# ChecksFinder
|
||||
|
328
worlds/ff1/Client.py
Normal file
328
worlds/ff1/Client.py
Normal file
@@ -0,0 +1,328 @@
|
||||
import logging
|
||||
from collections import deque
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from NetUtils import ClientStatus
|
||||
|
||||
import worlds._bizhawk as bizhawk
|
||||
from worlds._bizhawk.client import BizHawkClient
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from worlds._bizhawk.context import BizHawkClientContext
|
||||
|
||||
|
||||
base_id = 7000
|
||||
logger = logging.getLogger("Client")
|
||||
|
||||
|
||||
rom_name_location = 0x07FFE3
|
||||
locations_array_start = 0x200
|
||||
locations_array_length = 0x100
|
||||
items_obtained = 0x03
|
||||
gp_location_low = 0x1C
|
||||
gp_location_middle = 0x1D
|
||||
gp_location_high = 0x1E
|
||||
weapons_arrays_starts = [0x118, 0x158, 0x198, 0x1D8]
|
||||
armors_arrays_starts = [0x11C, 0x15C, 0x19C, 0x1DC]
|
||||
status_a_location = 0x102
|
||||
status_b_location = 0x0FC
|
||||
status_c_location = 0x0A3
|
||||
|
||||
key_items = ["Lute", "Crown", "Crystal", "Herb", "Key", "Tnt", "Adamant", "Slab", "Ruby", "Rod",
|
||||
"Floater", "Chime", "Tail", "Cube", "Bottle", "Oxyale", "EarthOrb", "FireOrb", "WaterOrb", "AirOrb"]
|
||||
|
||||
consumables = ["Shard", "Tent", "Cabin", "House", "Heal", "Pure", "Soft"]
|
||||
|
||||
weapons = ["WoodenNunchucks", "SmallKnife", "WoodenRod", "Rapier", "IronHammer", "ShortSword", "HandAxe", "Scimitar",
|
||||
"IronNunchucks", "LargeKnife", "IronStaff", "Sabre", "LongSword", "GreatAxe", "Falchon", "SilverKnife",
|
||||
"SilverSword", "SilverHammer", "SilverAxe", "FlameSword", "IceSword", "DragonSword", "GiantSword",
|
||||
"SunSword", "CoralSword", "WereSword", "RuneSword", "PowerRod", "LightAxe", "HealRod", "MageRod", "Defense",
|
||||
"WizardRod", "Vorpal", "CatClaw", "ThorHammer", "BaneSword", "Katana", "Xcalber", "Masamune"]
|
||||
|
||||
armor = ["Cloth", "WoodenArmor", "ChainArmor", "IronArmor", "SteelArmor", "SilverArmor", "FlameArmor", "IceArmor",
|
||||
"OpalArmor", "DragonArmor", "Copper", "Silver", "Gold", "Opal", "WhiteShirt", "BlackShirt", "WoodenShield",
|
||||
"IronShield", "SilverShield", "FlameShield", "IceShield", "OpalShield", "AegisShield", "Buckler", "ProCape",
|
||||
"Cap", "WoodenHelm", "IronHelm", "SilverHelm", "OpalHelm", "HealHelm", "Ribbon", "Gloves", "CopperGauntlets",
|
||||
"IronGauntlets", "SilverGauntlets", "ZeusGauntlets", "PowerGauntlets", "OpalGauntlets", "ProRing"]
|
||||
|
||||
gold_items = ["Gold10", "Gold20", "Gold25", "Gold30", "Gold55", "Gold70", "Gold85", "Gold110", "Gold135", "Gold155",
|
||||
"Gold160", "Gold180", "Gold240", "Gold255", "Gold260", "Gold295", "Gold300", "Gold315", "Gold330",
|
||||
"Gold350", "Gold385", "Gold400", "Gold450", "Gold500", "Gold530", "Gold575", "Gold620", "Gold680",
|
||||
"Gold750", "Gold795", "Gold880", "Gold1020", "Gold1250", "Gold1455", "Gold1520", "Gold1760", "Gold1975",
|
||||
"Gold2000", "Gold2750", "Gold3400", "Gold4150", "Gold5000", "Gold5450", "Gold6400", "Gold6720",
|
||||
"Gold7340", "Gold7690", "Gold7900", "Gold8135", "Gold9000", "Gold9300", "Gold9500", "Gold9900",
|
||||
"Gold10000", "Gold12350", "Gold13000", "Gold13450", "Gold14050", "Gold14720", "Gold15000", "Gold17490",
|
||||
"Gold18010", "Gold19990", "Gold20000", "Gold20010", "Gold26000", "Gold45000", "Gold65000"]
|
||||
|
||||
extended_consumables = ["FullCure", "Phoenix", "Blast", "Smoke",
|
||||
"Refresh", "Flare", "Black", "Guard",
|
||||
"Quick", "HighPotion", "Wizard", "Cloak"]
|
||||
|
||||
ext_consumables_lookup = {"FullCure": "Ext1", "Phoenix": "Ext2", "Blast": "Ext3", "Smoke": "Ext4",
|
||||
"Refresh": "Ext1", "Flare": "Ext2", "Black": "Ext3", "Guard": "Ext4",
|
||||
"Quick": "Ext1", "HighPotion": "Ext2", "Wizard": "Ext3", "Cloak": "Ext4"}
|
||||
|
||||
ext_consumables_locations = {"Ext1": 0x3C, "Ext2": 0x3D, "Ext3": 0x3E, "Ext4": 0x3F}
|
||||
|
||||
|
||||
movement_items = ["Ship", "Bridge", "Canal", "Canoe"]
|
||||
|
||||
no_overworld_items = ["Sigil", "Mark"]
|
||||
|
||||
|
||||
class FF1Client(BizHawkClient):
|
||||
game = "Final Fantasy"
|
||||
system = "NES"
|
||||
|
||||
weapons_queue: deque[int]
|
||||
armor_queue: deque[int]
|
||||
consumable_stack_amounts: dict[str, int] | None
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.wram = "RAM"
|
||||
self.sram = "WRAM"
|
||||
self.rom = "PRG ROM"
|
||||
self.consumable_stack_amounts = None
|
||||
self.weapons_queue = deque()
|
||||
self.armor_queue = deque()
|
||||
self.guard_character = 0x00
|
||||
|
||||
async def validate_rom(self, ctx: "BizHawkClientContext") -> bool:
|
||||
try:
|
||||
# Check ROM name/patch version
|
||||
rom_name = ((await bizhawk.read(ctx.bizhawk_ctx, [(rom_name_location, 0x0D, self.rom)]))[0])
|
||||
rom_name = rom_name.decode("ascii")
|
||||
if rom_name != "FINAL FANTASY":
|
||||
return False # Not a Final Fantasy 1 ROM
|
||||
except bizhawk.RequestFailedError:
|
||||
return False # Not able to get a response, say no for now
|
||||
|
||||
ctx.game = self.game
|
||||
ctx.items_handling = 0b111
|
||||
ctx.want_slot_data = True
|
||||
# Resetting these in case of switching ROMs
|
||||
self.consumable_stack_amounts = None
|
||||
self.weapons_queue = deque()
|
||||
self.armor_queue = deque()
|
||||
|
||||
return True
|
||||
|
||||
async def game_watcher(self, ctx: "BizHawkClientContext") -> None:
|
||||
if ctx.server is None:
|
||||
return
|
||||
|
||||
if ctx.slot is None:
|
||||
return
|
||||
try:
|
||||
self.guard_character = await self.read_sram_value(ctx, status_a_location)
|
||||
# If the first character's name starts with a 0 value, we're at the title screen/character creation.
|
||||
# In that case, don't allow any read/writes.
|
||||
# We do this by setting the guard to 1 because that's neither a valid character nor the initial value.
|
||||
if self.guard_character == 0:
|
||||
self.guard_character = 0x01
|
||||
|
||||
if self.consumable_stack_amounts is None:
|
||||
self.consumable_stack_amounts = {}
|
||||
self.consumable_stack_amounts["Shard"] = 1
|
||||
other_consumable_amounts = await self.read_rom(ctx, 0x47400, 10)
|
||||
self.consumable_stack_amounts["Tent"] = other_consumable_amounts[0] + 1
|
||||
self.consumable_stack_amounts["Cabin"] = other_consumable_amounts[1] + 1
|
||||
self.consumable_stack_amounts["House"] = other_consumable_amounts[2] + 1
|
||||
self.consumable_stack_amounts["Heal"] = other_consumable_amounts[3] + 1
|
||||
self.consumable_stack_amounts["Pure"] = other_consumable_amounts[4] + 1
|
||||
self.consumable_stack_amounts["Soft"] = other_consumable_amounts[5] + 1
|
||||
self.consumable_stack_amounts["Ext1"] = other_consumable_amounts[6] + 1
|
||||
self.consumable_stack_amounts["Ext2"] = other_consumable_amounts[7] + 1
|
||||
self.consumable_stack_amounts["Ext3"] = other_consumable_amounts[8] + 1
|
||||
self.consumable_stack_amounts["Ext4"] = other_consumable_amounts[9] + 1
|
||||
|
||||
await self.location_check(ctx)
|
||||
await self.received_items_check(ctx)
|
||||
await self.process_weapons_queue(ctx)
|
||||
await self.process_armor_queue(ctx)
|
||||
|
||||
except bizhawk.RequestFailedError:
|
||||
# The connector didn't respond. Exit handler and return to main loop to reconnect
|
||||
pass
|
||||
|
||||
async def location_check(self, ctx: "BizHawkClientContext"):
|
||||
locations_data = await self.read_sram_values_guarded(ctx, locations_array_start, locations_array_length)
|
||||
if locations_data is None:
|
||||
return
|
||||
locations_checked = []
|
||||
if len(locations_data) > 0xFE and locations_data[0xFE] & 0x02 != 0 and not ctx.finished_game:
|
||||
await ctx.send_msgs([
|
||||
{"cmd": "StatusUpdate",
|
||||
"status": ClientStatus.CLIENT_GOAL}
|
||||
])
|
||||
ctx.finished_game = True
|
||||
for location in ctx.missing_locations:
|
||||
# index will be - 0x100 or 0x200
|
||||
index = location
|
||||
if location < 0x200:
|
||||
# Location is a chest
|
||||
index -= 0x100
|
||||
flag = 0x04
|
||||
else:
|
||||
# Location is an NPC
|
||||
index -= 0x200
|
||||
flag = 0x02
|
||||
if locations_data[index] & flag != 0:
|
||||
locations_checked.append(location)
|
||||
|
||||
found_locations = await ctx.check_locations(locations_checked)
|
||||
for location in found_locations:
|
||||
ctx.locations_checked.add(location)
|
||||
location_name = ctx.location_names.lookup_in_game(location)
|
||||
logger.info(
|
||||
f'New Check: {location_name} ({len(ctx.locations_checked)}/'
|
||||
f'{len(ctx.missing_locations) + len(ctx.checked_locations)})')
|
||||
|
||||
|
||||
async def received_items_check(self, ctx: "BizHawkClientContext") -> None:
|
||||
assert self.consumable_stack_amounts, "shouldn't call this function without reading consumable_stack_amounts"
|
||||
write_list: list[tuple[int, list[int], str]] = []
|
||||
items_received_count = await self.read_sram_value_guarded(ctx, items_obtained)
|
||||
if items_received_count is None:
|
||||
return
|
||||
if items_received_count < len(ctx.items_received):
|
||||
current_item = ctx.items_received[items_received_count]
|
||||
current_item_id = current_item.item
|
||||
current_item_name = ctx.item_names.lookup_in_game(current_item_id, ctx.game)
|
||||
if current_item_name in key_items:
|
||||
location = current_item_id - 0xE0
|
||||
write_list.append((location, [1], self.sram))
|
||||
elif current_item_name in movement_items:
|
||||
location = current_item_id - 0x1E0
|
||||
if current_item_name != "Canal":
|
||||
write_list.append((location, [1], self.sram))
|
||||
else:
|
||||
write_list.append((location, [0], self.sram))
|
||||
elif current_item_name in no_overworld_items:
|
||||
if current_item_name == "Sigil":
|
||||
location = 0x28
|
||||
else:
|
||||
location = 0x12
|
||||
write_list.append((location, [1], self.sram))
|
||||
elif current_item_name in gold_items:
|
||||
gold_amount = int(current_item_name[4:])
|
||||
current_gold_value = await self.read_sram_values_guarded(ctx, gp_location_low, 3)
|
||||
if current_gold_value is None:
|
||||
return
|
||||
current_gold = int.from_bytes(current_gold_value, "little")
|
||||
new_gold = min(gold_amount + current_gold, 999999)
|
||||
lower_byte = new_gold % (2 ** 8)
|
||||
middle_byte = (new_gold // (2 ** 8)) % (2 ** 8)
|
||||
upper_byte = new_gold // (2 ** 16)
|
||||
write_list.append((gp_location_low, [lower_byte], self.sram))
|
||||
write_list.append((gp_location_middle, [middle_byte], self.sram))
|
||||
write_list.append((gp_location_high, [upper_byte], self.sram))
|
||||
elif current_item_name in consumables:
|
||||
location = current_item_id - 0xE0
|
||||
current_value = await self.read_sram_value_guarded(ctx, location)
|
||||
if current_value is None:
|
||||
return
|
||||
amount_to_add = self.consumable_stack_amounts[current_item_name]
|
||||
new_value = min(current_value + amount_to_add, 99)
|
||||
write_list.append((location, [new_value], self.sram))
|
||||
elif current_item_name in extended_consumables:
|
||||
ext_name = ext_consumables_lookup[current_item_name]
|
||||
location = ext_consumables_locations[ext_name]
|
||||
current_value = await self.read_sram_value_guarded(ctx, location)
|
||||
if current_value is None:
|
||||
return
|
||||
amount_to_add = self.consumable_stack_amounts[ext_name]
|
||||
new_value = min(current_value + amount_to_add, 99)
|
||||
write_list.append((location, [new_value], self.sram))
|
||||
elif current_item_name in weapons:
|
||||
self.weapons_queue.appendleft(current_item_id - 0x11B)
|
||||
elif current_item_name in armor:
|
||||
self.armor_queue.appendleft(current_item_id - 0x143)
|
||||
write_list.append((items_obtained, [items_received_count + 1], self.sram))
|
||||
write_successful = await self.write_sram_values_guarded(ctx, write_list)
|
||||
if write_successful:
|
||||
await bizhawk.display_message(ctx.bizhawk_ctx, f"Received {current_item_name}")
|
||||
|
||||
async def process_weapons_queue(self, ctx: "BizHawkClientContext"):
|
||||
empty_slots = deque()
|
||||
char1_slots = await self.read_sram_values_guarded(ctx, weapons_arrays_starts[0], 4)
|
||||
char2_slots = await self.read_sram_values_guarded(ctx, weapons_arrays_starts[1], 4)
|
||||
char3_slots = await self.read_sram_values_guarded(ctx, weapons_arrays_starts[2], 4)
|
||||
char4_slots = await self.read_sram_values_guarded(ctx, weapons_arrays_starts[3], 4)
|
||||
if char1_slots is None or char2_slots is None or char3_slots is None or char4_slots is None:
|
||||
return
|
||||
for i, slot in enumerate(char1_slots):
|
||||
if slot == 0:
|
||||
empty_slots.appendleft(weapons_arrays_starts[0] + i)
|
||||
for i, slot in enumerate(char2_slots):
|
||||
if slot == 0:
|
||||
empty_slots.appendleft(weapons_arrays_starts[1] + i)
|
||||
for i, slot in enumerate(char3_slots):
|
||||
if slot == 0:
|
||||
empty_slots.appendleft(weapons_arrays_starts[2] + i)
|
||||
for i, slot in enumerate(char4_slots):
|
||||
if slot == 0:
|
||||
empty_slots.appendleft(weapons_arrays_starts[3] + i)
|
||||
while len(empty_slots) > 0 and len(self.weapons_queue) > 0:
|
||||
current_slot = empty_slots.pop()
|
||||
current_weapon = self.weapons_queue.pop()
|
||||
await self.write_sram_guarded(ctx, current_slot, current_weapon)
|
||||
|
||||
async def process_armor_queue(self, ctx: "BizHawkClientContext"):
|
||||
empty_slots = deque()
|
||||
char1_slots = await self.read_sram_values_guarded(ctx, armors_arrays_starts[0], 4)
|
||||
char2_slots = await self.read_sram_values_guarded(ctx, armors_arrays_starts[1], 4)
|
||||
char3_slots = await self.read_sram_values_guarded(ctx, armors_arrays_starts[2], 4)
|
||||
char4_slots = await self.read_sram_values_guarded(ctx, armors_arrays_starts[3], 4)
|
||||
if char1_slots is None or char2_slots is None or char3_slots is None or char4_slots is None:
|
||||
return
|
||||
for i, slot in enumerate(char1_slots):
|
||||
if slot == 0:
|
||||
empty_slots.appendleft(armors_arrays_starts[0] + i)
|
||||
for i, slot in enumerate(char2_slots):
|
||||
if slot == 0:
|
||||
empty_slots.appendleft(armors_arrays_starts[1] + i)
|
||||
for i, slot in enumerate(char3_slots):
|
||||
if slot == 0:
|
||||
empty_slots.appendleft(armors_arrays_starts[2] + i)
|
||||
for i, slot in enumerate(char4_slots):
|
||||
if slot == 0:
|
||||
empty_slots.appendleft(armors_arrays_starts[3] + i)
|
||||
while len(empty_slots) > 0 and len(self.armor_queue) > 0:
|
||||
current_slot = empty_slots.pop()
|
||||
current_armor = self.armor_queue.pop()
|
||||
await self.write_sram_guarded(ctx, current_slot, current_armor)
|
||||
|
||||
|
||||
async def read_sram_value(self, ctx: "BizHawkClientContext", location: int):
|
||||
value = ((await bizhawk.read(ctx.bizhawk_ctx, [(location, 1, self.sram)]))[0])
|
||||
return int.from_bytes(value, "little")
|
||||
|
||||
async def read_sram_values_guarded(self, ctx: "BizHawkClientContext", location: int, size: int):
|
||||
value = await bizhawk.guarded_read(ctx.bizhawk_ctx,
|
||||
[(location, size, self.sram)],
|
||||
[(status_a_location, [self.guard_character], self.sram)])
|
||||
if value is None:
|
||||
return None
|
||||
return value[0]
|
||||
|
||||
async def read_sram_value_guarded(self, ctx: "BizHawkClientContext", location: int):
|
||||
value = await bizhawk.guarded_read(ctx.bizhawk_ctx,
|
||||
[(location, 1, self.sram)],
|
||||
[(status_a_location, [self.guard_character], self.sram)])
|
||||
if value is None:
|
||||
return None
|
||||
return int.from_bytes(value[0], "little")
|
||||
|
||||
async def read_rom(self, ctx: "BizHawkClientContext", location: int, size: int):
|
||||
return (await bizhawk.read(ctx.bizhawk_ctx, [(location, size, self.rom)]))[0]
|
||||
|
||||
async def write_sram_guarded(self, ctx: "BizHawkClientContext", location: int, value: int):
|
||||
return await bizhawk.guarded_write(ctx.bizhawk_ctx,
|
||||
[(location, [value], self.sram)],
|
||||
[(status_a_location, [self.guard_character], self.sram)])
|
||||
|
||||
async def write_sram_values_guarded(self, ctx: "BizHawkClientContext", write_list):
|
||||
return await bizhawk.guarded_write(ctx.bizhawk_ctx,
|
||||
write_list,
|
||||
[(status_a_location, [self.guard_character], self.sram)])
|
@@ -1,5 +1,5 @@
|
||||
import json
|
||||
from pathlib import Path
|
||||
import pkgutil
|
||||
from typing import Dict, Set, NamedTuple, List
|
||||
|
||||
from BaseClasses import Item, ItemClassification
|
||||
@@ -37,15 +37,13 @@ class FF1Items:
|
||||
_item_table_lookup: Dict[str, ItemData] = {}
|
||||
|
||||
def _populate_item_table_from_data(self):
|
||||
base_path = Path(__file__).parent
|
||||
file_path = (base_path / "data/items.json").resolve()
|
||||
with open(file_path) as file:
|
||||
items = json.load(file)
|
||||
# Hardcode progression and categories for now
|
||||
self._item_table = [ItemData(name, code, "FF1Item", ItemClassification.progression if name in
|
||||
FF1_PROGRESSION_LIST else ItemClassification.useful if name in FF1_USEFUL_LIST else
|
||||
ItemClassification.filler) for name, code in items.items()]
|
||||
self._item_table_lookup = {item.name: item for item in self._item_table}
|
||||
file = pkgutil.get_data(__name__, "data/items.json").decode("utf-8")
|
||||
items = json.loads(file)
|
||||
# Hardcode progression and categories for now
|
||||
self._item_table = [ItemData(name, code, "FF1Item", ItemClassification.progression if name in
|
||||
FF1_PROGRESSION_LIST else ItemClassification.useful if name in FF1_USEFUL_LIST else
|
||||
ItemClassification.filler) for name, code in items.items()]
|
||||
self._item_table_lookup = {item.name: item for item in self._item_table}
|
||||
|
||||
def _get_item_table(self) -> List[ItemData]:
|
||||
if not self._item_table or not self._item_table_lookup:
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import json
|
||||
from pathlib import Path
|
||||
import pkgutil
|
||||
from typing import Dict, NamedTuple, List, Optional
|
||||
|
||||
from BaseClasses import Region, Location, MultiWorld
|
||||
@@ -18,13 +18,11 @@ class FF1Locations:
|
||||
_location_table_lookup: Dict[str, LocationData] = {}
|
||||
|
||||
def _populate_item_table_from_data(self):
|
||||
base_path = Path(__file__).parent
|
||||
file_path = (base_path / "data/locations.json").resolve()
|
||||
with open(file_path) as file:
|
||||
locations = json.load(file)
|
||||
# Hardcode progression and categories for now
|
||||
self._location_table = [LocationData(name, code) for name, code in locations.items()]
|
||||
self._location_table_lookup = {item.name: item for item in self._location_table}
|
||||
file = pkgutil.get_data(__name__, "data/locations.json")
|
||||
locations = json.loads(file)
|
||||
# Hardcode progression and categories for now
|
||||
self._location_table = [LocationData(name, code) for name, code in locations.items()]
|
||||
self._location_table_lookup = {item.name: item for item in self._location_table}
|
||||
|
||||
def _get_location_table(self) -> List[LocationData]:
|
||||
if not self._location_table or not self._location_table_lookup:
|
||||
|
@@ -7,6 +7,7 @@ from .Items import ItemData, FF1Items, FF1_STARTER_ITEMS, FF1_PROGRESSION_LIST,
|
||||
from .Locations import EventId, FF1Locations, generate_rule, CHAOS_TERMINATED_EVENT
|
||||
from .Options import FF1Options
|
||||
from ..AutoWorld import World, WebWorld
|
||||
from .Client import FF1Client
|
||||
|
||||
|
||||
class FF1Settings(settings.Group):
|
||||
|
@@ -22,11 +22,6 @@ All items can appear in other players worlds, including consumables, shards, wea
|
||||
|
||||
## What does another world's item look like in Final Fantasy
|
||||
|
||||
All local and remote items appear the same. Final Fantasy will say that you received an item, then BOTH the client log and the
|
||||
emulator will display what was found external to the in-game text box.
|
||||
All local and remote items appear the same. Final Fantasy will say that you received an item, then the client log will
|
||||
display what was found external to the in-game text box.
|
||||
|
||||
## Unique Local Commands
|
||||
The following commands are only available when using the FF1Client for the Final Fantasy Randomizer.
|
||||
|
||||
- `/nes` Shows the current status of the NES connection.
|
||||
- `/toggle_msgs` Toggle displaying messages in EmuHawk
|
||||
|
@@ -2,10 +2,10 @@
|
||||
|
||||
## Required Software
|
||||
|
||||
- The FF1Client
|
||||
- Bundled with Archipelago: [Archipelago Releases Page](https://github.com/ArchipelagoMW/Archipelago/releases)
|
||||
- The BizHawk emulator. Versions 2.3.1 and higher are supported. Version 2.7 is recommended
|
||||
- [BizHawk at TASVideos](https://tasvideos.org/BizHawk)
|
||||
- BizHawk: [BizHawk Releases from TASVideos](https://tasvideos.org/BizHawk/ReleaseHistory)
|
||||
- Detailed installation instructions for BizHawk can be found at the above link.
|
||||
- Windows users must run the prerequisite installer first, which can also be found at the above link.
|
||||
- The built-in BizHawk client, which can be installed [here](https://github.com/ArchipelagoMW/Archipelago/releases)
|
||||
- Your legally obtained Final Fantasy (USA Edition) ROM file, probably named `Final Fantasy (USA).nes`. Neither
|
||||
Archipelago.gg nor the Final Fantasy Randomizer Community can supply you with this.
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
|
||||
1. Download and install the latest version of Archipelago.
|
||||
1. On Windows, download Setup.Archipelago.<HighestVersion\>.exe and run it
|
||||
2. Assign EmuHawk version 2.3.1 or higher as your default program for launching `.nes` files.
|
||||
2. Assign EmuHawk as your default program for launching `.nes` files.
|
||||
1. Extract your BizHawk folder to your Desktop, or somewhere you will remember. Below are optional additional steps
|
||||
for loading ROMs more conveniently
|
||||
1. Right-click on a ROM file and select **Open with...**
|
||||
@@ -46,7 +46,7 @@ please refer to the [game agnostic setup guide](/tutorial/Archipelago/setup/en).
|
||||
|
||||
Once the Archipelago server has been hosted:
|
||||
|
||||
1. Navigate to your Archipelago install folder and run `ArchipelagoFF1Client.exe`
|
||||
1. Navigate to your Archipelago install folder and run `ArchipelagoBizhawkClient.exe`
|
||||
2. Notice the `/connect command` on the server hosting page (It should look like `/connect archipelago.gg:*****`
|
||||
where ***** are numbers)
|
||||
3. Type the connect command into the client OR add the port to the pre-populated address on the top bar (it should
|
||||
@@ -54,16 +54,11 @@ Once the Archipelago server has been hosted:
|
||||
|
||||
### Running Your Game and Connecting to the Client Program
|
||||
|
||||
1. Open EmuHawk 2.3.1 or higher and load your ROM OR click your ROM file if it is already associated with the
|
||||
1. Open EmuHawk and load your ROM OR click your ROM file if it is already associated with the
|
||||
extension `*.nes`
|
||||
2. Navigate to where you installed Archipelago, then to `data/lua`, and drag+drop the `connector_ff1.lua` script onto
|
||||
the main EmuHawk window.
|
||||
1. You could instead open the Lua Console manually, click `Script` 〉 `Open Script`, and navigate to
|
||||
`connector_ff1.lua` with the file picker.
|
||||
2. If it gives a `NLua.Exceptions.LuaScriptException: .\socket.lua:13: module 'socket.core' not found:` exception
|
||||
close your emulator entirely, restart it and re-run these steps
|
||||
3. If it says `Must use a version of BizHawk 2.3.1 or higher`, double-check your BizHawk version by clicking **
|
||||
Help** -> **About**
|
||||
2. Navigate to where you installed Archipelago, then to `data/lua`, and drag+drop the `connector_bizhawk_generic.lua`
|
||||
script onto the main EmuHawk window. You can also instead open the Lua Console manually, click `Script` 〉 `Open Script`,
|
||||
and navigate to `connector_bizhawk_generic.lua` with the file picker.
|
||||
|
||||
## Play the game
|
||||
|
||||
|
Reference in New Issue
Block a user