Skip to content

Change to 180 TPS - #1237

Draft
Kurtsley wants to merge 3 commits into
OpenApoc:masterfrom
Kurtsley:180-tick
Draft

Kurtsley wants to merge 3 commits into
OpenApoc:masterfrom
Kurtsley:180-tick

Conversation

@Kurtsley

@Kurtsley Kurtsley commented Aug 6, 2023

Copy link
Copy Markdown
Contributor

Addresses #997, #1216 so far.

The following is a list of what appears to have been fixed, many are very lightly tested and the changes could very easily have affected things in other parts of the game that I don't yet know about. This is just a running list to help me keep track of what has been done.

Battlescape

  • Walking speed seems to be a bit faster, closer to the original.
  • Explosive hazard type doesn't leave smoke behind anymore, it stayed around way too long. Was this a feature?
  • Explosions now expand faster to more closely match the OG game.
  • Explosions seem to be the correct radius now, although this needs more testing. I have only tried a couple items.

Cityscape

NA

Both

  • Projectile speed has been increased, it seems much closer to the OG game. TICK_SCALE was set to 18 instead of 36 in this case. This has not been tested in cityscape, likely will cause issues somewhere.

Problems

  • Rate of fire is now too fast. Should seperate speed from ROF if that is what is happening.

@FilmBoy84

Copy link
Copy Markdown
Collaborator

Looking good and thanks for working on this complex task

Regarding smoke and explosions - they should leave behind residual smoke as this is present in the OG

However, the smoke dissipates faster than that of a smoke grenade - which is likely affected by the tick counter and the value for dissipation

Regarding explosion speed, can you confirm the effects of the More-Options toggle for "Explosions damage instantly" still work correctly. When off, it should follow the OG behaviour with damage being applied after the explosion has reached maximal radius. When on, it should apply damage to a unit as soon as the expanding radius hits it - this was an optional fix to avoid a situation with the OG where a unit could run out of an explosion unscathed. By applying damage as soon as the expanding explosion hits the unit, they take damage under the normal rules for armour/pen immediately and cannot avoid being harmed.

@Kurtsley

Kurtsley commented Dec 9, 2023

Copy link
Copy Markdown
Contributor Author

I reduced the length that the smoke from explosions sticks around.

I'm starting to think, as has been stated before, that most of the issues are related to values being set according to a 144hz monitor. The original developer set the tick rate to his monitor refresh rate of 144. This PR is attempting to set the tick rate to three times the game fraterate instead of the original 1:1 ratio that was used when writing this originally. It comes down to going through every single instance where the tick rate is being used in logic and deciding if it an animation or time issue. With the battle AI, most decisions are based on ticks per second. For example, the think interval is ticks per turn / 4. I would assume that the game wants the AI to make decisions every quarter second, so this value should not be touched. Grenade priming also works like this, it is based on time.

I think any value that works with the animation of sprites and is based on ticks may need to be looked into. So far, I haven't touched much beyond changing the tick multiplier and messing with the explosions. From what I can see, the explosion radius isn't a tick issue, dividing by 1.5 instead of 2 seems to fix it. I will need to look at damage next. The HE cannon does seem pretty weak, but the power is correct. The rate of fire is still correct, it just had to be divided by 5 instead of 4 to match the 36fps assumption.

TLDR: There will be no magic solution, basically everything needs to be tweaked to work with the new tick rate.

@FilmBoy84

Copy link
Copy Markdown
Collaborator

Many thanks for your continued work on this; agreed, navigating the mess of calcs linked to both TPS and FPS and correcting them (or separating them) has always been the scary side of this issue

Are we definitely sure on the Original Game being 18fps and 36tps or is that reversed at times (for example, in slow speed simulation)?

@JonnyH @Skin36 if you are able to advise on the exact values the original game assumes for each when your time allows, it would be really appreciated 🍻

Do we know who did the original work linking calcs to an assumed 144FPS? I can't seem to find on the tracker (so may be pre-github)

@JonnyH

JonnyH commented Dec 9, 2023

Copy link
Copy Markdown
Collaborator

Git blame suggests the original 60->144 "change" was 76dda03

Before that the numbers I had there were 100% made up - it was still when stuff was being reverse engineered so I tweaked some things to "look about right" for the vehicle movement. Anything that might have been correct at that time is likely due to luck rather than intent :)

@JonnyH

JonnyH commented Dec 9, 2023

Copy link
Copy Markdown
Collaborator

Once we have a consistent time base, we might "finally" decouple the frame rate and tick rate. But that's a separate change :)

I think the best way is to rework the main {(tick gamestate) (handle input and UI events) ( render)} loop with something more similar to timers - we have 2 events, the update tick, triggered every 1/180 seconds, and the imput/ui/render, which should probably be aligned to display refresh. The simplest way of doing this would be to have both on a thread, so they block and just queue up if the tick would have happened while the thread it busy. The render would then know it can access the current state without worrying about asynchronous access.

The input and UI updates would be coupled with the render, as interacting at a subframe latency probably doesn't actually make sense. While this might cause jitter of +- 1 frame's worth of ticks, apoc not being a super accuracy-based competitive twitch-reflex game likely means it doesn't really matter either.

Though we might need to think of what happens if the system cannot keep up - I don't want (for example) the render events to completely starve the queue, but an ordered event "queue" of those should correctly interleave so everything just goes slower when the system isn't able to keep up, which is probably fine for a single player game. Though might need to ensure the queue size is bounded.

@khallmark

Copy link
Copy Markdown
Contributor

@FilmBoy84 asked upthread whether we're definitely sure about 36 TPS. I have something concrete on that.

Provenance first, so you can weigh it: this comes from a fork where I've been running Ghidra against the retail UFO2P.EXE / TACP.EXE. That's the same class of activity as the extractor offsets and tools/extractors/docs/hexa.txt this repo already ships, but I'd rather flag it than have it discovered.

36 TPS holds up

The negative result first: there is no printable "36 ticks" string in either binary. So 36 was never directly bound, and community observation is all it ever rested on.

But the fire-overlay scheduler corroborates it structurally. FUN_0007b7f8 (TACP) drives fire progression on a fixed cadence, and its item-contact pass fires when its counter reaches 0x24 — 36. A once-per-36-tick contact pass is a once-per-second pass, which only makes sense on a 36 TPS base. Two other cadences in the same scheduler are recoverable and checkable: complete X-rows processed per vanilla tick is mapY * mapZ / 0x48, and the turn-based round wrap runs a 400-iteration batch (FUN_000b8c50).

So: 36 is right, and this PR is right to keep VANILLA_TICKS_PER_SECOND = 36.

The multiplier is the free parameter — the hazard is un-derived constants

TICKS_MULTIPLIER isn't constrained by the original at all; it's purely our own resolution. 4 or 5 are both defensible provided everything scales off it.

That's the real risk here, and this PR already demonstrates it: FUEL_TICKS_PER_SECOND had to be hand-edited 144 → 180. Good catch — but the fact that it needed a hand edit is the tell. Anything else in the tree that hardcodes a value derived from 144 rather than from TICKS_PER_SECOND is now silently 25% off, and won't announce itself.

Might be worth making those derive instead:

static const int FUEL_TICKS_PER_SECOND = TICKS_PER_SECOND;

and grepping for other bare 144s, so the multiplier becomes genuinely a one-line change.

A way to validate whichever multiplier you pick

The three scheduler cadences above are measurable against the original. If a chosen multiplier is right, fire spread rate and the turn-based wrap should line up when mapped through it. That's a falsifiable check rather than eyeballing walk speed.

Happy to write up the fire-scheduler findings in more detail (binary, generation and file offset for each) if that'd be useful to anyone auditing #997.

khallmark added a commit to khallmark/OpenApoc that referenced this pull request Aug 26, 2026
Regenerate the comparison report after the timing header moved and bring the previously stale parity status table in sync with its in-repo sources.

Related to OpenApoc#1237
khallmark added a commit to khallmark/OpenApoc that referenced this pull request Aug 26, 2026
Carry the upstream horizontal-scrollbar behavior through the develop reconciliation so wide research-lab rows remain navigable.

Related to OpenApoc#1237
khallmark added a commit to khallmark/OpenApoc that referenced this pull request Aug 26, 2026
Apply the repository clang-format 18 target across the reconciled source set and make the style file language-neutral so Objective-C++ sources are checked by the same target.

Related to OpenApoc#1237
khallmark added a commit to khallmark/OpenApoc that referenced this pull request Aug 26, 2026
Sort equal-count report rows deterministically, lock the ordering with a pure Python test, and refresh the generated report from the reconciled sources.

Related to OpenApoc#1237
khallmark added a commit to khallmark/OpenApoc that referenced this pull request Aug 26, 2026
Document the binary-proven 36 TPS base, add named vanilla-unit conversions, and scale the calibrated fuel threshold with the engine multiplier while retaining the 4/144 configuration canaries.

Related to OpenApoc#1237
khallmark added a commit to khallmark/OpenApoc that referenced this pull request Aug 26, 2026
Regenerate the comparison report after the named vanilla constants moved the timing definitions to new source lines.

Related to OpenApoc#1237
khallmark added a commit to khallmark/OpenApoc that referenced this pull request Aug 26, 2026
Remove redundant constructor-definition semicolons and disambiguate one multiplication expression so clang-format 18.1.3 and 18.1.8 produce the same tree. This keeps the reconciled base lint-clean on both CI and the local toolchain.

Related to OpenApoc#1237
khallmark added a commit to khallmark/OpenApoc that referenced this pull request Aug 26, 2026
Use the MainMenu Skirmish route directly, accept both sides of the current nested StageCmd loss, and preserve the complete battle as a known-red acceptance for the live-FIFO scheduler change.

Related to OpenApoc#1237
khallmark added a commit to khallmark/OpenApoc that referenced this pull request Aug 26, 2026
Document the binary-proven 36 TPS base, add named vanilla-unit conversions, and scale the calibrated fuel threshold with the engine multiplier while retaining the 4/144 configuration canaries.

Related to OpenApoc#1237
khallmark added a commit to khallmark/OpenApoc that referenced this pull request Aug 26, 2026
Regenerate the comparison report after the named vanilla constants moved the timing definitions to new source lines.

Related to OpenApoc#1237
khallmark added a commit to khallmark/OpenApoc that referenced this pull request Aug 26, 2026
The recovered invasion coefficients corroborate the community-observed
36 TPS interpretation without independently proving its absolute
cadence. Add named vanilla-unit conversions and scale the calibrated
fuel threshold while retaining explicit 4/144 configuration canaries.

Related to OpenApoc#1237
khallmark added a commit to khallmark/OpenApoc that referenced this pull request Aug 26, 2026
Record the invasion coefficient ratios across the durable timing evidence
and state their mathematical limit so future work does not mistake
corroboration for an independently recovered absolute clock.

Related to OpenApoc#1237
khallmark added a commit to khallmark/OpenApoc that referenced this pull request Aug 26, 2026
Centralize tactical result decisions, require exact arena workloads, and preserve engine exit truth in validation receipts so incomplete automation cannot pass as gameplay success.

Related to OpenApoc#1237
khallmark added a commit to khallmark/OpenApoc that referenced this pull request Aug 26, 2026
Reconcile durable reports, findings, runtime comments, and lock-test descriptions with the evidence boundary already stated in gametime.h. The repository selects 36 TPS as its observational compatibility canon; recovered coefficients corroborate only the unit-free 1:60:86400 ratio, while recovered shield logic binds a 36-tick interval rather than an absolute second.

Verification: 37/37 CTest, focused timing/shield locks, compare-report regeneration and test, ignored-binary check, diff check, and complete clang-format 18.1.3/18.1.8 dry runs.

Related to OpenApoc#1237
khallmark added a commit to khallmark/OpenApoc that referenced this pull request Aug 26, 2026
Prevent partial generations, invalid force specifications, and unclean engine shutdowns from producing scoreable robot evidence. Socket-free contracts cover every arena lifecycle.

Related to OpenApoc#1237
khallmark added a commit to khallmark/OpenApoc that referenced this pull request Aug 26, 2026
Replace the last two overstatements in timing comments with compatibility-base and observed-catalog language. Cite the canonical ISO non-4 UFO2P FUN_0006d384 evidence with its exact VA and file offset in the durable solution note.

Verification: focused 2/2 and full 37/37 CTest, report regeneration check/test, ignored-binary and diff checks, and complete clang-format 18.1.3/18.1.8 dry runs.

Related to OpenApoc#1237
khallmark added a commit to khallmark/OpenApoc that referenced this pull request Aug 26, 2026
Make finite Skirmish shutdown truth unconditional and treat adversarial ledgers as commit points so scores cannot outlive missing evidence. Roll back uncommitted generation scoring on persistence failure.

Related to OpenApoc#1237

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants