Last active
April 8, 2023 10:54
-
-
Save Zinfidel/d47287cec638f0f915c3d99bccef3a7d to your computer and use it in GitHub Desktop.
PSX Armored Core Overlay Scripts for BizHawk
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
--[[ | |
Armored Core Common Script for BizHawk | |
By Zinfidel | |
For Armored Core 1.1 [SLUS-01323] [NTSC] | |
This is a common library for various other scripts for Armored Core. It does not need to be "run" by Bizhawk | |
but is instead referenced by other scripts. There are some constants that the user should set below, and if | |
they are changed while other scripts are running, the core script should be loaded into the lua console and | |
refreshed to updates the values. | |
--]] | |
-- USER CONSTANTS (Change me!) | |
BUFFER_OVERSCAN_LEFT = 0; -- Overscan in pixels. For debug/mednafen with no clipping, this will be 18. For PixelPro mode, 86. | |
BUFFER_OVERSCAN_RIGHT = 0; -- 12 for mednafen, 74 for pixel pro | |
-- DONT CHANGE BELOW HERE | |
-- GENERAL CONSTANTS | |
MAX_ENTITIES = 16; | |
ANGLE_FACTOR = (math.pi * 2) / 4096; -- Convert in-game rotation value to radians | |
X_MAX = client.bufferwidth() + BUFFER_OVERSCAN_RIGHT; | |
Y_MAX = client.bufferheight(); | |
X_OFF = X_MAX/2 + BUFFER_OVERSCAN_LEFT; | |
Y_OFF = Y_MAX/2; | |
-- COLORS | |
C_TRANSPARENT = 0x00000000; | |
C_BLACK = 0xFF000000; | |
C_WHITE = 0xFFFFFFFF; | |
C_RED = 0xFFFF0000; | |
C_GREEN = 0xFF00FF00; | |
C_YELLOW = 0xFFFFFF00; | |
-- ADDRESSES AND OFFSETS | |
ENTITY_DATA = 0x001A26B8; | |
ENTITY_OFFSET = 0x170; | |
ENTITY_YAW_OFFSET = 0x12; | |
ENTITY_POS_OFFSET = 0x20; -- Offset of entity position vector (actually the last frame's value) | |
ENTITY_AABB_WIDTH_OFFSET = 0x6A; | |
ENTITY_AABB_HEIGHT_OFFSET = 0x6C; | |
ENTITY_HEALTH_OFFSET = 0x160; | |
FRAME_COUNTER_1 = 0x00198804; -- Address of one of game's frame counters | |
FRAME_COUNTER_2 = 0x001AC81C; -- Address of another frame counter | |
-- GLOBAL STATE | |
Entities = {}; | |
for i=0, MAX_ENTITIES, 1 do | |
Entities[i] = {}; | |
end | |
Player = Entities[1]; | |
CurrentFrame1 = 0; | |
CurrentFrame2 = 0; | |
LastFrame1 = 0; | |
LastFrame2 = 0; | |
-- CONSTRUCTORS | |
function Vector(x, y, z) | |
return {X=x, Y=y, Z=z} | |
end | |
-- LINEAR ALGEBRA | |
function VectorLength(v) | |
return math.sqrt(v.X * v.X + v.Y * v.Y + v.Z * v.Z); | |
end | |
function VectorSubtract(v, w) | |
return Vector(v.X - w.X, v.Y - w.Y, v.Z - w.Z); | |
end | |
function ScaleVector(v, a) | |
return Vector(v.X * a, v.Y * a, v.Z * a); | |
end | |
function DotProduct(v, w) | |
return v.X * w.X + v.Y * w.Y + v.Z * w.Z; | |
end | |
function ScaleMatrix(m, a) | |
local scaledMatrix = {}; | |
for i=0,table.getn(m)-1,1 do | |
scaledMatrix[i] = m[i] * a; | |
end | |
return scaledMatrix; | |
end | |
-- LINEAR ALGEBRA (IN-PLACE) | |
function VectorSubtract_IP(v,w) | |
v.X = v.X - w.X; | |
v.Y = v.Y - w.Y; | |
v.Z = v.Z - w.Z; | |
end | |
function ScaleVector_IP(v,a) | |
v.X = v.X * a; | |
v.Y = v.Y * a; | |
v.Z = v.Z * a; | |
end | |
function ScaleMatrix_IP(m,a) | |
for i=0,table.getn(m),1 do | |
m[i] = m[i] * a; | |
end | |
end | |
-- READ MEMORY | |
function ReadVector3(address) | |
return Vector(mainmemory.read_s16_le(address), | |
mainmemory.read_s16_le(address+2), | |
mainmemory.read_s16_le(address+4)); | |
end | |
function ReadMatrix33(address, outMatrix) | |
for i=0,8,1 do | |
outMatrix[i] = memory.read_s16_le(address + i*2); | |
end | |
end | |
-- READ MEMORY (BULK) | |
function ReadVector3_BULK(address) | |
local vector = {}; | |
local bytes = memory.readbyterange(address, 6, mainmemory.getname()); | |
for i=0,2,1 do | |
local j = i*2; | |
local k = ({'X', 'Y', 'Z'})[i+1]; | |
local lowerByte = bytes[j]; | |
local upperByte = bytes[j+1] * 256; | |
vector[k] = lowerByte + upperByte; | |
-- If negative, use bit trick to get lua to interpret as a negative number. | |
if (upperByte >= 0x8000) then | |
vector[k] = vector[k] - 0x10000; | |
end | |
end | |
return vector; | |
end | |
function ReadMatrix33_BULK(address) | |
local matrix = {}; | |
local bytes = memory.readbyterange(address, 18, mainmemory.getname( ) ); | |
for i=0,8,1 do | |
local j = i*2 | |
local lowerByte = bytes[j]; | |
local upperByte = bytes[j+1] * 256; | |
matrix[i] = lowerByte + upperByte; | |
-- If negative, use bit trick to get lua to interpret as a negative number. | |
if (upperByte >= 0x8000) then | |
matrix[i] = matrix[i] - 0x10000; | |
end | |
end | |
return matrix; | |
end | |
-- Update info for any active entities. | |
local function UpdateEntities() | |
for i=1,MAX_ENTITIES,1 do | |
local e = Entities; | |
local entityAddr = ENTITY_DATA + (i-1) * ENTITY_OFFSET; | |
e[i].Active = mainmemory.read_u16_le(entityAddr) ~= 0; -- Model pointer gets set to 0 if the entity is inactive. | |
if e[i].Active then | |
if (i == 1) then -- Player Only | |
e[i].LastPos = e[i].Pos or Vector(0,0,0); | |
local yawRaw = mainmemory.read_s16_le(entityAddr + ENTITY_YAW_OFFSET); | |
Player.Yaw = yawRaw * ANGLE_FACTOR; | |
end | |
e[i].Health = mainmemory.read_s16_le(entityAddr + ENTITY_HEALTH_OFFSET); | |
e[i].AABBWidth = mainmemory.read_s16_le(entityAddr + ENTITY_AABB_WIDTH_OFFSET); | |
e[i].AABBHeight = mainmemory.read_s16_le(entityAddr + ENTITY_AABB_HEIGHT_OFFSET); | |
e[i].Pos = ReadVector3_BULK(entityAddr + ENTITY_POS_OFFSET); | |
e[i].Pos.Y = e[i].Pos.Y - e[i].AABBHeight / 2; | |
end | |
end | |
end | |
local FrameCount = -1; | |
-- Does an entity update and frame counter update. This function is idempotent and will only run once per frame. | |
function CoreUpdate() | |
local curFrame = emu.framecount(); | |
if (curFrame ~= FrameCount) then | |
FrameCount = curFrame; | |
LastFrame1 = CurrentFrame1; | |
LastFrame2 = CurrentFrame2; | |
CurrentFrame1 = mainmemory.read_s16_le(FRAME_COUNTER_1); | |
CurrentFrame2 = mainmemory.read_s16_le(FRAME_COUNTER_2); | |
UpdateEntities(); | |
end | |
end | |
CoreUpdate(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
--[[ | |
Armored Core Radar Script for BizHawk | |
By Zinfidel (Inspired by JAX's enhancements in his TAS) | |
For Armored Core 1.1 [SLUS-01323] [NTSC] | |
The Radar | |
Works similarly to the in-game radar, and is styled as such. Each enemy is displayed as a red square with | |
the enemy's health near it. If the enemy is above the player, a red line will extend down to a smaller | |
red square that represents the enemy's position at the same elevation as the player. If the player is above | |
an enemy, the enemy's box will have a red line extending UP to a smaller red box. Modify the | |
RADAR_SIZE and RADAR_RANGE constants to taste. RADAR_SIZE is the size in pixels of the radar window. | |
RADAR_RANGE is the distance in in-game units of the radar. These values are the same as the radar | |
range statistic for radars in the game. | |
The radar can optionally display a navigation point and boundary polygons. Change the NavPoint variable to | |
a coordinate pair (x and z), and a yellow line will extend from the player to that point on the radar. | |
For drawing boundaries, set the Boundaries variable to an array of arrays of coordinate pairs, each pair | |
representing a vertex in the polygon. There are some example boundaries near the Boundaries variable to | |
work off of. These boundaries can be useful when clipping out of bounds and navigating to important places. | |
--]] | |
require "ac_core"; | |
-- USER CONSTANTS (Change me!) | |
local RADAR_SIZE = 600; -- Size of the radar on a side in pixels | |
local RADAR_RANGE = 18000; -- Range of the radar, in in-game units | |
local NavPoint = nil -- Set to nil to disable, otherwise a coordinate pair {x, y} | |
local Boundaries = nil; -- Set to nil to disable, otherwise set indices to collections of coordinate pairs {{x,y},{x,y},...} | |
-- Mop Up Chrome Remnants 2 Boundaries | |
-- Boundaries[1] = {{-21149, 1149}, {-11351, 1149}, {-11431, -6013}, {-21149, -6061} }; | |
-- Boundaries[2] = {{-524,8432},{524, 8432},{524, 3574},{-524, 3183}}; | |
-- Boundaries[3] = {{-475,5424},{475,5424},{475,4545},{-475,4545}}; | |
-- Boundaries[4] = {{-8649,13649},{-1315,13649},{-1315,3851},{-8649,3851}}; | |
-- Destroy Floating Mines Boundaries | |
-- Boundaries[1] = {{-7618, 12934}, {-5237, 12987}, {-5273, 12421}, {-7849, 12448} }; | |
-- DON'T CHANGE BELOW HERE | |
local RADAR_RANGE_SCALE = RADAR_SIZE / RADAR_RANGE; | |
local function IsMapOpen(theMap) | |
if theMap ~= nil then | |
return pcall(function() | |
theMap.GetMouseX(); --hack for lack of closed property | |
end) | |
else | |
return false; | |
end | |
end | |
if not IsMapOpen(MapCanvas) then -- Prevent opening new windows when refreshing the script | |
MapCanvas = gui.createcanvas(RADAR_SIZE, RADAR_SIZE); | |
end | |
MapCanvas.SetTitle("AC Radar (Range: " .. RADAR_RANGE .. ")"); | |
local function TransformPoint(x, z) | |
-- Move coordinate system so player is at origin. | |
local pointX = x - Player.Pos.X; | |
local pointZ = z - Player.Pos.Z; | |
-- Rotate point about origin equal to player yaw. Counterclockwise is positive. | |
local playerRot = -Player.Yaw; | |
local pointXRot = pointX * math.cos(playerRot) - pointZ * math.sin(playerRot); | |
local pointZRot = pointZ * math.cos(playerRot) + pointX * math.sin(playerRot); | |
-- Scale to our canvas size | |
pointXRot = pointXRot * RADAR_RANGE_SCALE; | |
pointZRot = pointZRot * RADAR_RANGE_SCALE; | |
-- Move to position relative to map center point | |
pointXRot = RADAR_SIZE/2+pointXRot; | |
pointZRot = RADAR_SIZE/2-pointZRot; | |
return pointXRot, pointZRot; | |
end | |
local function TransformRectangle(rect) | |
local ret = {}; | |
for i, point in ipairs(rect) do | |
local xformX, xformZ = TransformPoint(point[1], point[2]); | |
ret[i] = {xformX, xformZ} | |
end | |
return ret; | |
end | |
local function DrawMap() | |
MapCanvas.Clear(C_BLACK); | |
MapCanvas.DrawRectangle(20, 20, RADAR_SIZE - 40, RADAR_SIZE - 40, C_GREEN); | |
MapCanvas.DrawPie(-5, -5, RADAR_SIZE + 10, RADAR_SIZE + 10, 0, 360, C_BLACK, C_BLACK); | |
MapCanvas.DrawPie(0, 0, RADAR_SIZE, RADAR_SIZE, -45, 270, C_GREEN, C_GREEN); | |
MapCanvas.DrawArc(0, 0, RADAR_SIZE, RADAR_SIZE, 0, 360, C_GREEN); | |
-- Draw nav point line | |
if NavPoint ~= nil then | |
local navX, navY = TransformPoint(NavPoint[1], NavPoint[2]); | |
MapCanvas.DrawLine(RADAR_SIZE/2, RADAR_SIZE/2, navX, navY, C_YELLOW) | |
MapCanvas.DrawRectangle(navX-2, navY-2, 4, 4, C_WHITE, C_YELLOW); | |
end | |
-- Draw boundaries | |
if Boundaries ~= nil then | |
for _, bounds in ipairs(Boundaries) do | |
local xBounds = TransformRectangle(bounds); | |
MapCanvas.DrawPolygon(xBounds, 0, 0, C_RED); | |
end | |
end | |
-- Draw enemies | |
for i = 2, MAX_ENTITIES, 1 do | |
if Entities[i].Active then | |
local x, z = TransformPoint(Entities[i].Pos.X, Entities[i].Pos.Z); | |
local y = (Entities[i].Pos.Y - Player.Pos.Y) / 32; | |
MapCanvas.DrawRectangle(x-2, z-2, 4, 4, C_WHITE, C_RED); | |
MapCanvas.DrawLine(x, z, x, z+y, C_RED); | |
MapCanvas.DrawRectangle(x-5, z+y-5, 10, 10, C_WHITE, C_RED); | |
MapCanvas.DrawText(x, z+15, Entities[i].Health, C_WHITE, nil, nil, nil, nil, "center", "middle"); | |
end | |
end | |
MapCanvas.Refresh(); | |
end | |
while true do | |
CoreUpdate(); | |
DrawMap(); | |
emu.frameadvance(); | |
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
--[[ | |
Armored Core Speedometer Script for BizHawk | |
By Zinfidel (Inspired by JAX's enhancements in his TAS) | |
For Armored Core 1.1 [SLUS-01323] [NTSC] | |
The Speedometer | |
Adds a speedometer bar to the bottom of the game screen. Speed is measured in in-game units, which | |
is possibly meters per second. Text and an indicator is displayed below the speedometer which indicates | |
the current maximum ground-boost speed. The speedometer will turn red when moving at or faster than the | |
current maximum ground boost speed. Modify the BUFFER_OVERSCAN_LEFT and SPEED_FONT_SIZE constants | |
for your display configuration. It is set by default to display well with debug/mednafen and | |
framebuffer clipping turned on. | |
Turn on the DRAW_ACCEL option to display an accelerometer above the speedometer. Turn on the DRAW_VISUALIZER | |
option to draw a large overlay that visualizes the player's motion vector and its limits. The lines through | |
the square represent the cardinal directions (North, etc.) and the solid-colored line and square reprsent | |
the player's current motion vector. The faded lines and squares represent the X and Z components of the player's | |
current motion. This overlay visualizes the maximum speed the player can achive in a given direction | |
due to the way the game limits player translation per frame. | |
Notes on speed in Armored Core | |
The game implements strange limitations on maximum speed in the game. Maximum ground boost speed is | |
limited to 75, but only for cardinal directions. For intermediate directions, however, the maximum | |
ground boost speed is 106. My hypothesis on why this is is that the game actually determines how fast | |
your AC can move by allowing it to translate a certain amount in given X and Z directions, rather | |
than doing traditional velocity calculations. This means that your AC's ground boost speed in X and Z | |
directions are bounded to 75, but when you are moving in both directions, your combined speed is the | |
sum of the components, yielding ~106 via Pythagorean Theorem. | |
This leads to some interesting consequences. Namely is that, if you are traveling to a destination | |
in a cardinal direction, you can strafe left and right on the way to your destination as much as you | |
like (so long as you don't change your heading by more than 45 degrees) in a "zig-zag" pattern and you | |
will actually get there in eactly the same amount of time as a straight line. Conversely, if you are | |
traveling in an intermediate direction, any deviation from a straight line is actually even worse | |
than just the loss of efficiency from the pathing, since your lateral movement is also slower. | |
Y speed is not considered for the speedometer, and several types of movement in the game can go faster | |
than the maximum ground boost speed. Some of the faster legs (LN-501, etc.) move in bursts of speed, | |
and those bursts are generally faster than ground boost speed (though not overall since the other bursts | |
are slower, and the average is less than 75). The laser blade swings will also propel you forward very | |
quickly. Getting shot by rifles and knock-back imparting projectiles while in the air will also tend to | |
send lightweight ACs flying at a much faster rate than the ground boost limit. | |
]]-- | |
require "ac_core"; | |
-- USER CONSTANTS (Change me!) | |
local SPEED_FONT_SIZE = 8; -- Size of speedometer text, in pixels. Increase for Pixel Pro mode. | |
local DRAW_ACCEL = false; -- Turn on to draw an accelerometer. | |
local DRAW_VISUALIZER = false; -- Turn on to draw a speed visualization overlay. | |
local C_SPEEDOMETER = 0xFF3333FF; -- Speedometer color. Can be set to a color constant or whatever. | |
local C_ACCEL = 0xFF44AA44; -- Accelerometer color. Can be set to a color constant or whatever. | |
-- DON'T CHANGE BELOW HERE | |
local VEL_LIMIT_C = 75; -- Speed limit in a cardinal direction | |
local VEL_LIMIT_D = 106; -- Speed limit in a intermediate direction | |
local SpeedStartX = BUFFER_OVERSCAN_LEFT + ((client.bufferwidth() - (VEL_LIMIT_D * 2)) / 2); | |
local SpeedStartY = client.bufferheight() - 20; | |
local function UpdatePlayerVelocity() | |
Player.Vel = VectorSubtract(Player.Pos, Player.LastPos); | |
Player.Vel.XZ = math.sqrt(Player.Vel.X^2 + Player.Vel.Z^2); | |
Player.Vel.Angle = math.atan2(Player.Vel.X,Player.Vel.Z); | |
Player.Accel = VectorSubtract(Player.Vel, Player.LastVel); | |
Player.Accel.XZ = math.sqrt(Player.Accel.X^2 + Player.Accel.Z^2); | |
Player.LastVel = Player.Vel; | |
end | |
-- Calculate the current maximum speed for the player. The game allows the player to be translating in either the X or | |
-- the Z axis by up to 75 units at a time, resulting in a maximum of ~106 at 45 degrees. | |
local function CalculateMaxSpeed() | |
local function det(a,b) | |
return a[1]*b[2] - a[2]*b[1]; | |
end | |
-- To find the current maximum, the player's current movement vector is checked for intersections with lines at 75 for both | |
-- directions. The least intercept is the current maximum speed for the player. I have made several attempts to compute this | |
-- value using the pythagorean theorem/trigonometry and have been unsuccessful, and I don't know why; this technique is slower | |
-- but the only one I've found that matches the game's speed limits. | |
-- The line intercept formula used is outlined here: https://stackoverflow.com/a/20679579 | |
local A1 = Player.Vel.Z; | |
local B1 = -Player.Vel.X; | |
local C1 = 0; | |
-- X = 75 | |
local A2 = VEL_LIMIT_C; | |
local B2 = 0; | |
local C2 = A2*VEL_LIMIT_C; | |
-- Z = 75 | |
local A3 = 0; | |
local B3 = -VEL_LIMIT_C; | |
local C3 = B3*VEL_LIMIT_C; | |
-- Set intersections initially to high values so that in the case of parrellel case, default intersections are | |
-- not considered minimal solutions. | |
local x1 = 150; | |
local x2 = 150; | |
local y1 = 150; | |
local y2 = 150; | |
local d1 = det({A1, A2},{B1, B2}); | |
if d1 ~= 0 then | |
x1 = (B2*C1 - B1*C2)/d1; | |
y1 = (A1*C2 - A2*C1)/d1; | |
end | |
local d2 = det({A1,A3},{B1, B3}); | |
if d2 ~= 0 then | |
x2 = (B3*C1 - B1*C3)/d2; | |
y2 = (A1*C3 - A3*C1)/d2; | |
end | |
local zIntersection = math.sqrt(x1^2 + y1^2); | |
local xIntersection = math.sqrt(x2^2 + y2^2); | |
return math.floor(math.min(zIntersection, xIntersection)); | |
end | |
local function DrawSpeedometer() | |
local vel = math.floor(Player.Vel.XZ); | |
local velMax = CalculateMaxSpeed(); | |
local velColor = vel > velMax and C_RED or C_SPEEDOMETER; | |
gui.drawRectangle(SpeedStartX, SpeedStartY, VEL_LIMIT_D*2, 8, C_WHITE); | |
gui.drawRectangle(SpeedStartX, SpeedStartY, vel*2, 8, C_WHITE, velColor); | |
gui.drawText(SpeedStartX-20, SpeedStartY, "SPD " .. vel, C_WHITE, C_TRANSPARENT, SPEED_FONT_SIZE); | |
gui.drawText(SpeedStartX + velMax*2+4, SpeedStartY+10, velMax .. "|", C_WHITE, C_TRANSPARENT, 8, nil, nil, "right"); | |
if (DRAW_ACCEL) then | |
local accel = math.floor(Player.Accel.XZ); | |
gui.drawRectangle(SpeedStartX, SpeedStartY-10, VEL_LIMIT_D*2, 8, C_WHITE); | |
gui.drawRectangle(SpeedStartX, SpeedStartY-10, accel*2, 8, C_WHITE, C_ACCEL); | |
gui.drawText(SpeedStartX-20, SpeedStartY-10, "ACC " .. accel, C_WHITE, C_TRANSPARENT, SPEED_FONT_SIZE); | |
end | |
end | |
local function DrawSpeedVisualizer() | |
local tw1 = 0x99FFFFFF; | |
local tw2 = 0x55FFFFFF; | |
local tw3 = 0x44FFFFFF; | |
local tsp = 0x993333FF; | |
gui.drawRectangle(X_OFF-75, Y_OFF-75, 151, 151, tw1, tw3); | |
gui.drawLine(X_OFF-75, Y_OFF, X_OFF+75, Y_OFF, tw2); | |
gui.drawLine(X_OFF, Y_OFF-75, X_OFF, Y_OFF+75, tw2); | |
gui.drawText(X_OFF-75, Y_OFF, "75", tw1, nil, 10, nil, nil, "right", "middle"); | |
gui.drawText(X_OFF+75, Y_OFF, "75", tw1, nil, 10, nil, nil, "left", "middle"); | |
gui.drawText(X_OFF, Y_OFF-75, "75", tw1, nil, 10, nil, nil, "center", "bottom"); | |
gui.drawText(X_OFF, Y_OFF+75, "75", tw1, nil, 10, nil, nil, "center", "top"); | |
gui.drawText(X_OFF-75, Y_OFF-75, "106", tw1, nil, 10, nil, nil, "right", "bottom"); | |
gui.drawText(X_OFF+75, Y_OFF-75, "106", tw1, nil, 10, nil, nil, "left", "bottom"); | |
gui.drawText(X_OFF-75, Y_OFF+75, "106", tw1, nil, 10, nil, nil, "right", "top"); | |
gui.drawText(X_OFF+75, Y_OFF+75, "106", tw1, nil, 10, nil, nil, "left", "top"); | |
local loc = Vector(Player.Vel.X, Player.Vel.Z, 0); | |
-- Horiz | |
gui.drawLine(X_OFF, Y_OFF, X_OFF+loc.X, Y_OFF, tsp) | |
gui.drawBox(X_OFF+loc.X-3, Y_OFF+3, X_OFF+loc.X+3, Y_OFF-3, tw2, tsp) | |
gui.drawText(X_OFF+loc.X/2, Y_OFF, math.floor(math.abs(Player.Vel.X)), tw1, nil, 10, nil, nil, "center", "middle"); | |
-- Vert | |
gui.drawLine(X_OFF, Y_OFF, X_OFF, Y_OFF-loc.Y, tsp) | |
gui.drawBox(X_OFF-3, Y_OFF-loc.Y+3, X_OFF+3, Y_OFF-loc.Y-3, tw2, tsp) | |
gui.drawText(X_OFF, Y_OFF-loc.Y/2, math.floor(math.abs(Player.Vel.Z)), tw1, nil, 10, nil, nil, "center", "middle"); | |
-- Sum | |
gui.drawLine(X_OFF, Y_OFF, X_OFF+loc.X, Y_OFF-loc.Y, C_SPEEDOMETER) | |
gui.drawBox(X_OFF+loc.X-3, Y_OFF-loc.Y+3, X_OFF+loc.X+3, Y_OFF-loc.Y-3, C_WHITE, C_SPEEDOMETER) | |
gui.drawPixel(X_OFF+loc.X, Y_OFF-loc.Y, C_WHITE); | |
end | |
-- Initialize fields. | |
Player.LastPos = Vector(0,0,0); | |
Player.Vel = Vector(0,0,0); Player.Vel.XZ = 0; Player.Vel.Angle = 0; | |
Player.LastVel = Vector(0,0,0); Player.LastVel.XZ = 0; | |
Player.Accel = Vector(0,0,0); Player.Accel.XZ = 0; | |
while true do | |
CoreUpdate(); | |
if (CurrentFrame2 ~= 0) then -- This address tends to be 0 during non-mission parts of the game. | |
-- Track lag frames using the game's built-in frame counters. We don't update velocity on lag frames because they will | |
-- evaluate to 0, since position isn't updating. Either frame counter isn't perfect though, and will still update on lag | |
-- frames occasionally. Updating against both frame counters seems to eliminate all hiccups. | |
if (CurrentFrame1 ~= LastFrame1 and CurrentFrame2 ~= LastFrame2) then | |
UpdatePlayerVelocity(); | |
end | |
DrawSpeedometer(); | |
if DRAW_VISUALIZER then | |
DrawSpeedVisualizer(); | |
end | |
end | |
emu.frameadvance(); | |
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
--[[ | |
Armored Core Wallhack Script for BizHawk | |
By Zinfidel | |
For Armored Core 1.1 [SLUS-01323] [NTSC] | |
The Wallhack | |
This script draws markers over enemies in-game, optionally with tracer lines pointing to them, health and distance values, | |
and bounding boxes. Armored core keeps up to 15 enemies (plus the player) loaded at a given time, and so only up to 15 | |
markers will ever be displayed. The markers are displayed regardless of whether the player can actually see the enemy. | |
The default mode of operation is to simply draw a red square on entities' center of mass. Turning on the DRAW_LINE option | |
will also create a small marker square on the screen (with customizable position via LINE_ORIGIN) from which rays will be | |
drawn to each enemy marker. Turning on DRAW_BOUNDS will also draw a bounding box around the enemy, which represents the | |
hitbox (or collision planes) for the enemy. Finally, turning on DRAW_HEALTH and DRAW_DIST will show the entities' health | |
and distance to the player, respectively, along with the other marker information. | |
The MAX_DIST, LARGEST_TEXT, and SMALLEST_TEXT, variables are used in conjunction with health and distance displays. MAX_DIST | |
is the longest range at which the health and distance values will be displayed for entities (measured in in-game units). This | |
is handy as when entities are very far away, the text for those entities can be much larger than them and make the screen too | |
busy. The LARGEST_TEXT and SMALLEST_TEXT variables will affect the size of the health/distance text (in points) as the entity | |
gets further away. When the entity is right next to the player, the text will be the size of LARGEST_TEXT. When the entity is | |
at the maximum distance before text disappears, the text will be the size of SMALLEST_TEXT. | |
--]] | |
require('ac_core'); | |
-- USER CONSTANTS (Change me!) | |
local DRAW_LINE = true; -- Turn this on to draw tracking lines to entities. | |
local LINE_ORIGIN = {X = X_OFF, Y = Y_OFF}; -- Change the coordinates of the starting point of tracking lines. | |
local DRAW_BOUNDS = true; -- Turn this on to draw bounding boxes around entities. | |
local DRAW_DIST = true; -- Turn this on to display the entity's distance to the player. | |
local DRAW_HEALTH = true; -- Turn this on to display the entity's current health. | |
local MAX_DIST = 12000; -- The longest range at which health/distance information will be displayed. | |
local LARGEST_TEXT = 14; -- Size of health/distance text when very close to the player. | |
local SMALLEST_TEXT = 8; -- Size of health/distance text when very far from the player. | |
local C_MARK = C_RED; -- Color of the marks placed on entities. | |
local C_LINE = C_RED; -- Color of the lines drawn to entities. | |
local C_BOUNDS = C_WHITE; -- Color of the bounding boxes drawn around enemies. | |
local C_START = C_GREEN; -- Color of the square from which lines start. | |
local C_HEALTH = C_RED; -- Color of the health text, with a black outline. | |
local C_DIST = C_WHITE; -- Color of the distance text, with a black outline. | |
-- DON'T CHANGE BELOW HERE | |
local ADDR_CAM_POS = 0x001ad66c; -- Address of the camera's current in-world position. | |
local ADDR_CAM_MATRIX = 0x001ad67c; -- Address of the camera's transformation matrix. | |
local FIXED_POINT_SCALE = 1/4096; -- Game uses 1,3,12 fixed-point floats; shift right 12 to get value. | |
local H = 320; -- Distance to the near clip plane | |
local HALF_H = 160; -- Closest distance from the camera that the GTE can project points. | |
local SizeDiff = LARGEST_TEXT - SMALLEST_TEXT; | |
-- Gets the camera's current position and rotation matrix/basis vectors. Note that the matrix that is returned | |
-- is several frames ahead of the current rendered frame due to the game's lag. | |
local function GetCamera() | |
local camMatrix = ReadMatrix33_BULK(ADDR_CAM_MATRIX); | |
ScaleMatrix_IP(camMatrix, FIXED_POINT_SCALE); | |
return {Pos = ReadVector3_BULK(ADDR_CAM_POS), | |
Right = Vector(camMatrix[0], camMatrix[1], camMatrix[2]), | |
Up = Vector(camMatrix[3], camMatrix[4], camMatrix[5]), | |
Forward = Vector(camMatrix[6], camMatrix[7], camMatrix[8])}; | |
end | |
-- Projects points from world space to screen space. Will cull points that lie outside of screen space or too far behind | |
-- the near clip plane by default. This perspective transform is implemented as it is implemented on the Playstation's Geometry | |
-- Transformation Engine coprocessor (the RTPS instruction). | |
local function ProjectVertex(point, camera, cullOffscreen, cullBehind) | |
cullOffscreen = cullOffscreen and true; | |
cullBehind = cullBehind and true; | |
local p = VectorSubtract(point, camera.Pos); -- Move to camera space | |
local IR3 = DotProduct(camera.Forward, p); -- Project point's distance to the camera | |
if cullBehind and (IR3 <= HALF_H) then return nil end; -- Exit early if the point is too close to the camera | |
local pScale = H/IR3; -- Perspective adjustment | |
local IR1 = DotProduct(camera.Right, p); -- Project point's x position to screen's right vector | |
local SX = X_OFF + IR1 * pScale; -- Adjust projected point for perspective | |
if cullOffscreen and (0 > SX or SX > X_MAX) then return nil end; -- Exit early if the point is outside of the screen | |
local IR2 = DotProduct(camera.Up, p); -- Project point's y position to the screen's up vector | |
local SY = Y_OFF + IR2 * pScale; -- Adjust projected point for perspective | |
if cullOffscreen and (0 > SY or SY > Y_MAX) then return nil end; -- Exit early if the point is outside of the screen | |
return {X=SX, Y=SY, Z=IR3}; | |
end | |
-- Armored Core uses Axis-Aligned Bounding-Boxes (AABB) for collision detection. The half-width and height values | |
-- are stored for each entity rather than collision boxes and are dynamically constructed. | |
local function GetProjectedBoundingBox(camera, projVert, w, h) | |
local aabb = {}; | |
for j=0,1,1 do | |
local pos = projVert.Pos; | |
local floorY = pos.Y + h/2; -- Entity pos is reset to the floor, where it actually is | |
local b = Vector(pos.X + w, floorY-h*j, pos.Z+w); | |
table.insert(aabb, ProjectVertex(b, camera, false, false)); -- Calculate 4+4 vertices to create the AABB. | |
b = Vector(pos.X + w, floorY-h*j, pos.Z-w); | |
table.insert(aabb, ProjectVertex(b, camera, false, false)); | |
b = Vector(pos.X - w, floorY-h*j, pos.Z-w); | |
table.insert(aabb, ProjectVertex(b, camera, false, false)); | |
b = Vector(pos.X - w, floorY-h*j, pos.Z+w); | |
table.insert(aabb, ProjectVertex(b, camera, false, false)); | |
end | |
-- Laughably bad """clipping""" trick to make sure that AABB points that end up behind the camera due to entity points that are already behind | |
-- the near clipping plane don't get mirrored onto the screen. This works badly, but it *works* and requires 0 effort. | |
for _,b in ipairs(aabb) do | |
if b.Z < 0 then | |
b.X = -b.X; | |
b.Y = -b.Y; | |
b.Z = 0; | |
end | |
end | |
return aabb; | |
end | |
local function GetProjectedEntityVertices(camera) | |
local projVerts = {} | |
for i=2,MAX_ENTITIES,1 do | |
local ent = Entities[i]; | |
if ent.Active then | |
local pv = {}; | |
pv.Health = ent.Health; | |
pv.Pos = ent.Pos; | |
pv.ProjPos = ProjectVertex(ent.Pos, camera, not DRAW_LINE, true); | |
if DRAW_BOUNDS and pv.ProjPos ~= nil then | |
pv.AABB = GetProjectedBoundingBox(camera, pv, ent.AABBWidth, ent.AABBHeight); | |
end | |
table.insert(projVerts, pv); | |
end | |
end | |
return projVerts; | |
end | |
-- Linearly interpolate text size from LARGEST_TEXT to SMALLEST_TEXT from 0 to MAX_DIST. | |
local function TextInverseLerp(x) | |
if x > MAX_DIST then | |
return 0; | |
else | |
return LARGEST_TEXT - (x/MAX_DIST) * SizeDiff; | |
end | |
end | |
local function DrawEntities(verts) | |
for _, v in ipairs(verts) do | |
if v.ProjPos ~= nil then | |
gui.drawRectangle(v.ProjPos.X-1,v.ProjPos.Y-1,3,3,C_LINE) | |
if DRAW_BOUNDS then | |
local b = v.AABB; | |
gui.drawPolygon({{b[1].X, b[1].Y}, {b[2].X, b[2].Y}, {b[3].X, b[3].Y}, {b[4].X, b[4].Y}, {b[1].X, b[1].Y}, | |
{b[5].X, b[5].Y}, {b[6].X, b[6].Y}, {b[7].X, b[7].Y}, {b[8].X, b[8].Y}, {b[5].X, b[5].Y}}, | |
nil, nil, C_BOUNDS); | |
gui.drawLine(b[2].X, b[2].Y, b[6].X, b[6].Y, C_BOUNDS); | |
gui.drawLine(b[3].X, b[3].Y, b[7].X, b[7].Y, C_BOUNDS); | |
gui.drawLine(b[4].X, b[4].Y, b[8].X, b[8].Y, C_BOUNDS); | |
end | |
local dist = math.floor(VectorLength(VectorSubtract(Player.Pos, v.Pos))); | |
local textSize = TextInverseLerp(dist); | |
if DRAW_LINE then | |
gui.drawLine(LINE_ORIGIN.X, LINE_ORIGIN.Y, v.ProjPos.X, v.ProjPos.Y, C_MARK); | |
end; | |
if DRAW_DIST then | |
gui.drawText(v.ProjPos.X, v.ProjPos.Y+10, dist, C_DIST, 0xFF000000, textSize, nil, nil, "center", "middle"); | |
end; | |
if DRAW_HEALTH then | |
gui.drawText(v.ProjPos.X, v.ProjPos.Y+10+textSize, v.Health, C_HEALTH, 0xFF000000, textSize, nil, nil, "center", "middle"); | |
end; | |
end | |
if DRAW_LINE then | |
gui.drawRectangle(LINE_ORIGIN.X-1,LINE_ORIGIN.Y-1,3,3,C_START) | |
end | |
end | |
end | |
local PrevPoints = nil; | |
local CurPoints = nil; | |
local FuturePoints = nil; | |
local CurCam; | |
while true do | |
CoreUpdate(); | |
if (CurrentFrame2 ~= 0) then -- This address tends to be 0 during non-mission parts of the game. | |
if (PrevPoints == nil) then | |
CurCam = GetCamera(); | |
PrevPoints = GetProjectedEntityVertices(CurCam); | |
CurPoints = PrevPoints; | |
end | |
if (CurrentFrame1 ~= LastFrame1) then | |
CurCam = GetCamera(); | |
-- Camera/Enemy pos is several frames behind the camera matrix, so lag the matrix that is applied. | |
PrevPoints = CurPoints; | |
CurPoints = FuturePoints; | |
FuturePoints = GetProjectedEntityVertices(CurCam); | |
end | |
if (PrevPoints ~= nil) then | |
DrawEntities(PrevPoints) | |
end | |
end | |
emu.frameadvance() | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment