543 lines
16 KiB
Lua
543 lines
16 KiB
Lua
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 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 itemMessages = {}
|
|
local consumableStacks = nil
|
|
local prevstate = ""
|
|
local curstate = STATE_UNINITIALIZED
|
|
local ff1Socket = nil
|
|
local frame = 0
|
|
|
|
local u8 = nil
|
|
local wU8 = nil
|
|
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()
|
|
u8 = memory.read_u8
|
|
wU8 = memory.write_u8
|
|
uRange = memory.readbyterange
|
|
|
|
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 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
|
|
|
|
local bizhawk_version = client.getversion()
|
|
local is23Or24Or25 = (bizhawk_version=="2.3.1") or (bizhawk_version:sub(1,3)=="2.4") or (bizhawk_version:sub(1,3)=="2.5")
|
|
local is26To27 = (bizhawk_version:sub(1,3)=="2.6") or (bizhawk_version:sub(1,3)=="2.7")
|
|
|
|
local function getMaxMessageLength()
|
|
if is23Or24Or25 then
|
|
return client.screenwidth()/11
|
|
elseif is26To27 then
|
|
return client.screenwidth()/12
|
|
end
|
|
end
|
|
|
|
local function drawText(x, y, message, color)
|
|
if is23Or24Or25 then
|
|
gui.addmessage(message)
|
|
elseif is26To27 then
|
|
gui.drawText(x, y, message, color, 0xB0000000, 18, "Courier New", nil, nil, nil, "client")
|
|
end
|
|
end
|
|
|
|
local function clearScreen()
|
|
if is23Or24Or25 then
|
|
return
|
|
elseif is26To27 then
|
|
drawText(0, 0, "", "black")
|
|
end
|
|
end
|
|
|
|
local function drawMessages()
|
|
if table.empty(itemMessages) then
|
|
clearScreen()
|
|
return
|
|
end
|
|
local y = 10
|
|
found = false
|
|
maxMessageLength = getMaxMessageLength()
|
|
for k, v in pairs(itemMessages) do
|
|
if v["TTL"] > 0 then
|
|
message = v["message"]
|
|
while true do
|
|
drawText(5, y, message:sub(1, maxMessageLength), v["color"])
|
|
y = y + 16
|
|
|
|
message = message:sub(maxMessageLength + 1, message:len())
|
|
if message:len() == 0 then
|
|
break
|
|
end
|
|
end
|
|
newTTL = 0
|
|
if is26To27 then
|
|
newTTL = itemMessages[k]["TTL"] - 1
|
|
end
|
|
itemMessages[k]["TTL"] = newTTL
|
|
found = true
|
|
end
|
|
end
|
|
if found == false then
|
|
clearScreen()
|
|
end
|
|
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
|
|
|
|
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 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 >= 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 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 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 (is23Or24Or25 or is26To27) == false then
|
|
print("Must use a version of bizhawk 2.3.1 or higher")
|
|
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()
|