Skip to content

Instantly share code, notes, and snippets.

@rdococ
Last active November 22, 2023 02:30
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save rdococ/433a3c6461b187403a2f4f1f3f5ec0cd to your computer and use it in GitHub Desktop.
Save rdococ/433a3c6461b187403a2f4f1f3f5ec0cd to your computer and use it in GitHub Desktop.
Tachyons in TPT
--[[Tachyons v0.27
= How it works =
1. Track all the tachyons and negatachyons in the current frame.
2. If any tachyons are created, rewind the game, reconstructing the tachyon on every frame to determine its time-reversed trajectory.
3. Once all tachyons are destroyed, let the simulation run forwards again, constructing negatachyons when and where the tachyons were destroyed.
4. Once the game catches back up, if any negatachyons can't trace their trajectory to a tachyon, rewind to erase them from the timeline.
= Notes =
- Tachyon/negatachyon pairs are detected leniently in an attempt to prevent false positives. When a negatachyon dies, it must be within line of sight of a tachyon created at the exact same position as itself.
- If you set up a machine that always creates a tachyon only if it doesn't detect a tachyon, the game will rewind endlessly. This is a time paradox.
= Fields =
LIFE: Tracks a tachyon's age.
TMP2: A (hopefully) unique ID for every tachyon. Used to destroy time paradoxed negatachyons.
TMP3: Flag. Indicates that a newly created tachyon should be removed silently (like if rewinding) or that a tachyon/negatachyon pair has successfully cancelled out.
TMP4: A non-unique ID based on the tachyon's starting position.
]]
local elem, sim = elements, sim
local random, floor, cos, sin, atan2, sqrt, min, max, abs = math.random, math.floor, math.cos, math.sin, math.atan2, math.sqrt, math.min, math.max, math.abs
local pi = math.pi
local function round(x)
return floor(x + 0.5)
end
local function alloc(...)
local els = {}
for _, id in ipairs({...}) do
table.insert(els, elements.allocate("tachyons", id))
end
return unpack(els)
end
local function inBounds(x, y)
return max(min(x, sim.XRES - 5), 4), max(min(y, sim.YRES - 5), 4)
end
local NTCH, TACH = alloc("ntch", "tach")
local TIME_TOLERANCE = 1
local TACH_SPEED, TACH_TOP_SPEED, TACH_LIFETIME = 4, 4, 190
local speedFactor = (TACH_TOP_SPEED/TACH_SPEED)^(1/TACH_LIFETIME)
-- 'Doomed' negatachyons have been grandfather paradoxed and must be erased from the timeline.
local doomedTachyons, doomedCount = {}, 0
-- On every frame, all tachyons and negatachyons add themselves to these lists for tracking purposes.
local tachyons, negatachyons = {}, {}
local newTachyons = {} -- Tachyons whose life is below TIME_TOLERANCE
local rewinding = false
local rewindTick = 0
-- Tachyons that die during the rewinding phase are tracked to reconstruct the negatachyons at the right times.
local tachyonDeaths = {}
local function doomTachyon(tach)
local uuid = sim.partProperty(tach, sim.FIELD_TMP2)
if doomedTachyons[uuid] then return end
doomedTachyons[uuid] = tach
doomedCount = doomedCount + 1
end
local function pickAngle(self)
local angle = random(0, 7) / 4 * pi
local speed = 4
sim.partProperty(self, sim.FIELD_VX, cos(angle) * TACH_SPEED)
sim.partProperty(self, sim.FIELD_VY, sin(angle) * TACH_SPEED)
end
local function accelerate(self)
local vx, vy = sim.partProperty(self, sim.FIELD_VX), sim.partProperty(self, sim.FIELD_VY)
local speed = sqrt(vx * vx + vy * vy)
speed = speed * speedFactor
local angle = atan2(vy, vx)
sim.partProperty(self, sim.FIELD_VX, cos(angle) * speed)
sim.partProperty(self, sim.FIELD_VY, sin(angle) * speed)
end
local function decelerate(self)
local vx, vy = sim.partProperty(self, sim.FIELD_VX), sim.partProperty(self, sim.FIELD_VY)
local speed = sqrt(vx * vx + vy * vy)
speed = speed / speedFactor
local angle = atan2(vy, vx)
sim.partProperty(self, sim.FIELD_VX, cos(angle) * speed)
sim.partProperty(self, sim.FIELD_VY, sin(angle) * speed)
end
local function cancelNegatachyons(self, x, y)
for _, other in ipairs(negatachyons) do
if sim.partProperty(other, sim.FIELD_TYPE) == NTCH then
local otherX, otherY = sim.partProperty(other, sim.FIELD_X), sim.partProperty(other, sim.FIELD_Y)
local vx, vy = sim.partProperty(other, sim.FIELD_VX), sim.partProperty(other, sim.FIELD_VY)
local dx, dy = otherX - x, otherY - y
local speed = max(sqrt(vx * vx + vy * vy), TACH_SPEED)
local dist = sqrt(dx * dx + dy * dy)
local dirx, diry = dx / dist, dy / dist
local originId = sim.partProperty(self, sim.FIELD_TMP4)
local otherOId = sim.partProperty(other, sim.FIELD_TMP4)
local originX, originY = originId % sim.XRES, floor(originId / sim.XRES)
local otherOX, otherOY = otherOId % sim.XRES, floor(otherOId / sim.XRES)
if originId == otherOId then
local success = true
if dist > 0 then
for i = 0, dist, speed do
local obstacle = sim.pmap(otherX - dirx * dist, otherY - diry * dist)
if obstacle and sim.can_move(NTCH, obstacle) ~= 2 then
success = false
break
end
end
end
if success then
local otherX, otherY = sim.partProperty(other, sim.FIELD_X), sim.partProperty(other, sim.FIELD_Y)
sim.partProperty(other, sim.FIELD_TMP3, 1)
sim.partKill(other)
sim.partProperty(self, sim.FIELD_TMP3, 1)
sim.partKill(self)
return true
end
end
end
end
end
local function decay(x, y)
for i = 1, 8 do
sim.partProperty(sim.partCreate(-3, x, y, elem.DEFAULT_PT_PHOT), sim.FIELD_CTYPE, 32767)
end
end
local function graphicsFunc(self, r, g, b)
return 1, 0x00010001, 255, r, g, b, 255, r, g, b
end
elem.element(TACH, elem.element(elem.DEFAULT_PT_NEUT))
elem.property(TACH, "Name", "TACH")
elem.property(TACH, "Description", "Tachyons. Reverse the flow of time, decays into photons. Use sparingly.")
elem.property(TACH, "Color", 0x4400FF)
elem.property(TACH, "Collision", -0.99)
elem.property(TACH, "Properties", elem.TYPE_ENERGY)
elem.property(TACH, "Create", function (self, x, y, type, v)
sim.partProperty(self, sim.FIELD_TEMP, 1273.15)
if v and v ~= 0 then return end
if rewinding then
sim.partProperty(self, sim.FIELD_TMP3, 1)
sim.partKill(self)
return
end
sim.partProperty(self, sim.FIELD_TMP4, round(y) * sim.XRES + round(x))
pickAngle(self)
table.insert(tachyons, self)
table.insert(newTachyons, self)
end)
elem.property(TACH, "Update", function (self, x, y)
local life = sim.partProperty(self, sim.FIELD_LIFE)
if life > TACH_LIFETIME then
sim.partKill(self)
return
end
sim.partProperty(self, sim.FIELD_LIFE, life + 1)
accelerate(self)
table.insert(tachyons, self)
if life <= 10 then
table.insert(newTachyons, self)
end
end)
elem.property(TACH, "Graphics", graphicsFunc)
elem.property(TACH, "ChangeType", function (self, x, y, type, new)
if new ~= 0 then return end
if sim.partProperty(self, sim.FIELD_TMP3) == 1 then return end
if rewindTick > 0 then
local props = {}
for _, prop in ipairs {sim.FIELD_X, sim.FIELD_Y, sim.FIELD_VX, sim.FIELD_VY, sim.FIELD_TEMP, sim.FIELD_LIFE, sim.FIELD_TMP4} do
props[prop] = sim.partProperty(self, prop)
end
-- props[sim.FIELD_X], props[sim.FIELD_Y] = inBounds(props[sim.FIELD_X], props[sim.FIELD_Y])
table.insert(tachyonDeaths[rewindTick], props)
end
end)
elem.element(NTCH, elem.element(TACH))
elem.property(NTCH, "Name", "NTCH")
elem.property(NTCH, "Description", "Negative tachyons, tachyons travelling forward in time.")
elem.property(NTCH, "MenuVisible", 0)
elem.property(NTCH, "Properties", elem.TYPE_ENERGY)
elem.property(NTCH, "Create", function (self, x, y, type, v)
local uuid = random(-2147483647, 2147483647)
if uuid <= 0 then uuid = uuid - 1 end
sim.partProperty(self, sim.FIELD_TMP2, uuid)
if not v or v == 0 then
sim.partProperty(self, sim.FIELD_TMP3, 1)
sim.partProperty(self, sim.FIELD_LIFE, TACH_LIFETIME)
pickAngle(self)
end
end)
elem.property(NTCH, "Update", function (self, x, y)
local life = sim.partProperty(self, sim.FIELD_LIFE)
if life <= TIME_TOLERANCE then
sim.partProperty(self, sim.FIELD_VX, 0)
sim.partProperty(self, sim.FIELD_VY, 0)
if sim.partProperty(self, sim.FIELD_TMP3) == 1 then
decay(x, y)
sim.partKill(self)
return
end
end
if life <= 0 then
for part in sim.parts() do
if sim.partProperty(part, sim.FIELD_TYPE) == NTCH then
local partLife = sim.partProperty(part, sim.FIELD_LIFE)
if partLife <= TIME_TOLERANCE then
sim.partKill(part)
end
end
end
sim.partKill(self)
return
end
if life > TIME_TOLERANCE then
decelerate(self)
end
sim.partProperty(self, sim.FIELD_LIFE, life - 1)
table.insert(negatachyons, self)
end)
elem.property(NTCH, "Graphics", graphicsFunc)
elem.property(NTCH, "ChangeType", function (self, x, y, type, new)
-- A negatachyon was destroyed. If it was destroyed too soon, we must erase it from the timeline.
if new ~= 0 then return end
if sim.partProperty(self, sim.FIELD_TMP3) == 1 then return end
if rewinding then return end
-- This is an (attempted) workaround for weird behaviour when particles disappear off the simulation edges.
x, y = inBounds(x, y)
if sim.partProperty(self, sim.FIELD_LIFE) > TIME_TOLERANCE then
-- I think this part works around a TPT bug where particles are destroyed instead of bouncing...?
local part = sim.partCreate(-3, x, y, NTCH, 1)
local vx, vy = -sim.partProperty(self, sim.FIELD_VX), -sim.partProperty(self, sim.FIELD_VY)
sim.partProperty(part, sim.FIELD_VX, vx)
sim.partProperty(part, sim.FIELD_VY, vy)
local life = sim.partProperty(self, sim.FIELD_LIFE)
if part > self then life = life + 1 end
sim.partProperty(part, sim.FIELD_LIFE, life)
local uuid = sim.partProperty(self, sim.FIELD_TMP2)
sim.partProperty(part, sim.FIELD_TMP2, uuid)
sim.partProperty(part, sim.FIELD_TMP4, sim.partProperty(self, sim.FIELD_TMP4))
return
end
doomTachyon(self)
end)
-- Time is playing forwards. Create negatachyons at each point where tachyons were destroyed while rewinding
local function unwind()
if rewindTick <= 0 then return end
for _, data in ipairs(tachyonDeaths[rewindTick]) do
local x, y, vx, vy = data[sim.FIELD_X], data[sim.FIELD_Y], -data[sim.FIELD_VX], -data[sim.FIELD_VY]
x, y = x + vx, y + vy
if sim.edgeMode() == 0 then
x, y = min(max(x, 4), sim.XRES - 5), min(max(y, 4), sim.YRES - 5)
end
local part = sim.partCreate(-3, x, y, NTCH, 1)
sim.partProperty(part, sim.FIELD_VX, vx)
sim.partProperty(part, sim.FIELD_VY, vy)
sim.partProperty(part, sim.FIELD_TEMP, data[sim.FIELD_TEMP])
sim.partProperty(part, sim.FIELD_LIFE, data[sim.FIELD_LIFE] + TIME_TOLERANCE)
sim.partProperty(part, sim.FIELD_TMP4, data[sim.FIELD_TMP4])
decay(data[sim.FIELD_X], data[sim.FIELD_Y])
end
rewindTick = rewindTick - 1
end
event.register(event.beforesim, function ()
if not rewinding then
sim.takeSnapshot()
end
end)
event.register(event.aftersim, function ()
-- Adjacent tachyon/negatachyon pairs are assumed to be the same particle and removed
-- If there is no time paradox, eventually *all* tachyons will be cancelled out this way
if sim.elementCount(NTCH) > 0 and sim.elementCount(TACH) > 0 then
for _, part in ipairs(newTachyons) do
if part and sim.partExists(part) and sim.partProperty(part, sim.FIELD_TYPE) == TACH and sim.partProperty(part, sim.FIELD_TMP3) == 0 then
cancelNegatachyons(part, sim.partProperty(part, sim.FIELD_X), sim.partProperty(part, sim.FIELD_Y))
end
end
end
tachyons, negatachyons = {}, {}
newTachyons = {}
rewinding = sim.elementCount(TACH) > 0 or doomedCount > 0
if not rewinding then
unwind()
return
end
-- Rewinding: collect all existing tachyons so we can reinstate them "later"
local props = {sim.FIELD_X, sim.FIELD_Y, sim.FIELD_VX, sim.FIELD_VY, sim.FIELD_LIFE, sim.FIELD_TMP4}
local tachyonData = {}
for part in sim.parts() do
if sim.partProperty(part, sim.FIELD_TYPE) == TACH then
local data = {}
for _, prop in ipairs(props) do
data[prop] = sim.partProperty(part, prop)
end
tachyonData[part] = data
end
end
if not sim.historyRestore() then
-- If we can't rewind any further, kill all present tachyons and unwind
for part, _ in pairs(tachyonData) do
sim.partKill(part)
end
doomedTachyons, doomedCount = {}, 0
unwind()
return
end
rewindTick = rewindTick + 1
tachyonDeaths[rewindTick] = {}
-- Erase negatachyons from the timeline if their UUIDs correspond to that of a grandfather paradoxed tachyon
local clearedTachyons = {}
for uuid, part in pairs(doomedTachyons) do
if not (part and sim.partExists(part) and sim.partProperty(part, sim.FIELD_TYPE) == NTCH and sim.partProperty(part, sim.FIELD_TMP2) == uuid) then
for candidate in sim.parts() do
if sim.partProperty(candidate, sim.FIELD_TYPE) == NTCH and sim.partProperty(candidate, sim.FIELD_TMP2) == uuid then
part = candidate
doomedTachyons[uuid] = part
break
end
end
end
if part and sim.partExists(part) and sim.partProperty(part, sim.FIELD_TYPE) == NTCH and sim.partProperty(part, sim.FIELD_TMP2) == uuid then
sim.partProperty(part, sim.FIELD_TMP3, 1)
sim.partKill(part)
else
clearedTachyons[uuid] = true
end
end
for uuid, _ in pairs(clearedTachyons) do
doomedTachyons[uuid] = nil
doomedCount = doomedCount - 1
end
-- Replace all the tachyons now that we've rewound
for _, data in pairs(tachyonData) do
local part = sim.partCreate(-3, data[sim.FIELD_X], data[sim.FIELD_Y], TACH, 1)
for prop, value in pairs(data) do
sim.partProperty(part, prop, value)
end
end
end)
local function vhsLine(y, alpha)
local x = 0
local streak = random(1, 2) == 1
while x < graphics.WIDTH do
local length = streak and random(1, 10) or random(1, 50)
if streak then
graphics.drawLine(x, y, x + length, y, 255, 255, 255, alpha * 128)
end
streak = not streak
x = x + length
end
end
local function vhsBand(startY, endY)
for y = startY, endY do
local dist = (y - startY) / (endY - startY)
local y = y % graphics.HEIGHT
vhsLine(y, min((1 - abs(1 - dist * 2) * 2) * 5, 1))
end
end
local vhsTick = 0
event.register(event.tick, function ()
if rewinding then
graphics.drawText(16, 30, "REWIND <<")
local offset = vhsTick
local h = graphics.HEIGHT
vhsBand(h * 1.75/3 + offset, h * 2/3 + offset)
vhsBand(h * 2.15/3 + offset, h * 2.2/3 + offset)
vhsTick = vhsTick + 1
else
vhsTick = 0
end
end)
local w, h = 255, 110
local info = Window:new(graphics.WIDTH / 2 - w / 2, graphics.HEIGHT / 2 - h / 2, w, h)
local label = Label:new(27.5, 25, 200, 44, [[
For tachyons to work you must:
1. Go to Simulation Options.
2. Scroll down and click "Open Data Folder".
3. Open powder.pref.
4. Find the "UndoHistoryLimit" and set it to 200.
Actual UNDO functionality will no longer work.]])
local ok = Button:new(0, h - 15, w, 15, "OK")
ok:action(function() interface.closeWindow(info) end)
info:addComponent(label)
info:addComponent(ok)
interface.showWindow(info)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment