From b23555e152255ea70a92d528386a69dfea1642db Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Tue, 25 Jun 2024 12:49:17 -0700 Subject: [PATCH 01/10] update docs and architecture and add to control panel --- docs/timestream.rst | 159 +++-- internal/control-panel/registry.lua | 1 + timestream.lua | 931 ++++++++++++++++------------ 3 files changed, 645 insertions(+), 446 deletions(-) diff --git a/docs/timestream.rst b/docs/timestream.rst index aa15ea5ff0..1ac0e318ec 100644 --- a/docs/timestream.rst +++ b/docs/timestream.rst @@ -3,52 +3,139 @@ timestream .. dfhack-tool:: :summary: Fix FPS death. - :tags: unavailable + :tags: fort gameplay fps Do you remember when you first start a new fort, your initial 7 dwarves zip around the screen and get things done so quickly? As a player, you never had -to wait for your initial dwarves to move across the map. Don't you wish that -your fort of 200 dwarves could be as zippy? This tool can help. +to wait for your initial dwarves to move across the map. Do you wish that your +fort of 200 dwarves and 800 animals could be as zippy? This tool can help. -``timestream`` keeps the game running quickly by dynamically adjusting the -calendar speed relative to the frames per second that your computer can support. -Your dwarves spend the same amount of in-game time to do their tasks, but the -time that you, the player, have to wait for the dwarves to do things speeds up. -This means that the dwarves in your fully developed fort appears as energetic as -a newly created one, and mature forts are much more fun to play. +``timestream`` keeps the game running quickly by tweaking the game simulation +according to the frames per second that your computer can support. This means +that your dwarves spend the same amount of time relative to the in-game +calendar to do their tasks, but the time that you, the player, have to wait for +the dwarves to do get things done is reduced. The result is that the dwarves in +your fully developed fort appear as energetic as the dwarves in a newly created +fort, and mature forts are much more fun to play. -If you just want to change the game calendar speed without adjusting dwarf -speed, this tool can do that too. Your dwarves will just be able to get -less/more done per season (depending on whether you speed up or slow down the -calendar). +Note that whereas your dwarves zip around like you're running at 100 FPS, the +onscreen FPS counter, if enabled, will still show a lower number. See the +`Technical details`_` section below if you're interested in what's going on +under the hood. + +You can also use this tool to change the in-game calendar speed. Your dwarves +will be able to get less/more done per season (depending on whether you speed +up or slow down the calendar). Usage ----- -``timestream --units [--fps ]`` - Keep the game running as responsively as it did when it was running at the - given frames per second. Dwarves get the same amount done per game day, but - game days go by faster. If a target FPS is not given, it defaults to 100. -``timestream --rate ``, ``timestream --fps `` - Just change the rate of the calendar, without corresponding adjustments to - units. Game responsiveness will not change, but dwarves will be able to get - more (or less) done per game day. A rate of ``1`` is "normal" calendar - speed. Alternately, you can run the calendar at a rate that it would have - moved at while the game was running at the specified frames per second. +:: + + enable timestream + timestream [status] + timestream set + timestream reset Examples -------- -``timestream --units`` - Keep the game running as quickly and smoothly as it did when it ran - "naturally" at 100 FPS. This mode makes things much more pleasant for the - player without giving any advantage/disadvantage to your in-game dwarves. -``timestream --rate 2`` - Calendar runs at 2x normal speed and units get half as much done as usual - per game day. -``timestream --fps 100`` - Calendar runs at a dynamic speed to simulate 100 FPS. Units get a varying - amount of work done per game day, but will get less and less done as your - fort grows and your unadjusted FPS decreases. -``timestream --rate 1`` - Reset everything back to normal. +``enable timestream`` + Start adjusting the simulation to run at the currently configured apparent + FPS (default is whatever you have the FPS cap set to in the DF settings, + which is usually 100). + +``timestream set fps 50`` + Tweak the simulation so dwarves move at an apparent 50 frames per second. + +``timestream set calendar-rate 0.5`` + Make the days twice as long and allow dwarves to accomplish twice as much + per day. + +``timestream set fps -1`` +``timestream set calendar-rate 2`` + Don't change the granularity of the simulation, but make the in-game days + pass twice as quickly, as if the sun sped up across the sky. Units will get + half as much done as usual per game day. + +``timestream reset`` + Reset settings to defaults: the vanilla FPS cap with no calendar speed + advantage or disadvantage. + +Settings +-------- + +:fps: Set the target simulated FPS. The default target FPS is whatever you have + the FPS cap set to in the DF settings, and the minimum is 10. Setting the + target FPS *below* your current actual FPS will have no effect. You have + to set the vanilla FPS cap for that. Set a target FPS of -1 to make no + adjustment at all to the apparent FPS of the game. + +:calendar-rate: Set the calendar rate in relation to the target FPS. A calendar + rate factor of 1 means time flows "normally" for the units in the game. + Values between 0 and 1 slow the calendar relative to the world, allowing + units to get more done per day, and values above 1 speed the calendar + relative to the world, causing the days to pass quicker and preventing + units from getting as much done per day. + +:max-boost: Set the maximum difference between the actual FPS that the computer + can support and the simulated FPS. The default value is 50. For example, if + the computer can support 30 FPS and your target FPS is set to 100, the + ``timestream`` simulation will target 80 FPS. This prevents unit movement + from becoming "jerky". Raise this value if speed of the simulation is more + important to you than its accuracy. + +Technical details +----------------- + +So what is this magic? How does this tool make it look like the game is +suddenly running so much faster? + +Maybe an analogy would help. Pretend you're standing at the bottom of a +staircase and you want to walk up the stairs. You can walk up one stair every +second, and there are 100 stairs, so it will take you 100 seconds to walk up +all the stairs. + +Now let's use the Hand of Armok and fiddle with reality a bit. Let's say that +instead of walking up one step, you walk up 5 steps at once. At the same time +we move the wall clock 5 seconds ahead. If you look at the clock after reaching +the top of the stairs, it will still look like it took 100 seconds, but you did +it all in fewer "steps". + +That's essentially what ``timestream`` is doing to the game. All "actions" in +DF have counters associated with them. For example, when a dwarf wants to walk +to the next tile, a counter is initialized to 500. Every "tick" of the game +(the "frame" in FPS) decrements that counter by by a certain amount. When the +counter gets to zero, the dwarf appears on the next tile. + +When ``timestream`` is active, it monitors all those counters and makes them +decrement more per tick. It then balances things out by proportionally +advancing the in-game calendar. Therefore, more "happens" per step, and DF has +to simulate fewer "steps" for the same amount of work to get done. + +The cost of this simplification is that the world becomes less "smooth". As the +discrepancy between the "natural" and simulated FPS grows, more and more +dwarves will move to their next tiles at *exactly* the same time. Moreover, the +rate of action completion per unit is effectively capped at the granularity of +the simulation, so very fast units will lose some of their advantage. In the +extreme case, with the computer struggling to run at 1 FPS and ``timestream`` +simulating thousands of FPS (and the ``--max-boost`` cap increased to crazy +values), all units will perform exactly one action per frame. This would make +the game look robotic. With default settings, it will never get this bad, but +you can always choose to alter the ``timestream`` configuration to your +preferred balance of speed vs. accuracy. + +Limitations +----------- + +Right now, not all aspects of the game are perfectly adjusted. For example, +armies on world map will move at the same (real-time) rate regardless of +changes that ``timestream`` is making to the calendar. + +Here is a (likely incomplete) list of game elements that are not affected by +``timestream``: + +- Army movement across the world map (including raids sent out from the fort) +- Liquid movement and evaporation +- Time that caravans stay at the trade depot +- Crop growth rates diff --git a/internal/control-panel/registry.lua b/internal/control-panel/registry.lua index ded9eb3832..2d296e838f 100644 --- a/internal/control-panel/registry.lua +++ b/internal/control-panel/registry.lua @@ -121,6 +121,7 @@ COMMANDS_BY_IDX = { desc='Displays percentages on partially-consumed items like hospital cloth.'}, {command='pop-control', group='gameplay', mode='enable'}, {command='starvingdead', group='gameplay', mode='enable'}, + {command='timestream', group='gameplay', mode='enable'}, {command='work-now', group='gameplay', mode='enable'}, } diff --git a/timestream.lua b/timestream.lua index ff4b978ed4..3e3fd73793 100644 --- a/timestream.lua +++ b/timestream.lua @@ -1,438 +1,549 @@ --- speeds up the calendar, units, or both --- based on https://gist.github.com/IndigoFenix/cf358b8c994caa0f93d5 +--@module = true +--@enable = true ---[====[ +local argparse = require('argparse') +local repeatutil = require("repeat-util") +local utils = require('utils') -timestream -========== -Controls the speed of the calendar and creatures. Fortress mode only. Experimental. +local GLOBAL_KEY = 'timestream' -The script is also capable of dynamically speeding up the game based on your current FPS to mitigate the effects of FPS death. See examples below to see how. +local SETTINGS = { + { + name='fps', + validate=function(arg) + local val = argparse.positiveInt(arg, 'fps') + if val < 10 then qerror('target fps must be at least 10') end + return val + end, + default=function() return df.global.init.fps_cap end, + }, + { + name='calendar-rate', + internal_name='calendar_rate', + validate=function(arg) + local val = tonumber(arg) + if not val or val <= 0 then qerror('calendar-rate must be larger than 0') end + return val + end, + default=1.0, + }, + { + name='max-boost', + internal_name='max_boost', + validate=function(arg) return argparse.nonnegativeInt(arg, 'max-boost') end, + default=50, + }, +} -Usage:: - - timestream [-rate R] [-fps FPS] [-units [FLAG]] [-debug] - -Examples: +local function get_default_state() + local settings = {} + for _, v in ipairs(SETTINGS) do + settings[v.internal_name or v.name] = utils.getval(v.default) + end + return { + enabled=false, + settings=settings, + } +end -- ``timestream -rate 2``: - Calendar runs at x2 normal speed, units run at normal speed -- ``timestream -fps 100``: - Calendar runs at dynamic speed to simulate 100 FPS, units normal -- ``timestream -fps 100 -units``: - Calendar & units are simulated at 100 FPS -- ``timestream -rate 1``: - Resets everything back to normal, regardless of other arguments -- ``timestream -rate 1 -fps 50 -units``: - Same as above -- ``timestream -fps 100 -units 2``: - Activates a different mode for speeding up units, using the native DF - ``debug_turbospeed`` flag (similar to `fastdwarf` 2) instead of adjusting - timers of all units. This results in rubberbanding unit motion, so it is not - recommended over the default method. +state = state or get_default_state() -Original timestream.lua: https://gist.github.com/IndigoFenix/cf358b8c994caa0f93d5 -]====] +function isEnabled() + return state.enabled +end +local function persist_state() + dfhack.persistent.saveSiteData(GLOBAL_KEY, state) +end -local MINIMAL_FPS = 10 -- This ensures you won't get crazy values on pausing/saving, or other artefacts on extremely low FPS. -local DEFAULT_MAX_FPS = 100 +local function event_loop() + print('doin the stream') +end +local function do_enable() + state.enabled = true + repeatutil.scheduleEvery(GLOBAL_KEY, 1, 'ticks', event_loop) +end ---- DO NOT CHANGE BELOW UNLESS YOU KNOW WHAT YOU'RE DOING --- +local function do_disable() + state.enabled = false + repeatutil.cancel(GLOBAL_KEY) +end -local utils = require("utils") -args = utils.processArgs({...}, utils.invert({ - 'rate', - 'fps', - 'units', - 'debug', -})) -local rate = tonumber(args.rate) or -1 -local desired_fps = tonumber(args.fps) -local simulating_units = tonumber(args.units) -if args.units == '' then - simulating_units = 1 +dfhack.onStateChange[GLOBAL_KEY] = function(sc) + if sc == SC_MAP_UNLOADED then + do_disable() + return + end + if sc ~= SC_MAP_LOADED or not dfhack.world.isFortressMode() then + return + end + state = get_default_state() + utils.assign(state, dfhack.persistent.getSiteData(GLOBAL_KEY, state)) + if state.enabled then + do_enable() + end end -local debug_mode = not not args.debug - -local current_fps = desired_fps -local prev_tick = 0 -local ticks_left = 0 -local simulating_desired_fps = false -local prev_frames = df.global.world.frame_counter -local last_frame = df.global.world.frame_counter -local prev_time = df.global.enabler.clock -local ui_main = df.global.plotinfo.main -local saved_game_frame = -1 -local frames_until_speeding = 0 -local speedy_frame_delta = desired_fps or DEFAULT_MAX_FPS - -local SEASON_LEN = 3360 -local YEAR_LEN = 403200 - -if not dfhack.world.isFortressMode() then - print("timestream: Will start when fortress mode is loaded.") + +if dfhack_flags.module then + return end -if rate == nil then - rate = 1 -elseif rate < 0 then - rate = 0 +if not dfhack.world.isFortressMode() or not dfhack.isMapLoaded() then + qerror('needs a loaded fortress map to work') end -simulating_desired_fps = true -if desired_fps == nil then - desired_fps = DEFAULT_MAX_FPS - if simulating_units ~= 1 and simulating_units ~= 2 then - simulating_desired_fps = false +local function print_status() + print(GLOBAL_KEY .. ' is ' .. (state.enabled and 'enabled' or 'not enabled')) + print() + print('settings:') + for _,v in ipairs(SETTINGS) do + print((' %15s: %s'):format(v.name, state.settings[v.internal_name or v.name])) end -elseif desired_fps < MINIMAL_FPS then - desired_fps = MINIMAL_FPS -end -current_fps = desired_fps - -eventNow = false -seasonNow = false -timestream = 0 -counter = 0 -if df.global.cur_season_tick < SEASON_LEN then - month = 1 -elseif df.global.cur_season_tick < SEASON_LEN * 2 then - month = 2 -else - month = 3 end -dfhack.onStateChange.loadTimestream = function(code) - if code==SC_MAP_LOADED then - if rate ~= 1 then - last_frame = df.global.world.frame_counter - --if rate > 0 then -- Won't behave well with unit simulation - if rate > 1 and not simulating_desired_fps then - print('timestream: Time running at x'..rate..".") - else - print('timestream: Time running dynamically to simulate '..desired_fps..' FPS.') - if rate ~= 0 then - print('timestream: Rate setting ignored.') - end - reset_frame_count() - rate = 1 - if simulating_units == 1 or simulating_units == 2 then - print("timestream: Unit simulation is on.") - if simulating_units ~= 2 then - df.global.debug_turbospeed = false - end - end - end - ticks_left = rate - 1 - - eventNow = false - seasonNow = false - timestream = 0 - if df.global.cur_season_tick < SEASON_LEN then - month = 1 - elseif df.global.cur_season_tick < SEASON_LEN * 2 then - month = 2 - else - month = 3 - end - if loaded ~= true then - dfhack.timeout(1,"frames",function() update() end) - loaded = true - end - else - print('timestream: Time set to normal speed.') - loaded = false - df.global.debug_turbospeed = false - end - if debug_mode then - print("timestream: Debug mode is on.") - end +local function do_set(setting_name, arg) + if not setting_name or not arg then + qerror('must specify setting and value') + end + local _, setting = utils.linear_index(SETTINGS, setting_name, 'name') + if not setting then + qerror('setting not found: ' .. setting_name) end + state.settings[setting.internal_name or setting.name] = setting.validate(arg) + print(('set %s to %s'):format(setting_name, state.settings[setting.internal_name or setting.name])) end -function update() - loaded = false - prev_tick = df.global.cur_year_tick - local current_frame = df.global.world.frame_counter - if (rate ~= 1 or simulating_desired_fps) and dfhack.world.isFortressMode() then - if last_frame + 1 == current_frame then - timestream = 0 - - --[[if rate < 1 then - if df.global.cur_year_tick - math.floor(df.global.cur_year_tick/10)*10 == 5 then - if counter > 1 then - counter = counter - 1 - timestream = -1 - else - counter = counter + math.floor(ticks_left) - end - end - else - --]] - --counter = counter + rate-1 - counter = counter + math.floor(ticks_left) - while counter >= 10 do - counter = counter - 10 - timestream = timestream + 1 - end - --end - eventFound = false - for i=0,#df.global.timed_events-1,1 do - event=df.global.timed_events[i] - if event.season == df.global.cur_season and event.season_ticks <= df.global.cur_season_tick then - if eventNow == false then - --df.global.cur_season_tick=event.season_ticks - event.season_ticks = df.global.cur_season_tick - eventNow = true - end - eventFound = true - end - end - if eventFound == false then eventNow = false end - - if df.global.cur_season_tick >= SEASON_LEN - 1 and df.global.cur_season_tick < SEASON_LEN * 2 - 1 and month == 1 then - seasonNow = true - month = 2 - if df.global.cur_season_tick > SEASON_LEN - 1 then - df.global.cur_season_tick = SEASON_LEN - end - elseif df.global.cur_season_tick >= SEASON_LEN * 2 - 1 and df.global.cur_season_tick < SEASON_LEN * 3 - 1 and month == 2 then - seasonNow = true - month = 3 - if df.global.cur_season_tick > SEASON_LEN * 2 - 1 then - df.global.cur_season_tick = SEASON_LEN * 2 - end - elseif df.global.cur_season_tick >= SEASON_LEN * 3 - 1 then - seasonNow = true - month = 1 - if df.global.cur_season_tick > SEASON_LEN * 3 then - df.global.cur_season_tick = SEASON_LEN * 3 - 1 - end - else - seasonNow = false - end - - if df.global.cur_year > 0 then - if timestream ~= 0 then - if df.global.cur_season_tick < 0 then - df.global.cur_season_tick = df.global.cur_season_tick + SEASON_LEN * 3 - df.global.cur_season = df.global.cur_season-1 - eventNow = true - end - if df.global.cur_season < 0 then - df.global.cur_season = df.global.cur_season + 4 - df.global.cur_year_tick = df.global.cur_year_tick + YEAR_LEN - df.global.cur_year = df.global.cur_year - 1 - eventNow = true - end - if (eventNow == false and seasonNow == false) or timestream < 0 then - if timestream > 0 then - df.global.cur_season_tick=df.global.cur_season_tick + timestream - remainder = df.global.cur_year_tick - math.floor(df.global.cur_year_tick/10)*10 - df.global.cur_year_tick=(df.global.cur_season_tick*10)+((df.global.cur_season)*(SEASON_LEN * 3 * 10)) + remainder - elseif timestream < 0 then - df.global.cur_season_tick=df.global.cur_season_tick - df.global.cur_year_tick=(df.global.cur_season_tick*10)+((df.global.cur_season)*(SEASON_LEN * 3 * 10)) - end - end - end - end - - if simulating_desired_fps then - if saved_game_frame ~= -1 and saved_game_frame + 2 == current_frame then - if debug_mode then - print("Game was saved two ticks ago (saved_game_frame(".. saved_game_frame .. ") + 2 == current_frame(" .. current_frame ..")") - end - reset_frame_count() - saved_game = -1 - end - local counted_frames = current_frame - prev_frames - if counted_frames >= desired_fps then - current_fps = 1000 * desired_fps / (df.global.enabler.clock - prev_time) - if current_fps < desired_fps then - rate = desired_fps/current_fps - else - rate = 1 -- We don't want to slow down the game - end - reset_frame_count() - if current_fps < MINIMAL_FPS then - current_fps = MINIMAL_FPS - end - local missing_frames = desired_fps - current_fps - speedy_frame_delta = desired_fps/missing_frames - if missing_frames == 0 then - speedy_frame_delta = desired_fps - end - if debug_mode then - print("prev_frames: " .. prev_frames .. ", current_fps: ".. current_fps.. ", rate: " .. rate) - end - end - - if simulating_units == 2 then - if frames_until_speeding <= 0 then - frames_until_speeding = frames_until_speeding + speedy_frame_delta - if debug_mode then - print("speedy_frame_delta: "..speedy_frame_delta..", speedy_frame: "..counted_frames.."/"..desired_fps) - end - df.global.debug_turbospeed = true - last_frame_sped_up = current_frame - else - frames_until_speeding = frames_until_speeding - 1 - if df.global.debug_turbospeed then - df.global.debug_turbospeed = false - end - end - elseif simulating_units == 1 then - local dec = math.floor(ticks_left) - 1 -- A value used to determine how much more to decrement from the timers per tick. - for k1, unit in pairs(df.global.world.units.active) do - if dfhack.units.isActive(unit) then - if unit.sex == 0 then -- Check to see if unit is female. - local ptimer = unit.pregnancy_timer - if ptimer > 0 then - ptimer = ptimer - dec - if ptimer < 1 then - ptimer = 1 - end - unit.pregnancy_timer = ptimer - end - end - for k2, action in pairs(unit.actions) do - local action_type = action.type - if action_type == df.unit_action_type.Move then - local d = action.data.move.timer - dec - if d < 1 then - d = 1 - end - action.data.move.timer = d - - elseif action_type == df.unit_action_type.Attack then - local d = action.data.attack.timer1 - dec - if d <= 1 then - d = 1 - action.data.attack.timer2 = 1 -- I don't know why, but if I don't add this line then there's a bug where people just dogpile each other and don't fight. - end - d = action.data.attack.timer2 - dec - if d < 1 then - d = 1 - end - action.data.attack.timer2 = d - elseif action_type == df.unit_action_type.HoldTerrain then - local d = action.data.holdterrain.timer - dec - if d < 1 then - d = 1 - end - action.data.holdterrain.timer = d - elseif action_type == df.unit_action_type.Climb then - local d = action.data.climb.timer - dec - if d < 1 then - d = 1 - end - action.data.climb.timer = d - elseif action_type == df.unit_action_type.Job then - local d = action.data.job.timer - dec - if d < 1 then - d = 1 - end - action.data.job.timer = d - elseif action_type == df.unit_action_type.Talk then - local d = action.data.talk.timer - dec - if d < 1 then - d = 1 - end - action.data.talk.timer = d - elseif action_type == df.unit_action_type.Unsteady then - local d = action.data.unsteady.timer - dec - if d < 1 then - d = 1 - end - action.data.unsteady.timer = d - elseif action_type == df.unit_action_type.StandUp then - local d = action.data.standup.timer - dec - if d < 1 then - d = 1 - end - action.data.standup.timer = d - elseif action_type == df.unit_action_type.LieDown then - local d = action.data.liedown.timer - dec - if d < 1 then - d = 1 - end - action.data.liedown.timer = d - elseif action_type == df.unit_action_type.JobRecover then - local d = action.data.jobrecover.timer - dec - if d < 1 then - d = 1 - end - action.data.job2.timer = d - elseif action_type == df.unit_action_type.PushObject then - local d = action.data.pushobject.timer - dec - if d < 1 then - d = 1 - end - action.data.pushobject.timer = d - elseif action_type == df.unit_action_type.SuckBlood then - local d = action.data.suckblood.timer - dec - if d < 1 then - d = 1 - end - action.data.suckblood.timer = d - elseif action_type == df.unit_action_type.Mount then - local d = action.data.mount.timer - dec - if d < 1 then - d = 1 - end - action.data.mount.timer = d - elseif action_type == df.unit_action_type.Dismount then - local d = action.data.dismount.timer - dec - if d < 1 then - d = 1 - end - action.data.dismount.timer = d - elseif action_type == df.unit_action_type.HoldItem then - local d = action.data.holditem.timer - dec - if d < 1 then - d = 1 - end - action.data.holditem.timer = d - end - end - end - end - end - end - ticks_left = ticks_left - math.floor(ticks_left) + rate - last_frame = current_frame - else - if debug_mode then - print("last_frame("..last_frame..") + 1 != current_frame("..current_frame..")") - end - reset_frame_count() - end - if ui_main.autosave_request then - if debug_mode then - print("Save state detected") - end - saved_game_frame = current_frame - end - if not loaded then - loaded = true - dfhack.timeout(1,"frames",function() update() end) - end - end +local function do_reset() + state = get_default_state() end -function reset_frame_count() - if debug_mode then - print("Resetting frame count") +local args = {...} +local command = table.remove(args, 1) + +if dfhack_flags and dfhack_flags.enable then + if dfhack_flags.enable_state then do_enable() + else do_disable() end - prev_time = df.global.enabler.clock - prev_frames = df.global.world.frame_counter +elseif command == 'set' then + do_set(args[1], args[2]) +elseif command == 'reset' then + do_reset() +elseif not command or command == 'status' then + print_status() + return +else + print(dfhack.script_help()) + return end ---Initial call +persist_state() -if dfhack.isMapLoaded() then - dfhack.onStateChange.loadTimestream(SC_MAP_LOADED) -end + + +-- local TICKS_PER_DAY = 1200 +-- local TICKS_PER_MONTH = 28 * TICKS_PER_DAY +-- local TICKS_PER_SEASON = 3 * TICKS_PER_MONTH +-- local TICKS_PER_YEAR = 4 * TICKS_PER_SEASON + +-- local MINIMAL_FPS = 10 -- This ensures you won't get crazy values on pausing/saving, or other artefacts on extremely low FPS. +-- local DEFAULT_MAX_FPS = 100 + + +-- --- DO NOT CHANGE BELOW UNLESS YOU KNOW WHAT YOU'RE DOING --- + +-- local utils = require("utils") +-- args = utils.processArgs({...}, utils.invert({ +-- 'rate', +-- 'fps', +-- 'units', +-- 'debug', +-- })) +-- local rate = tonumber(args.rate) or -1 +-- local desired_fps = tonumber(args.fps) +-- local simulating_units = tonumber(args.units) +-- if args.units == '' then +-- simulating_units = 1 +-- end +-- local debug_mode = not not args.debug + +-- local current_fps = desired_fps +-- local prev_tick = 0 +-- local ticks_left = 0 +-- local simulating_desired_fps = false +-- local prev_frames = df.global.world.frame_counter +-- local last_frame = df.global.world.frame_counter +-- local prev_time = df.global.enabler.clock +-- local ui_main = df.global.plotinfo.main +-- local saved_game_frame = -1 +-- local frames_until_speeding = 0 +-- local speedy_frame_delta = desired_fps or DEFAULT_MAX_FPS + +-- local SEASON_LEN = 3360 +-- local YEAR_LEN = 403200 + +-- if not dfhack.world.isFortressMode() then +-- print("timestream: Will start when fortress mode is loaded.") +-- end + +-- if rate == nil then +-- rate = 1 +-- elseif rate < 0 then +-- rate = 0 +-- end + +-- simulating_desired_fps = true +-- if desired_fps == nil then +-- desired_fps = DEFAULT_MAX_FPS +-- if simulating_units ~= 1 and simulating_units ~= 2 then +-- simulating_desired_fps = false +-- end +-- elseif desired_fps < MINIMAL_FPS then +-- desired_fps = MINIMAL_FPS +-- end +-- current_fps = desired_fps + +-- eventNow = false +-- seasonNow = false +-- timestream = 0 +-- counter = 0 +-- if df.global.cur_season_tick < SEASON_LEN then +-- month = 1 +-- elseif df.global.cur_season_tick < SEASON_LEN * 2 then +-- month = 2 +-- else +-- month = 3 +-- end + +-- dfhack.onStateChange.loadTimestream = function(code) +-- if code==SC_MAP_LOADED then +-- if rate ~= 1 then +-- last_frame = df.global.world.frame_counter +-- --if rate > 0 then -- Won't behave well with unit simulation +-- if rate > 1 and not simulating_desired_fps then +-- print('timestream: Time running at x'..rate..".") +-- else +-- print('timestream: Time running dynamically to simulate '..desired_fps..' FPS.') +-- if rate ~= 0 then +-- print('timestream: Rate setting ignored.') +-- end +-- reset_frame_count() +-- rate = 1 +-- if simulating_units == 1 or simulating_units == 2 then +-- print("timestream: Unit simulation is on.") +-- if simulating_units ~= 2 then +-- df.global.debug_turbospeed = false +-- end +-- end +-- end +-- ticks_left = rate - 1 + +-- eventNow = false +-- seasonNow = false +-- timestream = 0 +-- if df.global.cur_season_tick < SEASON_LEN then +-- month = 1 +-- elseif df.global.cur_season_tick < SEASON_LEN * 2 then +-- month = 2 +-- else +-- month = 3 +-- end +-- if loaded ~= true then +-- dfhack.timeout(1,"frames",function() update() end) +-- loaded = true +-- end +-- else +-- print('timestream: Time set to normal speed.') +-- loaded = false +-- df.global.debug_turbospeed = false +-- end +-- if debug_mode then +-- print("timestream: Debug mode is on.") +-- end +-- end +-- end + +-- function update() +-- loaded = false +-- prev_tick = df.global.cur_year_tick +-- local current_frame = df.global.world.frame_counter +-- if (rate ~= 1 or simulating_desired_fps) and dfhack.world.isFortressMode() then +-- if last_frame + 1 == current_frame then +-- timestream = 0 + +-- --[[if rate < 1 then +-- if df.global.cur_year_tick - math.floor(df.global.cur_year_tick/10)*10 == 5 then +-- if counter > 1 then +-- counter = counter - 1 +-- timestream = -1 +-- else +-- counter = counter + math.floor(ticks_left) +-- end +-- end +-- else +-- --]] +-- --counter = counter + rate-1 +-- counter = counter + math.floor(ticks_left) +-- while counter >= 10 do +-- counter = counter - 10 +-- timestream = timestream + 1 +-- end +-- --end +-- eventFound = false +-- for i=0,#df.global.timed_events-1,1 do +-- event=df.global.timed_events[i] +-- if event.season == df.global.cur_season and event.season_ticks <= df.global.cur_season_tick then +-- if eventNow == false then +-- --df.global.cur_season_tick=event.season_ticks +-- event.season_ticks = df.global.cur_season_tick +-- eventNow = true +-- end +-- eventFound = true +-- end +-- end +-- if eventFound == false then eventNow = false end + +-- if df.global.cur_season_tick >= SEASON_LEN - 1 and df.global.cur_season_tick < SEASON_LEN * 2 - 1 and month == 1 then +-- seasonNow = true +-- month = 2 +-- if df.global.cur_season_tick > SEASON_LEN - 1 then +-- df.global.cur_season_tick = SEASON_LEN +-- end +-- elseif df.global.cur_season_tick >= SEASON_LEN * 2 - 1 and df.global.cur_season_tick < SEASON_LEN * 3 - 1 and month == 2 then +-- seasonNow = true +-- month = 3 +-- if df.global.cur_season_tick > SEASON_LEN * 2 - 1 then +-- df.global.cur_season_tick = SEASON_LEN * 2 +-- end +-- elseif df.global.cur_season_tick >= SEASON_LEN * 3 - 1 then +-- seasonNow = true +-- month = 1 +-- if df.global.cur_season_tick > SEASON_LEN * 3 then +-- df.global.cur_season_tick = SEASON_LEN * 3 - 1 +-- end +-- else +-- seasonNow = false +-- end + +-- if df.global.cur_year > 0 then +-- if timestream ~= 0 then +-- if df.global.cur_season_tick < 0 then +-- df.global.cur_season_tick = df.global.cur_season_tick + SEASON_LEN * 3 +-- df.global.cur_season = df.global.cur_season-1 +-- eventNow = true +-- end +-- if df.global.cur_season < 0 then +-- df.global.cur_season = df.global.cur_season + 4 +-- df.global.cur_year_tick = df.global.cur_year_tick + YEAR_LEN +-- df.global.cur_year = df.global.cur_year - 1 +-- eventNow = true +-- end +-- if (eventNow == false and seasonNow == false) or timestream < 0 then +-- if timestream > 0 then +-- df.global.cur_season_tick=df.global.cur_season_tick + timestream +-- remainder = df.global.cur_year_tick - math.floor(df.global.cur_year_tick/10)*10 +-- df.global.cur_year_tick=(df.global.cur_season_tick*10)+((df.global.cur_season)*(SEASON_LEN * 3 * 10)) + remainder +-- elseif timestream < 0 then +-- df.global.cur_season_tick=df.global.cur_season_tick +-- df.global.cur_year_tick=(df.global.cur_season_tick*10)+((df.global.cur_season)*(SEASON_LEN * 3 * 10)) +-- end +-- end +-- end +-- end + +-- if simulating_desired_fps then +-- if saved_game_frame ~= -1 and saved_game_frame + 2 == current_frame then +-- if debug_mode then +-- print("Game was saved two ticks ago (saved_game_frame(".. saved_game_frame .. ") + 2 == current_frame(" .. current_frame ..")") +-- end +-- reset_frame_count() +-- saved_game = -1 +-- end +-- local counted_frames = current_frame - prev_frames +-- if counted_frames >= desired_fps then +-- current_fps = 1000 * desired_fps / (df.global.enabler.clock - prev_time) +-- if current_fps < desired_fps then +-- rate = desired_fps/current_fps +-- else +-- rate = 1 -- We don't want to slow down the game +-- end +-- reset_frame_count() +-- if current_fps < MINIMAL_FPS then +-- current_fps = MINIMAL_FPS +-- end +-- local missing_frames = desired_fps - current_fps +-- speedy_frame_delta = desired_fps/missing_frames +-- if missing_frames == 0 then +-- speedy_frame_delta = desired_fps +-- end +-- if debug_mode then +-- print("prev_frames: " .. prev_frames .. ", current_fps: ".. current_fps.. ", rate: " .. rate) +-- end +-- end + +-- if simulating_units == 2 then +-- if frames_until_speeding <= 0 then +-- frames_until_speeding = frames_until_speeding + speedy_frame_delta +-- if debug_mode then +-- print("speedy_frame_delta: "..speedy_frame_delta..", speedy_frame: "..counted_frames.."/"..desired_fps) +-- end +-- df.global.debug_turbospeed = true +-- last_frame_sped_up = current_frame +-- else +-- frames_until_speeding = frames_until_speeding - 1 +-- if df.global.debug_turbospeed then +-- df.global.debug_turbospeed = false +-- end +-- end +-- elseif simulating_units == 1 then +-- local dec = math.floor(ticks_left) - 1 -- A value used to determine how much more to decrement from the timers per tick. +-- for k1, unit in pairs(df.global.world.units.active) do +-- if dfhack.units.isActive(unit) then +-- if unit.sex == 0 then -- Check to see if unit is female. +-- local ptimer = unit.pregnancy_timer +-- if ptimer > 0 then +-- ptimer = ptimer - dec +-- if ptimer < 1 then +-- ptimer = 1 +-- end +-- unit.pregnancy_timer = ptimer +-- end +-- end +-- for k2, action in pairs(unit.actions) do +-- local action_type = action.type +-- if action_type == df.unit_action_type.Move then +-- local d = action.data.move.timer - dec +-- if d < 1 then +-- d = 1 +-- end +-- action.data.move.timer = d + +-- elseif action_type == df.unit_action_type.Attack then +-- local d = action.data.attack.timer1 - dec +-- if d <= 1 then +-- d = 1 +-- action.data.attack.timer2 = 1 -- I don't know why, but if I don't add this line then there's a bug where people just dogpile each other and don't fight. +-- end +-- d = action.data.attack.timer2 - dec +-- if d < 1 then +-- d = 1 +-- end +-- action.data.attack.timer2 = d +-- elseif action_type == df.unit_action_type.HoldTerrain then +-- local d = action.data.holdterrain.timer - dec +-- if d < 1 then +-- d = 1 +-- end +-- action.data.holdterrain.timer = d +-- elseif action_type == df.unit_action_type.Climb then +-- local d = action.data.climb.timer - dec +-- if d < 1 then +-- d = 1 +-- end +-- action.data.climb.timer = d +-- elseif action_type == df.unit_action_type.Job then +-- local d = action.data.job.timer - dec +-- if d < 1 then +-- d = 1 +-- end +-- action.data.job.timer = d +-- elseif action_type == df.unit_action_type.Talk then +-- local d = action.data.talk.timer - dec +-- if d < 1 then +-- d = 1 +-- end +-- action.data.talk.timer = d +-- elseif action_type == df.unit_action_type.Unsteady then +-- local d = action.data.unsteady.timer - dec +-- if d < 1 then +-- d = 1 +-- end +-- action.data.unsteady.timer = d +-- elseif action_type == df.unit_action_type.StandUp then +-- local d = action.data.standup.timer - dec +-- if d < 1 then +-- d = 1 +-- end +-- action.data.standup.timer = d +-- elseif action_type == df.unit_action_type.LieDown then +-- local d = action.data.liedown.timer - dec +-- if d < 1 then +-- d = 1 +-- end +-- action.data.liedown.timer = d +-- elseif action_type == df.unit_action_type.JobRecover then +-- local d = action.data.jobrecover.timer - dec +-- if d < 1 then +-- d = 1 +-- end +-- action.data.job2.timer = d +-- elseif action_type == df.unit_action_type.PushObject then +-- local d = action.data.pushobject.timer - dec +-- if d < 1 then +-- d = 1 +-- end +-- action.data.pushobject.timer = d +-- elseif action_type == df.unit_action_type.SuckBlood then +-- local d = action.data.suckblood.timer - dec +-- if d < 1 then +-- d = 1 +-- end +-- action.data.suckblood.timer = d +-- elseif action_type == df.unit_action_type.Mount then +-- local d = action.data.mount.timer - dec +-- if d < 1 then +-- d = 1 +-- end +-- action.data.mount.timer = d +-- elseif action_type == df.unit_action_type.Dismount then +-- local d = action.data.dismount.timer - dec +-- if d < 1 then +-- d = 1 +-- end +-- action.data.dismount.timer = d +-- elseif action_type == df.unit_action_type.HoldItem then +-- local d = action.data.holditem.timer - dec +-- if d < 1 then +-- d = 1 +-- end +-- action.data.holditem.timer = d +-- end +-- end +-- end +-- end +-- end +-- end +-- ticks_left = ticks_left - math.floor(ticks_left) + rate +-- last_frame = current_frame +-- else +-- if debug_mode then +-- print("last_frame("..last_frame..") + 1 != current_frame("..current_frame..")") +-- end +-- reset_frame_count() +-- end +-- if ui_main.autosave_request then +-- if debug_mode then +-- print("Save state detected") +-- end +-- saved_game_frame = current_frame +-- end +-- if not loaded then +-- loaded = true +-- dfhack.timeout(1,"frames",function() update() end) +-- end +-- end +-- end + +-- function reset_frame_count() +-- if debug_mode then +-- print("Resetting frame count") +-- end +-- prev_time = df.global.enabler.clock +-- prev_frames = df.global.world.frame_counter +-- end + +-- --Initial call + +-- if dfhack.isMapLoaded() then +-- dfhack.onStateChange.loadTimestream(SC_MAP_LOADED) +-- end From b60a8492ae25a4e0f0296f773a7346419b05ab9e Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Tue, 25 Jun 2024 17:17:17 -0700 Subject: [PATCH 02/10] first draft of high-level logic --- timestream.lua | 541 ++++++++++++------------------------------------- 1 file changed, 129 insertions(+), 412 deletions(-) diff --git a/timestream.lua b/timestream.lua index 3e3fd73793..832a7d2fc9 100644 --- a/timestream.lua +++ b/timestream.lua @@ -5,6 +5,9 @@ local argparse = require('argparse') local repeatutil = require("repeat-util") local utils = require('utils') +------------------------------------ +-- state management + local GLOBAL_KEY = 'timestream' local SETTINGS = { @@ -56,13 +59,133 @@ local function persist_state() dfhack.persistent.saveSiteData(GLOBAL_KEY, state) end -local function event_loop() - print('doin the stream') +------------------------------------ +-- business logic + +local TICKS_PER_DAY = 1200 +local TICKS_PER_WEEK = 7 * TICKS_PER_DAY + +-- determined from reverse engineering; don't skip these tick thresholds +-- something important happens when cur_season_tick % == +-- please keep remainder list elements in **descending** order +local SEASON_TICK_TRIGGERS = { + {mod=TICKS_PER_DAY//10, rem={0x6e, 0x50, 0x46, 0x3c, 0x32, 0x28, 0x14, 10, 0}}, + {mod=TICKS_PER_WEEK//10, rem={0x32, 0x1e}}, +} + +-- additional ticks we would like to skip at the next opportunity +local timeskip_deficit = 0.0 + +local function get_timeskip_per_tick(real_fps, desired_fps) + return (real_fps*(desired_fps - real_fps)) / desired_fps +end + +local function get_next_timed_event_season_tick() + local next_event_tick = math.huge + for _, event in ipairs(df.global.timed_events) do + if event.season == df.global.cur_season then + next_event_tick = math.min(next_event_tick, event.season_ticks) + end + end + return next_event_tick +end + +local function get_next_trigger_season_tick() + local tick_offset = (df.global.cur_year_tick+1) % 10 + local is_season_tick = tick_offset == 0 + local next_season_tick = df.global.cur_season_tick + (is_season_tick and 0 or 1) + + local next_trigger_tick = math.huge + for _, trigger in ipairs(SEASON_TICK_TRIGGERS) do + local cur_rem = next_season_tick % trigger.mod + for _, rem in ipairs(trigger.rem) do + if cur_rem < rem or (cur_rem == rem and is_season_tick) then + next_trigger_tick = math.min(next_trigger_tick, next_season_tick + (rem - cur_rem)) + break + end + end + end + return next_trigger_tick +end + +local function clamp_timeskip(timeskip) + if timeskip <= 0 then return 0 end + local next_important_season_tick = math.min(get_next_timed_event_season_tick(), get_next_trigger_season_tick()) + return math.min(timeskip, (next_important_season_tick-df.global.cur_season_tick)*10) +end + +local function adjust_units(timeskip) + for _, unit in ipairs(df.global.world.units.active) do + if not dfhack.units.isActive(unit) then goto continue end + if unit.sex == df.pronoun_type.she then + if unit.pregnancy_timer > 0 then + unit.pregnancy_timer = math.max(1, unit.pregnancy_timer - timeskip) + end + end + dfhack.units.subtractGroupActionTimers(unit, timeskip, df.unit_action_type_group.All) + ::continue:: + end end +local function adjust_crops(timeskip) + -- TODO +end + +local function adjust_armies(timeskip) + -- TODO +end + +local function adjust_evaporation(timeskip) + -- TODO +end + +local function adjust_caravans(timeskip) + -- TODO +end + +local function adjust_item_wear(timeskip) + -- TODO +end + +local function adjust_buildings(timeskip) + -- TODO +end + +local function on_tick() + local real_fps = df.global.enabler.calculated_fps + if real_fps >= state.settings.fps then + timeskip_deficit = 0.0 + return + end + + local desired_timeskip = get_timeskip_per_tick(real_fps, state.settings.fps) + timeskip_deficit + local timeskip = math.floor(clamp_timeskip(desired_timeskip)) + timeskip_deficit = desired_timeskip - timeskip + print(('desired_timeskip=%s, timeskip=%s, timeskip_deficit=%s'):format(desired_timeskip, timeskip, timeskip_deficit)) + if timeskip <= 0 then return end + + if timeskip > 0 then return end + + local new_cur_year_tick = df.global.cur_year_tick + timeskip + df.global.cur_season_tick = df.global.cur_season_tick + new_cur_year_tick//10 - df.global.cur_year_tick//10 + df.global.cur_year_tick = new_cur_year_tick + + adjust_units(timeskip) + adjust_crops(timeskip) + adjust_armies(timeskip) + adjust_evaporation(timeskip) + adjust_caravans(timeskip) + adjust_item_wear(timeskip) + adjust_buildings(timeskip) +end + +------------------------------------ +-- hook management + local function do_enable() + timeskip_deficit = 0 state.enabled = true - repeatutil.scheduleEvery(GLOBAL_KEY, 1, 'ticks', event_loop) + repeatutil.scheduleEvery(GLOBAL_KEY, 1, 'ticks', on_tick) end local function do_disable() @@ -85,6 +208,9 @@ dfhack.onStateChange[GLOBAL_KEY] = function(sc) end end +------------------------------------ +-- interface + if dfhack_flags.module then return end @@ -138,412 +264,3 @@ else end persist_state() - - - --- local TICKS_PER_DAY = 1200 --- local TICKS_PER_MONTH = 28 * TICKS_PER_DAY --- local TICKS_PER_SEASON = 3 * TICKS_PER_MONTH --- local TICKS_PER_YEAR = 4 * TICKS_PER_SEASON - --- local MINIMAL_FPS = 10 -- This ensures you won't get crazy values on pausing/saving, or other artefacts on extremely low FPS. --- local DEFAULT_MAX_FPS = 100 - - --- --- DO NOT CHANGE BELOW UNLESS YOU KNOW WHAT YOU'RE DOING --- - --- local utils = require("utils") --- args = utils.processArgs({...}, utils.invert({ --- 'rate', --- 'fps', --- 'units', --- 'debug', --- })) --- local rate = tonumber(args.rate) or -1 --- local desired_fps = tonumber(args.fps) --- local simulating_units = tonumber(args.units) --- if args.units == '' then --- simulating_units = 1 --- end --- local debug_mode = not not args.debug - --- local current_fps = desired_fps --- local prev_tick = 0 --- local ticks_left = 0 --- local simulating_desired_fps = false --- local prev_frames = df.global.world.frame_counter --- local last_frame = df.global.world.frame_counter --- local prev_time = df.global.enabler.clock --- local ui_main = df.global.plotinfo.main --- local saved_game_frame = -1 --- local frames_until_speeding = 0 --- local speedy_frame_delta = desired_fps or DEFAULT_MAX_FPS - --- local SEASON_LEN = 3360 --- local YEAR_LEN = 403200 - --- if not dfhack.world.isFortressMode() then --- print("timestream: Will start when fortress mode is loaded.") --- end - --- if rate == nil then --- rate = 1 --- elseif rate < 0 then --- rate = 0 --- end - --- simulating_desired_fps = true --- if desired_fps == nil then --- desired_fps = DEFAULT_MAX_FPS --- if simulating_units ~= 1 and simulating_units ~= 2 then --- simulating_desired_fps = false --- end --- elseif desired_fps < MINIMAL_FPS then --- desired_fps = MINIMAL_FPS --- end --- current_fps = desired_fps - --- eventNow = false --- seasonNow = false --- timestream = 0 --- counter = 0 --- if df.global.cur_season_tick < SEASON_LEN then --- month = 1 --- elseif df.global.cur_season_tick < SEASON_LEN * 2 then --- month = 2 --- else --- month = 3 --- end - --- dfhack.onStateChange.loadTimestream = function(code) --- if code==SC_MAP_LOADED then --- if rate ~= 1 then --- last_frame = df.global.world.frame_counter --- --if rate > 0 then -- Won't behave well with unit simulation --- if rate > 1 and not simulating_desired_fps then --- print('timestream: Time running at x'..rate..".") --- else --- print('timestream: Time running dynamically to simulate '..desired_fps..' FPS.') --- if rate ~= 0 then --- print('timestream: Rate setting ignored.') --- end --- reset_frame_count() --- rate = 1 --- if simulating_units == 1 or simulating_units == 2 then --- print("timestream: Unit simulation is on.") --- if simulating_units ~= 2 then --- df.global.debug_turbospeed = false --- end --- end --- end --- ticks_left = rate - 1 - --- eventNow = false --- seasonNow = false --- timestream = 0 --- if df.global.cur_season_tick < SEASON_LEN then --- month = 1 --- elseif df.global.cur_season_tick < SEASON_LEN * 2 then --- month = 2 --- else --- month = 3 --- end --- if loaded ~= true then --- dfhack.timeout(1,"frames",function() update() end) --- loaded = true --- end --- else --- print('timestream: Time set to normal speed.') --- loaded = false --- df.global.debug_turbospeed = false --- end --- if debug_mode then --- print("timestream: Debug mode is on.") --- end --- end --- end - --- function update() --- loaded = false --- prev_tick = df.global.cur_year_tick --- local current_frame = df.global.world.frame_counter --- if (rate ~= 1 or simulating_desired_fps) and dfhack.world.isFortressMode() then --- if last_frame + 1 == current_frame then --- timestream = 0 - --- --[[if rate < 1 then --- if df.global.cur_year_tick - math.floor(df.global.cur_year_tick/10)*10 == 5 then --- if counter > 1 then --- counter = counter - 1 --- timestream = -1 --- else --- counter = counter + math.floor(ticks_left) --- end --- end --- else --- --]] --- --counter = counter + rate-1 --- counter = counter + math.floor(ticks_left) --- while counter >= 10 do --- counter = counter - 10 --- timestream = timestream + 1 --- end --- --end --- eventFound = false --- for i=0,#df.global.timed_events-1,1 do --- event=df.global.timed_events[i] --- if event.season == df.global.cur_season and event.season_ticks <= df.global.cur_season_tick then --- if eventNow == false then --- --df.global.cur_season_tick=event.season_ticks --- event.season_ticks = df.global.cur_season_tick --- eventNow = true --- end --- eventFound = true --- end --- end --- if eventFound == false then eventNow = false end - --- if df.global.cur_season_tick >= SEASON_LEN - 1 and df.global.cur_season_tick < SEASON_LEN * 2 - 1 and month == 1 then --- seasonNow = true --- month = 2 --- if df.global.cur_season_tick > SEASON_LEN - 1 then --- df.global.cur_season_tick = SEASON_LEN --- end --- elseif df.global.cur_season_tick >= SEASON_LEN * 2 - 1 and df.global.cur_season_tick < SEASON_LEN * 3 - 1 and month == 2 then --- seasonNow = true --- month = 3 --- if df.global.cur_season_tick > SEASON_LEN * 2 - 1 then --- df.global.cur_season_tick = SEASON_LEN * 2 --- end --- elseif df.global.cur_season_tick >= SEASON_LEN * 3 - 1 then --- seasonNow = true --- month = 1 --- if df.global.cur_season_tick > SEASON_LEN * 3 then --- df.global.cur_season_tick = SEASON_LEN * 3 - 1 --- end --- else --- seasonNow = false --- end - --- if df.global.cur_year > 0 then --- if timestream ~= 0 then --- if df.global.cur_season_tick < 0 then --- df.global.cur_season_tick = df.global.cur_season_tick + SEASON_LEN * 3 --- df.global.cur_season = df.global.cur_season-1 --- eventNow = true --- end --- if df.global.cur_season < 0 then --- df.global.cur_season = df.global.cur_season + 4 --- df.global.cur_year_tick = df.global.cur_year_tick + YEAR_LEN --- df.global.cur_year = df.global.cur_year - 1 --- eventNow = true --- end --- if (eventNow == false and seasonNow == false) or timestream < 0 then --- if timestream > 0 then --- df.global.cur_season_tick=df.global.cur_season_tick + timestream --- remainder = df.global.cur_year_tick - math.floor(df.global.cur_year_tick/10)*10 --- df.global.cur_year_tick=(df.global.cur_season_tick*10)+((df.global.cur_season)*(SEASON_LEN * 3 * 10)) + remainder --- elseif timestream < 0 then --- df.global.cur_season_tick=df.global.cur_season_tick --- df.global.cur_year_tick=(df.global.cur_season_tick*10)+((df.global.cur_season)*(SEASON_LEN * 3 * 10)) --- end --- end --- end --- end - --- if simulating_desired_fps then --- if saved_game_frame ~= -1 and saved_game_frame + 2 == current_frame then --- if debug_mode then --- print("Game was saved two ticks ago (saved_game_frame(".. saved_game_frame .. ") + 2 == current_frame(" .. current_frame ..")") --- end --- reset_frame_count() --- saved_game = -1 --- end --- local counted_frames = current_frame - prev_frames --- if counted_frames >= desired_fps then --- current_fps = 1000 * desired_fps / (df.global.enabler.clock - prev_time) --- if current_fps < desired_fps then --- rate = desired_fps/current_fps --- else --- rate = 1 -- We don't want to slow down the game --- end --- reset_frame_count() --- if current_fps < MINIMAL_FPS then --- current_fps = MINIMAL_FPS --- end --- local missing_frames = desired_fps - current_fps --- speedy_frame_delta = desired_fps/missing_frames --- if missing_frames == 0 then --- speedy_frame_delta = desired_fps --- end --- if debug_mode then --- print("prev_frames: " .. prev_frames .. ", current_fps: ".. current_fps.. ", rate: " .. rate) --- end --- end - --- if simulating_units == 2 then --- if frames_until_speeding <= 0 then --- frames_until_speeding = frames_until_speeding + speedy_frame_delta --- if debug_mode then --- print("speedy_frame_delta: "..speedy_frame_delta..", speedy_frame: "..counted_frames.."/"..desired_fps) --- end --- df.global.debug_turbospeed = true --- last_frame_sped_up = current_frame --- else --- frames_until_speeding = frames_until_speeding - 1 --- if df.global.debug_turbospeed then --- df.global.debug_turbospeed = false --- end --- end --- elseif simulating_units == 1 then --- local dec = math.floor(ticks_left) - 1 -- A value used to determine how much more to decrement from the timers per tick. --- for k1, unit in pairs(df.global.world.units.active) do --- if dfhack.units.isActive(unit) then --- if unit.sex == 0 then -- Check to see if unit is female. --- local ptimer = unit.pregnancy_timer --- if ptimer > 0 then --- ptimer = ptimer - dec --- if ptimer < 1 then --- ptimer = 1 --- end --- unit.pregnancy_timer = ptimer --- end --- end --- for k2, action in pairs(unit.actions) do --- local action_type = action.type --- if action_type == df.unit_action_type.Move then --- local d = action.data.move.timer - dec --- if d < 1 then --- d = 1 --- end --- action.data.move.timer = d - --- elseif action_type == df.unit_action_type.Attack then --- local d = action.data.attack.timer1 - dec --- if d <= 1 then --- d = 1 --- action.data.attack.timer2 = 1 -- I don't know why, but if I don't add this line then there's a bug where people just dogpile each other and don't fight. --- end --- d = action.data.attack.timer2 - dec --- if d < 1 then --- d = 1 --- end --- action.data.attack.timer2 = d --- elseif action_type == df.unit_action_type.HoldTerrain then --- local d = action.data.holdterrain.timer - dec --- if d < 1 then --- d = 1 --- end --- action.data.holdterrain.timer = d --- elseif action_type == df.unit_action_type.Climb then --- local d = action.data.climb.timer - dec --- if d < 1 then --- d = 1 --- end --- action.data.climb.timer = d --- elseif action_type == df.unit_action_type.Job then --- local d = action.data.job.timer - dec --- if d < 1 then --- d = 1 --- end --- action.data.job.timer = d --- elseif action_type == df.unit_action_type.Talk then --- local d = action.data.talk.timer - dec --- if d < 1 then --- d = 1 --- end --- action.data.talk.timer = d --- elseif action_type == df.unit_action_type.Unsteady then --- local d = action.data.unsteady.timer - dec --- if d < 1 then --- d = 1 --- end --- action.data.unsteady.timer = d --- elseif action_type == df.unit_action_type.StandUp then --- local d = action.data.standup.timer - dec --- if d < 1 then --- d = 1 --- end --- action.data.standup.timer = d --- elseif action_type == df.unit_action_type.LieDown then --- local d = action.data.liedown.timer - dec --- if d < 1 then --- d = 1 --- end --- action.data.liedown.timer = d --- elseif action_type == df.unit_action_type.JobRecover then --- local d = action.data.jobrecover.timer - dec --- if d < 1 then --- d = 1 --- end --- action.data.job2.timer = d --- elseif action_type == df.unit_action_type.PushObject then --- local d = action.data.pushobject.timer - dec --- if d < 1 then --- d = 1 --- end --- action.data.pushobject.timer = d --- elseif action_type == df.unit_action_type.SuckBlood then --- local d = action.data.suckblood.timer - dec --- if d < 1 then --- d = 1 --- end --- action.data.suckblood.timer = d --- elseif action_type == df.unit_action_type.Mount then --- local d = action.data.mount.timer - dec --- if d < 1 then --- d = 1 --- end --- action.data.mount.timer = d --- elseif action_type == df.unit_action_type.Dismount then --- local d = action.data.dismount.timer - dec --- if d < 1 then --- d = 1 --- end --- action.data.dismount.timer = d --- elseif action_type == df.unit_action_type.HoldItem then --- local d = action.data.holditem.timer - dec --- if d < 1 then --- d = 1 --- end --- action.data.holditem.timer = d --- end --- end --- end --- end --- end --- end --- ticks_left = ticks_left - math.floor(ticks_left) + rate --- last_frame = current_frame --- else --- if debug_mode then --- print("last_frame("..last_frame..") + 1 != current_frame("..current_frame..")") --- end --- reset_frame_count() --- end --- if ui_main.autosave_request then --- if debug_mode then --- print("Save state detected") --- end --- saved_game_frame = current_frame --- end --- if not loaded then --- loaded = true --- dfhack.timeout(1,"frames",function() update() end) --- end --- end --- end - --- function reset_frame_count() --- if debug_mode then --- print("Resetting frame count") --- end --- prev_time = df.global.enabler.clock --- prev_frames = df.global.world.frame_counter --- end - --- --Initial call - --- if dfhack.isMapLoaded() then --- dfhack.onStateChange.loadTimestream(SC_MAP_LOADED) --- end From 2232188ce3164cb92dddd6d5d7dfefab5639cf0b Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Tue, 25 Jun 2024 22:48:23 -0700 Subject: [PATCH 03/10] convert max-boost to max-frame-skip and get the basic version all working --- docs/timestream.rst | 23 ++++++++++++----------- timestream.lua | 26 ++++++++++++-------------- 2 files changed, 24 insertions(+), 25 deletions(-) diff --git a/docs/timestream.rst b/docs/timestream.rst index 1ac0e318ec..35dc3b81ae 100644 --- a/docs/timestream.rst +++ b/docs/timestream.rst @@ -78,12 +78,13 @@ Settings relative to the world, causing the days to pass quicker and preventing units from getting as much done per day. -:max-boost: Set the maximum difference between the actual FPS that the computer - can support and the simulated FPS. The default value is 50. For example, if - the computer can support 30 FPS and your target FPS is set to 100, the - ``timestream`` simulation will target 80 FPS. This prevents unit movement - from becoming "jerky". Raise this value if speed of the simulation is more - important to you than its accuracy. +:max-frame-skip: Set the maximum number of ticks that can be skipped in one + step. Dwarves can perform at most one action per step, and if too many + frames are skipped in one step, dwarves will "lose time" compared to the + movement of the calendar. The default is 4, which allows a target FPS of up + to 4x your actual FPS while still allowing dwarves to walk at full speed. + Raise this value if speed of the simulation is more important to you than + its accuracy and smoothness. Technical details ----------------- @@ -119,11 +120,11 @@ dwarves will move to their next tiles at *exactly* the same time. Moreover, the rate of action completion per unit is effectively capped at the granularity of the simulation, so very fast units will lose some of their advantage. In the extreme case, with the computer struggling to run at 1 FPS and ``timestream`` -simulating thousands of FPS (and the ``--max-boost`` cap increased to crazy -values), all units will perform exactly one action per frame. This would make -the game look robotic. With default settings, it will never get this bad, but -you can always choose to alter the ``timestream`` configuration to your -preferred balance of speed vs. accuracy. +simulating thousands of FPS (and the ``max-frame-skip`` cap increased to 20), +all units will perform exactly one action per frame. This would make the game +look robotic. With default settings, it will never get this bad, but you can +always choose to alter the ``timestream`` configuration to your preferred +balance of speed vs. accuracy. Limitations ----------- diff --git a/timestream.lua b/timestream.lua index 832a7d2fc9..6ccfa1b89c 100644 --- a/timestream.lua +++ b/timestream.lua @@ -31,10 +31,10 @@ local SETTINGS = { default=1.0, }, { - name='max-boost', - internal_name='max_boost', - validate=function(arg) return argparse.nonnegativeInt(arg, 'max-boost') end, - default=50, + name='max-frame-skip', + internal_name='max_frame_skip', + validate=function(arg) return argparse.positiveInt(arg, 'max-frame-skip') end, + default=4, }, } @@ -76,8 +76,8 @@ local SEASON_TICK_TRIGGERS = { -- additional ticks we would like to skip at the next opportunity local timeskip_deficit = 0.0 -local function get_timeskip_per_tick(real_fps, desired_fps) - return (real_fps*(desired_fps - real_fps)) / desired_fps +local function get_desired_timeskip(real_fps, desired_fps) + return (desired_fps / real_fps) - 1 end local function get_next_timed_event_season_tick() @@ -152,21 +152,19 @@ local function adjust_buildings(timeskip) end local function on_tick() - local real_fps = df.global.enabler.calculated_fps + local real_fps = math.max(1, df.global.enabler.calculated_fps) if real_fps >= state.settings.fps then timeskip_deficit = 0.0 return end - local desired_timeskip = get_timeskip_per_tick(real_fps, state.settings.fps) + timeskip_deficit - local timeskip = math.floor(clamp_timeskip(desired_timeskip)) - timeskip_deficit = desired_timeskip - timeskip - print(('desired_timeskip=%s, timeskip=%s, timeskip_deficit=%s'):format(desired_timeskip, timeskip, timeskip_deficit)) + local desired_timeskip = get_desired_timeskip(real_fps, state.settings.fps) + timeskip_deficit + local timeskip = math.min(math.floor(clamp_timeskip(desired_timeskip)), state.settings.max_frame_skip) + timeskip_deficit = math.min(desired_timeskip - timeskip, state.settings.max_frame_skip) if timeskip <= 0 then return end - if timeskip > 0 then return end - - local new_cur_year_tick = df.global.cur_year_tick + timeskip + local calendar_timeskip = timeskip * state.settings.calendar_rate + local new_cur_year_tick = df.global.cur_year_tick + calendar_timeskip df.global.cur_season_tick = df.global.cur_season_tick + new_cur_year_tick//10 - df.global.cur_year_tick//10 df.global.cur_year_tick = new_cur_year_tick From 8fdda5f15d77d2c3e965cc7bdb56707bb063e829 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Wed, 26 Jun 2024 01:07:06 -0700 Subject: [PATCH 04/10] ensure caravans and crops stay in sync --- docs/timestream.rst | 6 ++-- timestream.lua | 88 ++++++++++++++++++++++++--------------------- 2 files changed, 51 insertions(+), 43 deletions(-) diff --git a/docs/timestream.rst b/docs/timestream.rst index 35dc3b81ae..ab60f31dae 100644 --- a/docs/timestream.rst +++ b/docs/timestream.rst @@ -76,7 +76,9 @@ Settings Values between 0 and 1 slow the calendar relative to the world, allowing units to get more done per day, and values above 1 speed the calendar relative to the world, causing the days to pass quicker and preventing - units from getting as much done per day. + units from getting as much done per day. The actual fps must be below the + configured target ``fps`` setting for the ``calendar-rate`` setting to take + effect. :max-frame-skip: Set the maximum number of ticks that can be skipped in one step. Dwarves can perform at most one action per step, and if too many @@ -138,5 +140,3 @@ Here is a (likely incomplete) list of game elements that are not affected by - Army movement across the world map (including raids sent out from the fort) - Liquid movement and evaporation -- Time that caravans stay at the trade depot -- Crop growth rates diff --git a/timestream.lua b/timestream.lua index 6ccfa1b89c..c4b7158601 100644 --- a/timestream.lua +++ b/timestream.lua @@ -66,15 +66,18 @@ local TICKS_PER_DAY = 1200 local TICKS_PER_WEEK = 7 * TICKS_PER_DAY -- determined from reverse engineering; don't skip these tick thresholds --- something important happens when cur_season_tick % == +-- something important happens when tick % == -- please keep remainder list elements in **descending** order local SEASON_TICK_TRIGGERS = { {mod=TICKS_PER_DAY//10, rem={0x6e, 0x50, 0x46, 0x3c, 0x32, 0x28, 0x14, 10, 0}}, {mod=TICKS_PER_WEEK//10, rem={0x32, 0x1e}}, } +local YEAR_TICK_TRIGGERS = { + {mod=100, rem={0}}, -- crop growth +} -- additional ticks we would like to skip at the next opportunity -local timeskip_deficit = 0.0 +local timeskip_deficit, calendar_timeskip_deficit = 0.0, 0.0 local function get_desired_timeskip(real_fps, desired_fps) return (desired_fps / real_fps) - 1 @@ -90,28 +93,38 @@ local function get_next_timed_event_season_tick() return next_event_tick end -local function get_next_trigger_season_tick() - local tick_offset = (df.global.cur_year_tick+1) % 10 - local is_season_tick = tick_offset == 0 - local next_season_tick = df.global.cur_season_tick + (is_season_tick and 0 or 1) - +local function get_next_trigger_tick(triggers, next_tick, is_tick_boundary) local next_trigger_tick = math.huge - for _, trigger in ipairs(SEASON_TICK_TRIGGERS) do - local cur_rem = next_season_tick % trigger.mod + for _, trigger in ipairs(triggers) do + local cur_rem = next_tick % trigger.mod for _, rem in ipairs(trigger.rem) do - if cur_rem < rem or (cur_rem == rem and is_season_tick) then - next_trigger_tick = math.min(next_trigger_tick, next_season_tick + (rem - cur_rem)) - break + if cur_rem < rem or (cur_rem == rem and is_tick_boundary) then + next_trigger_tick = math.min(next_trigger_tick, next_tick + (rem - cur_rem)) + goto continue end end + next_trigger_tick = math.min(next_trigger_tick, next_tick + trigger.mod - cur_rem + trigger.rem[#trigger.rem]) + ::continue:: end return next_trigger_tick end +local function get_next_trigger_year_tick() + return get_next_trigger_tick(YEAR_TICK_TRIGGERS, df.global.cur_year_tick + 1, true) +end + +local function get_next_trigger_season_tick() + local is_season_tick = (df.global.cur_year_tick+1) % 10 == 0 + local next_season_tick = df.global.cur_season_tick + (is_season_tick and 1 or 0) + return get_next_trigger_tick(SEASON_TICK_TRIGGERS, next_season_tick, is_season_tick) +end + local function clamp_timeskip(timeskip) if timeskip <= 0 then return 0 end local next_important_season_tick = math.min(get_next_timed_event_season_tick(), get_next_trigger_season_tick()) - return math.min(timeskip, (next_important_season_tick-df.global.cur_season_tick)*10) + return math.min(timeskip, + get_next_trigger_year_tick()-df.global.cur_year_tick-1, + df.global.cur_year_tick - (df.global.cur_year_tick % 10 + 1) + (next_important_season_tick - df.global.cur_season_tick)*10) end local function adjust_units(timeskip) @@ -127,34 +140,28 @@ local function adjust_units(timeskip) end end -local function adjust_crops(timeskip) - -- TODO -end - local function adjust_armies(timeskip) -- TODO end -local function adjust_evaporation(timeskip) - -- TODO -end - -local function adjust_caravans(timeskip) - -- TODO -end - -local function adjust_item_wear(timeskip) - -- TODO -end - -local function adjust_buildings(timeskip) - -- TODO +local function adjust_caravans(season_timeskip) + for i, caravan in ipairs(df.global.plotinfo.caravans) do + if caravan.trade_state == df.caravan_state.T_trade_state.Approaching or + caravan.trade_state == df.caravan_state.T_trade_state.AtDepot + then + caravan.time_remaining = caravan.time_remaining - season_timeskip + end + if caravan.time_remaining <= 0 then + caravan.time_remaining = 0 + dfhack.run_script('caravan', 'leave', tostring(i)) + end + end end local function on_tick() local real_fps = math.max(1, df.global.enabler.calculated_fps) if real_fps >= state.settings.fps then - timeskip_deficit = 0.0 + timeskip_deficit, calendar_timeskip_deficit = 0.0, 0.0 return end @@ -163,25 +170,26 @@ local function on_tick() timeskip_deficit = math.min(desired_timeskip - timeskip, state.settings.max_frame_skip) if timeskip <= 0 then return end - local calendar_timeskip = timeskip * state.settings.calendar_rate + local desired_calendar_timeskip = (timeskip * state.settings.calendar_rate) + calendar_timeskip_deficit + local calendar_timeskip = math.max(1, math.floor(desired_calendar_timeskip)) + calendar_timeskip_deficit = math.max(0, desired_calendar_timeskip - calendar_timeskip) + local new_cur_year_tick = df.global.cur_year_tick + calendar_timeskip - df.global.cur_season_tick = df.global.cur_season_tick + new_cur_year_tick//10 - df.global.cur_year_tick//10 + local season_timeskip = new_cur_year_tick//10 - df.global.cur_year_tick//10 + + df.global.cur_season_tick = df.global.cur_season_tick + season_timeskip df.global.cur_year_tick = new_cur_year_tick adjust_units(timeskip) - adjust_crops(timeskip) adjust_armies(timeskip) - adjust_evaporation(timeskip) - adjust_caravans(timeskip) - adjust_item_wear(timeskip) - adjust_buildings(timeskip) + adjust_caravans(season_timeskip) end ------------------------------------ -- hook management local function do_enable() - timeskip_deficit = 0 + timeskip_deficit, calendar_timeskip_deficit = 0.0, 0.0 state.enabled = true repeatutil.scheduleEvery(GLOBAL_KEY, 1, 'ticks', on_tick) end From cbfed9f6d5c0f1e392a197ef9969125aa16593fd Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Wed, 26 Jun 2024 02:05:02 -0700 Subject: [PATCH 05/10] ensure caravan message is displayed and handle noble job countdowns --- timestream.lua | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/timestream.lua b/timestream.lua index c4b7158601..72e000ee4d 100644 --- a/timestream.lua +++ b/timestream.lua @@ -149,7 +149,12 @@ local function adjust_caravans(season_timeskip) if caravan.trade_state == df.caravan_state.T_trade_state.Approaching or caravan.trade_state == df.caravan_state.T_trade_state.AtDepot then + local was_before_message_threshold = caravan.time_remaining >= 501 caravan.time_remaining = caravan.time_remaining - season_timeskip + if was_before_message_threshold and caravan.time_remaining <= 500 then + caravan.time_remaining = 501 + need_season_tick = true + end end if caravan.time_remaining <= 0 then caravan.time_remaining = 0 @@ -158,6 +163,16 @@ local function adjust_caravans(season_timeskip) end end +local noble_cooldowns = {'manager_cooldown', 'bookkeeper_cooldown'} +local function adjust_nobles(season_timeskip) + for _, field in ipairs(noble_cooldowns) do + df.global.plotinfo.nobles[field] = df.global.plotinfo.nobles[field] - season_timeskip + if df.global.plotinfo.nobles[field] < 0 then + df.global.plotinfo.nobles[field] = 0 + end + end +end + local function on_tick() local real_fps = math.max(1, df.global.enabler.calculated_fps) if real_fps >= state.settings.fps then @@ -172,6 +187,16 @@ local function on_tick() local desired_calendar_timeskip = (timeskip * state.settings.calendar_rate) + calendar_timeskip_deficit local calendar_timeskip = math.max(1, math.floor(desired_calendar_timeskip)) + if need_season_tick then + local old_ones = df.global.cur_year_tick % 10 + local new_ones = (df.global.cur_year_tick + calendar_timeskip) % 10 + if new_ones == 9 then + need_season_tick = false + elseif old_ones + calendar_timeskip >= 10 then + calendar_timeskip = 9 - old_ones + need_season_tick = false + end + end calendar_timeskip_deficit = math.max(0, desired_calendar_timeskip - calendar_timeskip) local new_cur_year_tick = df.global.cur_year_tick + calendar_timeskip @@ -183,6 +208,7 @@ local function on_tick() adjust_units(timeskip) adjust_armies(timeskip) adjust_caravans(season_timeskip) + adjust_nobles(season_timeskip) end ------------------------------------ @@ -190,6 +216,7 @@ end local function do_enable() timeskip_deficit, calendar_timeskip_deficit = 0.0, 0.0 + need_season_tick = false state.enabled = true repeatutil.scheduleEvery(GLOBAL_KEY, 1, 'ticks', on_tick) end From a852677106624913d3c8ae74808c87b15fdcf9c5 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Wed, 26 Jun 2024 02:35:33 -0700 Subject: [PATCH 06/10] update unit hunger, thirst, and sleepiness --- timestream.lua | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/timestream.lua b/timestream.lua index 72e000ee4d..4f16b53e05 100644 --- a/timestream.lua +++ b/timestream.lua @@ -127,6 +127,12 @@ local function clamp_timeskip(timeskip) df.global.cur_year_tick - (df.global.cur_year_tick % 10 + 1) + (next_important_season_tick - df.global.cur_season_tick)*10) end +local function has_caste_flag(unit, flag) + if unit.curse.rem_tags1[flag] then return false end + if unit.curse.add_tags1[flag] then return true end + return dfhack.units.casteFlagSet(unit.race, unit.caste, df.caste_raw_flags[flag]) +end + local function adjust_units(timeskip) for _, unit in ipairs(df.global.world.units.active) do if not dfhack.units.isActive(unit) then goto continue end @@ -136,6 +142,21 @@ local function adjust_units(timeskip) end end dfhack.units.subtractGroupActionTimers(unit, timeskip, df.unit_action_type_group.All) + local c2 = unit.counters2 + if not has_caste_flag(unit, 'NO_EAT') then + c2.hunger_timer = c2.hunger_timer + timeskip + end + if not has_caste_flag(unit, 'NO_DRINK') then + c2.thirst_timer = c2.thirst_timer + timeskip + end + if not has_caste_flag(unit, 'NO_SLEEP') then + local job = unit.job.current_job + if job and job.job_type == df.job_type.Sleep then + c2.sleepiness_timer = math.max(0, c2.sleepiness_timer - timeskip * 19) + else + c2.sleepiness_timer = c2.sleepiness_timer + timeskip + end + end ::continue:: end end From 1754295491bc83df1c9da6636a9bd43eb7197f99 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Wed, 26 Jun 2024 02:41:05 -0700 Subject: [PATCH 07/10] update sleepiness from Rest jobs --- timestream.lua | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/timestream.lua b/timestream.lua index 4f16b53e05..69b00da650 100644 --- a/timestream.lua +++ b/timestream.lua @@ -149,14 +149,17 @@ local function adjust_units(timeskip) if not has_caste_flag(unit, 'NO_DRINK') then c2.thirst_timer = c2.thirst_timer + timeskip end + local job = unit.job.current_job if not has_caste_flag(unit, 'NO_SLEEP') then - local job = unit.job.current_job if job and job.job_type == df.job_type.Sleep then c2.sleepiness_timer = math.max(0, c2.sleepiness_timer - timeskip * 19) else c2.sleepiness_timer = c2.sleepiness_timer + timeskip end end + if job and job.job_type == df.job_type.Rest then + c2.sleepiness_timer = math.max(0, c2.sleepiness_timer - timeskip * 200) + end ::continue:: end end From b5efa8dfedb6a83df0c79dffbb09357c2fa3a229 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Wed, 26 Jun 2024 02:45:37 -0700 Subject: [PATCH 08/10] remove no-longer-true example --- docs/timestream.rst | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/docs/timestream.rst b/docs/timestream.rst index ab60f31dae..73d92e9a3b 100644 --- a/docs/timestream.rst +++ b/docs/timestream.rst @@ -50,13 +50,8 @@ Examples ``timestream set calendar-rate 0.5`` Make the days twice as long and allow dwarves to accomplish twice as much - per day. - -``timestream set fps -1`` -``timestream set calendar-rate 2`` - Don't change the granularity of the simulation, but make the in-game days - pass twice as quickly, as if the sun sped up across the sky. Units will get - half as much done as usual per game day. + per day (as long as the target FPS is sufficiently above the FPS the game + is actually running at). ``timestream reset`` Reset settings to defaults: the vanilla FPS cap with no calendar speed From 40bbf266585fd18d5323917a7cdcca7cecfade99 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Wed, 26 Jun 2024 02:54:18 -0700 Subject: [PATCH 09/10] fix typo --- docs/timestream.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/timestream.rst b/docs/timestream.rst index 73d92e9a3b..ff9fa0a3ed 100644 --- a/docs/timestream.rst +++ b/docs/timestream.rst @@ -20,7 +20,7 @@ fort, and mature forts are much more fun to play. Note that whereas your dwarves zip around like you're running at 100 FPS, the onscreen FPS counter, if enabled, will still show a lower number. See the -`Technical details`_` section below if you're interested in what's going on +`Technical details`_ section below if you're interested in what's going on under the hood. You can also use this tool to change the in-game calendar speed. Your dwarves From f9955609eb01bd699b722b21e5aaf9da0b264647 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Wed, 26 Jun 2024 02:56:51 -0700 Subject: [PATCH 10/10] update changelog --- changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.txt b/changelog.txt index db0f77fb0e..0d94e9a3c9 100644 --- a/changelog.txt +++ b/changelog.txt @@ -36,6 +36,7 @@ Template for new versions: - `fix/population-cap`: fixes the situation where you continue to get migrant waves even when you are above your configured population cap - `fix/occupancy`: fixes issues where you can't build somewhere because the game tells you an item/unit/building is in the way but there's nothing there - `fix/sleepers`: (reinstated) fixes sleeping units belonging to a camp that never wake up. +- `timestream`: (reinstated) keep the game running quickly even when there are large numbers of units on the map ## New Features - `buildingplan`: dimension tooltip is now displayed for constructions and buildings that are designated over an area, like bridges and farm plots