View difference between Paste ID: cF10xgKM and 35rsZnmF
SHOW: | | - or go back to the newest paste.
1
-- Used with "BizHawk" emulator - load ROM -> Tools -> Lua Console -> Script -> Open Script -> double-click until green play sign
2
3
-- Enable or disable the overlay with [R+SELECT]. Haven't played with this yet, so
4
-- you may end up seeing overlay popups during the normal game at odd times.
5
6
-- mod type for offsets, works off of the 'rom name' (what it says in title bar/saves games as)
7
-- if your rom isn't detected just add it in this format (including the , and any " marks):
8
-- ["ROM NAME"] = romtype,
9
-- where romtype is one of these: nil, "ROTDS", "BNW"
10
-- any other romtype will use nil (vanilla) settings
11
12
local MODS = { 
13
14
["Final Fantasy III (USA)"]     = nil,     -- vanilla, default. -- means comments
15
["Return of the Dark Sorcerer"] = "ROTDS", -- uses rotds extended item/spell names
16
["Brave New World 2.0"]         = "BNW",   -- mod'd dance chance
17
["FFVI T Edition"]              = "japan", -- nothing yet, the version is stripped further down
18
19
}
20
21
--- =======================
22
--- all settings are above this
23
--- =======================
24
25
26
27
local romname = gameinfo.getromname()
28
29
-- t-edition has ever-changing patch number on the end
30
if (romname:sub(1,14) == "FFVI T Edition") then romname = "FFVI T Edition" end
31
local MOD_TYPE = MODS[romname]
32
function initialize()
33
	 gui.defaultPixelFont("fceux")
34-
	local x = bit.band(memory.readbyte(0x0A,"WRAM"),0x40)
34+
35-
	if (x > 0) then return true end
35+
36-
	local y = bit.band(memory.readbyte(0x0B,"WRAM"),0x40)
36+
37-
	if (y > 0) then return true end
37+
38
		espers = { 0x26F6E1, 8, 54 },
39
	   attacks = { 0x26F7B9, 10, 81 },
40
	espattacks = { 0x26FE8F, 10, 256 },
41
		dances = { 0x26FF9D, 12, 283 },
42
}
43
	offMonsterData = 0xF0000
44
	lenMonsterData = 32
45
	offItemNames = 0x12B300
46
	lenItemNames = 13
47
	if (MOD_TYPE == "ROTDS") then
48
		offNames.spells = { 0x316100, 9, 0 }
49
		offNames.espers = { 0x3162E6, 12, 54 }
50
		offNames.attacks = { 0x31645E, 16, 81 }
51
		offNames.espattacks = { 0x316DDE, 16, 256 }
52
		offNames.dances = { 0x3170FE, 13, 283 }
53
		lenItemNames = 13
54
	end
55
end
56
initialize()
57
58
-- maps each font byte to the appropriate character, FF should break loop, space for anything else
59
local fontMap = { [0x80] = "A", [0x81] = "B", [0x82] = "C", [0x83] = "D", [0x84] = "E", [0x85] = "F", [0x86] = "G", [0x87] = "H", [0x88] = "I", [0x89] = "J", [0x8A] = "K", [0x8B] = "L", [0x8C] = "M", [0x8D] = "N", [0x8E] = "O", [0x8F] = "P", [0x90] = "Q", [0x91] = "R", [0x92] = "S", [0x93] = "T", [0x94] = "U", [0x95] = "V", [0x96] = "W", [0x97] = "X", [0x98] = "Y", [0x99] = "Z", [0x9A] = "a", [0x9B] = "b", [0x9C] = "c", [0x9D] = "d", [0x9E] = "e", [0x9F] = "f", [0xA0] = "g", [0xA1] = "h", [0xA2] = "i", [0xA3] = "j", [0xA4] = "k", [0xA5] = "l", [0xA6] = "m", [0xA7] = "n", [0xA8] = "o", [0xA9] = "p", [0xAA] = "q", [0xAB] = "r", [0xAC] = "s", [0xAD] = "t", [0xAE] = "u", [0xAF] = "v", [0xB0] = "w", [0xB1] = "x", [0xB2] = "y", [0xB3] = "z", [0xB4] = "0", [0xB5] = "1", [0xB6] = "2", [0xB7] = "3", [0xB8] = "4", [0xB9] = "5", [0xBA] = "6", [0xBB] = "7", [0xBC] = "8", [0xBD] = "9", [0xBE] = "!", [0xBF] = "?", [0xC3] = "'", [0xC4] = "-", [0xC5] = ".", }
60
61
local monSpecials = {
62
[0x00]="+Darkness", [0x01]="+Zombie", [0x02]="+Poison", [0x03]="+Magitek", [0x04]="+Invis", [0x05]="+Imp", [0x06]="+Stone", [0x07]="+Death", [0x08]="+Doom", [0x09]="+Critical", [0x0A]="+Blink", [0x0B]="+Silence", [0x0C]="+Berserk", [0x0D]="+Confuse", [0x0E]="+Sap", [0x0F]="+Sleep", [0x10]="+Dance", [0x11]="+Regen", [0x12]="+Slow", [0x13]="+Haste", [0x14]="+Stop", [0x15]="+Shell", [0x16]="+Protect", [0x17]="+Reflect", [0x18]="+Rage", [0x19]="+Frozen", [0x1A]="+Reraise", [0x1B]="+Morph", [0x1C]="+Spellcast", [0x1D]="+Flee", [0x1E]="+Interceptor", [0x1F]="+Float", [0x20]=" Damage + 50%", [0x21]=" Damage +100%", [0x22]=" Damage +150%", [0x23]=" Damage +200%", [0x24]=" Damage +250%", [0x25]=" Damage +300%", [0x26]=" Damage +350%", [0x27]=" Damage +400%", [0x28]=" Damage +450%", [0x29]=" Damage +500%", [0x2A]=" Damage +550%", [0x2B]=" Damage +600%", [0x2C]=" Damage +650%", [0x2D]=" Damage +700%", [0x2E]=" Damage +750%", [0x2F]=" Damage +800%",
63
}
64
function hexformat(input) return string.format("%02x", input):upper() end
65
function trim(out) return (out:gsub("^%s*(.-)%s*$", "%1")) end
66
-- skipFF to anything to prevent breaking on FF
67
function translate(byteTable,skipFF)
68
	local out = ""
69
	local start = 0
70
	if (not byteTable[start]) then start = 1 end
71
	for i=start,#byteTable do
72
		local hex = byteTable[i]
73
		if ((hex == 0xFF or not hex) and not skipFF) then break end
74
		if (fontMap[hex]) then 
75
			out = out .. fontMap[hex]
76
		else
77
			out = out .. " "
78
		end
79
	end
80
	out = trim(out)
81
	return out
82
end
83
function getStatuses(byteA,byteB,byteC)
84
	local statuses = { {"Blind", "Zombi", "Poisn", "M-Tek", "Invis", "Imp  ", "Stone", "Death"},
85
					   {"Doom ", "Hurt ", "Image", "Mute ", "Brsrk", "Muddl", "Sap  ", "Sleep"},
86
					   {"Float", "Regen", "Slow ", "Haste", "Stop ", "Shell", "Safe ", "Rflct"} }
87
	local outStatuses = {}
88
	for _,byte in pairs({ byteA, byteB, byteC }) do
89
    	for i=1,#statuses[_] do
90
			if (bit.band(byte,bit.lshift(1,(i-1))) > 0) then
91
				outStatuses[#outStatuses+1] = statuses[_][i]
92
			end
93
		end
94
	end
95
	local out = ""
96
	for _,v in pairs(outStatuses) do
97
		out = out .. v .. " "
98
		if (_ % 6 == 0) then out = out .. "\n      " end
99
	end
100
	return trim(out)
101
end
102
function getElements(byte)
103
	local elements = { "FIR", "ICE", "THN", "POI", "WND", "HLY", "ERT", "WTR" }
104
	local outElements = {}
105
    for i=1,#elements do
106
		if (bit.band(byte,bit.lshift(1,(i-1))) > 0) then
107
			outElements[#outElements+1] = elements[i]
108
		end
109
	end
110
	local out = ""
111
	for _,v in pairs(outElements) do
112
		out = out .. v .. " "
113
		--if (_ % 5 == 0) then out = out .. "\n" end
114
	end
115
	return trim(out)
116
end
117
function getSpellName(index)
118
	local type = nil
119
	if (index >= offNames.dances[3]) then type = offNames.dances
120
	elseif (index >= offNames.espattacks[3]) then type = offNames.espattacks
121
	elseif (index >= offNames.attacks[3]) then type = offNames.attacks
122
	elseif (index >= offNames.espers[3]) then type = offNames.espers
123
	else type = offNames.spells
124
	end
125
	local beginOffset = type[1]
126
	local textLength = type[2]
127
	local startSkillIndex = type[3]
128
	local firstByte = beginOffset + ((index-startSkillIndex)*textLength)
129
	local out = memory.readbyterange(firstByte,textLength,"CARTROM")
130
	out = translate(out)
131
	return out
132
end
133
function getItemName(index)
134
	local out = memory.readbyterange(offItemNames + (index*lenItemNames),lenItemNames,"CARTROM")
135
	out = translate(out,"don't-break-on-FF")
136
	if (out == "") then out = "(None)" end
137
	return out
138
end
139
local cursor_state = nil
140
local inputs = { { nil, nil, nil, nil, "R", "L", "X", "A", },
141
				 { "Right", "Left", "Down", "Up", "Start", "Select", "Y", "B" } }
142
local keymap = {}
143
local keymapLastFrame = {}
144
145
function buttonHandler()
146
	-- X is at 0x0A as the 7th bit (0x40), Y is at 0x0B same
147
	local input1 = memory.readbyte(0x04,"WRAM")
148
	local input2 = memory.readbyte(0x05,"WRAM")
149
	if (input1 == 0x00 and input2 == 0x00) then
150
		input1 = memory.readbyte(0x0A)
151
		input2 = memory.readbyte(0x0B)
152
	end
153
	keymapLastFrame = keymap
154
	keymap = {}
155
	for _,byte in pairs({ input1, input2 }) do
156
    	for i=1,#inputs[_] do
157
			if (inputs[_][i] ~= nil) then
158
				if (bit.band(byte,bit.lshift(1,(i-1))) > 0) then
159
					keymap[inputs[_][i]] = true
160
				end
161
			end
162
		end
163
	end
164
end
165
166
function checkIfHelpButton()
167
	-- X is at 0x0A as the 7th bit (0x40), Y is at 0x0B same
168
	if (keymap["X"] or keymap["Y"] or keymapLastFrame["X"] or keymapLastFrame["Y"]) then return true end
169
	return false
170
end
171
172
local enabledToggle = true
173
local messageStartTime = 0
174
local messageText = ""
175
local messageDuration = 0
176
function disabledCheck()
177
	if (keymap["Select"] and keymap["R"] and not (keymapLastFrame["R"] and keymapLastFrame["Select"])) then
178
		enabledToggle = not enabledToggle
179
		if (enabledToggle) then message("Overlay enabled.",4)
180
		else message("Overlay disabled.", 4)		
181
		end
182
	end
183
end
184
function message(msg,duration)
185
	messageStartTime = os.time()
186
	messageText = msg
187
	messageDuration = duration
188
end
189
function messageCheck()
190
	messageTime = os.time()
191
	if (messageTime < (messageStartTime + messageDuration)) then 
192
		gui.drawText(0,210,messageText,"white","black")
193
	end
194
end
195
function processEnemySpecial(idxTargetMonster)
196
	local monStats = memory.readbyterange(offMonsterData + (lenMonsterData * idxTargetMonster),lenMonsterData,"CARTROM")
197
	local specialAttack = monStats[0x1F]
198
	local noEvade = (bit.band(specialAttack,0x80) > 0)
199
	local noDamage = (bit.band(specialAttack,0x40) > 0)
200
	specialAttack = bit.band(specialAttack,0x3F)
201
	specialAttack = monSpecials[specialAttack]
202
	if (specialAttack) then
203
		if (noEvade or noDamage) then
204
			specialAttack = specialAttack..")\n(Special:"
205
			if (noEvade) then specialAttack = specialAttack .. "Can't dodge" end
206
			if (noEvade and noDamage) then specialAttack = specialAttack .. " & " end
207
			if (noDamage) then specialAttack = specialAttack .. "No damage" end
208
		end
209
	else 
210
		specialAttack = "+???"
211
	end
212
	return "(Special:Attack"..specialAttack..")"
213
end
214
local hintLoc = { x = 8, y = 142, height = 9 }
215
function printHint(msg)
216
	local _, extraLineCount = msg:gsub('\n', '\n')
217
	gui.pixelText(hintLoc.x,hintLoc.y-(hintLoc.height * extraLineCount),msg,"white")
218
end
219
function doStealStuff(targetted_monster)
220
	local stealLoc = 0x3310 + (2*targetted_monster)
221
	local rare_steal = memory.read_u8(stealLoc)
222
	local common_steal = memory.read_u8(stealLoc+1)
223
	local itemNames = { getItemName(common_steal), getItemName(rare_steal) }
224
	if (common_steal == 0xFF) then itemNames[1] = "---" end
225
	if (rare_steal   == 0xFF) then itemNames[2] = "---" end	
226
	local out = "Nothing to steal!"
227
	if (itemNames[1] ~= "---" or itemNames[2] ~= "---") then 
228
		out = "Common Steal (7/8): "..itemNames[1].."\n  Rare Steal (1/8): "..itemNames[2]
229
	end
230
	return out
231
end
232
function main()
233
	disabledCheck()
234
	messageCheck()
235
	if (not enabledToggle) then return end
236
237
238
	local paused = memory.read_u8(0x62ab)
239
	if (paused > 0) then
240
-- should probably get a check so it ONLY works in battle
241
--[[
242
		-- show enemy health if paused
243
		for i=0,5 do
244
			local hp = memory.read_u16_le(0x3bfc + (2*i))
245
			if (hp > 0) then
246
				local x = memory.read_u16_le(0x80c3 + (2*i)) -- monster loc x in pixels
247
				local y = memory.read_u16_le(0x80cf + (2*i)) -- monster loc y in pixels
248
				gui.drawText(x,y,hp)
249
			end
250
		end
251
]]--
252
	end
253
254
	-- reads memory of 'cursor state' which appears to be the type of command being hovered over
255
	local last_cursor_state = cursor_state
256
	cursor_state = memory.read_u8(0x7bc2)
257
	local which_character = memory.read_u8(0x62ca) -- offsets cursor info
258
	local idxRecentCommand = memory.read_u8(0x890F + which_character) -- the command we're hovering on main menu
259
	local recentCommandType = memory.read_u8(0x202E + (12*which_character) + (3*idxRecentCommand))
260
	--23 = scrolling page up or down
261
	if (cursor_state == 23 or cursor_state == 24) then cursor_state = last_cursor_state end
262
	--30 = rage
263
	if (cursor_state == 30) then
264
		local rages = memory.readbyterange(0x257E,256)
265
		local battleMenuColumn = 0x892F -- row and scrolled are derived from this (+4/-4)
266
		local column = memory.read_u8(battleMenuColumn + which_character)
267
		local row = memory.read_u8(battleMenuColumn + which_character + 4)
268
		local scrolled = memory.read_u8(battleMenuColumn + which_character - 4)
269
		local hoverRage = rages[((row+scrolled)*2) + column]
270
		local menuClosing = (memory.read_u8(0x7BCB) > 0) -- get rid of little text-blink as rage menu closes
271
		if (hoverRage < 255 and not menuClosing) then 
272
			local rageLoc = 0xF4600 + (2*hoverRage)
273
			local rageAttacks = { memory.readbyte(rageLoc,"CARTROM"), memory.readbyte(rageLoc+1,"CARTROM") }
274
			for i=1,2 do rageAttacks[i] = getSpellName(rageAttacks[i]) end --.."("..rageAttacks[i]..")" end
275
			local monStats = memory.readbyterange(offMonsterData + (lenMonsterData * hoverRage),lenMonsterData,"CARTROM")
276
			local out = "Rage: "..rageAttacks[1] .. "/" .. rageAttacks[2]
277
			if (rageAttacks[2] == "Special" or rageAttacks[1] == "Special") then 
278
				specialAttack = processEnemySpecial(hoverRage)
279
				out = specialAttack .. "\n"..out
280
			end
281
			printHint(out)
282
			if (checkIfHelpButton()) then
283
				local blockedStatuses = { monStats[0x14], monStats[0x15], monStats[0x16] }
284
				local appliedStatuses = { monStats[0x1B], monStats[0x1C], monStats[0x1D] }
285
				local absorbElements = monStats[0x17]
286
				local immuneElements = monStats[0x18]
287
				local weakElements = monStats[0x19]
288
				local bytes = ""
289
				for i=0,#monStats do
290
					bytes = bytes .. " " .. hexformat(monStats[i])
291
					if ((i+1) % 8 == 0) then bytes = bytes .. "\n" end
292
				end
293
				local infoText = ""
294
				blockedStatuses = getStatuses(blockedStatuses[1],blockedStatuses[2],blockedStatuses[3])
295
				appliedStatuses = getStatuses(appliedStatuses[1],appliedStatuses[2],appliedStatuses[3])
296
				if (#appliedStatuses > 0) then infoText = infoText.."GAINS:"..appliedStatuses.."\n" end
297
				if (#blockedStatuses > 0) then infoText = infoText.. "BLOCK:"..blockedStatuses.."\n" end
298
				absorbElements = getElements(absorbElements)
299
				immuneElements = getElements(immuneElements)
300
				weakElements = getElements(weakElements)
301
				if (#absorbElements > 0) then infoText = infoText.. "ABS:"..absorbElements.."\n" end
302
				if (#immuneElements > 0) then infoText = infoText.. "IMM:"..immuneElements.."\n" end
303
				if (#weakElements > 0) then infoText = infoText.. "WEAK:"..weakElements.."\n" end
304
				infoText = trim(infoText)
305
				gui.pixelText(0,0,string.format("%-99s",infoText),"white")
306
			end
307
		end
308
	-- dances
309
	elseif (cursor_state == 33) then
310
		local dances = memory.readbyterange(0x267E,8)
311
		local battleMenuColumn = 0x8937 -- row and scrolled are derived from this (+4/-4)
312
		local column = memory.read_u8(battleMenuColumn + which_character)
313
		local row = memory.read_u8(battleMenuColumn + which_character + 4)
314
		local scrolled = memory.read_u8(battleMenuColumn + which_character - 4)
315
	
316
		local hoverDance = dances[((row+scrolled)*2) + column]
317
		local menuClosing = (memory.read_u8(0x7BCB) > 0) -- get rid of little text-blink as dance menu closes
318
		if (hoverDance < 255 and not menuClosing) then 
319
			local danceLoc = 0xFFE80 + (4*hoverDance)
320
			local danceAttacks = { memory.readbyte(danceLoc,"CARTROM"), memory.readbyte(danceLoc+1,"CARTROM"), 
321
							memory.readbyte(danceLoc+2,"CARTROM"), memory.readbyte(danceLoc+3,"CARTROM") }
322
			for i=1,4 do danceAttacks[i] = getSpellName(danceAttacks[i]) end
323
			local danceChances = { 7, 6, 2, 1 }
324
			if (MOD_TYPE == "BNW") then danceChances = { 7, 5, 3, 1 } end
325
			local out = string.format("Dance: (%s/16) %s\n       (%s/16) %s\n       (%s/16) %s\n       (%s/16) %s", danceChances[1], danceAttacks[1], danceChances[2], danceAttacks[2], danceChances[3], danceAttacks[3], danceChances[4], danceAttacks[4])
326
			printHint(out)
327
		end
328
	-- targetting enemies
329
	elseif (cursor_state == 0x38) then
330
		local targetted_monster = memory.read_u8(0x7B7E)
331
 		-- only bother if we're actually targetting a monster (ie not an ally)
332
		if (targetted_monster > 0) then
333
			targetted_monster = (math.log(targetted_monster) / math.log(2))
334
			if (recentCommandType == 0x00) then -- fight
335
				local weaponPropLoc = 0x3CBC --main, then off
336
				weaponPropLoc = weaponPropLoc + (2*which_character)
337
				local my_weapons = { memory.read_u8(weaponPropLoc), memory.read_u8(weaponPropLoc + 1) }
338
				-- the offhand will be shortcircuited if first passes
339
				my_weapons[1] = (bit.band(my_weapons[1],0x10) > 0 and my_weapons[1] < 32)
340
				if (my_weapons[1] or (bit.band(my_weapons[2],0x10) > 0 and my_weapons[2] < 32)) then
341
					local out = doStealStuff(targetted_monster)
342
					local weaponLoc = 0x3CA8
343
					weaponLoc = weaponLoc + (2*which_character)
344
					local providingWeapon = "ThiefKnife"
345
					if (my_weapons[1]) then 
346
						providingWeapon = getItemName(memory.read_u8(weaponLoc))
347
					else -- use off-hand since it wasn't from mainhand
348
						providingWeapon = getItemName(memory.read_u8(weaponLoc + 1))
349
					end
350
					printHint("* Shown due to "..providingWeapon..":\n"..out)
351
				end
352
			elseif (recentCommandType == 0x05 or recentCommandType == 0x06) then -- we're stealing and targetting enemies
353
				printHint(doStealStuff(targetted_monster))
354
			elseif (recentCommandType == 0x0D) then --sketch
355
				local idxTargetMonster = memory.read_u16_le(0x2001 + (2*targetted_monster))
356
				local cantSketchByte = memory.readbyte(offMonsterData + (lenMonsterData * idxTargetMonster) + 0x13,"CARTROM")
357
				if (bit.band(cantSketchByte,0x20) > 0) then
358
					printHint("Can't sketch!")
359
				else
360
					local sketchLoc = 0xF4300 + (2*idxTargetMonster)
361
					local sketchAttacks = { memory.readbyte(sketchLoc,"CARTROM"), memory.readbyte(sketchLoc+1,"CARTROM") }
362
					for i=1,2 do sketchAttacks[i] = getSpellName(sketchAttacks[i]) end
363
					local out = "Sketch: (75%) "..sketchAttacks[2] .. "\n        (25%) " .. sketchAttacks[1] ..""
364
					if (sketchAttacks[2] == "Special" or sketchAttacks[1] == "Special") then 
365
						specialAttack = processEnemySpecial(idxTargetMonster)
366
						out = specialAttack.."\n"..out
367
					end
368
					printHint(out)
369
				end
370
			elseif (recentCommandType == 0x0E) then -- chaos control
371
				local idxTargetMonster = memory.read_u16_le(0x2001 + (2*targetted_monster))
372
				local cantControlByte = memory.readbyte(offMonsterData + (lenMonsterData * idxTargetMonster) + 0x13,"CARTROM")
373
				if (bit.band(cantControlByte,0x80) > 0) then
374
					printHint("Can't control!")
375
				else
376
					local controlLoc = 0xF3D00 + (4*idxTargetMonster)
377
					local controlAttacks = { memory.readbyte(controlLoc,"CARTROM"), memory.readbyte(controlLoc+1,"CARTROM"), memory.readbyte(controlLoc+2,"CARTROM"), memory.readbyte(controlLoc+3,"CARTROM") }
378
					local hasSpecial = false
379
					local namesControlAttacks = {}
380
					for i=1,4 do 
381
						if (controlAttacks[i] ~= 0xFF) then 
382
							namesControlAttacks[#namesControlAttacks + 1] = getSpellName(controlAttacks[i]) 
383
							if (namesControlAttacks[#namesControlAttacks] == "Special") then hasSpecial = true end
384
							if (#namesControlAttacks == 3) then namesControlAttacks[3] = "\n         "..namesControlAttacks[3] end
385
						end
386
					end
387
					local out = "Control: "..table.concat(namesControlAttacks,", ")
388
					if (hasSpecial) then
389
						specialAttack = processEnemySpecial(idxTargetMonster)
390
						out = specialAttack.."\n"..out
391
					end
392
					printHint(out)
393
				end
394
			end
395
		end
396
	end
397
end
398
399
while true do
400
	emu.frameadvance();
401
	-- prevent it from doing dumb things while rom isn't even loaded
402
	if (romname ~= "Null") then 
403
		buttonHandler()
404
		main()
405
	end
406
end