local collisionScaleFactor = 0.8 / client.getwindowsize()
local showExactMovement = true
local showAnglesAsDegrees = false
local drawAllObjects = false
local increaseCollisionRenderDistance = false
local giveGhostShrooms = false
local seekSettings = {
alertOnRewindAfterBranch = true,
}
local showBizHawkDumbnessWarning = false
local somePointerWithRegionAgnosticAddress = memory.read_u32_le(0x2000B54)
local valueForUSVersion = 0x0216F320
local ptrOffset = somePointerWithRegionAgnosticAddress - valueForUSVersion
local ptrRacerDataAddr = 0x0217ACF8 + ptrOffset
local ptrPlayerInputsAddr = 0x02175630 + ptrOffset
local ptrGhostInputsAddr = 0x0217568C + ptrOffset
local ptrItemInfoAddr = 0x0217BC2C + ptrOffset
local ptrRaceTimersAddr = 0x0217AA34 + ptrOffset
local ptrMissionInfoAddr = 0x021A9B70 + ptrOffset
local ptrObjStuff = 0x0217B588 + ptrOffset
local racerCountAddr = 0x0217ACF4 + ptrOffset
local ptrSomethingPlayerAddr = 0x021755FC + ptrOffset
local ptrSomeRaceDataAddr = 0x021759A0 + ptrOffset
local ptrCheckNumAddr = 0x021755FC + ptrOffset
local ptrCheckDataAddr = 0x02175600 + ptrOffset
local ptrScoreCountersAddr = 0x0217ACFC + ptrOffset
local ptrCollisionDataAddr = 0x0217b5f4 + ptrOffset
local ptrCurrentCourse = 0x23cdcd8 + ptrOffset
local ptrCameraAddr = 0x217AA4C + ptrOffset
local carHitboxFunc = memory.read_u32_le(0x2158ad4)
local bumperHitboxFunc = memory.read_u32_le(0x209c190)
local clockHandHitboxFunc = memory.read_u32_le(0x2159158)
local pendulumHitboxFunc = memory.read_u32_le(0x21592e8)
local rockyWrenchHitboxFunc = memory.read_u32_le(0x2095fe8)
if script_id == nil then
script_id = 1
else
script_id = script_id + 1
end
local my_script_id = script_id
local shouldExit = false
local redrawSeek = nil
local function redraw()
gui.clearGraphics("client")
gui.clearGraphics("emucore")
if not client.ispaused() then
return
end
if not tastudio.engaged() then
return
end
local frame = emu.framecount()
local f = frame - 2
while not tastudio.hasstate(f) do
f = f - 1
end
tastudio.setplayback(f + 1)
redrawSeek = frame
client.unpause()
end
local function ZeroVector()
return { x = 0, y = 0, z = 0 }
end
local function NewMyData()
local n = {}
n.positionDelta = 0
n.angleDelta = 0
n.driftAngleDelta = 0
n.pos = ZeroVector()
n.facingAngle = 0
n.driftAngle = 0
n.movementDirection = ZeroVector()
n.movementTarget = ZeroVector()
return n
end
local function UpdateMag(vector)
local x = vector.x / 4096
local y = vector.y / 4096
local z = vector.z / 4096
local sxz = x * x + z * z
vector.mag2 = math.sqrt(sxz)
vector.mag3 = math.sqrt(sxz + y * y)
end
local myData = NewMyData()
local ghostData = NewMyData()
local raceData = {}
local nearestObjectData = nil
local lastFrame = 0
local form = nil
local watchingGhost = false
local drawWhileUnpaused = true
local courseId = nil
local function clearDataOutsideRace()
ghostData = NewMyData()
raceData = {
coinsBeingCollected = 0,
}
nearestObjectData = nil
form.ghostInputs = nil
forms.settext(form.ghostInputHackButton, "Copy from player")
courseId = nil
end
function contains(list, x)
for _, v in ipairs(list) do
if v == x then return true end
end
return false
end
local function time(secs)
t_sec = math.floor(secs) % 60
if (t_sec < 10) then t_secaux = "0" else t_secaux = "" end
t_min = math.floor(secs / 60)
t_mili = math.floor(secs * 1000) % 1000
if (t_mili < 10) then t_miliaux = "00" elseif (t_mili < 100) then t_miliaux = "0" else t_miliaux = "" end
return (t_min .. ":" .. t_secaux .. t_sec .. ":" .. t_miliaux .. t_mili)
end
local function padLeft(str, len)
return string.format("%" .. len .. "s", str)
end
local function numToStr(value)
return string.format("%#.2f", value)
end
local function prettyFloat(value, neg)
value = math.floor(value * 10000) / 10000
local ret
if (not (value == 1 or value == -1)) then
if (value >= 0) then
value = " " .. value end
ret = string.sub(value .. "000000", 0, 6)
else
if (value == 1) then
ret = " 1 "
else
ret = "-1 "
end
end
if (not neg) then
ret = string.sub(ret, 2, 6)
end
return ret
end
local function format01(value)
if (not showExactMovement) then
return prettyFloat(value / 4096, false)
else
return value .. ""
end
end
local function posVecToStr(vector)
return string.format("%9s, %9s, %8s", vector.x, vector.z, vector.y)
end
local function normalVectorToStr(vector)
if showExactMovement then
return string.format("%5s, %5s, %5s", vector.x, vector.z, vector.y)
else
return format01(vector.x) .. ", " .. format01(vector.z) .. ", " .. format01(vector.y)
end
end
local function v4ToStr(vector)
if showExactMovement then
return string.format("%5s, %5s, %5s, %5s", vector.x, vector.z, vector.y, vector.w)
else
return format01(vector.x) .. ", " .. format01(vector.z) .. ", " .. format01(vector.y) .. ", " .. format01(vector.w)
end
end
local function bstr(bool)
if bool == true then
return "true"
elseif bool == false then
return "false"
else
return "nab"
end
end
local function get_u32(data, offset)
return data[offset] | (data[offset + 1] << 8) | (data[offset + 2] << 16) | (data[offset + 3] << 24)
end
local function get_s32(data, offset)
local u = data[offset] | (data[offset + 1] << 8) | (data[offset + 2] << 16) | (data[offset + 3] << 24)
return u - ((data[offset + 3] & 0x80) << 25)
end
local function get_u16(data, offset)
return data[offset] | (data[offset + 1] << 8)
end
local function get_s16(data, offset)
local u = data[offset] | (data[offset + 1] << 8)
return u - ((data[offset + 1] & 0x80) << 9)
end
local function read_pos(addr)
return {
x = memory.read_s32_le(addr),
y = memory.read_s32_le(addr + 4),
z = memory.read_s32_le(addr + 8),
}
end
local function get_pos(data, offset)
return {
x = get_s32(data, offset),
y = get_s32(data, offset + 4),
z = get_s32(data, offset + 8),
}
end
local function read_pos_16(addr)
local d = memory.read_bytes_as_array(addr, 6)
return {
x = get_s16(d, 1),
y = get_s16(d, 3),
z = get_s16(d, 5),
}
end
local function get_pos_16(data, offset)
return {
x = get_s16(data, offset),
y = get_s16(data, offset + 2),
z = get_s16(data, offset + 4),
}
end
local function readVec4(addr)
return {
x = memory.read_s32_le(addr),
y = memory.read_s32_le(addr + 4),
z = memory.read_s32_le(addr + 8),
w = memory.read_s32_le(addr + 12),
}
end
local function get_Vec4(data, offset)
return {
x = get_s32(data, offset),
y = get_s32(data, offset + 4),
z = get_s32(data, offset + 8),
w = get_s32(data, offset + 12),
}
end
local function distanceSqBetween(p1, p2)
local x = p2.x - p1.x
local y = p2.y - p1.y
local z = p2.z - p1.z
return x * x + y * y + z * z
end
local function mul_fx(a, b)
return a * b // 0x1000
end
local function dotProduct_float(v1, v2)
local a = v1.x * v2.x + v1.y * v2.y + v1.z * v2.z
return a / 0x1000
end
local function dotProduct_t(v1, v2)
local a = v1.x * v2.x + v1.y * v2.y + v1.z * v2.z
return a // 0x1000
end
local function dotProduct_r(v1, v2)
local a = v1.x * v2.x + v1.y * v2.y + v1.z * v2.z + 0x800
return a // 0x1000
end
local function crossProduct(v1, v2)
return {
x = (v1.y * v2.z - v1.z * v2.y) / 0x1000,
y = (v1.z * v2.x - v1.x * v2.z) / 0x1000,
z = (v1.x * v2.y - v1.y * v2.x) / 0x1000,
}
end
local function multiplyVector(v, s)
return {
x = v.x * s,
y = v.y * s,
z = v.z * s,
}
end
local function addVector(v1, v2)
return {
x = v1.x + v2.x,
y = v1.y + v2.y,
z = v1.z + v2.z,
}
end
local function subtractVector(v1, v2)
return {
x = v1.x - v2.x,
y = v1.y - v2.y,
z = v1.z - v2.z,
}
end
local function truncateVector(v)
return {
x = math.floor(v.x),
y = math.floor(v.y),
z = math.floor(v.z),
}
end
local function vectorEqual(v1, v2)
if v1.x == v2.x and v1.y == v2.y and v1.z == v2.z then
return true
end
end
local function vectorEqual_ignoreSign(v1, v2)
if v1.x == v2.x and v1.y == v2.y and v1.z == v2.z then
return true
end
v1 = multiplyVector(v1, -1)
return v1.x == v2.x and v1.y == v2.y and v1.z == v2.z
end
local function copyVector(v)
return { x = v.x, y = v.y, z = v.z }
end
local function normalizeVector_float(v)
local m = math.sqrt(v.x * v.x + v.y * v.y + v.z * v.z) / 0x1000
return {
x = v.x / m,
y = v.y / m,
z = v.z / m,
}
end
local axisDirections = {
x = { x = 0x1000, y = 0, z = 0 },
y = { x = 0, y = 0x1000, z = 0 },
z = { x = 0, y = 0, z = 0x1000 },
}
local function getBoxyPolygons(center, directions, sizes, sizes2)
if sizes2 == nil then sizes2 = sizes end
local offsets = {
x1 = multiplyVector(directions.x, sizes.x / 0x1000),
y1 = multiplyVector(directions.y, sizes.y / 0x1000),
z1 = multiplyVector(directions.z, sizes.z / 0x1000),
x2 = multiplyVector(directions.x, sizes2.x / 0x1000),
y2 = multiplyVector(directions.y, sizes2.y / 0x1000),
z2 = multiplyVector(directions.z, sizes2.z / 0x1000),
}
local s = subtractVector
local a = addVector
local verts = {
s(s(s(center, offsets.x2), offsets.y2), offsets.z2),
a(s(s(center, offsets.x2), offsets.y2), offsets.z1),
s(a(s(center, offsets.x2), offsets.y1), offsets.z2),
a(a(s(center, offsets.x2), offsets.y1), offsets.z1),
s(s(a(center, offsets.x1), offsets.y2), offsets.z2),
a(s(a(center, offsets.x1), offsets.y2), offsets.z1),
s(a(a(center, offsets.x1), offsets.y1), offsets.z2),
a(a(a(center, offsets.x1), offsets.y1), offsets.z1),
}
return {
{ verts[1], verts[5], verts[7], verts[3] },
{ verts[1], verts[5], verts[6], verts[2] },
{ verts[1], verts[3], verts[4], verts[2] },
{ verts[8], verts[4], verts[2], verts[6] },
{ verts[8], verts[4], verts[3], verts[7] },
{ verts[8], verts[6], verts[5], verts[7] },
}
end
local function getCylinderPolygons(center, directions, radius, h1, h2)
local offsets = {
x = multiplyVector(directions.x, radius / 0x1000),
y = multiplyVector(directions.y, h1 / 0x1000),
d = multiplyVector(directions.y, -h2 / 0x1000),
z = multiplyVector(directions.z, radius / 0x1000),
}
local s = subtractVector
local a = addVector
local m = multiplyVector
local norm = normalizeVector_float
radius = radius / 0x1000
local around = {
offsets.x,
m(norm(a(m(offsets.x, 2), offsets.z)), radius),
m(norm(a(offsets.x, offsets.z)), radius),
m(norm(a(offsets.x, m(offsets.z, 2))), radius),
offsets.z,
m(norm(a(m(offsets.x, -1), m(offsets.z, 2))), radius),
m(norm(a(m(offsets.x, -1), offsets.z)), radius),
m(norm(a(m(offsets.x, -2), offsets.z)), radius),
}
local count = #around
for i = 1, count do
around[#around + 1] = m(around[i], -1)
end
local tc = addVector(center, offsets.y)
local bc = addVector(center, offsets.d)
local vertsT = {}
local vertsB = {}
for i = 1, #around do
vertsT[i] = a(tc, around[i])
vertsB[i] = a(bc, around[i])
end
local polys = {}
for i = 1, #around - 1 do
polys[i] = { vertsT[i], vertsT[i + 1], vertsB[i + 1], vertsB[i] }
end
polys[#polys + 1] = vertsT
polys[#polys + 1] = vertsB
return polys
end
local triangles = nil
local kclData = nil
local someCourseData = nil
local collisionMap = nil
local function getPlayerData(ptr, previousData)
local newData = NewMyData()
if ptr == 0 then
return newData
end
local allData = memory.read_bytes_as_array(ptr + 1, 0x5a8 - 1)
allData[0] = memory.read_u8(ptr)
newData.pos = get_pos(allData, 0x80)
newData.posForObjects = get_pos(allData, 0x1B8)
newData.preMovementPosForObjects = get_pos(allData, 0x1C4)
newData.posForItems = get_pos(allData, 0x1D8)
newData.speed = get_s32(allData, 0x2A8)
newData.boostAll = allData[0x238]
newData.boostMt = allData[0x23C]
newData.verticalVelocity = get_s32(allData, 0x260)
newData.mtTime = get_s32(allData, 0x30C)
newData.maxSpeed = get_s32(allData, 0xD0)
newData.turnLoss = get_s32(allData, 0x2D4)
newData.offroadSpeed = get_s32(allData, 0xDC)
newData.wallSpeedMult = get_s32(allData, 0x38C)
newData.airSpeed = get_s32(allData, 0x3F8)
newData.effectSpeed = get_s32(allData, 0x394)
local posDelta = math.sqrt((previousData.pos.z - newData.pos.z) ^ 2 + (previousData.pos.x - newData.pos.x) ^ 2)
newData.posDelta = math.floor(posDelta * 10) / 10
newData.basePosDelta = get_pos(allData, 0xA4)
newData.actualPosDelta = subtractVector(newData.pos, previousData.pos)
newData.collisionPush = subtractVector(newData.actualPosDelta, newData.basePosDelta)
newData.facingAngle = get_s16(allData, 0x236)
newData.pitch = get_s16(allData, 0x234)
newData.driftAngle = get_s16(allData, 0x388)
newData.facingDelta = newData.facingAngle - previousData.facingAngle
newData.driftDelta = newData.driftAngle - previousData.driftAngle
newData.movementDirection = get_pos(allData, 0x68)
UpdateMag(newData.movementDirection)
newData.movementTarget = get_pos(allData, 0x50)
UpdateMag(newData.movementTarget)
newData.surfaceNormalVector = get_pos(allData, 0x244)
UpdateMag(newData.surfaceNormalVector)
newData.grip = get_s32(allData, 0x240)
newData.radius = get_s32(allData, 0x1d0)
newData.framesInAir = get_s32(allData, 0x380)
if allData[0x3DD] == 0 then
newData.air = "Ground"
else
newData.air = "Air"
end
newData.spawnPoint = get_s32(allData, 0x3C4)
newData.movementAdd1fc = get_pos(allData, 0x1fc)
newData.movementAdd2f0 = get_pos(allData, 0x2f0)
newData.movementAdd374 = get_pos(allData, 0x374)
UpdateMag(newData.movementAdd1fc)
UpdateMag(newData.movementAdd2f0)
UpdateMag(newData.movementAdd374)
newData.f0 = get_Vec4(allData, 0xf0)
return newData
end
local function getCheckpointData(dataObj)
local ptrCheckNum = memory.read_s32_le(ptrCheckNumAddr)
local ptrCheckData = memory.read_s32_le(ptrCheckDataAddr)
if ptrCheckNum == 0 or ptrCheckData == 0 then
return
end
dataObj.checkpoint = memory.read_u8(ptrCheckNum + 0x46)
dataObj.keyCheckpoint = memory.read_s8(ptrCheckNum + 0x48)
dataObj.checkpointGhost = memory.read_s8(ptrCheckNum + 0xD2)
dataObj.keyCheckpointGhost = memory.read_s8(ptrCheckNum + 0xD4)
dataObj.lap = memory.read_s8(ptrCheckNum + 0x38)
dataObj.lap_f = memory.read_s32_le(ptrCheckNum + 0x18) * 1.0 / 60 - 0.05
if (dataObj.lap_f < 0) then dataObj.lap_f = 0 end
end
local function updateGhost(form)
local ptr = memory.read_s32_le(ptrGhostInputsAddr)
if ptr == 0 then return end
memory.write_bytes_as_array(ptr, form.ghostInputs)
memory.write_s32_le(ptr, 1765)
ptr = memory.read_s32_le(ptrSomeRaceDataAddr)
memory.write_bytes_as_array(ptr + 0x3ec, form.ghostLapTimes)
form.firstStateWithGhost = emu.framecount() + 1
end
local function setGhostInputs(form)
local ptr = memory.read_s32_le(ptrGhostInputsAddr)
if ptr == 0 then return end
local currentInputs = memory.read_bytes_as_array(ptr, 0xdce)
updateGhost(form)
local frames = 0
for i = 5, #currentInputs, 2 do
if form.ghostInputs[i] ~= currentInputs[i] then
break
elseif form.ghostInputs[i + 1] ~= currentInputs[i + 1] then
frames = frames + math.min(form.ghostInputs[i + 1], currentInputs[i + 1])
break
else
frames = frames + currentInputs[i + 1]
end
end
local targetFrame = frames + form.firstGhostInputFrame
targetFrame = targetFrame - 1
local currentFrame = emu.framecount()
if currentFrame > targetFrame then
local inputs = movie.getinput(targetFrame)
local isOn = inputs["A"]
tastudio.submitinputchange(targetFrame, "A", not isOn)
tastudio.applyinputchanges()
tastudio.submitinputchange(targetFrame, "A", isOn)
tastudio.applyinputchanges()
end
end
local function ensureGhostInputs(form)
local firstInputFrame = emu.framecount() - memory.read_s32_le(memory.read_s32_le(ptrRaceTimersAddr) + 4) + 121
if firstInputFrame ~= form.firstGhostInputFrame then
return
end
local frame = emu.framecount()
if frame < lastFrame or form.firstStateWithGhost > frame then
updateGhost(form)
end
end
local isCameraView = false
local function _getNearbyTriangles(pos)
local boundary = get_pos(someCourseData, 0x14)
if pos.x < boundary.x or pos.y < boundary.y or pos.z < boundary.z then
return {}
end
local shift = someCourseData[0x2C]
local fb = {
x = (pos.x - boundary.x) >> 12,
y = (pos.y - boundary.y) >> 12,
z = (pos.z - boundary.z) >> 12,
}
local base = get_u32(someCourseData, 0xC)
local a = base
local b = a + 4 * (
((fb.x >> shift)) |
((fb.y >> shift) << someCourseData[0x30]) |
((fb.z >> shift) << someCourseData[0x34])
)
if b >= 0x02800000 then
return nil
end
b = get_u32(collisionMap, b - base)
local safety = 0
while b < 0x80000000 do
safety = safety + 1
if safety > 1000 then error("infinite loop: reading nearby triangle map") end
a = a + b
shift = shift - 1
b = a + 4 * (
(((fb.x >> shift) & 1)) |
(((fb.y >> shift) & 1) << 1) |
(((fb.z >> shift) & 1) << 2)
)
b = get_u32(collisionMap, b - base)
end
a = a + (b - 0x80000000) + 2
local nearby = {}
local index = get_u16(collisionMap, a - base)
safety = 0
while index ~= 0 do
nearby[#nearby + 1] = triangles[index]
index = get_u16(collisionMap, a + 2 * #nearby - base)
safety = safety + 1
if safety > 1000 then
error("infinite loop: reading nearby triangle list")
end
end
return nearby
end
local _mergeSet = {}
local function merge(l1, l2)
for i = 1, #l2 do
local v = l2[i]
if _mergeSet[v] == nil then
l1[#l1 + 1] = v
_mergeSet[v] = true
end
end
end
local function getNearbyTriangles(pos)
if increaseCollisionRenderDistance ~= true then
return _getNearbyTriangles(pos)
end
_mergeSet = {}
local nearby = {}
local step = 128 * 0x1000
local stepCount = 1
if isCameraView then stepCount = 3 end
for iX = -stepCount, stepCount do
for iY = -stepCount, stepCount do
for iZ = -stepCount, stepCount do
local p = {
x = pos.x + iX * step,
y = pos.y + iY * step,
z = pos.z + iZ * step,
}
merge(nearby, _getNearbyTriangles(p))
end
end
end
return nearby
end
local function updateMinMax(current, new)
current.min.x = math.min(current.min.x, new.x)
current.min.y = math.min(current.min.y, new.y)
current.min.z = math.min(current.min.z, new.z)
current.max.x = math.max(current.max.x, new.x)
current.max.y = math.max(current.max.y, new.y)
current.max.z = math.max(current.max.z, new.z)
end
local function someKindOfTransformation(a, d1, d2, v1, v2)
local m = (mul_fx(a, d1) - d2) / (mul_fx(a, a) - 0x1000) * 0x1000
if a == 0x1000 or a == -0x1000 then
m = 1
end
local n = d1 - mul_fx(m, a)
local out = addVector(
multiplyVector(v2, m / 0x1000),
multiplyVector(v1, n / 0x1000)
)
UpdateMag(out)
return out
end
local function getSurfaceDistanceData(toucher, surface)
local data = {}
local relativePos = subtractVector(toucher.pos, surface.vertex[1])
local previousPos = toucher.previousPos and subtractVector(toucher.previousPos, surface.vertex[1])
local upDistance = dotProduct_t(relativePos, surface.surfaceNormal)
local inDistance = dotProduct_t(relativePos, surface.inVector)
local planeDistances = {
{
d = dotProduct_t(relativePos, surface.outVector[1]),
v = surface.outVector[1],
}, {
d = dotProduct_t(relativePos, surface.outVector[2]),
v = surface.outVector[2],
}, {
d = inDistance - surface.triangleSize,
v = surface.outVector[3],
}
}
table.sort(planeDistances, function(a, b) return a.d > b.d end )
data.isBehind = upDistance < 0
if previousPos ~= nil and dotProduct_t(previousPos, surface.surfaceNormal) < 0 then
data.wasBehind = true
if dotProduct_t(previousPos, surface.outVector[1]) <= 0 and dotProduct_t(previousPos, surface.outVector[2]) <= 0 and dotProduct_t(previousPos, surface.inVector) <= surface.triangleSize then
data.wasInside = true
end
end
data.distanceVector = multiplyVector(surface.surfaceNormal, -upDistance / 0x1000)
local pDist
local distanceOffset = nil
if planeDistances[1].d <= 0 then
pDist = 0
data.inside = true
data.nearestPointIsVertex = false
data.distance = math.max(0, math.abs(upDistance) - toucher.radius)
else
data.inside = false
local lmdp = dotProduct_t(planeDistances[1].v, planeDistances[2].v)
data.nearestPointIsVertex = mul_fx(lmdp, planeDistances[1].d) <= planeDistances[2].d
if data.nearestPointIsVertex then
local b = planeDistances[1].v
local m = planeDistances[2].v
local t = nil
if
(m == surface.outVector[1] and b == surface.inVector) or
(m == surface.outVector[2] and b == surface.outVector[1]) or
(m == surface.inVector and b == surface.outVector[2])
then
t = someKindOfTransformation(lmdp, planeDistances[1].d, planeDistances[2].d, b, m)
else
t = someKindOfTransformation(lmdp, planeDistances[2].d, planeDistances[1].d, m, b)
end
pDist = t.mag3 * 0x1000
if t.mag3 > 0 then
distanceOffset = t
end
else
pDist = planeDistances[1].d
distanceOffset = multiplyVector(planeDistances[1].v, planeDistances[1].d / 0x1000)
end
data.distance = math.max(0, math.sqrt(pDist * pDist + upDistance * upDistance) - toucher.radius)
end
if distanceOffset ~= nil then
data.distanceVector = subtractVector(data.distanceVector, distanceOffset)
end
if pDist > toucher.radius then
data.pushOutBy = -1
else
data.pushOutBy = math.sqrt(toucher.radius * toucher.radius - pDist * pDist) - upDistance
end
data.interacting = true
if data.pushOutBy < 0 or toucher.radius - upDistance >= 0x1e001 then
data.interacting = false
elseif data.isBehind then
if previousPos == nil then
data.interacting = false
elseif data.inside then
if data.wasBehind == true and data.wasInside ~= true then
data.interacting = false
end
else
local o = 0
if planeDistances[1].v == surface.inVector then
o = surface.triangleSize
end
if dotProduct_t(previousPos, planeDistances[1].v) > o then
data.interacting = false
end
end
end
if data.wasBehind and previousPos ~= nil and dotProduct_t(previousPos, surface.surfaceNormal) < -0xa000 then
data.wasFarBehind = true
end
if data.interacting then
data.touchSlopedEdge = false
if not data.inside and not data.nearestPointIsVertex and 0x424 >= planeDistances[1].v.y and planeDistances[1].v.y >= -0x424 then
data.touchSlopedEdge = true
end
data.push = true
if toucher.previousPos ~= nil then
local posDelta = subtractVector(toucher.pos, toucher.previousPos)
local outwardMovement = dotProduct_t(posDelta, surface.surfaceNormal)
if outwardMovement > 819 then
data.push = false
data.outwardMovement = outwardMovement
end
if data.wasBehind and (toucher.flags & 0x3b ~= 0 or data.wasFarBehind) then
data.push = false
end
end
end
return data
end
local function getTouchDataForSurface(toucher, surface)
local data = {}
local st = surface.surfaceType
if toucher.flags & 0x10 ~= 0 and st & 0xa000 ~= 0 then
return { canTouch = false }
end
local unknown1 = st & 0x2010 == 0
local unknown2 = toucher.flags & 4 == 0 or st & 0x2000 == 0
local unknown3 = toucher.flags & 1 == 0 or st & 0x10 == 0
if not (unknown1 or (unknown2 and unknown3)) then
return { canTouch = false }
end
data.canTouch = true
local dd = getSurfaceDistanceData(toucher, surface)
data.touching = dd.interacting
data.pushOutDistance = dd.pushOutBy
data.distance = dd.distance
data.behind = dd.isBehind
local pushVec = multiplyVector(surface.surfaceNormal, data.pushOutDistance)
data.centerToTriangle = dd.distanceVector
data.wasBehind = dd.wasBehind
data.isInside = dd.inside
data.push = dd.push
data.outwardMovement = dd.outwardMovement
return data
end
local function getCollisionDataForRacer(racerData)
local nearby = getNearbyTriangles(racerData.posForObjects)
if #nearby == 0 then
return {}
end
local data = {}
for i = 1, #nearby do
data[#data + 1] = {
triangle = nearby[i],
touch = getTouchDataForSurface({
pos = racerData.posForObjects,
previousPos = racerData.preMovementPosForObjects,
radius = racerData.radius,
flags = 1,
}, nearby[i]),
}
end
local maxPushOut = nil
for i, v in pairs(data) do
if v.touch.push and not v.triangle.isWall and (maxPushOut == nil or v.touch.pushOutDistance > data[maxPushOut].touch.pushOutDistance) then
maxPushOut = i
end
end
if maxPushOut ~= nil then
data[maxPushOut].controlsSlope = true
end
return data
end
local function getCourseCollisionData()
local dataPtr = get_u32(someCourseData, 8)
local endData = get_u32(someCourseData, 12)
local triangleData = memory.read_bytes_as_array(dataPtr + 1, endData - dataPtr)
triangleData[0] = memory.read_u8(dataPtr)
triangles = {}
local triCount = (endData - dataPtr) / 0x10 - 1
for i = 1, triCount do
local offs = i * 0x10
triangles[i] = {
id = i,
triangleSize = get_s32(triangleData, offs + 0),
vertexId = get_s16(triangleData, offs + 4),
surfaceNormalId = get_s16(triangleData, offs + 6),
outVector1Id = get_s16(triangleData, offs + 8),
outVector2Id = get_s16(triangleData, offs + 10),
inVectorId = get_s16(triangleData, offs + 12),
surfaceType = get_u16(triangleData, offs + 14),
}
triangles[i].collisionType = (triangles[i].surfaceType >> 8) & 0x1f
triangles[i].unkType = (triangles[i].surfaceType >> 2) & 3
triangles[i].props = (1 << triangles[i].collisionType) | (1 << (triangles[i].unkType + 0x1a))
triangles[i].isWall = triangles[i].props & 0x214300 ~= 0
triangles[i].isFloor = triangles[i].props & 0x1e34ef ~= 0
end
local vectorsPtr = get_u32(someCourseData, 4)
local vectorData = memory.read_bytes_as_array(vectorsPtr + 1, dataPtr - vectorsPtr + 0x10)
vectorData[0] = memory.read_u8(vectorsPtr)
local vectors = {}
local vecCount = (dataPtr - vectorsPtr + 0x10) // 6
for i = 0, vecCount - 1 do
local offs = i * 6
vectors[i] = get_pos_16(vectorData, offs)
end
local vertexesPtr = get_u32(someCourseData, 0)
local vertexData = memory.read_bytes_as_array(vertexesPtr + 1, vectorsPtr - vertexesPtr)
vertexData[0] = memory.read_u8(vertexesPtr)
local vertexes = {}
local vertCount = (vectorsPtr - vertexesPtr) / 12
for i = 0, vertCount - 1 do
local offs = i * 12
vertexes[i] = get_pos(vertexData, offs)
end
for i = 1, #triangles do
local tri = triangles[i]
tri.surfaceNormal = vectors[tri.surfaceNormalId]
tri.inVector = vectors[tri.inVectorId]
tri.vertex = {}
tri.slope = {}
tri.vertex[1] = vertexes[tri.vertexId]
tri.outVector = {}
tri.outVector[1] = vectors[tri.outVector1Id]
tri.outVector[2] = vectors[tri.outVector2Id]
tri.outVector[3] = vectors[tri.inVectorId]
tri.slope[1] = crossProduct(tri.surfaceNormal, tri.outVector[1])
tri.slope[2] = crossProduct(tri.surfaceNormal, tri.outVector[2])
tri.slope[3] = crossProduct(tri.surfaceNormal, tri.outVector[3])
tri.slope[1] = multiplyVector(tri.slope[1], -1)
local a = dotProduct_float(vectors[tri.inVectorId], tri.slope[1])
local b = tri.triangleSize / a
if a == 0 then
b = 0x1000 * 1000
tri.ignore = true
end
local c = multiplyVector(tri.slope[1], b)
tri.vertex[3] = addVector(tri.vertex[1], c)
a = dotProduct_float(vectors[tri.inVectorId], tri.slope[2])
b = tri.triangleSize / a
if a == 0 then
b = 0x1000 * 1000
tri.ignore = true
end
c = multiplyVector(tri.slope[2], b)
tri.vertex[2] = addVector(tri.vertex[1], c)
end
local cmPtr = get_u32(someCourseData, 0xC)
local cmSize = 0x28000
collisionMap = memory.read_bytes_as_array(cmPtr + 1, cmSize - 1)
collisionMap[0] = memory.read_u8(cmPtr)
end
local function getCourseData()
someCourseData = memory.read_bytes_as_array(ptrCollisionDataAddr + 1, 0x38 - 1)
someCourseData[0] = memory.read_u8(ptrCollisionDataAddr)
getCourseCollisionData()
end
local function inRace()
if memory.read_s32_le(ptrRacerDataAddr) == 0 then
clearDataOutsideRace()
return false
end
local timer = memory.read_s32_le(memory.read_s32_le(ptrRaceTimersAddr) + 4)
if timer == 0 then
clearDataOutsideRace()
return false
end
local course = memory.read_u8(ptrCurrentCourse)
if course ~= courseId then
courseId = course
getCourseData()
end
return true
end
local allObjects = {}
local t = { }
if true then
t[0x000] = "follows player"
t[0x00b] = "STOP! signage"; t[0x00d] = "puddle";
t[0x065] = "item box"; t[0x066] = "post";
t[0x067] = "wooden crate"; t[0x068] = "coin";
t[0x06e] = "gate trigger";
t[0x0cc] = "drawbridge";
t[201] = "moving item box"; t[202] = "moving block";
t[203] = "gear"; t[204] = "bridge";
t[205] = "clock hand"; t[206] = "gear";
t[207] = "pendulum"; t[208] = "rotating floor";
t[209] = "rotating bridge"; t[210] = "roulette";
t[0x12e] = "coconut tree"; t[0x12f] = "pipe";
t[0x130] = "wumpa-fruit tree";
t[0x138] = "striped tree";
t[0x145] = "autumn tree"; t[0x146] = "winter tree";
t[0x148] = "palm tree";
t[0x14f] = "pinecone tree"; t[0x150] = "beanstalk";
t[0x156] = "N64 winter tree";
t[401] = "goomba"; t[402] = "giant snowball";
t[403] = "thwomp";
t[405] = "bus"; t[406] = "chain chomp";
t[407] = "chain chomp post"; t[408] = "leaping fireball";
t[409] = "mole"; t[410] = "car";
t[411] = "cheep cheep"; t[412] = "truck";
t[413] = "snowman"; t[414] = "coffin";
t[415] = "bats";
t[0x1a2] = "bullet bill"; t[0x1a3] = "walking tree";
t[0x1a4] = "flamethrower"; t[0x1a5] = "stray chain chomp";
t[0x1ac] = "crab";
t[0x1a6] = "piranha plant"; t[0x1a7] = "rocky wrench";
t[0x1a8] = "bumper"; t[0x1a9] = "flipper";
t[0x1af] = "fireballs"; t[0x1b0] = "pinball";
t[0x1b1] = "boulder"; t[0x1b2] = "pokey";
t[0x1f5] = "bully"; t[0x1f6] = "Chief Chilly";
t[0x1f8] = "King Bomb-omb";
t[0x1fb] = "Eyerok"; t[0x1fd] = "King Boo";
t[0x1fe] = "Wiggler";
end
local mapObjTypes = t
local function getBoxyDistances(obj, pos, radius)
local posDelta = subtractVector(pos, obj.dynPos)
local dir = obj.orientation
local sizes = obj.sizes
local orientedPosDelta = {
x = dotProduct_t(posDelta, dir.x),
y = dotProduct_t(posDelta, dir.y),
z = dotProduct_t(posDelta, dir.z),
}
local orientedDistanceTo = {
math.abs(orientedPosDelta.x) - radius - sizes.x,
math.abs(orientedPosDelta.y) - radius - sizes.y,
math.abs(orientedPosDelta.z) - radius - sizes.z,
}
local outsideTheBox = 0
for i = 1, 3 do
if orientedDistanceTo[i] > 0 then
outsideTheBox = outsideTheBox + orientedDistanceTo[i] * orientedDistanceTo[i]
end
end
local totalDistance = nil
if outsideTheBox ~= 0 then
totalDistance = math.sqrt(outsideTheBox)
else
totalDistance = math.max(orientedDistanceTo[1], orientedDistanceTo[2], orientedDistanceTo[3])
end
return {
x = orientedDistanceTo[1],
y = orientedDistanceTo[2],
z = orientedDistanceTo[3],
d = totalDistance,
}
end
local function getCylinderDistances(obj, pos, radius)
local posDelta = subtractVector(pos, obj.dynPos)
local dir = obj.orientation
local orientedPosDelta = posDelta
if obj.hitboxType == "cylinder2" then
orientedPosDelta = {
x = dotProduct_t(posDelta, dir.x),
y = dotProduct_t(posDelta, dir.y),
z = dotProduct_t(posDelta, dir.z),
}
end
orientedPosDelta = {
h = math.sqrt(orientedPosDelta.x * orientedPosDelta.x + orientedPosDelta.z * orientedPosDelta.z),
v = orientedPosDelta.y
}
local bHeight = obj.bHeight
if bHeight == nil then bHeight = obj.height end
local orientedDistanceTo = {
math.abs(orientedPosDelta.h) - radius - obj.radius,
math.max(
orientedPosDelta.v - radius - obj.height,
-(orientedPosDelta.v + radius + bHeight)
),
}
local outside = 0
for i = 1, 2 do
if orientedDistanceTo[i] > 0 then
outside = outside + orientedDistanceTo[i] * orientedDistanceTo[i]
end
end
local totalDistance = nil
if outside ~= 0 then
totalDistance = math.sqrt(outside)
else
totalDistance = math.max(orientedDistanceTo[1], orientedDistanceTo[2])
end
return {
h = math.floor(orientedDistanceTo[1]),
v = math.floor(orientedDistanceTo[2]),
d = totalDistance,
}
end
local function getDetailsForBoxyObject(obj)
obj.boxy = true
if obj.hitboxFunc == carHitboxFunc then
obj.sizes = read_pos(obj.ptr + 0x114)
obj.backSizes = {
x = obj.sizes.x,
y = 0,
z = memory.read_s32_le(obj.ptr + 0x120),
}
elseif obj.hitboxFunc == clockHandHitboxFunc then
obj.sizes = read_pos(obj.ptr + 0x58)
obj.backSizes = copyVector(obj.sizes)
obj.backSizes.z = 0
elseif obj.hitboxFunc == pendulumHitboxFunc then
obj.sizes = {
x = obj.radius,
y = obj.radius,
z = memory.read_s32_le(obj.ptr + 0x108),
}
elseif obj.hitboxFunc == rockyWrenchHitboxFunc then
obj.sizes = {
x = obj.radius,
y = memory.read_s32_le(obj.ptr + 0xa0),
z = obj.radius,
}
else
obj.sizes = read_pos(obj.ptr + 0x58)
end
obj.dynPos = obj.pos
obj.polygons = getBoxyPolygons(obj.pos, obj.orientation, obj.sizes, obj.backSizes)
end
local function getDetailsForCylinder2Object(obj, isBumper)
obj.cylinder2 = true
obj.dynPos = obj.pos
if isBumper then
obj.bHeight = 0
if memory.read_u16_le(obj.ptr + 2) & 0x800 == 0 and memory.read_u32_le(obj.ptr + 0x11c) == 1 then
obj.radius = mul_fx(obj.radius, memory.read_u32_le(obj.ptr + 0xbc))
end
else
obj.bHeight = obj.height
end
obj.polygons = getCylinderPolygons(obj.pos, obj.orientation, obj.radius, obj.height, obj.bHeight)
end
local function getDetailsForDynamicBoxyObject(obj)
obj.sizes = read_pos(obj.ptr + 0x100)
obj.dynPos = read_pos(obj.ptr + 0xf4)
obj.backSizes = copyVector(obj.sizes)
obj.backSizes.z = memory.read_s32_le(obj.ptr + 0x10c)
obj.polygons = getBoxyPolygons(obj.dynPos, obj.orientation, obj.sizes, obj.backSizes)
end
local function getDetailsForDynamicCylinderObject(obj)
obj.radius = memory.read_s32_le(obj.ptr + 0x100)
obj.halfHeight = memory.read_s32_le(obj.ptr + 0x104)
obj.dynPos = read_pos(obj.ptr + 0xf4)
end
local function getMapObjDetails(obj)
local objPtr = obj.ptr
local typeId = memory.read_u16_le(objPtr)
obj.typeId = typeId
obj.type = mapObjTypes[typeId] or "unknown " .. typeId
obj.boxy = false
obj.cylinder = false
obj.radius = memory.read_s32_le(objPtr + 0x58)
obj.height = memory.read_s32_le(objPtr + 0x5C)
obj.orientation = {
x = read_pos(obj.ptr + 0x28),
y = read_pos(obj.ptr + 0x34),
z = read_pos(obj.ptr + 0x40),
}
local hitboxType = ""
if memory.read_u16_le(objPtr + 2) & 1 == 0 then
local maybePtr = memory.read_s32_le(objPtr + 0x98)
local hbType = 0
if maybePtr > 0 then
hbType = memory.read_s32_le(maybePtr + 8)
end
if hbType == 0 or hbType > 5 or hbType < 0 then
hitboxType = ""
elseif hbType == 1 then
hitboxType = "spherical"
elseif hbType == 2 then
hitboxType = "cylindrical"
obj.polygons = getCylinderPolygons(obj.pos, obj.orientation, obj.radius, obj.height, obj.height)
elseif hbType == 3 then
hitboxType = "cylinder2"
getDetailsForCylinder2Object(obj, false)
elseif hbType == 4 then
hitboxType = "boxy"
getDetailsForBoxyObject(obj)
elseif hbType == 5 then
hitboxType = "custom"
obj.chb = memory.read_u32_le(objPtr + 0x98)
obj.hitboxFunc = memory.read_u32_le(obj.chb + 0x18)
if obj.hitboxFunc == carHitboxFunc then
hitboxType = "boxy"
getDetailsForBoxyObject(obj)
elseif obj.hitboxFunc == bumperHitboxFunc then
hitboxType = "cylinder2"
getDetailsForCylinder2Object(obj, true)
elseif obj.hitboxFunc == clockHandHitboxFunc then
hitboxType = "boxy"
getDetailsForBoxyObject(obj)
elseif obj.hitboxFunc == pendulumHitboxFunc then
hitboxType = "spherical"
obj.radius = memory.read_s32_le(obj.ptr + 0x104)
getDetailsForBoxyObject(obj)
obj.multiBox = true
elseif obj.hitboxFunc == rockyWrenchHitboxFunc then
if memory.read_u8(obj.ptr + 0xb0) == 1 then
hitboxType = "no hitbox"
else
hitboxType = "spherical"
obj.multiBox = true
getDetailsForBoxyObject(obj)
end
else
hitboxType = hitboxType .. " " .. string.format("%x", obj.hitboxFunc)
end
end
end
if hitboxType == "" then hitboxType = "no hitbox" end
obj.hitboxType = hitboxType
end
local function getItemDetails(obj)
local ptr = obj.ptr
obj.radius = memory.read_s32_le(ptr + 0xE0)
obj.typeId = 0
obj.type = "item"
obj.hitboxType = "item"
end
local function getRacerObjDetails(obj)
local ptr = obj.ptr
obj.radius = memory.read_s32_le(ptr + 0x1d0)
obj.typeId = 0
obj.type = "racer"
obj.hitboxType = "spherical"
end
local function isCoinAndCollected(objPtr)
if memory.read_s16_le(objPtr) ~= 0x68 then
return false
else
return memory.read_u16_le(objPtr + 2) & 0x01 ~= 0
end
end
local function isPlayerOrGhost(objPtr)
local flags7c = memory.read_u8(objPtr + 0x7C)
return flags7c & 0x05 ~= 0
end
local function getObjectDetails(obj)
local flags = obj.flags
if flags & 0x2000 ~= 0 then
getMapObjDetails(obj)
elseif flags & 0x4000 ~= 0 then
getItemDetails(obj)
elseif flags & 0x8000 ~= 0 then
getRacerObjDetails(obj)
else
return
end
if flags & 0x1000 ~= 0 then
obj.dynamic = true
local aCodePtr = memory.read_u8(obj.ptr + 0x134)
if aCodePtr == 0 then
obj.dynamicType = "boxy"
obj.boxy = true
getDetailsForDynamicBoxyObject(obj)
elseif aCodePtr == 1 then
obj.dynamicType = "cylinder"
getDetailsForDynamicCylinderObject(obj)
end
if obj.dynamicType ~= nil then
if obj.hitboxType == "no hitbox" then
obj.hitboxType = "dynamic " .. obj.dynamicType
else
obj.hitboxType = obj.hitboxType .. " + " .. obj.dynamicType
end
end
else
obj.dynamic = false
end
end
local function getAllObjects(racer)
allObjects = {}
local playerPos = racer.posForObjects
local ptrObjArray = memory.read_s32_le(ptrObjStuff + 0x10)
local maxCount = memory.read_u16_le(ptrObjStuff + 0x08)
local count = 0
for id = 0, 255 do
local current = ptrObjArray + (id * 0x1c)
local objPtr = memory.read_u32_le(current + 0x18)
local flags = memory.read_u16_le(current + 0x14)
if objPtr ~= 0 then
count = count + 1
if flags & 0x200 == 0 then
local skip = false
local obj = {
id = id,
pos = read_pos(memory.read_s32_le(current + 0xC)),
flags = flags,
ptr = objPtr,
}
if flags & 0x2000 ~= 0 then
if isCoinAndCollected(objPtr) then
skip = true
end
elseif flags & 0x8000 ~= 0 then
if isPlayerOrGhost(objPtr) then
skip = true
end
elseif flags & 0x5000 == 0 then
skip = true
end
if not skip then
if flags & 0x4000 ~= 0 then
obj.distanceRaw = distanceSqBetween(racer.posForItems, obj.pos)
else
obj.distanceRaw = distanceSqBetween(racer.posForObjects, obj.pos)
end
allObjects[#allObjects + 1] = obj
end
end
if count == maxCount then
break
end
end
end
end
local function getNearestTangibleObject(racer)
local obj = nil
local distanceToNearest = 1e300
local positionOfNearest = {}
for i = 1, #allObjects do
local o = allObjects[i]
if o.distanceRaw < distanceToNearest then
distanceToNearest = o.distanceRaw
obj = o
end
end
if obj == nil then
return nil
end
getObjectDetails(obj)
if obj.hitboxType == "cylindrical" then
local relative = subtractVector(racer.posForObjects, obj.pos)
local distance = relative.x * relative.x + relative.z * relative.z
distanceToNearest = math.sqrt(distance) - racer.radius - obj.radius
elseif obj.hitboxType == "spherical" or obj.hitboxType == "item" then
local distance = math.sqrt(obj.distanceRaw)
distanceToNearest = distance - racer.radius - obj.radius
if obj.hitboxFunc == pendulumHitboxFunc then
local relative = subtractVector(racer.posForObjects, obj.pos)
obj.distanceComponents = {
h = math.floor(distanceToNearest),
v = dotProduct_t(relative, obj.orientation.z) - racer.radius - obj.sizes.z,
}
distanceToNearest = math.max(obj.distanceComponents.h, obj.distanceComponents.v)
end
elseif obj.boxy then
obj.distanceComponents = getBoxyDistances(obj, racer.posForObjects, racer.radius)
obj.innerDistComps = getBoxyDistances(obj, racer.posForObjects, 0)
distanceToNearest = obj.distanceComponents.d
elseif obj.dynamicType == "cylinder" or obj.hitboxType == "cylinder2" then
obj.distanceComponents = getCylinderDistances(obj, racer.posForObjects, racer.radius)
distanceToNearest = obj.distanceComponents.d
else
distanceToNearest = math.sqrt(obj.distanceRaw)
end
obj.distance = math.floor(distanceToNearest)
return obj
end
local function _mkdsinfo_run_data()
local frame = emu.framecount()
local ptrPlayerData = memory.read_s32_le(ptrRacerDataAddr)
myData = getPlayerData(ptrPlayerData, myData)
getCheckpointData(myData)
local ghostExists = memory.read_s32_le(racerCountAddr) >= 2 and isPlayerOrGhost(ptrPlayerData + 0x5a8)
if ghostExists then
ptrPlayerData = 0x5A8 + ptrPlayerData
ghostData = getPlayerData(ptrPlayerData, ghostData)
myData.ghost = ghostData
else
myData.ghost = nil
end
local racer = myData
if watchGhost and ghostExists then
racer = myData.ghost
end
kclData = getCollisionDataForRacer(racer)
getAllObjects(racer)
nearestObjectData = getNearestTangibleObject(racer)
if form.ghostInputs ~= nil then
ensureGhostInputs(form)
end
lastFrame = frame
if giveGhostShrooms then
local itemPtr = memory.read_s32_le(ptrItemInfoAddr)
itemPtr = itemPtr + 0x210
memory.write_u8(itemPtr + 0x4c, 5)
memory.write_u8(itemPtr + 0x54, 3)
end
local ptrRaceTimers = memory.read_s32_le(ptrRaceTimersAddr)
raceData.framesMod8 = memory.read_s32_le(ptrRaceTimers + 0xC)
local ptrMissionInfo = memory.read_s32_le(ptrMissionInfoAddr)
raceData.coinsBeingCollected = memory.read_s16_le(ptrMissionInfo + 0x8)
end
local colView = {}
local iView = {}
local guiScale = client.getwindowsize()
local function drawText(x, y, str, color)
gui.text(x + iView.x, y + iView.y, str, color)
end
local function p(s, l) return padLeft(s, l or 6) end
local function drawInfo(data)
gui.use_surface("client")
local lineHeight = 15
local sectionMargin = 8
local y = 4
local x = 4
local function dt(s)
if s == nil then
print("drawing nil at y " .. y)
end
drawText(x, y, s)
y = y + lineHeight
end
local sectionIsDark = false
local lastSectionBegin = 0
local function endSection()
y = y + sectionMargin / 2 + 1
if sectionIsDark then
gui.drawBox(iView.x, lastSectionBegin + iView.y, iView.x + iView.w, y + iView.y, 0xff000000, 0xff000000)
else
gui.drawBox(iView.x, lastSectionBegin + iView.y, iView.x + iView.w, y + iView.y, 0x60000000, 0x60000000)
end
gui.drawLine(iView.x, y + iView.y, iView.x + iView.w, y + iView.y, "red")
sectionIsDark = not sectionIsDark
lastSectionBegin = y + 1
y = y + sectionMargin / 2 - 1
end
dt("Boost: " .. p(data.boostAll, 2) .. ", MT: " .. p(data.boostMt, 2) .. ", " .. data.mtTime)
dt("Speed: " .. data.speed .. ", real: " .. data.posDelta)
dt("Y Sp : " .. data.verticalVelocity.. ", Max Sp: " .. data.maxSpeed)
local wallClip = data.wallSpeedMult
local losses = "turnLoss = " .. format01(data.turnLoss)
if wallClip ~= 4096 then
losses = losses .. ", wall: " .. format01(data.wallSpeedMult)
end
if data.airSpeed ~= 4096 then
losses = losses .. ", air: " .. format01(data.airSpeed)
end
if data.effectSpeed ~= 4096 then
losses = losses .. ", small: " .. format01(data.effectSpeed)
end
dt(losses)
endSection()
dt(data.air .. " (" .. data.framesInAir .. ")")
dt("X, Z, Y : " .. posVecToStr(data.pos))
dt("Delta : " .. posVecToStr(data.actualPosDelta))
local bm = addVector(subtractVector(data.pos, data.actualPosDelta), data.basePosDelta)
local pod = subtractVector(data.posForObjects, bm)
dt("Collision: " .. posVecToStr(data.collisionPush))
dt("Hitbox : " .. posVecToStr(pod))
endSection()
if showAnglesAsDegrees then
local function atd(a)
local deg = (((a / 0x10000) * 360) + 360) % 360
return math.floor(deg * 1000) / 1000
end
local function ttd(v)
local radians = math.atan(v.x, v.z)
local deg = radians * 360 / (2 * math.pi)
return math.floor(deg * 1000) / 1000
end
dt("Facing angle: " .. atd(data.facingAngle))
dt("Drift angle: " .. atd(data.driftAngle))
dt("Movement angle: " .. ttd(data.movementDirection) .. " (" .. ttd(data.movementTarget) .. ")")
else
dt("Angle: " .. p(data.facingAngle) .. " + " .. p(data.driftAngle) .. " = " .. p(data.facingAngle + data.driftAngle))
dt("Delta: " .. p(data.facingDelta) .. " + " .. p(data.driftDelta) .. " = " .. p(data.facingDelta + data.driftDelta))
local function tta(v)
local radians = math.atan(v.x, v.z)
local dsUnits = math.floor(radians * 0x10000 / (2 * math.pi))
return prettyFloat(v.mag2) .. ", " .. p(dsUnits)
end
dt("Movement: " .. normalVectorToStr(data.movementDirection) .. " (" .. tta(data.movementDirection) .. ")")
dt("Target : " .. normalVectorToStr(data.movementTarget) .. " (" .. tta(data.movementTarget) .. ")")
end
dt("Pitch: " .. data.pitch)
endSection()
local n = data.surfaceNormalVector
local steepness = n.mag2 / (n.y / 0x1000)
steepness = numToStr(steepness)
dt("Surface grip: " .. format01(data.grip) .. ", sp: " .. format01(data.offroadSpeed) .. ",")
dt("normal: " .. normalVectorToStr(n) .. ", steep: " .. steepness)
endSection()
if data.ghost then
local distX = data.pos.x - data.ghost.pos.x
local distZ = data.pos.z - data.ghost.pos.z
local dist = math.sqrt(distX * distX + distZ * distZ)
dt("Distance from ghost (2D): " .. math.floor(dist))
endSection()
end
if form.comparisonPoint ~= nil then
local delta = {
x = data.pos.x - form.comparisonPoint.x,
z = data.pos.z - form.comparisonPoint.z
}
local dist = math.floor(math.sqrt(delta.x * delta.x + delta.z * delta.z))
local angleRad = math.atan(delta.x, delta.z)
dt("Distance travelled: " .. dist)
dt("Angle: " .. math.floor(angleRad * 0x10000 / (2 * math.pi)))
endSection()
end
if nearestObjectData ~= nil then
local obj = nearestObjectData
dt("Distance to nearest object: " .. obj.distance .. " (" .. obj.hitboxType .. ")")
if obj.distanceComponents ~= nil then
if obj.innerDistComps ~= nil then
dt("outer: " .. posVecToStr(obj.distanceComponents))
dt("inner: " .. posVecToStr(obj.innerDistComps))
elseif obj.distanceComponents.v == nil then
dt(posVecToStr(obj.distanceComponents))
else
dt(string.format("%9s, %8s", obj.distanceComponents.h, obj.distanceComponents.v))
end
end
dt(obj.id .. ": " .. obj.type)
endSection()
end
if data.movementAdd1fc.mag3 ~= 0 then
dt("bounce 1: " .. normalVectorToStr(data.movementAdd1fc))
end
if data.movementAdd2f0.mag3 ~= 0 then
dt("bounce 2: " .. normalVectorToStr(data.movementAdd2f0))
end
if data.movementAdd374.mag3 ~= 0 then
dt("bounce 3: " .. normalVectorToStr(data.movementAdd374))
end
dt("fo: " .. v4ToStr(data.f0))
endSection()
if data.checkpoint ~= nil then
if (data.spawnPoint > -1) then dt("Spawn Point: " .. data.spawnPoint) end
dt("Checkpoint number (player) = " .. data.checkpoint .. " (" .. data.keyCheckpoint .. ")")
endSection()
end
if raceData.coinsBeingCollected ~= nil and raceData.coinsBeingCollected > 0 then
y = y + sectionMargin
local coinCheckIn = "in " .. (8 - raceData.framesMod8) .. " frames"
if raceData.framesMod8 == 0 then
coinCheckIn = "this frame"
end
dt("Coin increment " .. coinCheckIn)
end
end
local scale = 0x1000 * collisionScaleFactor
local drawingCenter = {x = 0, y = 0, z = 0}
local perspectiveId = -5
local cameraRotationVector = {}
local cameraRotationMatrix = {}
local satr = 2 * math.pi / 0x10000
local cameraFW = nil
local cameraFH = nil
local drawingQue = {}
local queHistory = {}
local PIXEL = 1
local CIRCLE = 2
local LINE = 3
local POLYGON = 4
local TEXT = 5
local function addToDrawingQue(priority, kind, data)
priority = priority or 0
if drawingQue[priority] == nil then
drawingQue[priority] = {}
end
local que = drawingQue[priority]
data.kind = kind
que[#que + 1] = data
end
local function clearDrawingQue()
drawingQue = {}
local clientWidth = client.screenwidth()
local clientHeight = client.screenheight()
local layout = nds.getscreenlayout()
local gap = nds.getscreengap()
local invert = nds.getscreeninvert()
local gameBaseWidth = nil
local gameBaseHeight = nil
if layout == "Natural" then
layout = "Vertical"
end
if layout == "Vertical" then
gameBaseWidth = 256
gameBaseHeight = 192 * 2 + gap
elseif layout == "Horizontal" then
gameBaseWidth = 256 * 2
gameBaseHeight = 192
else
gameBaseWidth = 256
gameBaseHeight = 192
end
local gameScale = math.min(clientWidth / gameBaseWidth, clientHeight / gameBaseHeight)
colView = {
w = 0.5 * 256 * gameScale,
h = 0.5 * 192 * gameScale,
}
colView.x = (clientWidth - gameBaseWidth * gameScale) * 0.5 + colView.w
colView.y = (clientHeight - gameBaseHeight * gameScale) * 0.5 + colView.h
iView = {
x = (clientWidth - (gameBaseWidth * gameScale)) * 0.5,
y = (clientHeight - (gameBaseHeight * gameScale)) * 0.5,
w = 256 * gameScale,
h = 192 * gameScale,
}
if layout ~= "Horizontal" then
iView.x = 0
iView.y = iView.y + (192 + gap) * gameScale
else
iView.x = iView.x + 256 * gameScale
end
end
clearDrawingQue()
local function atv(v)
return {x = v[1], y = v[2], z = v[3]}
end
local function setPerspective(surfaceNormal)
cameraRotationVector = surfaceNormal
local p = multiplyVector(surfaceNormal, -1)
local mZ = { p.x, p.y, p.z }
local mX = nil
if surfaceNormal.x ~= 0 or surfaceNormal.z ~= 0 then
mX = crossProduct(atv(mZ), { x = 0, y = 0x1000, z = 0 })
UpdateMag(mX)
mX = multiplyVector(mX, 1 / mX.mag3)
else
mX = { x = 0x1000, y = 0, z = 0 }
end
mX = { mX.x, mX.y, mX.z }
local mY = crossProduct(atv(mX), atv(mZ))
mY = { mY.x, mY.y, mY.z }
cameraRotationMatrix = { mX, mY, mZ }
end
setPerspective({x = 0, y = 0x1000, z = 0})
local function scaleAtDistance(point, s)
local v = subtractVector(point, drawingCenter)
local m = math.sqrt(v.x * v.x + v.y * v.y + v.z * v.z)
s = s * (0x1000 / m)
return s / cameraFW * colView.w
end
local function point3Dto2D(vector)
local v = subtractVector(vector, drawingCenter)
local mat = cameraRotationMatrix
local rotated = {
x = (v.x * mat[1][1] + v.y * mat[1][2] + v.z * mat[1][3]) / 0x1000,
y = (v.x * mat[2][1] + v.y * mat[2][2] + v.z * mat[2][3]) / 0x1000,
z = (v.x * mat[3][1] + v.y * mat[3][2] + v.z * mat[3][3]) / 0x1000,
}
if isCameraView then
if rotated.z < 0x1000 then
return { x = 0xffffff, y = 0xffffff }
end
local scaledByDistance = multiplyVector(rotated, 0x1000 / rotated.z)
return {
x = scaledByDistance.x / cameraFW,
y = -scaledByDistance.y / cameraFH,
}
else
return {
x = rotated.x / scale / colView.w,
y = -rotated.y / scale / colView.h,
}
end
end
local function line3Dto2D(v1, v2)
v1 = subtractVector(v1, drawingCenter)
v2 = subtractVector(v2, drawingCenter)
local mat = cameraRotationMatrix
v1 = {
x = (v1.x * mat[1][1] + v1.y * mat[1][2] + v1.z * mat[1][3]) / 0x1000,
y = (v1.x * mat[2][1] + v1.y * mat[2][2] + v1.z * mat[2][3]) / 0x1000,
z = (v1.x * mat[3][1] + v1.y * mat[3][2] + v1.z * mat[3][3]) / 0x1000,
}
v2 = {
x = (v2.x * mat[1][1] + v2.y * mat[1][2] + v2.z * mat[1][3]) / 0x1000,
y = (v2.x * mat[2][1] + v2.y * mat[2][2] + v2.z * mat[2][3]) / 0x1000,
z = (v2.x * mat[3][1] + v2.y * mat[3][2] + v2.z * mat[3][3]) / 0x1000,
}
if isCameraView then
if v1.z < 0x1000 and v2.z < 0x1000 then
return nil
end
local flip = false
if v1.z < 0x1000 then
flip = true
local temp = v1
v1 = v2
v2 = temp
end
local changed = nil
if v2.z < 0x1000 then
local diff = subtractVector(v1, v2)
local percent = (v1.z - 0x1000) / diff.z
if percent > 1 then error("invalid math") end
v2 = subtractVector(v1, multiplyVector(diff, percent))
if v2.z > 0x1001 or v2.z < 0xfff then
print(v2)
error("invalid math")
end
changed = 2
if flip then changed = 1 end
end
if flip then
local temp = v1
v1 = v2
v2 = temp
end
local s1 = multiplyVector(v1, 0x1000 / v1.z)
local s2 = multiplyVector(v2, 0x1000 / v2.z)
local p1 = {
x = s1.x / cameraFW,
y = -s1.y / cameraFH,
}
local p2 = {
x = s2.x / cameraFW,
y = -s2.y / cameraFH,
}
return { p1, p2, changed }
else
return {
{
x = v1.x / scale / colView.w,
y = -v1.y / scale / colView.h,
},
{
x = v2.x / scale / colView.w,
y = -v2.y / scale / colView.h,
},
}
end
end
local function drawQue()
local oldQue = queHistory[1]
queHistory[1] = queHistory[2]
queHistory[2] = drawingQue
local currentQue = drawingQue
if isCameraView and oldQue ~= nil then
currentQue = oldQue
end
local priorities = {}
for k, _ in pairs(currentQue) do
priorities[#priorities + 1] = k
end
table.sort(priorities)
local cw = colView.w
local ch = colView.h
local cx = colView.x
local cy = colView.y
for i = 1, #priorities do
local p = priorities[i]
local que = currentQue[p]
for _, v in pairs(que) do
if v.kind == PIXEL then
gui.drawPixel(v.x * cw + cx, v.y * ch + cy, v.color)
elseif v.kind == CIRCLE then
gui.drawEllipse(v.x * cw + cx - v.radius, v.y * ch + cy - v.radius, v.radius * 2, v.radius * 2, v.fillColor, v.edgeColor)
elseif v.kind == LINE then
gui.drawLine(v.x1 * cw + cx, v.y1 * ch + cy, v.x2 * cw + cx, v.y2 * ch + cy, v.color)
elseif v.kind == POLYGON then
local p = {}
for i = 1, #v.points do
p[i] = {
math.floor(v.points[i][1] * cw + cx + 0.5),
math.floor(v.points[i][2] * ch + cy + 0.5),
}
end
gui.drawPolygon(p, 0, 0, v.line, v.fill)
elseif v.kind == TEXT then
gui.drawText(v.x * cw + cx, v.y + ch + cy, v.text)
end
end
end
end
local function fixLine(x1, y1, x2, y2)
if y1 > 1 and y2 > 1 then
return nil
elseif y1 > 1 then
local cut = (y1 - 1) / (y1 - y2)
y1 = 1
x1 = x2 + ((x1 - x2) * (1 - cut))
if y2 < -1 then
cut = (-1 - y2) / (y1 - y2)
y2 = -1
x2 = x1 + ((x2 - x1) * (1 - cut))
end
elseif y2 > 1 then
local cut = (y2 - 1) / (y2 - y1)
y2 = 1
x2 = x1 + ((x2 - x1) * (1 - cut))
if y1 < -1 then
cut = (-1 - y1) / (y2 - y1)
y1 = -1
x1 = x2 + ((x1 - x2) * (1 - cut))
end
end
return { x1 = x1, y1 = y1, x2 = x2, y2 = y2 }
end
local function pixel(vector, color, priority)
local point = point3Dto2D(vector)
if point.y < 0 or point.y >= maxY then return end
if point.x < 0 or point.x >= maxX then return end
addToDrawingQue(priority, PIXEL, {
x = point.x,
y = point.y,
color = color,
})
end
local function circle(origin, radius, fillColor, priority, edgeColor, rawRadius)
local point = point3Dto2D(origin)
if rawRadius ~= true then
if isCameraView then
radius = scaleAtDistance(origin, radius)
else
radius = radius / scale
radius = radius - 0.5
end
end
if point.y * colView.h + radius < -colView.h or point.y * colView.h - radius > colView.h then
return
end
if point.x * colView.w + radius < -colView.w or point.x * colView.w - radius > colView.w then
return
end
addToDrawingQue(priority, CIRCLE, {
x = point.x,
y = point.y,
radius = radius,
fillColor = fillColor,
edgeColor = edgeColor or fillColor,
})
end
local function line(vector1, vector2, color, priority)
local p = line3Dto2D(vector1, vector2)
if p == nil then
return
end
local points = fixLine(p[1].x, p[1].y, p[2].x, p[2].y)
if points == nil then
return
end
addToDrawingQue(priority, LINE, {
x1 = points.x1, y1 = points.y1,
x2 = points.x2, y2 = points.y2,
color = color or "red"
})
end
local function polygon(verts, lineColor, fillColor, priority)
local edges = {}
for i = 1, #verts do
local e = nil
if i ~= #verts then
e = line3Dto2D(verts[i], verts[i + 1])
else
e = line3Dto2D(verts[i], verts[1])
end
if e ~= nil then
edges[#edges + 1] = e
end
end
if #edges == 0 then
return
end
local points = {}
for i = 1, #edges do
points[#points + 1] = edges[i][1]
if edges[i][3] ~= nil then
points[#points + 1] = edges[i][2]
end
end
local fp = {}
for i = 1, #points do
local nextId = (i % #points) + 1
local line = fixLine(points[i].x, points[i].y, points[nextId].x, points[nextId].y)
if line ~= nil then
if #fp == 0 or line.x1 ~= fp[#fp][1] or line.y1 ~= fp[#fp][2] then
fp[#fp + 1] = { line.x1, line.y1 }
end
if line.x2 ~= fp[1][1] or line.y2 ~= fp[1][2] then
fp[#fp + 1] = { line.x2, line.y2 }
end
end
end
addToDrawingQue(priority, POLYGON, {
points = fp,
line = lineColor,
fill = fillColor
})
end
local function triangle(v1, v2, v3, lineColor, fillColor, priority)
polygon({v1, v2, v3}, lineColor, fillColor, priority)
end
local function text(x, y, t, priority)
if y < 0 or y >= maxY then return end
if x < 0 or x >= maxX then return end
addToDrawingQue(priority, TEXT, {
x = x, y = y, text = t
})
end
local function drawVectorDot(v, color, priority)
if maxX == 256 then
pixel(v, color, 9)
else
circle(v, 1.5, color, priority or 9, color, true)
end
end
local function lineFromVector(base, vector, scale, color, priority)
local scaledVector = multiplyVector(vector, scale / 0x1000)
line(base, addVector(base, scaledVector), color, prority)
end
local function _drawObjectCollision(racer, obj)
local objColor = 0xff40c0e0
local fill = objColor
if obj.multiBox == true or isCameraView then
fill = 0x5040c0e0
end
local skipPolys = false
if obj.hitboxType == "spherical" or obj.hitboxType == "item" then
circle(obj.pos, obj.radius, objColor, -4, fill)
if not isCameraView then
local relative = subtractVector(racer.posForObjects, obj.pos)
local vDist = dotProduct_float(relative, cameraRotationVector)
local totalRadius = obj.radius + racer.radius
if totalRadius > vDist then
local touchHorizDist = math.sqrt(totalRadius * totalRadius - vDist * vDist)
circle(obj.pos, (obj.radius / totalRadius) * touchHorizDist, 0xffffffff, -1, 0)
circle(racer.posForObjects, (racer.radius / totalRadius) * touchHorizDist, 0xffffffff, -1, 0)
end
end
elseif obj.hitboxType == "cylindrical" then
if vectorEqual(cameraRotationVector, {x=0,y=0x1000,z=0}) then
skipPolys = true
circle(obj.pos, obj.radius, objColor, -4, fill)
end
end
if not skipPolys and obj.polygons ~= nil and #obj.polygons ~= 0 then
local wireframeToo = obj.hitboxType == "boxy"
if obj.cylinder2 == true or obj.hitboxType == "cylindrical" then
fill = nil
end
for i = 1, #obj.polygons do
polygon(obj.polygons[i], objColor, fill, -4)
if wireframeToo then
polygon(obj.polygons[i], 0xffffffff, 0, -3)
end
end
if obj.hitboxType == "boxy" then
local racerPolys = getBoxyPolygons(
racer.posForObjects,
obj.orientation,
{ x = racer.radius, y = racer.radius, z = racer.radius }
)
for i = 1, #racerPolys do
polygon(racerPolys[i], 0xffffffff, 0, -2)
end
end
end
end
local function drawObjectCollision(racer)
if drawAllObjects then
for i = 1, #allObjects do
getObjectDetails(allObjects[i])
_drawObjectCollision(racer, allObjects[i])
end
else
local obj = nearestObjectData
if obj ~= nil then
_drawObjectCollision(racer, obj)
end
end
end
local function drawKcl(graphical)
clearDrawingQue()
local racer = myData
local other = myData.ghost
if watchingGhost then
racer = myData.ghost
other = myData
end
if not form.kclFreezeCamera then
drawingCenter = racer.posForObjects
end
if isCameraView then
racer = myData
other = myData.ghost
local cameraPtr = memory.read_u32_le(ptrCameraAddr)
local camPos = read_pos(cameraPtr + 0x24)
local camOffset = read_pos(cameraPtr + 0x54)
drawingCenter = camPos
local camTargetPos = read_pos(cameraPtr + 0x18)
local direction = subtractVector(camPos, camTargetPos)
direction = normalizeVector_float(direction)
setPerspective(direction)
local cameraFoVV = memory.read_u16_le(cameraPtr + 0x60) * satr
local camAspectRatio = memory.read_s32_le(cameraPtr + 0x6C) / 0x1000
cameraFW = math.tan(cameraFoVV * camAspectRatio) * 0xec0
cameraFH = math.tan(cameraFoVV) * 0x1000
end
local playerColor = 0xff0000ff
if isCameraView then
playerColor = 0x500000ff
end
if scale > 60 then
circle(myData.posForObjects, myData.radius, playerColor, -3)
lineFromVector(myData.posForObjects, myData.movementDirection, myData.radius, "white", 5)
if other ~= nil then
circle(myData.ghost.posForObjects, myData.ghost.radius, 0x48ff5080, -1)
lineFromVector(myData.ghost.posForObjects, myData.ghost.movementDirection, myData.ghost.radius, 0xcccccccc, 5)
end
end
if scale <= 1 then
circle(racer.posForObjects, 1, 0xffffffff, -2)
circle(racer.posForObjects, 1, 0xffff0000, -2)
elseif scale < 200 then
circle(racer.posForObjects, 200, 0xffff0000, -2)
circle(racer.preMovementPosForObjects, 200, 0xffa04000, -2)
end
local data = kclData
local touchList = {}
local nearestWall = nil
local nearestFloor = nil
for i = 1, #data do
local d = data[i]
local tri = d.triangle
if tri.isActuallyLine or not d.touch.canTouch then
goto continue
end
if d.touch.touching then
if graphical then
local color = 0x30ff8888
if d.touch.push then
if d.controlsSlope then
color = 0x4088ff88
lineFromVector(racer.posForObjects, tri.surfaceNormal, racer.radius, 0xff00ff00, 5)
elseif d.isWall then
color = 0x20ffff22
else
color = 0x50ffffff
end
else
lineFromVector(racer.posForObjects, tri.surfaceNormal, racer.radius, 0xffff0000, 5)
end
polygon(tri.vertex, 0, color, -5)
end
touchList[#touchList + 1] = d
end
if graphical then
local color, priority = "white", 0
if tri.isWall then
if d.touch.touching and d.touch.push then
color, priority = "yellow", 2
else
color, priority = "orange", 1
end
end
if color ~= nil then
polygon(tri.vertex, color, 0, priority)
end
drawVectorDot(tri.vertex[1], "red", 9)
drawVectorDot(tri.vertex[2], "red", 9)
drawVectorDot(tri.vertex[3], "red", 9)
end
if tri.isWall and not d.touch.push and (nearestWall == nil or d.touch.distance < data[nearestWall].touch.distance) then
nearestWall = i
end
if tri.isFloor and not d.touch.push and (nearestFloor == nil or d.touch.distance < data[nearestFloor].touch.distance) then
nearestFloor = i
end
::continue::
end
local y = -19
if nearestWall ~= nil then
drawText(2, y, "closest wall: " .. numToStr(data[nearestWall].touch.distance))
y = y - 18
end
if nearestFloor ~= nil then
drawText(2, y, "closest floor: " .. numToStr(data[nearestFloor].touch.distance))
y = y - 18
end
y = y - 3
for i = 1, #touchList do
local d = touchList[i]
local tri = d.triangle
local stype = ""
if tri.isWall then stype = stype .. "w" end
if tri.isFloor then stype = stype .. "f" end
if stype == "" then stype = "?" end
local str = tri.id .. ": " .. stype .. ", "
if d.touch.push == false then
if d.touch.wasBehind then
str = str .. "n (behind)"
else
str = str .. "n " .. numToStr(d.touch.outwardMovement)
end
else
str = str .. "p " .. numToStr(d.touch.pushOutDistance)
end
drawText(2, y, str)
y = y - 18
end
if graphical then
gui.use_surface("client")
drawObjectCollision(racer)
if not isCameraView then
gui.drawRectangle(colView.x - colView.w, colView.y - colView.h, colView.w * 2, colView.h * 2, "black", "black")
end
drawQue()
end
end
local function _mkdsinfo_run_draw(inRace)
if not client.ispaused() and not drawWhileUnpaused then
if client.isseeking() then
emu.yield()
if not client.ispaused() then
return
end
else
return
end
end
if inRace then
local data = myData
if watchingGhost then data = ghostData end
drawInfo(data)
drawKcl(form.drawCollision)
else
gui.clearGraphics("client")
gui.clearGraphics("emucore")
drawText(10, 10, "Not in a race.")
end
end
local function useInputsClick()
if not inRace() then
print("You aren't in a race.")
return
end
if not tastudio.engaged() then
return
end
if form.ghostInputs == nil then
form.ghostInputs = memory.read_bytes_as_array(memory.read_s32_le(ptrPlayerInputsAddr), 0xdce)
form.firstGhostInputFrame = emu.framecount() - memory.read_s32_le(memory.read_s32_le(ptrRaceTimersAddr) + 4) + 121
form.ghostLapTimes = memory.read_bytes_as_array(memory.read_s32_le(ptrSomethingPlayerAddr) + 0x20, 0x4 * 5)
setGhostInputs(form)
forms.settext(form.ghostInputHackButton, "input hack active")
else
form.ghostInputs = nil
forms.settext(form.ghostInputHackButton, "Copy from player")
end
end
local function watchGhostClick()
if myData.ghost ~= nil then
watchingGhost = not watchingGhost
else
watchingGhost = false
end
local s = "player"
if watchingGhost then s = "ghost" end
forms.settext(form.watchGhost, s)
getCollisionDataForRacer((watchingGhost and myData.ghost) or myData)
if form.drawCollision then
redraw()
else
gui.cleartext()
_mkdsinfo_run_draw(true)
end
end
local function setComparisonPointClick()
if form.comparisonPoint == nil then
local pos = myData.pos
if watchingGhost then pos = ghostData.pos end
form.comparisonPoint = { x = pos.x, z = pos.z }
forms.settext(form.setComparisonPoint, "Clear comparison point")
else
form.comparisonPoint = nil
forms.settext(form.setComparisonPoint, "Set comparison point")
end
end
local function loadGhostClick()
local fileName = forms.openfile(nil,nil,"TAStudio Macros (*.bk2m)|*.bk2m|All Files (*.*)|*.*")
local inputFile = assert(io.open(fileName, "rb"))
local inputHeader = inputFile:read("*line")
local names = {}
local index = 0
local nextIndex = string.find(inputHeader, "|", index)
while nextIndex ~= nil do
names[#names + 1] = string.sub(inputHeader, index, nextIndex - 1)
index = nextIndex + 1
nextIndex = string.find(inputHeader, "|", index)
if #names > 100 then
error("unable to parse header")
end
end
nextIndex = string.len(inputHeader)
names[#names + 1] = string.sub(inputHeader, index, nextIndex - 1)
local line = inputFile:read("*line")
while string.sub(line, 1, 1) ~= "|" do
line = inputFile:read("*line")
end
local inputs = {}
while line ~= nil and string.sub(line, 1, 1) == "|" do
local id = 1
index = 0
local nextComma = string.find(line, ",", index)
while nextComma ~= nil do
id = id + 1
index = nextComma + 1
nextComma = string.find(line, ",", index)
if id > 100 then
error("unable to parse input")
end
end
local buttons = 0
while id <= #names do
if string.sub(line, index, index) ~= "." then
if names[id] == "A" then buttons = buttons | 0x01
elseif names[id] == "B" then buttons = buttons | 0x02
elseif names[id] == "R" then buttons = buttons | 0x04
elseif names[id] == "X" or names[id] == "L" then buttons = buttons | 0x08
elseif names[id] == "Right" then buttons = buttons | 0x10
elseif names[id] == "Left" then buttons = buttons | 0x20
elseif names[id] == "Up" then buttons = buttons | 0x40
elseif names[id] == "Down" then buttons = buttons | 0x80
end
end
id = id + 1
index = index + 1
end
inputs[#inputs + 1] = buttons
line = inputFile:read("*line")
end
inputFile:close()
local bytes = { 0, 0, 0, 0 }
local count = 1
local lastInput = inputs[1]
for i = 2, #inputs do
if inputs[i] ~= lastInput or count == 255 then
bytes[#bytes + 1] = lastInput
bytes[#bytes + 1] = count
lastInput = inputs[i]
count = 1
if #bytes == 0xdcc then
print("Maximum ghost recording length reached.")
break
end
else
count = count + 1
end
end
while #bytes < 0xdcc do bytes[#bytes + 1] = 0 end
form.ghostInputs = bytes
form.firstGhostInputFrame = emu.framecount() - memory.read_s32_le(memory.read_s32_le(ptrRaceTimersAddr) + 4) + 121
form.ghostLapTimes = {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}
setGhostInputs(form)
forms.settext(form.ghostInputHackButton, "input hack active")
end
local function saveCurrentInputsClick()
local firstInputFrame = emu.framecount() - memory.read_s32_le(memory.read_s32_le(ptrRaceTimersAddr) + 4) + 121
print("BizHawk doesn't give Lua a save file dialog.")
print("You can manually save your current inputs as a .bk2m:")
print("1) Select frames " .. firstInputFrame .. " to " .. emu.framecount() .. " (or however many frames you want to include).")
print("2) File -> Save Selection to Macro")
end
local function branchLoadHandler()
if form.firstStateWithGhost ~= 0 then
form.firstStateWithGhost = 0
end
if form.ghostInputs ~= nil then
local currentFrame = emu.framecount()
setGhostInputs(form)
if emu.framecount() ~= currentFrame then
print("Movie rewind: ghost inputs changed after branch load.")
print("Stop ghost input hacker to load branch without rewind.")
end
end
end
local function drawUnpausedClick()
drawWhileUnpaused = not drawWhileUnpaused
if drawWhileUnpaused then
forms.settext(form.drawUnpausedButton, "Draw while unpaused: ON")
else
forms.settext(form.drawUnpausedButton, "Draw while unpaused: OFF")
end
end
local function kclClick()
form.drawCollision = not form.drawCollision
redraw()
end
local function zoomInClick()
scale = scale * 0.8
redraw()
end
local function zoomOutClick()
scale = scale / 0.8
redraw()
end
local function focuseHereClick()
form.kclFreezeCamera = not form.kclFreezeCamera
if form.kclFreezeCamera then
forms.settext(form.kclFreezeCameraButton, "unfreeze")
else
forms.settext(form.kclFreezeCameraButton, "freeze")
redraw()
end
end
local function _changePerspective()
local id = perspectiveId
if id < 0 then
local presets = {
{ "camera", nil },
{ "top down", { 0, 0x1000, 0 }},
{ "north-south", { 0, 0, -0x1000 }},
{ "south-north", { 0, 0, 0x1000 }},
{ "east-west", { 0x1000, 0, 0 }},
{ "west-east", { -0x1000, 0, 0 }},
}
if id == -6 then
local cameraPtr = memory.read_u32_le(ptrCameraAddr)
local direction = read_pos(cameraPtr + 0x15c)
setPerspective(multiplyVector(direction, -1))
isCameraView = true
else
setPerspective(atv(presets[id + 7][2]))
isCameraView = false
end
forms.settext(form.perspectiveLabel, presets[id + 7][1])
else
setPerspective(triangles[id].surfaceNormal)
isCameraView = false
forms.settext(form.perspectiveLabel, "triangle " .. id)
end
redraw()
end
local function changePerspectiveLeft()
local id = perspectiveId
id = id - 1
if id < -6 then
id = 9999
end
if id >= 0 then
local nextId = 0
for i = 1, #kclData do
local ti = kclData[i].triangle.id
if ti < id and ti > nextId then
if not vectorEqual(cameraRotationVector, kclData[i].triangle.surfaceNormal) then
nextId = ti
end
end
end
if nextId == 0 then
id = -1
else
id = nextId
end
end
perspectiveId = id
_changePerspective()
end
local function changePerspectiveRight()
local id = perspectiveId
id = id + 1
if id >= 0 then
local nextId = 9999
for i = 1, #kclData do
local ti = kclData[i].triangle.id
if ti >= id and ti < nextId then
if not vectorEqual(cameraRotationVector, kclData[i].triangle.surfaceNormal) then
nextId = ti
end
end
end
if nextId == 9999 then
id = -6
else
id = nextId
end
end
perspectiveId = id
_changePerspective()
end
local bizHawkEventIds = {}
local function _mkdsinfo_setup()
if emu.framecount() < 400 then
print("Looks like some data might not be loaded yet. Re-start this Lua script at a later frame.")
shouldExit = true
elseif showBizHawkDumbnessWarning then
print("BizHawk's Lua API is horrible. In order to work around bugs and other limitations, do not stop this script through BizHawk. Instead, close the window it creates and it will stop itself.")
end
form = {}
form.firstStateWithGhost = 0
form.comparisonPoint = nil
form.handle = forms.newform(305, 153, "MKDS Info Thingy", function()
if my_script_id == script_id then
shouldExit = true
redraw()
end
end)
local buttonMargin = 5
local labelMargin = 2
local y = 10
local temp = forms.label(form.handle, "Watching: ", 10, y + 4)
forms.setproperty(temp, "AutoSize", true)
form.watchGhost = forms.button(
form.handle, "player", watchGhostClick,
forms.getproperty(temp, "Right") + labelMargin, y,
50, 23
)
form.setComparisonPoint = forms.button(
form.handle, "Set comparison point", setComparisonPointClick,
forms.getproperty(form.watchGhost, "Right") + buttonMargin, y,
100, 23
)
y = 38
temp = forms.label(form.handle, "Ghost: ", 10, y + 4)
forms.setproperty(temp, "AutoSize", true)
temp = forms.button(
form.handle, "Copy from player", useInputsClick,
forms.getproperty(temp, "Right") + buttonMargin, y,
100, 23
)
form.ghostInputHackButton = temp
temp = forms.button(
form.handle, "Load bk2m", loadGhostClick,
forms.getproperty(temp, "Right") + labelMargin, y,
70, 23
)
temp = forms.button(
form.handle, "Save bk2m", saveCurrentInputsClick,
forms.getproperty(temp, "Right") + labelMargin, y,
70, 23
)
y = y + 28
form.drawUnpausedButton = forms.button(
form.handle, "Draw while unpaused: ON", drawUnpausedClick,
10, y, 150, 23
)
y = y + 28
form.kclButton = forms.button(
form.handle, "View collision", kclClick,
10, y, 88, 23
)
form.zoomInButton = forms.button(
form.handle, "+", zoomInClick,
forms.getproperty(form.kclButton, "Right") + labelMargin, y,
23, 23
)
form.zoomOutButton = forms.button(
form.handle, "-", zoomOutClick,
forms.getproperty(form.zoomInButton, "Right") + labelMargin, y,
23, 23
)
form.kclFreezeCameraButton = forms.button(
form.handle, "freeze", focuseHereClick,
forms.getproperty(form.zoomOutButton, "Right") + labelMargin*2, y,
70, 23
)
y = y + 28
temp = forms.label(form.handle, "Perspective:", 10, y + 4)
forms.setproperty(temp, "AutoSize", true)
form.perspectiveLeft = forms.button(
form.handle, "<", changePerspectiveLeft,
forms.getproperty(temp, "Right") + labelMargin*2, y,
18, 23
)
form.perspectiveLabel = forms.label(
form.handle, "top down",
forms.getproperty(form.perspectiveLeft, "Right") + labelMargin*2, y + 4
)
forms.setproperty(form.perspectiveLabel, "AutoSize", true)
form.perspectiveRight = forms.button(
form.handle, ">", changePerspectiveRight,
forms.getproperty(form.perspectiveLabel, "Right") + 22, y,
18, 23
)
end
local function _mkdsinfo_close()
client.SetClientExtraPadding(0, 0, 0, 0)
forms.destroy(form.handle)
for i = 1, #bizHawkEventIds do
event.unregisterbyid(bizHawkEventIds[i])
end
end
memory.usememorydomain("ARM9 System Bus")
local function main()
_mkdsinfo_setup()
while (not shouldExit) or (redrawSeek ~= nil) do
if not shouldExit then
if inRace() then
_mkdsinfo_run_data()
_mkdsinfo_run_draw(true)
else
_mkdsinfo_run_draw(false)
end
end
local frame = emu.framecount()
local stopSeeking = false
if redrawSeek ~= nil and redrawSeek == frame then
stopSeeking = true
elseif client.ispaused() then
stopSeeking = true
end
if stopSeeking then
client.pause()
redrawSeek = nil
if not shouldExit then
emu.frameadvance()
else
end
else
emu.frameadvance()
end
end
_mkdsinfo_close()
gui.clearGraphics("client")
gui.clearGraphics("emucore")
gui.cleartext()
end
gui.clearGraphics("client")
gui.clearGraphics("emucore")
gui.use_surface("emucore")
gui.cleartext()
if tastudio.engaged() then
bizHawkEventIds[#bizHawkEventIds + 1] = tastudio.onbranchload(branchLoadHandler)
end
main()