Okay, I am equipped with knowledge of opcodes! Been studying the RAM a touch.
It helps that, if the game encounters a BRK, we get an immediate RTI, so execution continues from where it left off.
0000 * 0002: Unused in build mode?
0003 - 0007: Sound related. Zeroes out every frame.
0008 * 0011: Not used during build mode. Part of it apparently used for menus.
0012 : Used for something. It won't get in our way, at least.
0013 * 003F: Not used during build mode. No reads, no writes. Anything in the way can just be corrupted right out. Something in there is used for menus, though.
0040 - 0041: Apparently tested for specific IDs. Normally zero.
0042 * 0055: More unused stuff in gameplay.
0056 - 0057: Tested for something. Not sure.
0058 * 005E: Looks unused.
005F - 0072: Most of the stuff here is transferred to 0x002100 region. Not precisely sure how the game would look when this is corrupted. It helps that no matter how badly we ruin this, it won't stop us from further corruptions.
... Still more to look over. I just want to get this bit of information out and relax from trying to analyze what's here, for a time.
Whatever the exact details may be, 007C looks like a small timer, 00D1 looks like a single byte incrementing frame timer, and the thing we really want to reach is 01BD as that's where our camera position is, something we might have full control over.
Okay, I have a small plan now. It isn't much, but hopefully the setup gets the game executing stuff we want.
0064 - Place a Mass Transit tile here. Some of the graphics will likely get screwy. Just a sign things are working! Well, if we leave this alone, we get a long jump right back to before this spot, giving us an undesired loop we won't escape. Placing 000072 as our destination at least lets us jump ahead a little. We can also place something at 0062 instead for what is more likely a bigger graphical mess.
007C - Some kind of timer. Its speed is adjusted by whether you have buttons pressed. Might be helpful to keep in mind, although its range of possible values is limited.
0091 - If this stays 0x40, we may get an RTI, which could easily put a stop to our plans. It looks safe to corrupt, as it's related to menus, and unused in build mode, so placing roads on 0090 means we avoid this mess.
00A9,2x - One of the stack addresses.
00AB,2x - The other stack address.
00AA - Once we're ready to break the game, we place something here. Probably a Power Line or something.
00C7 - Looks "random". I hope it doesn't get us into trouble after carefully aligning the timers.
00D1 - A frame timer. Counts up to FF, then wraps back to 00 to start over again. I'm not sure if we can corrupt 00D2, but if it sticks, it shouldn't cause problems. An easy jump ahead by 0032 would get us past a few addresses, and make it easier to reach 012D, although whether the timer lines up nicely with what we're doing is another question.
012D - Frame timers are here! Well, there's one at 012B, but it's used for something else and will not have the values we need. However, 012D, 012F, and 0131 are each two-byte timers that tick down every frame. It should be pretty safe to corrupt them, then wait for the right instructions to turn up. A string of bytes like 5C 98 A5 7F would produce JMP $7FA598, right into our power map stat, which we have almost complete control over with normal gameplay. We might prefer an address a dozen or so bytes after, as we can control the power in the middle of the map better than the top edge only.
01BD - Our camera takes up the four bytes at this location. It's notably farther away than 012D, but it is the alternative thing to try to give ourselves the arbitrary jump we might like. I haven't analyzed the stuff between 012D and here, though, as I'm more attracted to the idea of making the timers work.
These are the most interesting notes that come to mind at the moment. The more I look at this RAM, the more optimistic I feel that ACE is possible. There's a lot of 00 bytes, which are safe thanks to the BRK instruction giving us an RTI bouncing us back to where we were.
And yet, through all this, I haven't actually asked about the exact process of moving the cursor and camera to allow for overwriting these addresses. Probably just as simple as going off the top edge or left edge and placing something in that black gunk, but I figure I should ask anyway. Exactly where does the cursor end up relative to the top-left corner of the map for this sort of targeted corruption?
It'll probably take me a few days to find some time to sit down with it and step through line by line of the execution. I guess I'll set a breakpoint on reading 0xAA and try to follow it from there. It seems like we have all the tools we need to get a really cool ACE, as long as we figure out the bootstrap step.
Okay, plan of attack:
* Start game in any difficulty, it shouldn't matter too much (although Hard would be tight on cash)
* Build rows of power lines. We are writing a bootstrap code, with each tile representing one bit.
* Build power plant, maybe two, to power these lines we just placed
* Wait long enough for the game to calculate the power
* Build Mass Transit (rails) tile on 0064 (206 tiles left of the normal top-left corner)
* Build Roads on 0090 (184 tiles left of that same corner)
* Spam-build over the timers at 012C (106 tiles left), 12E (105), and 12F (104) for great justice
* Build Roads on 00AA
* ACE achieved. Now press buttons several thousand times faster than normal humans.
Well, I'm hoping this plan is simple and effective. It should, at least, give something concrete to try out as opposed to simply hand waving possible ideas around.
About the spam-building for great justice step: There are a few two-byte timers we can corrupt and abuse to our liking. Whenever we build over one of these, we set the upper byte of one timer to one of three values (0x32, 0x62, or 0x72, depending on tool) and zero out the lower byte of another. The timers tick down every frame, so zeroing out the low byte will effectively decrement the upper byte. We can simply zero out the low byte multiple times to adjust the high byte as needed, and pick a different timing on when we zero out that low byte for the last time to line up the values of different timers.
I took a look at moving the cursor off the top edge of the map. Judging from where it's corrupting stuff, it's probably just taking the effective Y position you would be building on, modulo 256. This would modify 7FF110 for going one tile up, and will never reach the 7E0000 region we want to modify. I'm thinking we're stuck with moving the cursor to the left a ways.
As the width of the map is 120 tiles, and we are guaranteed to be able to have two functioning rows of power lines for purposes of ACE, we can get ourselves 30 easy bytes of bootstrap code in the power array. If it's necessary, we can probably get more bytes by adding more rows, but we start to lose precision control on how we set the individual bits as we need some continuous connection to every power line. The more 1 bits in your code, the less of a problem this would be, though. On the other hand, 30 bytes of bootstrapping with absolute control over every byte of this 30 really should be a dream compared to getting an ACE in something like Super Mario World.
But once we hit ACE, what then? Fix the stack, and any critical memory we ruined along the way, then run the 600k population message routine? Well, that would seem like a really basic way to achieve the "ending". Use it as a shortcut to glitch our money? But then we're not actually using the full power to get to the "ending" as soon as possible. Something... Interesting? A bit outside what I'd do myself, but snap, it would be awesome if someone has ideas and can execute those. I, for one, would like to see someone bulldoze a tornado. Or the plane, it keeps crashing on nice buildings even on Easy.
In any case, I feel as though I have 99% confirmation that ACE is possible. My method was basically running a debugger and doing direct memory edits on spots we should be able to corrupt as the Program Counter was getting close. To my knowledge, the spots I wrote over do not refresh so long as you don't enter any menus, and apparently don't have critical function for reads, apart from the final step of corrupting the stack.
Finally, we have a LOT of room in the 7F bank. Well, a lot of it is dedicated to the 120x100 building area, but after that is the History stats and the various map stats. Since we should have ACE, we can simply prevent the game from ever recalculating what the stats should be, possibly by convincing it the calculation was already done.
I don't really have good ideas for arbitrary things. I guess it might be funny to build a city full of TOPs instantaneously, or trigger Monster Attacks, or update the graphics to look like Sim City 2000...
But I'm a fan of just proof-of-concept jumping to the routine to the 600k message popup. That way no waiting is needed. My creativity is limited : )
Just to post an update: I have confirmed that indeed ACE is possible. I've gotten code execution at 0x0, and written instructions carefully over bytes which would have otherwise jumped to useless parts of code, etc. I now have it executing at 0x0120, which is the timer section.
I'm currently trying to figure out what part of the code executes the "Awesome Mayor" screen. This is my first ACE run so I'm happy to just get a working PoC, instead of anything too showy. (At least at first. Once I have the PoC, I'm open to suggestions of alternate payloads).
Do you think the ACE needs to return to normal gameplay, or is it ok if I just leave the game in a broken state?
Edit: For the curious, we are blessed because 0x72 is a 2-byte opcode, and 0x62 is a 3-byte opcode. This means 62 can be used to skip over an extra byte, if clobbering it would have broken the game.
I added mouse support for a better game experience
print("SimCity grid viewer, for snes9x-rr 1.43 ~ 1.51")
print("written by Dammit, 2/21/2009 ~ 6/2/2011") --dammit9x at hotmail dot com
print("mouse support by crazygerry 2018")
print("tested with: https://github.com/gocha/snes9x/ 1.53 ~ 1.55")
--(old) use with: http://code.google.com/p/snes9x-rr/
--purpose: display land values and other parameters on the playfield for the SimCity game
--discussion: http://tasvideos.org/forum/viewtopic.php?p=192582#192582
local hotkey = { --hotkey settings
mode_inc = {"K", "cycle view modes forward (or middleclick)"},
mode_dec = {"L", "cycle view modes backward"},
show_values = {"U", "show/hide grid values"},
show_grid = {"I", "show/hide the grid"},
show_coords = {"O", "show/hide the map coordinates and city center"},
num_format = {"P", "switch between decimal & hex numbers"},
}
local globals = { --initial settings
view_mode = 1,
show_values = true,
show_grid = true,
show_coords = false,
hex_numbers = false,
use_mouse = true,
}
local label_x, label_y = 128, 216 --where to draw the view mode label
local coord_x, coord_y = 216, 216 --where to draw the map coordinates
local color = {
["level 8"] = 0xFF0000, --these are the in-game map colors
["level 7"] = 0xFF6300,
["level 6"] = 0xFFB500,
["level 5"] = 0xFFFF00,
["level 4"] = 0x00FF00,
["level 3"] = 0x00BD00,
["level 2"] = 0x008C00,
["level 1"] = 0x005A00,
["powered"] = 0xFF8400,
["unpowered"] = 0x00B500,
["city center"] = 0xFF00FF,
["dec text"] = 0xFFFFFF, --grid values in decimal mode
["hex text"] = 0xFFFF00, --grid values in hexadecimal mode
["label text"] = 0x00FF00, --view mode label text
["coord text"] = 0x00FFFF, --coordinate text
}
local opacity = {
fill = 0x20, --inside of boxes
outline = 0xA0, --outside of boxes
text = 0xFF,
}
--------------------------------------------------------------------------------
--prepare color settings
local fill, outline = {}, {}
local function map_colors(index, name)
fill[index] = bit.lshift(color[name], 8) + opacity.fill
outline[index] = bit.lshift(color[name], 8) + opacity.outline
end
for i = 0xFF, 0, -1 do
if i > 0xE0 then map_colors(i, "level 8")
elseif i > 0xC0 then map_colors(i, "level 7")
elseif i > 0xA0 then map_colors(i, "level 6")
elseif i > 0x80 then map_colors(i, "level 5")
elseif i > 0x60 then map_colors(i, "level 4")
elseif i > 0x40 then map_colors(i, "level 3")
elseif i > 0x20 then map_colors(i, "level 2")
elseif i > 0x00 then map_colors(i, "level 1")
else
fill[i] = 0 --blank/transparent
outline[i] = 0
end
end
map_colors(true, "powered")
map_colors(false, "unpowered")
map_colors("city center", "city center")
fill["dec text"] = bit.lshift(color["dec text"], 8) + opacity.text
fill["hex text"] = bit.lshift(color["hex text"], 8) + opacity.text
fill["label text"] = bit.lshift(color["label text"], 8) + opacity.text
fill["coord text"] = bit.lshift(color["coord text"], 8) + opacity.text
--memory addresses
local address = {
x_tile = 0x7E01BD, --camera position by upper-left tile
y_tile = 0x7E01BF,
x_center = 0x7E0BA9, --coordinate of the city center
y_center = 0x7E0BAA,
playing = 0x7E0012, --a game is in progress: draw coords
hud = 0x7E2210, --in-game HUD is shown: don't draw grid
dark = 0x7E90FF, --darkened BG: don't draw grid
magnify = 0x7E019B, --magnifier tool is open: draw grid
time = 0x7E0B51, --increments four times per game month; currently unused
x_cursor = 0x7E01EB,
y_cursor = 0x7E01ED,
taxind = 0x7E2016, -- holds some values to identify tax screen
}
--parameter values are stored here
local view = {
{address = 0x7F6B00, sqsize = 2, bytes = 1, name = "Land value"},
{address = 0x7F76B8, sqsize = 2, bytes = 1, name = "Crime"},
{address = 0x7F8270, sqsize = 2, bytes = 1, name = "Pollution"},
{address = 0x7F8E28, sqsize = 2, bytes = 1, name = "Population density"},
{address = 0x7F99E0, sqsize = 2, bytes = 1, name = "Traffic"},
{address = 0x7FA598, sqsize = 1, bytes = 1/8, name = "Power"},
{address = 0x7FAB74, sqsize = 4, bytes = 1, name = "Land value modifier"},
{address = 0x7FAE62, sqsize = 8, bytes = 2, name = "Growth rate"},
{address = 0x7FAFE8, sqsize = 8, bytes = 1, name = "Police coverage"},
{address = 0x7FB0AB, sqsize = 8, bytes = 1, name = "Fire coverage"},
}
local screenwidth, screenheight, tilesize = 256, 224, 8 --size in pixels
local mapwidth, mapheight = 120, 100 --size in tiles
local pressing_old = {} --array for keyboard input
for _, mode in ipairs(view) do
mode.y_step = mapwidth / mode.sqsize
mode.read = mode.bytes == 2 and memory.readwordsigned or memory.readbyte
end
print()
print("To enable the grid, hide the HUD with button Y or open the magnifier.")
print()
for _, v in pairs(hotkey) do
print("Press the '" .. v[1] .. "' key to " .. v[2] .. ".")
end
function bit(p) --http://lua-users.org/wiki/BitwiseOperators
return 2 ^ (p - 1)
end
function hasbit(x, p)
return x % (p + p) >= p
end
local function draw_boxes()
local x_tile, y_tile, mode = globals.x_tile, globals.y_tile, view[globals.view_mode]
local y = 0
while y < screenheight do
local y_next = y + mode.sqsize * tilesize
if y == 0 then
y_next = (mode.sqsize - y_tile % mode.sqsize) * tilesize
end
if y_next > screenheight then
y_next = screenheight
end
local x = 0
while x < screenwidth do
local x_next = x + mode.sqsize * tilesize
if x == 0 then
x_next = (mode.sqsize - x_tile % mode.sqsize) * tilesize
end
if x_next > screenwidth then
x_next = screenwidth
end
if x_tile + x/tilesize >= 0 and y_tile + y/tilesize >= 0 --check the map boundaries
and x_tile + x/tilesize < mapwidth and y_tile + y/tilesize < mapheight then
local value = mode.read(mode.address + math.floor(
(math.floor((x_tile + x/tilesize) / mode.sqsize)
+ math.floor((y_tile + y/tilesize) / mode.sqsize) * mode.y_step) * mode.bytes))
if globals.show_grid then
if mode.read == memory.readwordsigned then
if value > 0xFF then value = 0xFF end
if value < -0xFF then value = -0xFF end
value = math.floor((value + 0xFF) / 2)
end
if mode.bytes < 1 then
value = hasbit(value, bit(8 - (x_tile + x/tilesize + (y_tile + y/tilesize) * mode.y_step) % 8))
end
gui.box(x, y, x_next-1, y_next-1, fill[value], outline[value])
end
if globals.show_values and value ~= 0 and mode.sqsize > 1 and
x_next >= 2 * tilesize and y_next >= 2 * tilesize and
x + 2 * tilesize <= screenwidth and y + 2 * tilesize <= screenheight then
local color
if globals.hex_numbers then
value, color = string.format("%02X", value), fill["hex text"]
else
value, color = string.format("%d", value), fill["dec text"]
end
gui.text(x_next - value:len() * 4 - 1, y + 1, value, color)
end
end
x = x_next
end
y = y_next
end
if globals.show_grid or globals.show_values then --show the label only with the grid or values active
gui.text(label_x, label_y, mode.name, fill["label text"])
end
end
local function draw_data()
local x_tile, y_tile, x_center, y_center = globals.x_tile, globals.y_tile, globals.x_center, globals.y_center
if globals.grid_ok then
draw_boxes()
end
if globals.show_coords then --show the coordinates & city center
if globals.use_mouse == false then
gui.text(coord_x, coord_y, "(" .. x_tile .. ", " .. y_tile .. ")", fill["coord text"])
end
local xdraw, ydraw = (x_center - x_tile) * tilesize, (y_center - y_tile) * tilesize
if x_center < x_tile then
xdraw = 0
end
if x_center >= x_tile + screenwidth/tilesize then
xdraw = screenwidth - tilesize
end
if y_center < y_tile then
ydraw = 0
end
if y_center >= y_tile + screenheight/tilesize then
ydraw = screenheight - tilesize
end
gui.box(xdraw, ydraw, xdraw + 7, ydraw + 7, fill["city center"], outline["city center"])
end
end
local function mouse_support(inp)
local taxscreenindicatorvalue = memory.readbyte(address.taxind)
local taxscreenindicatorvalues = {131,133,135,137,139,141,195,197,199,201}
local taxscreenvisible, movemap = false, false
local pad = joypad.get(1)
local x_mousepos, y_mousepos = math.floor(inp.xmouse/8), math.floor(inp.ymouse/8)
local x_mouseonmap, y_mouseonmap = x_mousepos + memory.readbytesigned(address.x_tile), y_mousepos + memory.readbytesigned(address.y_tile)
for i = 1, 10 do
if taxscreenindicatorvalues[i] == taxscreenindicatorvalue then taxscreenvisible = true break end
end
if taxscreenvisible == false then
if(x_mouseonmap >= 0 and x_mouseonmap < mapwidth and y_mouseonmap >= 0 and y_mouseonmap < mapheight) then
gui.text(coord_x, coord_y, "(" .. x_mouseonmap+1 .. ", " .. y_mouseonmap+1 .. ")", fill["coord text"])
end
if(x_mouseonmap >= -6 and x_mouseonmap < mapwidth+6 and y_mouseonmap >= -6 and y_mouseonmap < mapheight+6) then
memory.writebyte(address.x_cursor,x_mousepos*8)
memory.writebyte(address.y_cursor,y_mousepos*8)
end
if(inp.xmouse > 232) then memory.writebyte(address.x_cursor,232) end
if(inp.xmouse < 16) then memory.writebyte(address.x_cursor,16) end
if(inp.ymouse > 192) then memory.writebyte(address.y_cursor,192) end
if(inp.ymouse < 24) then memory.writebyte(address.y_cursor,24) end
end
if (inp.xmouse >= screenwidth-1) then movemap, pad.right = true, true joypad.set(1,pad) end
if (inp.xmouse <= 0) then movemap, pad.left = true, true joypad.set(1,pad) end
if (inp.ymouse >= screenheight-1) then movemap, pad.down = true, true joypad.set(1,pad) end
if (inp.ymouse <= 0) then movemap, pad.up = true, true joypad.set(1,pad) end
if (movemap and inp.leftclick == false) then pad.Y = true end
if (inp.leftclick) then pad.B = true joypad.set(1,pad) end
if (inp.rightclick) then pad.X = true joypad.set(1,pad) end
end
emu.registerafter(function()
globals.game_playing = memory.readbyte(address.playing) > 0
if not globals.game_playing then
pressing_old = {}
return
end
globals.grid_ok = memory.readbyte(address.dark) == 0 and
(memory.readword(address.hud) == 0x5555 or memory.readbyte(address.magnify) > 0)
globals.x_tile, globals.y_tile = memory.readbytesigned(address.x_tile), memory.readbytesigned(address.y_tile)
globals.x_center, globals.y_center = memory.readbyte(address.x_center), memory.readbyte(address.y_center)
local pressing = input.get()
if (globals.show_grid or globals.show_values) and globals.grid_ok then
if (pressing[hotkey.mode_inc[1]] or pressing.middleclick) and not (pressing_old[hotkey.mode_inc[1]] or pressing_old.middleclick) then
globals.view_mode = globals.view_mode >= #view and 1 or globals.view_mode + 1
elseif pressing[hotkey.mode_dec[1]] and not pressing_old[hotkey.mode_dec[1]] then
globals.view_mode = globals.view_mode == 1 and #view or globals.view_mode - 1
end
end
if pressing[hotkey.show_values[1]] and not pressing_old[hotkey.show_values[1]] and globals.grid_ok then
globals.show_values = not globals.show_values
end
if pressing[hotkey.show_grid[1]] and not pressing_old[hotkey.show_grid[1]] and globals.grid_ok then
globals.show_grid = not globals.show_grid
end
if pressing[hotkey.show_coords[1]] and not pressing_old[hotkey.show_coords[1]] then
globals.show_coords = not globals.show_coords
end
if pressing[hotkey.num_format[1]] and not pressing_old[hotkey.num_format[1]] and globals.show_values then
globals.hex_numbers = not globals.hex_numbers
end
pressing_old = pressing
end)
gui.register(function()
if globals.game_playing then
draw_data()
if globals.use_mouse then
mouse_support(input.get())
end
end
end)