Skip to content

Instantly share code, notes, and snippets.

@Zinfidel
Last active April 8, 2023 10:54
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Zinfidel/d47287cec638f0f915c3d99bccef3a7d to your computer and use it in GitHub Desktop.
Save Zinfidel/d47287cec638f0f915c3d99bccef3a7d to your computer and use it in GitHub Desktop.
PSX Armored Core Overlay Scripts for BizHawk
--[[
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();
--[[
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
--[[
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
--[[
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