723 lines
27 KiB
Lua
723 lines
27 KiB
Lua
|
local socket = require("socket")
|
|||
|
local json = require('json')
|
|||
|
local math = require('math')
|
|||
|
require('common')
|
|||
|
|
|||
|
local last_modified_date = '2023-31-05' -- Should be the last modified date
|
|||
|
local script_version = 4
|
|||
|
|
|||
|
local bizhawk_version = client.getversion()
|
|||
|
local bizhawk_major, bizhawk_minor, bizhawk_patch = bizhawk_version:match("(%d+)%.(%d+)%.?(%d*)")
|
|||
|
bizhawk_major = tonumber(bizhawk_major)
|
|||
|
bizhawk_minor = tonumber(bizhawk_minor)
|
|||
|
if bizhawk_patch == "" then
|
|||
|
bizhawk_patch = 0
|
|||
|
else
|
|||
|
bizhawk_patch = tonumber(bizhawk_patch)
|
|||
|
end
|
|||
|
|
|||
|
local STATE_OK = "Ok"
|
|||
|
local STATE_TENTATIVELY_CONNECTED = "Tentatively Connected"
|
|||
|
local STATE_INITIAL_CONNECTION_MADE = "Initial Connection Made"
|
|||
|
local STATE_UNINITIALIZED = "Uninitialized"
|
|||
|
|
|||
|
local prevstate = ""
|
|||
|
local curstate = STATE_UNINITIALIZED
|
|||
|
local mmbn3Socket = nil
|
|||
|
local frame = 0
|
|||
|
|
|||
|
-- States
|
|||
|
local ITEMSTATE_NONINITIALIZED = "Game Not Yet Started" -- Game has not yet started
|
|||
|
local ITEMSTATE_NONITEM = "Non-Itemable State" -- Do not send item now. RAM is not capable of holding
|
|||
|
local ITEMSTATE_IDLE = "Item State Ready" -- Ready for the next item if there are any
|
|||
|
local ITEMSTATE_SENT = "Item Sent Not Claimed" -- The ItemBit is set, but the dialog has not been closed yet
|
|||
|
local itemState = ITEMSTATE_NONINITIALIZED
|
|||
|
|
|||
|
local itemQueued = nil
|
|||
|
local itemQueueCounter = 120
|
|||
|
|
|||
|
local debugEnabled = false
|
|||
|
local game_complete = false
|
|||
|
|
|||
|
local backup_bytes = nil
|
|||
|
|
|||
|
local itemsReceived = {}
|
|||
|
local previousMessageBit = 0x00
|
|||
|
|
|||
|
local key_item_start_address = 0x20019C0
|
|||
|
|
|||
|
-- The Canary Byte is a flag byte that is intentionally left unused. If this byte is FF, then we know the flag
|
|||
|
-- data cannot be trusted, so we don't send checks.
|
|||
|
local canary_byte = 0x20001A9
|
|||
|
|
|||
|
local charDict = {
|
|||
|
[' ']=0x00,['0']=0x01,['1']=0x02,['2']=0x03,['3']=0x04,['4']=0x05,['5']=0x06,['6']=0x07,['7']=0x08,['8']=0x09,['9']=0x0A,
|
|||
|
['A']=0x0B,['B']=0x0C,['C']=0x0D,['D']=0x0E,['E']=0x0F,['F']=0x10,['G']=0x11,['H']=0x12,['I']=0x13,['J']=0x14,['K']=0x15,
|
|||
|
['L']=0x16,['M']=0x17,['N']=0x18,['O']=0x19,['P']=0x1A,['Q']=0x1B,['R']=0x1C,['S']=0x1D,['T']=0x1E,['U']=0x1F,['V']=0x20,
|
|||
|
['W']=0x21,['X']=0x22,['Y']=0x23,['Z']=0x24,['a']=0x25,['b']=0x26,['c']=0x27,['d']=0x28,['e']=0x29,['f']=0x2A,['g']=0x2B,
|
|||
|
['h']=0x2C,['i']=0x2D,['j']=0x2E,['k']=0x2F,['l']=0x30,['m']=0x31,['n']=0x32,['o']=0x33,['p']=0x34,['q']=0x35,['r']=0x36,
|
|||
|
['s']=0x37,['t']=0x38,['u']=0x39,['v']=0x3A,['w']=0x3B,['x']=0x3C,['y']=0x3D,['z']=0x3E,['-']=0x3F,['×']=0x40,[']=']=0x41,
|
|||
|
[':']=0x42,['+']=0x43,['÷']=0x44,['※']=0x45,['*']=0x46,['!']=0x47,['?']=0x48,['%']=0x49,['&']=0x4A,[',']=0x4B,['⋯']=0x4C,
|
|||
|
['.']=0x4D,['・']=0x4E,[';']=0x4F,['\'']=0x50,['\"']=0x51,['~']=0x52,['/']=0x53,['(']=0x54,[')']=0x55,['「']=0x56,['」']=0x57,
|
|||
|
["[V2]"]=0x58,["[V3]"]=0x59,["[V4]"]=0x5A,["[V5]"]=0x5B,['@']=0x5C,['♥']=0x5D,['♪']=0x5E,["[MB]"]=0x5F,['■']=0x60,['_']=0x61,
|
|||
|
["[circle1]"]=0x62,["[circle2]"]=0x63,["[cross1]"]=0x64,["[cross2]"]=0x65,["[bracket1]"]=0x66,["[bracket2]"]=0x67,["[ModTools1]"]=0x68,
|
|||
|
["[ModTools2]"]=0x69,["[ModTools3]"]=0x6A,['Σ']=0x6B,['Ω']=0x6C,['α']=0x6D,['β']=0x6E,['#']=0x6F,['…']=0x70,['>']=0x71,
|
|||
|
['<']=0x72,['エ']=0x73,["[BowneGlobal1]"]=0x74,["[BowneGlobal2]"]=0x75,["[BowneGlobal3]"]=0x76,["[BowneGlobal4]"]=0x77,
|
|||
|
["[BowneGlobal5]"]=0x78,["[BowneGlobal6]"]=0x79,["[BowneGlobal7]"]=0x7A,["[BowneGlobal8]"]=0x7B,["[BowneGlobal9]"]=0x7C,
|
|||
|
["[BowneGlobal10]"]=0x7D,["[BowneGlobal11]"]=0x7E,['\n']=0xE8
|
|||
|
}
|
|||
|
|
|||
|
local TableConcat = function(t1,t2)
|
|||
|
for i=1,#t2 do
|
|||
|
t1[#t1+1] = t2[i]
|
|||
|
end
|
|||
|
return t1
|
|||
|
end
|
|||
|
local int32ToByteList_le = function(x)
|
|||
|
bytes = {}
|
|||
|
hexString = string.format("%08x", x)
|
|||
|
for i=#hexString, 1, -2 do
|
|||
|
hbyte = hexString:sub(i-1, i)
|
|||
|
table.insert(bytes,tonumber(hbyte,16))
|
|||
|
end
|
|||
|
return bytes
|
|||
|
end
|
|||
|
local int16ToByteList_le = function(x)
|
|||
|
bytes = {}
|
|||
|
hexString = string.format("%04x", x)
|
|||
|
for i=#hexString, 1, -2 do
|
|||
|
hbyte = hexString:sub(i-1, i)
|
|||
|
table.insert(bytes,tonumber(hbyte,16))
|
|||
|
end
|
|||
|
return bytes
|
|||
|
end
|
|||
|
|
|||
|
local IsInMenu = function()
|
|||
|
return bit.band(memory.read_u8(0x0200027A),0x10) ~= 0
|
|||
|
end
|
|||
|
local IsInTransition = function()
|
|||
|
return bit.band(memory.read_u8(0x02001880), 0x10) ~= 0
|
|||
|
end
|
|||
|
local IsInDialog = function()
|
|||
|
return bit.band(memory.read_u8(0x02009480),0x01) ~= 0
|
|||
|
end
|
|||
|
local IsInBattle = function()
|
|||
|
return memory.read_u8(0x020097F8) == 0x08
|
|||
|
end
|
|||
|
local IsItemQueued = function()
|
|||
|
return memory.read_u8(0x2000224) == 0x01
|
|||
|
end
|
|||
|
|
|||
|
-- This function actually determines when you're on ANY full-screen menu (navi cust, link battle, etc.) but we
|
|||
|
-- don't want to check any locations there either so it's fine.
|
|||
|
local IsOnTitle = function()
|
|||
|
return bit.band(memory.read_u8(0x020097F8),0x04) == 0
|
|||
|
end
|
|||
|
local IsItemable = function()
|
|||
|
return not IsInMenu() and not IsInTransition() and not IsInDialog() and not IsInBattle() and not IsOnTitle() and not IsItemQueued()
|
|||
|
end
|
|||
|
|
|||
|
local is_game_complete = function()
|
|||
|
if IsOnTitle() or itemState == ITEMSTATE_NONINITIALIZED then return game_complete end
|
|||
|
|
|||
|
-- If the game is already marked complete, do not read memory
|
|||
|
if game_complete then return true end
|
|||
|
local is_alpha_defeated = bit.band(memory.read_u8(0x2000433), 0x01) ~= 0
|
|||
|
|
|||
|
if (is_alpha_defeated) then
|
|||
|
game_complete = true
|
|||
|
return true
|
|||
|
end
|
|||
|
|
|||
|
-- Game is still ongoing
|
|||
|
return false
|
|||
|
end
|
|||
|
|
|||
|
local saveItemIndexToRAM = function(newIndex)
|
|||
|
memory.write_s16_le(0x20000AE,newIndex)
|
|||
|
end
|
|||
|
|
|||
|
local loadItemIndexFromRAM = function()
|
|||
|
last_index = memory.read_s16_le(0x20000AE)
|
|||
|
if (last_index < 0) then
|
|||
|
last_index = 0
|
|||
|
saveItemIndexToRAM(0)
|
|||
|
end
|
|||
|
return last_index
|
|||
|
end
|
|||
|
|
|||
|
local loadPlayerNameFromROM = function()
|
|||
|
return memory.read_bytes_as_array(0x7FFFC0,63,"ROM")
|
|||
|
end
|
|||
|
|
|||
|
local check_all_locations = function()
|
|||
|
local location_checks = {}
|
|||
|
-- Title Screen should not check items
|
|||
|
if itemState == ITEMSTATE_NONINITIALIZED or IsInTransition() then
|
|||
|
return location_checks
|
|||
|
end
|
|||
|
if memory.read_u8(canary_byte) == 0xFF then
|
|||
|
return location_checks
|
|||
|
end
|
|||
|
for k,v in pairs(memory.read_bytes_as_dict(0x02000000, 0x434)) do
|
|||
|
str_k = string.format("%x", k)
|
|||
|
location_checks[str_k] = v
|
|||
|
end
|
|||
|
return location_checks
|
|||
|
end
|
|||
|
|
|||
|
local Check_Progressive_Undernet_ID = function()
|
|||
|
ordered_offsets = { 0x020019DB,0x020019DC,0x020019DD,0x020019DE,0x020019DF,0x020019E0,0x020019FA,0x020019E2 }
|
|||
|
for i=1,#ordered_offsets do
|
|||
|
offset=ordered_offsets[i]
|
|||
|
|
|||
|
if memory.read_u8(offset) == 0 then
|
|||
|
return i
|
|||
|
end
|
|||
|
end
|
|||
|
return 9
|
|||
|
end
|
|||
|
local GenerateTextBytes = function(message)
|
|||
|
bytes = {}
|
|||
|
for i = 1, #message do
|
|||
|
local c = message:sub(i,i)
|
|||
|
table.insert(bytes, charDict[c])
|
|||
|
end
|
|||
|
return bytes
|
|||
|
end
|
|||
|
|
|||
|
-- Item Message Generation functions
|
|||
|
local Next_Progressive_Undernet_ID = function(index)
|
|||
|
ordered_IDs = { 27,28,29,30,31,32,58,34}
|
|||
|
if index > #ordered_IDs then
|
|||
|
--It shouldn't reach this point, but if it does, just give another GigFreez I guess
|
|||
|
return 34
|
|||
|
end
|
|||
|
item_index=ordered_IDs[index]
|
|||
|
return item_index
|
|||
|
end
|
|||
|
local Extra_Progressive_Undernet = function()
|
|||
|
fragBytes = int32ToByteList_le(20)
|
|||
|
bytes = {
|
|||
|
0xF6, 0x50, fragBytes[1], fragBytes[2], fragBytes[3], fragBytes[4], 0xFF, 0xFF, 0xFF
|
|||
|
}
|
|||
|
bytes = TableConcat(bytes, GenerateTextBytes("The extra data\ndecompiles into:\n\"20 BugFrags\"!!"))
|
|||
|
return bytes
|
|||
|
end
|
|||
|
|
|||
|
local GenerateChipGet = function(chip, code, amt)
|
|||
|
chipBytes = int16ToByteList_le(chip)
|
|||
|
bytes = {
|
|||
|
0xF6, 0x10, chipBytes[1], chipBytes[2], code, amt,
|
|||
|
charDict['G'], charDict['o'], charDict['t'], charDict[' '], charDict['a'], charDict[' '], charDict['c'], charDict['h'], charDict['i'], charDict['p'], charDict[' '], charDict['f'], charDict['o'], charDict['r'], charDict['\n'],
|
|||
|
|
|||
|
}
|
|||
|
if chip < 256 then
|
|||
|
bytes = TableConcat(bytes, {
|
|||
|
charDict['\"'], 0xF9,0x00,chipBytes[1],0x01,0x00,0xF9,0x00,code,0x03, charDict['\"'],charDict['!'],charDict['!']
|
|||
|
})
|
|||
|
else
|
|||
|
bytes = TableConcat(bytes, {
|
|||
|
charDict['\"'], 0xF9,0x00,chipBytes[1],0x02,0x00,0xF9,0x00,code,0x03, charDict['\"'],charDict['!'],charDict['!']
|
|||
|
})
|
|||
|
end
|
|||
|
return bytes
|
|||
|
end
|
|||
|
local GenerateKeyItemGet = function(item, amt)
|
|||
|
bytes = {
|
|||
|
0xF6, 0x00, item, amt,
|
|||
|
charDict['G'], charDict['o'], charDict['t'], charDict[' '], charDict['a'], charDict['\n'],
|
|||
|
charDict['\"'], 0xF9, 0x00, item, 0x00, charDict['\"'],charDict['!'],charDict['!']
|
|||
|
}
|
|||
|
return bytes
|
|||
|
end
|
|||
|
local GenerateSubChipGet = function(subchip, amt)
|
|||
|
-- SubChips have an extra bit of trouble. If you have too many, they're supposed to skip to another text bank that doesn't give you the item
|
|||
|
-- Instead, I'm going to just let it get eaten
|
|||
|
bytes = {
|
|||
|
0xF6, 0x20, subchip, amt, 0xFF, 0xFF, 0xFF,
|
|||
|
charDict['G'], charDict['o'], charDict['t'], charDict[' '], charDict['a'], charDict['\n'],
|
|||
|
charDict['S'], charDict['u'], charDict['b'], charDict['C'], charDict['h'], charDict['i'], charDict['p'], charDict[' '], charDict['f'], charDict['o'], charDict['r'], charDict['\n'],
|
|||
|
charDict['\"'], 0xF9, 0x00, subchip, 0x00, charDict['\"'],charDict['!'],charDict['!']
|
|||
|
}
|
|||
|
return bytes
|
|||
|
end
|
|||
|
local GenerateZennyGet = function(amt)
|
|||
|
zennyBytes = int32ToByteList_le(amt)
|
|||
|
bytes = {
|
|||
|
0xF6, 0x30, zennyBytes[1], zennyBytes[2], zennyBytes[3], zennyBytes[4], 0xFF, 0xFF, 0xFF,
|
|||
|
charDict['G'], charDict['o'], charDict['t'], charDict[' '], charDict['a'], charDict['\n'], charDict['\"']
|
|||
|
}
|
|||
|
-- The text needs to be added one char at a time, so we need to convert the number to a string then iterate through it
|
|||
|
zennyStr = tostring(amt)
|
|||
|
for i = 1, #zennyStr do
|
|||
|
local c = zennyStr:sub(i,i)
|
|||
|
table.insert(bytes, charDict[c])
|
|||
|
end
|
|||
|
bytes = TableConcat(bytes, {
|
|||
|
charDict[' '], charDict['Z'], charDict['e'], charDict['n'], charDict['n'], charDict['y'], charDict['s'], charDict['\"'],charDict['!'],charDict['!']
|
|||
|
})
|
|||
|
return bytes
|
|||
|
end
|
|||
|
local GenerateProgramGet = function(program, color, amt)
|
|||
|
bytes = {
|
|||
|
0xF6, 0x40, (program * 4), amt, color,
|
|||
|
charDict['G'], charDict['o'], charDict['t'], charDict[' '], charDict['a'], charDict[' '], charDict['N'], charDict['a'], charDict['v'], charDict['i'], charDict['\n'],
|
|||
|
charDict['C'], charDict['u'], charDict['s'], charDict['t'], charDict['o'], charDict['m'], charDict['i'], charDict['z'], charDict['e'], charDict['r'], charDict[' '], charDict['P'], charDict['r'], charDict['o'], charDict['g'], charDict['r'], charDict['a'], charDict['m'], charDict[':'], charDict['\n'],
|
|||
|
charDict['\"'], 0xF9, 0x00, program, 0x05, charDict['\"'],charDict['!'],charDict['!']
|
|||
|
}
|
|||
|
|
|||
|
return bytes
|
|||
|
end
|
|||
|
local GenerateBugfragGet = function(amt)
|
|||
|
fragBytes = int32ToByteList_le(amt)
|
|||
|
bytes = {
|
|||
|
0xF6, 0x50, fragBytes[1], fragBytes[2], fragBytes[3], fragBytes[4], 0xFF, 0xFF, 0xFF,
|
|||
|
charDict['G'], charDict['o'], charDict['t'], charDict[':'], charDict['\n'], charDict['\"']
|
|||
|
}
|
|||
|
-- The text needs to be added one char at a time, so we need to convert the number to a string then iterate through it
|
|||
|
bugFragStr = tostring(amt)
|
|||
|
for i = 1, #bugFragStr do
|
|||
|
local c = bugFragStr:sub(i,i)
|
|||
|
table.insert(bytes, charDict[c])
|
|||
|
end
|
|||
|
bytes = TableConcat(bytes, {
|
|||
|
charDict[' '], charDict['B'], charDict['u'], charDict['g'], charDict['F'], charDict['r'], charDict['a'], charDict['g'], charDict['s'], charDict['\"'],charDict['!'],charDict['!']
|
|||
|
})
|
|||
|
return bytes
|
|||
|
end
|
|||
|
local GenerateGetMessageFromItem = function(item)
|
|||
|
--Special case for progressive undernet
|
|||
|
if item["type"] == "undernet" then
|
|||
|
undernet_id = Check_Progressive_Undernet_ID()
|
|||
|
if undernet_id > 8 then
|
|||
|
return Extra_Progressive_Undernet()
|
|||
|
end
|
|||
|
return GenerateKeyItemGet(Next_Progressive_Undernet_ID(undernet_id),1)
|
|||
|
elseif item["type"] == "chip" then
|
|||
|
return GenerateChipGet(item["itemID"], item["subItemID"], item["count"])
|
|||
|
elseif item["type"] == "key" then
|
|||
|
return GenerateKeyItemGet(item["itemID"], item["count"])
|
|||
|
elseif item["type"] == "subchip" then
|
|||
|
return GenerateSubChipGet(item["itemID"], item["count"])
|
|||
|
elseif item["type"] == "zenny" then
|
|||
|
return GenerateZennyGet(item["count"])
|
|||
|
elseif item["type"] == "program" then
|
|||
|
return GenerateProgramGet(item["itemID"], item["subItemID"], item["count"])
|
|||
|
elseif item["type"] == "bugfrag" then
|
|||
|
return GenerateBugfragGet(item["count"])
|
|||
|
end
|
|||
|
|
|||
|
return GenerateTextBytes("Empty Message")
|
|||
|
end
|
|||
|
|
|||
|
local GetMessage = function(item)
|
|||
|
startBytes = {0x02, 0x00}
|
|||
|
playerLockBytes = {0xF8,0x00, 0xF8, 0x10}
|
|||
|
msgOpenBytes = {0xF1, 0x02}
|
|||
|
textBytes = GenerateTextBytes("Receiving\ndata from\n"..item["sender"]..".")
|
|||
|
dotdotWaitBytes = {0xEA,0x00,0x0A,0x00,0x4D,0xEA,0x00,0x0A,0x00,0x4D}
|
|||
|
continueBytes = {0xEB, 0xE9}
|
|||
|
-- continueBytes = {0xE9}
|
|||
|
playReceiveAnimationBytes = {0xF8,0x04,0x18}
|
|||
|
chipGiveBytes = GenerateGetMessageFromItem(item)
|
|||
|
playerFinishBytes = {0xF8, 0x0C}
|
|||
|
playerUnlockBytes={0xEB, 0xF8, 0x08}
|
|||
|
-- playerUnlockBytes={0xF8, 0x08}
|
|||
|
endMessageBytes = {0xF8, 0x10, 0xE7}
|
|||
|
|
|||
|
bytes = {}
|
|||
|
bytes = TableConcat(bytes,startBytes)
|
|||
|
bytes = TableConcat(bytes,playerLockBytes)
|
|||
|
bytes = TableConcat(bytes,msgOpenBytes)
|
|||
|
bytes = TableConcat(bytes,textBytes)
|
|||
|
bytes = TableConcat(bytes,dotdotWaitBytes)
|
|||
|
bytes = TableConcat(bytes,continueBytes)
|
|||
|
bytes = TableConcat(bytes,playReceiveAnimationBytes)
|
|||
|
bytes = TableConcat(bytes,chipGiveBytes)
|
|||
|
bytes = TableConcat(bytes,playerFinishBytes)
|
|||
|
bytes = TableConcat(bytes,playerUnlockBytes)
|
|||
|
bytes = TableConcat(bytes,endMessageBytes)
|
|||
|
return bytes
|
|||
|
end
|
|||
|
|
|||
|
local getChipCodeIndex = function(chip_id, chip_code)
|
|||
|
chipCodeArrayStartAddress = 0x8011510 + (0x20 * chip_id)
|
|||
|
for i=1,6 do
|
|||
|
currentCode = memory.read_u8(chipCodeArrayStartAddress + (i-1))
|
|||
|
if currentCode == chip_code then
|
|||
|
return i-1
|
|||
|
end
|
|||
|
end
|
|||
|
return 0
|
|||
|
end
|
|||
|
|
|||
|
local getProgramColorIndex = function(program_id, program_color)
|
|||
|
-- The general case, most programs use white pink or yellow. This is the values the enums already have
|
|||
|
if program_id >= 23 and program_id <= 47 then
|
|||
|
return program_color-1
|
|||
|
end
|
|||
|
--The final three programs only have a color index 0, so just return those
|
|||
|
if program_id > 47 then
|
|||
|
return 0
|
|||
|
end
|
|||
|
--BrakChrg as an AP item only comes in orange, index 0
|
|||
|
if program_id == 3 then
|
|||
|
return 0
|
|||
|
end
|
|||
|
-- every other AP obtainable program returns only color index 3
|
|||
|
return 3
|
|||
|
end
|
|||
|
|
|||
|
local addChip = function(chip_id, chip_code, amount)
|
|||
|
chipStartAddress = 0x02001F60
|
|||
|
chipOffset = 0x12 * chip_id
|
|||
|
chip_code_index = getChipCodeIndex(chip_id, chip_code)
|
|||
|
currentChipAddress = chipStartAddress + chipOffset + chip_code_index
|
|||
|
currentChipCount = memory.read_u8(currentChipAddress)
|
|||
|
memory.write_u8(currentChipAddress,currentChipCount+amount)
|
|||
|
end
|
|||
|
|
|||
|
local addProgram = function(program_id, program_color, amount)
|
|||
|
programStartAddress = 0x02001A80
|
|||
|
programOffset = 0x04 * program_id
|
|||
|
program_code_index = getProgramColorIndex(program_id, program_color)
|
|||
|
currentProgramAddress = programStartAddress + programOffset + program_code_index
|
|||
|
currentProgramCount = memory.read_u8(currentProgramAddress)
|
|||
|
memory.write_u8(currentProgramAddress, currentProgramCount+amount)
|
|||
|
end
|
|||
|
|
|||
|
local addSubChip = function(subchip_id, amount)
|
|||
|
subChipStartAddress = 0x02001A30
|
|||
|
--SubChip indices start after the key items, so subtract 112 from the index to get the actual subchip index
|
|||
|
currentSubChipAddress = subChipStartAddress + (subchip_id - 112)
|
|||
|
currentSubChipCount = memory.read_u8(currentSubChipAddress)
|
|||
|
--TODO check submem, reject if number too big
|
|||
|
memory.write_u8(currentSubChipAddress, currentSubChipCount+amount)
|
|||
|
end
|
|||
|
|
|||
|
local changeZenny = function(val)
|
|||
|
if val == nil then
|
|||
|
return 0
|
|||
|
end
|
|||
|
if memory.read_u32_le(0x20018F4) <= math.abs(tonumber(val)) and tonumber(val) < 0 then
|
|||
|
memory.write_u32_le(0x20018f4, 0)
|
|||
|
val = 0
|
|||
|
return "empty"
|
|||
|
end
|
|||
|
memory.write_u32_le(0x20018f4, memory.read_u32_le(0x20018F4) + tonumber(val))
|
|||
|
if memory.read_u32_le(0x20018F4) > 999999 then
|
|||
|
memory.write_u32_le(0x20018F4, 999999)
|
|||
|
end
|
|||
|
return val
|
|||
|
end
|
|||
|
|
|||
|
local changeFrags = function(val)
|
|||
|
if val == nil then
|
|||
|
return 0
|
|||
|
end
|
|||
|
if memory.read_u16_le(0x20018F8) <= math.abs(tonumber(val)) and tonumber(val) < 0 then
|
|||
|
memory.write_u16_le(0x20018f8, 0)
|
|||
|
val = 0
|
|||
|
return "empty"
|
|||
|
end
|
|||
|
memory.write_u16_le(0x20018f8, memory.read_u16_le(0x20018F8) + tonumber(val))
|
|||
|
if memory.read_u16_le(0x20018F8) > 9999 then
|
|||
|
memory.write_u16_le(0x20018F8, 9999)
|
|||
|
end
|
|||
|
return val
|
|||
|
end
|
|||
|
|
|||
|
-- Fix Health Pools
|
|||
|
local fix_hp = function()
|
|||
|
-- Current Health fix
|
|||
|
if IsInBattle() and not (memory.read_u16_le(0x20018A0) == memory.read_u16_le(0x2037294)) then
|
|||
|
memory.write_u16_le(0x20018A0, memory.read_u16_le(0x2037294))
|
|||
|
end
|
|||
|
|
|||
|
-- Max Health Fix
|
|||
|
if IsInBattle() and not (memory.read_u16_le(0x20018A2) == memory.read_u16_le(0x2037296)) then
|
|||
|
memory.write_u16_le(0x20018A2, memory.read_u16_le(0x2037296))
|
|||
|
end
|
|||
|
end
|
|||
|
|
|||
|
local changeRegMemory = function(amt)
|
|||
|
regMemoryAddress = 0x02001897
|
|||
|
currentRegMem = memory.read_u8(regMemoryAddress)
|
|||
|
memory.write_u8(regMemoryAddress, currentRegMem + amt)
|
|||
|
end
|
|||
|
|
|||
|
local changeMaxHealth = function(val)
|
|||
|
fix_hp()
|
|||
|
if val == nil then
|
|||
|
fix_hp()
|
|||
|
return 0
|
|||
|
end
|
|||
|
if math.abs(tonumber(val)) >= memory.read_u16_le(0x20018A2) and tonumber(val) < 0 then
|
|||
|
memory.write_u16_le(0x20018A2, 0)
|
|||
|
if IsInBattle() then
|
|||
|
memory.write_u16_le(0x2037296, memory.read_u16_le(0x20018A2))
|
|||
|
if memory.read_u16_le(0x2037296) >= memory.read_u16_le(0x20018A2) then
|
|||
|
memory.write_u16_le(0x2037296, memory.read_u16_le(0x20018A2))
|
|||
|
end
|
|||
|
end
|
|||
|
fix_hp()
|
|||
|
return "lethal"
|
|||
|
end
|
|||
|
memory.write_u16_le(0x20018A2, memory.read_u16_le(0x20018A2) + tonumber(val))
|
|||
|
if memory.read_u16_le(0x20018A2) > 9999 then
|
|||
|
memory.write_u16_le(0x20018A2, 9999)
|
|||
|
end
|
|||
|
if IsInBattle() then
|
|||
|
memory.write_u16_le(0x2037296, memory.read_u16_le(0x20018A2))
|
|||
|
end
|
|||
|
fix_hp()
|
|||
|
return val
|
|||
|
end
|
|||
|
|
|||
|
local SendItem = function(item)
|
|||
|
if item["type"] == "undernet" then
|
|||
|
undernet_id = Check_Progressive_Undernet_ID()
|
|||
|
if undernet_id > 8 then
|
|||
|
-- Generate Extra BugFrags
|
|||
|
changeFrags(20)
|
|||
|
gui.addmessage("Receiving extra Undernet Rank from "..item["sender"]..", +20 BugFrags")
|
|||
|
-- print("Receiving extra Undernet Rank from "..item["sender"]..", +20 BugFrags")
|
|||
|
else
|
|||
|
itemAddress = key_item_start_address + Next_Progressive_Undernet_ID(undernet_id)
|
|||
|
|
|||
|
itemCount = memory.read_u8(itemAddress)
|
|||
|
itemCount = itemCount + item["count"]
|
|||
|
memory.write_u8(itemAddress, itemCount)
|
|||
|
gui.addmessage("Received Undernet Rank from player "..item["sender"])
|
|||
|
-- print("Received Undernet Rank from player "..item["sender"])
|
|||
|
end
|
|||
|
elseif item["type"] == "chip" then
|
|||
|
addChip(item["itemID"], item["subItemID"], item["count"])
|
|||
|
gui.addmessage("Received Chip "..item["itemName"].." from player "..item["sender"])
|
|||
|
-- print("Received Chip "..item["itemName"].." from player "..item["sender"])
|
|||
|
elseif item["type"] == "key" then
|
|||
|
itemAddress = key_item_start_address + item["itemID"]
|
|||
|
itemCount = memory.read_u8(itemAddress)
|
|||
|
itemCount = itemCount + item["count"]
|
|||
|
memory.write_u8(itemAddress, itemCount)
|
|||
|
-- HPMemory will increase the internal counter but not actually increase the HP. If the item is one of those, do that
|
|||
|
if item["itemID"] == 96 then
|
|||
|
changeMaxHealth(20)
|
|||
|
end
|
|||
|
-- Same for the RegUps, but there's three of those
|
|||
|
if item["itemID"] == 98 then
|
|||
|
changeRegMemory(1)
|
|||
|
end
|
|||
|
if item["itemID"] == 99 then
|
|||
|
changeRegMemory(2)
|
|||
|
end
|
|||
|
if item["itemID"] == 100 then
|
|||
|
changeRegMemory(3)
|
|||
|
end
|
|||
|
gui.addmessage("Received Key Item "..item["itemName"].." from player "..item["sender"])
|
|||
|
-- print("Received Key Item "..item["itemName"].." from player "..item["sender"])
|
|||
|
elseif item["type"] == "subchip" then
|
|||
|
addSubChip(item["itemID"], item["count"])
|
|||
|
gui.addmessage("Received SubChip "..item["itemName"].." from player "..item["sender"])
|
|||
|
-- print("Received SubChip "..item["itemName"].." from player "..item["sender"])
|
|||
|
elseif item["type"] == "zenny" then
|
|||
|
changeZenny(item["count"])
|
|||
|
gui.addmessage("Received "..item["count"].."z from "..item["sender"])
|
|||
|
-- print("Received "..item["count"].."z from "..item["sender"])
|
|||
|
elseif item["type"] == "program" then
|
|||
|
addProgram(item["itemID"], item["subItemID"], item["count"])
|
|||
|
gui.addmessage("Received Program "..item["itemName"].." from player "..item["sender"])
|
|||
|
-- print("Received Program "..item["itemName"].." from player "..item["sender"])
|
|||
|
elseif item["type"] == "bugfrag" then
|
|||
|
changeFrags(item["count"])
|
|||
|
gui.addmessage("Received "..item["count"].." BugFrag(s) from "..item["sender"])
|
|||
|
-- print("Received "..item["count"].." BugFrag(s) from "..item["sender"])
|
|||
|
end
|
|||
|
end
|
|||
|
|
|||
|
-- Set the flags for opening the shortcuts as soon as the Cybermetro passes are received to save having to check email
|
|||
|
local OpenShortcuts = function()
|
|||
|
if (memory.read_u8(key_item_start_address + 92) > 0) then
|
|||
|
memory.write_u8(0x2000032, bit.bor(memory.read_u8(0x2000032),0x10))
|
|||
|
end
|
|||
|
-- if CSciPass
|
|||
|
if (memory.read_u8(key_item_start_address + 93) > 0) then
|
|||
|
memory.write_u8(0x2000032, bit.bor(memory.read_u8(0x2000032),0x08))
|
|||
|
end
|
|||
|
if (memory.read_u8(key_item_start_address + 94) > 0) then
|
|||
|
memory.write_u8(0x2000032, bit.bor(memory.read_u8(0x2000032),0x20))
|
|||
|
end
|
|||
|
if (memory.read_u8(key_item_start_address + 95) > 0) then
|
|||
|
memory.write_u8(0x2000032, bit.bor(memory.read_u8(0x2000032),0x40))
|
|||
|
end
|
|||
|
end
|
|||
|
|
|||
|
local RestoreItemRam = function()
|
|||
|
if backup_bytes ~= nil then
|
|||
|
memory.write_bytes_as_array(0x203fe10, backup_bytes)
|
|||
|
end
|
|||
|
backup_bytes = nil
|
|||
|
end
|
|||
|
|
|||
|
local process_block = function(block)
|
|||
|
-- Sometimes the block is nothing, if this is the case then quietly stop processing
|
|||
|
if block == nil then
|
|||
|
return
|
|||
|
end
|
|||
|
debugEnabled = block['debug']
|
|||
|
-- Queue item for receiving, if one exists
|
|||
|
if (itemsReceived ~= block['items']) then
|
|||
|
itemsReceived = block['items']
|
|||
|
end
|
|||
|
return
|
|||
|
end
|
|||
|
|
|||
|
local itemStateMachineProcess = function()
|
|||
|
if itemState == ITEMSTATE_NONINITIALIZED then
|
|||
|
itemQueueCounter = 120
|
|||
|
-- Only exit this state the first time a dialog window pops up. This way we know for sure that we're ready to receive
|
|||
|
if not IsInMenu() and (IsInDialog() or IsInTransition()) then
|
|||
|
itemState = ITEMSTATE_NONITEM
|
|||
|
end
|
|||
|
elseif itemState == ITEMSTATE_NONITEM then
|
|||
|
itemQueueCounter = 120
|
|||
|
-- Always attempt to restore the previously stored memory in this state
|
|||
|
-- Exit this state whenever the game is in an itemable status
|
|||
|
if IsItemable() then
|
|||
|
itemState = ITEMSTATE_IDLE
|
|||
|
end
|
|||
|
elseif itemState == ITEMSTATE_IDLE then
|
|||
|
-- Remain Idle until an item is sent or we enter a non itemable status
|
|||
|
if not IsItemable() then
|
|||
|
itemState = ITEMSTATE_NONITEM
|
|||
|
end
|
|||
|
if itemQueueCounter == 0 then
|
|||
|
if #itemsReceived > loadItemIndexFromRAM() and not IsItemQueued() then
|
|||
|
itemQueued = itemsReceived[loadItemIndexFromRAM()+1]
|
|||
|
SendItem(itemQueued)
|
|||
|
itemState = ITEMSTATE_SENT
|
|||
|
end
|
|||
|
else
|
|||
|
itemQueueCounter = itemQueueCounter - 1
|
|||
|
end
|
|||
|
elseif itemState == ITEMSTATE_SENT then
|
|||
|
-- Once the item is sent, wait for the dialog to close. Then clear the item bit and be ready for the next item.
|
|||
|
if IsInTransition() or IsInMenu() or IsOnTitle() then
|
|||
|
itemState = ITEMSTATE_NONITEM
|
|||
|
itemQueued = nil
|
|||
|
RestoreItemRam()
|
|||
|
elseif not IsInDialog() then
|
|||
|
itemState = ITEMSTATE_IDLE
|
|||
|
saveItemIndexToRAM(itemQueued["itemIndex"])
|
|||
|
itemQueued = nil
|
|||
|
RestoreItemRam()
|
|||
|
end
|
|||
|
end
|
|||
|
end
|
|||
|
local receive = function()
|
|||
|
l, e = mmbn3Socket:receive()
|
|||
|
|
|||
|
-- Handle incoming message
|
|||
|
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
|
|||
|
process_block(json.decode(l))
|
|||
|
end
|
|||
|
|
|||
|
local send = function()
|
|||
|
-- Determine message to send back
|
|||
|
local retTable = {}
|
|||
|
retTable["playerName"] = loadPlayerNameFromROM()
|
|||
|
retTable["scriptVersion"] = script_version
|
|||
|
retTable["locations"] = check_all_locations()
|
|||
|
retTable["gameComplete"] = is_game_complete()
|
|||
|
|
|||
|
-- Send the message
|
|||
|
msg = json.encode(retTable).."\n"
|
|||
|
local ret, error = mmbn3Socket: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 (bizhawk_major > 2 or (bizhawk_major == 2 and bizhawk_minor >= 7)==false) then
|
|||
|
print("Must use a version of bizhawk 2.7.0 or higher")
|
|||
|
return
|
|||
|
end
|
|||
|
server, error = socket.bind('localhost', 28922)
|
|||
|
|
|||
|
while true do
|
|||
|
frame = frame + 1
|
|||
|
|
|||
|
if not (curstate == prevstate) then
|
|||
|
prevstate = curstate
|
|||
|
end
|
|||
|
|
|||
|
itemStateMachineProcess()
|
|||
|
|
|||
|
if (curstate == STATE_OK) or (curstate == STATE_INITIAL_CONNECTION_MADE) or (curstate == STATE_TENTATIVELY_CONNECTED) then
|
|||
|
-- If we're connected and everything's fine, receive and send data from the network
|
|||
|
if (frame % 60 == 0) then
|
|||
|
receive()
|
|||
|
send()
|
|||
|
-- Perform utility functions which read and write data but aren't directly related to checks
|
|||
|
OpenShortcuts()
|
|||
|
end
|
|||
|
elseif (curstate == STATE_UNINITIALIZED) then
|
|||
|
-- If we're uninitialized, attempt to make the connection.
|
|||
|
if (frame % 120 == 0) then
|
|||
|
server:settimeout(2)
|
|||
|
local client, timeout = server:accept()
|
|||
|
if timeout == nil then
|
|||
|
print('Initial Connection Made')
|
|||
|
curstate = STATE_INITIAL_CONNECTION_MADE
|
|||
|
mmbn3Socket = client
|
|||
|
mmbn3Socket:settimeout(0)
|
|||
|
else
|
|||
|
print('Connection failed, ensure MMBN3Client is running and rerun connector_mmbn3.lua')
|
|||
|
return
|
|||
|
end
|
|||
|
end
|
|||
|
end
|
|||
|
|
|||
|
-- Handle the debug data display
|
|||
|
gui.cleartext()
|
|||
|
if debugEnabled then
|
|||
|
-- gui.text(0,0,"Item Queued: "..tostring(IsItemQueued()))
|
|||
|
-- gui.text(0,16,"In Battle: "..tostring(IsInBattle()))
|
|||
|
-- gui.text(0,32,"In Dialog: "..tostring(IsInDialog()))
|
|||
|
-- gui.text(0,48,"In Menu: "..tostring(IsInMenu()))
|
|||
|
gui.text(0,48,"Item Wait Time: "..tostring(itemQueueCounter))
|
|||
|
gui.text(0,64,itemState)
|
|||
|
if itemQueued == nil then
|
|||
|
gui.text(0,80,"No item queued")
|
|||
|
else
|
|||
|
gui.text(0,80,itemQueued["type"].." "..itemQueued["itemID"])
|
|||
|
end
|
|||
|
gui.text(0,96,"Item Index: "..loadItemIndexFromRAM())
|
|||
|
end
|
|||
|
|
|||
|
emu.frameadvance()
|
|||
|
end
|
|||
|
end
|
|||
|
|
|||
|
main()
|