Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
b41c88c
Added headless mode and command-line launching of saves and activities
Causeless Oct 10, 2026
04c86c7
Added DeterminismChecker for finding where runs of the sim diverge
Causeless Oct 10, 2026
e61b3f9
Added a script for testing that the sim is deterministic
Causeless Oct 10, 2026
d1c5fd3
Made headless mode not show message boxes or overwrite the abort save
Causeless Oct 10, 2026
2983728
Made MOs run their threaded scripts in order of unique ID
Causeless Oct 10, 2026
c9ec72f
Made LuaJIT seed its PRNG with a fixed value
Causeless Oct 10, 2026
44765a7
Gave AI its own random number generators
Causeless Oct 10, 2026
e54d43a
Made the unique IDs of MOs not depend on thread timing
Causeless Oct 10, 2026
f7c0aae
Made the numbers saved by Activities 64 bit
Causeless Oct 10, 2026
dca2299
Made the Writer write floating point numbers out exactly
Causeless Oct 10, 2026
04268c7
Fixed MuteAudioOnFocusLoss not being initialized
Causeless Oct 10, 2026
6cbaaf8
Added DisableCrashDialogs setting
Causeless Oct 10, 2026
f521860
Gave drawing and audio code a random number generator of their own
Causeless Oct 10, 2026
c8189b8
Made MOs added by several threads at once get added in order of uniqu…
Causeless Oct 10, 2026
65f076c
Made pathfinding updates go by sim time
Causeless Oct 10, 2026
6780b4b
Made LuaJIT hash objects by an ID kept in them instead of by their ad…
Causeless Oct 10, 2026
77f9a3d
Fixed TryPenetrate leaving the retardation unset when there's only air
Causeless Oct 10, 2026
6469a1b
Updated changelog
Causeless Oct 10, 2026
8a3aff7
Removed a sentence from the comment on s_DisableCrashDialogs
Causeless Oct 10, 2026
4c97f0f
Merge branch 'development' into deterministic-sim
Causeless Oct 11, 2026
5594ba4
Merge branch 'development' into deterministic-sim
Causeless Oct 11, 2026
fe00f22
Made LuaJIT give strings their IDs in order instead of from its PRNG
Causeless Oct 12, 2026
1ef5a5b
Fixed AHuman looking at its item in reach after the item was deleted
Causeless Oct 12, 2026
927d40b
Made the count of active pathing requests cover the whole of an async…
Causeless Oct 12, 2026
9657288
Made the pathfinding update wait for pathing requests set to complete…
Causeless Oct 12, 2026
5853fa7
Made the AI's area shooting go by sim time instead of real time
Causeless Oct 12, 2026
6a6c26a
Made the determinism test able to run several scenarios at the same time
Causeless Oct 12, 2026
22f2c80
Added -seed launch argument for giving the sim different random numbers
Causeless Oct 11, 2026
ec9bd95
Added -quit-when-over launch argument for quitting once the Activity …
Causeless Oct 12, 2026
c9f1438
Made the determinism test able to run with several seeds, and until t…
Causeless Oct 12, 2026
eabbaab
Made headless mode keep the audio going, without playing it to anyone
Causeless Oct 12, 2026
aa675d1
Fixed the HUD of ACraft using up the simulation's random numbers
Causeless Oct 12, 2026
62be25f
Fixed a typo in the comment on Actor::GetAboveHUDPos
Causeless Oct 12, 2026
0ceb84f
Made the heal effects and the AA drone not go by AboveHUDPos
Causeless Oct 12, 2026
3d0e32b
Made scripts not go by whether a sound is playing for what they do in…
Causeless Oct 12, 2026
1d7acb9
Fixed the Nanolyzer never playing its dissipate sound
Causeless Oct 12, 2026
a4ad008
Fixed Atom trails using up the simulation's random numbers
Causeless Oct 13, 2026
b7f1f8b
Made the global random functions use the lua state's generator during…
Causeless Oct 12, 2026
4f2c4e1
Merge branch 'development' into deterministic-sim
Causeless Oct 18, 2026
a9e33aa
Made headless runs skip drawing the MO color layer
Causeless Oct 17, 2026
b0f57a4
Made headless runs skip clearing the backbuffer every frame
Causeless Oct 17, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,15 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),

- `RealTimeLimitProgress` and `SimTimeLimitProgress` on `Timer` are now both writable properties in Lua, and not only readable.

- New launch arguments for running the game without anyone at the controls:
`-headless` runs it with a hidden window and no drawing, with the audio going on as usual but not played to anyone, and with the simulation going as fast as it can instead of in real time.
`-activity <class name> <preset name> <scene name>` and `-save <save name>` skip the menus and launch straight into an Activity or a saved game.
`-sim-updates <count>` quits after that many simulation updates.

- New determinism checker, for finding where two runs of the simulation stop matching. The launch argument `-determinism-record <file>` writes checkpoints of the simulation's state to a trace file as it goes, and `-determinism-verify <file>` checks a run against such a file, reporting the first checkpoint that doesn't match and quitting with a failure code. `Tools/determinism_test.py` runs missions this way and compares the runs.

- New `Settings.ini` property `DisableCrashDialogs = 0/1`. With it on, the game still logs an abort, a failed assert or an unhandled exception, but then quits without showing a dialog. This is for when the game is being run by something automated.

- New `Settings.ini` property `LaunchMinimised = 0/1` to have the game window start out minimised, so that launching the game doesn't take input focus or cover other windows. Disabled by default.

</details>
Expand Down Expand Up @@ -222,6 +231,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),

- Updated SDL2 to SDL3

- Much of what made two runs of the same mission play out differently is gone. Scripts on a Lua state now run in order of `UniqueID`, objects added by several threads at once get added in order of `UniqueID`, AI and drawing and audio code no longer use the simulation's random numbers, and pathfinding updates go by simulation time instead of real time.

- The `UniqueID` of a `MovableObject` is now 64 bit, with its upper bits saying which thread it was made on. Scripts still get it as a number, but it can be a lot larger than it used to be.

- LuaJIT now goes through a table that has tables, functions or userdata for keys in an order that doesn't depend on where those are in memory, so `pairs` gives the same order every run. This only holds on 64 bit x86 and ARM.

- Numbers saved with `Activity:SaveNumber()` are now kept as doubles instead of floats, and floating point numbers in INI and save files are written out exactly instead of being rounded to 6 digits.

</details>

<details><summary><b>Fixed</b></summary>
Expand Down Expand Up @@ -266,12 +283,18 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),

- Fixed an issue where if the first objects in the buy cart are items instead of an actor, they would be added to the first actor's inventory- even if it was an actor without an inventory (i.e a crab)

- Fixed the option to mute audio when the game window loses focus starting out with a garbage value, which then got written to `Settings.ini`.

- Fixed objects sometimes getting a garbage velocity when the terrain they hit was removed before they got to try and penetrate it, for example by a script's `OnCollideWithTerrain`.

- Fixed a macOS worker-thread message box that could hang, and a libc++ build error formatting save-game timestamps.

- Fixed a divide-by-zero on `AtomGroup` segments with no steps, which could feed bad values into the physics.

- Fixed a couple of uninitialized values (Atom step state and the simulation frame counter) that could cause inconsistent behavior between runs.

- Fixed the Techion Nanolyzer never playing its dissipate sound.

- Fixed `AHuman:EquipShieldInBGArm()` being able to take the wrong item out of the inventory, which left the device it equipped both held and in the inventory. The device then got dropped twice when its holder died, and the game could crash when quitting.

</details>
Expand Down
4 changes: 2 additions & 2 deletions Data/Base.rte/AI/HumanBehaviors.lua
Original file line number Diff line number Diff line change
Expand Up @@ -1706,7 +1706,7 @@ function HumanBehaviors.ShootArea(AI, Owner, Abort)
if Owner.FirearmIsReady then
AI.deviceState = AHuman.AIMING;
AI.Ctrl.AnalogAim = Vector(1,0):RadRotate(aim+aimError+RangeRand(-0.02, 0.02)*AI.aimSkill);
if ShootTimer:IsPastRealMS(aimTime) then
if ShootTimer:IsPastSimMS(aimTime) then
if Weapon.FullAuto then
AI.fire = true;
else
Expand All @@ -1732,7 +1732,7 @@ function HumanBehaviors.ShootArea(AI, Owner, Abort)
local _ai, _ownr, _abrt = coroutine.yield(); -- wait until next frame
if _abrt then return true end

if AI.UnseenTarget and CheckTargetTimer:IsPastRealMS(400*AI.aimSkill) then
if AI.UnseenTarget and CheckTargetTimer:IsPastSimMS(400*AI.aimSkill) then
if MovableMan:ValidMO(AI.UnseenTarget) and (AI.UnseenTarget.ClassName == "AHuman" or AI.UnseenTarget.ClassName == "ACrab") then
CheckTargetTimer:Reset();
if AI.UnseenTarget:GetController() and AI.UnseenTarget:GetController():IsState(Controller.WEAPON_FIRE) then
Expand Down
10 changes: 6 additions & 4 deletions Data/Base.rte/Actors/Mecha/AADrone/AADrone.lua
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,8 @@ function Update(self)
self.SAM = nil
end
elseif self.Frame > 0 and self.Vel.Largest < 12 then -- we have SAMs left
-- Look from above the drone. Not from AboveHUDPos, since that goes by how the HUD was last drawn, which can't be allowed to change the simulation
local ScanPos = self.Pos + Vector(0, -self.Height * 0.5 - 6)
if #self.ValidTargets < 1 then -- Find valid targets
if self.SearchTimer:IsPastSimMS(100) then
self.SearchTimer:Reset() -- Only search a few times/sec to reduce calculations per update
Expand All @@ -148,7 +150,7 @@ function Update(self)
local Trace = self:RotateOffset(Vector(0, -200))
if not SceneMan:CastStrengthRay(self.Pos, Trace, 5, Vector(), 9, -1, true) then -- Terrain str 5
local obstructed = false
local ID = SceneMan:CastMORay(self.AboveHUDPos, Trace, self.ID, self.IgnoresWhichTeam, 0, true, 15)
local ID = SceneMan:CastMORay(ScanPos, Trace, self.ID, self.IgnoresWhichTeam, 0, true, 15)
if ID < rte.NoMOID then
local MO = MovableMan:GetMOFromID(ID)
if ID ~= MO.RootID then
Expand Down Expand Up @@ -215,7 +217,7 @@ function Update(self)
else -- Check if the missile have a clear line of sight to any of the selected targets
local NewTarget = table.remove(self.ValidTargets).Actor -- Only check one target to reduce calculations per update
if NewTarget and MovableMan:ValidMO(NewTarget) and not NewTarget:IsDead() then
local Trace = SceneMan:ShortestDistance(self.AboveHUDPos, NewTarget.Pos, false)
local Trace = SceneMan:ShortestDistance(ScanPos, NewTarget.Pos, false)
-- Don't shoot at targets that are out of reach
if Trace.Magnitude < self.radarRange then
-- Don't shoot at targets that are very close to the brain
Expand All @@ -227,7 +229,7 @@ function Update(self)
-- First do a very inexact scan of half the distance to the target for friendly dropships and terrain
if
SceneMan:CastObstacleRay(
self.AboveHUDPos,
ScanPos,
Trace * 0.5,
Vector(),
Vector(),
Expand All @@ -238,7 +240,7 @@ function Update(self)
) < 0
then
-- If nothing was found, do a more exact scan for terrain all the way to the target
if not SceneMan:CastStrengthRay(self.AboveHUDPos, Trace, 5, Vector(), 9, -1, true) then -- Terrain str 5
if not SceneMan:CastStrengthRay(ScanPos, Trace, 5, Vector(), 9, -1, true) then -- Terrain str 5
self.SearchTimer:Reset()

-- Spawn the SAM
Expand Down
3 changes: 2 additions & 1 deletion Data/Base.rte/Actors/Mecha/Medic/Medic.lua
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,8 @@ function Update(self)
if self.crossTimer:IsPastSimTimeLimit() then
local cross = CreateMOSParticle("Particle Heal Effect", "Base.rte");
if cross then
cross.Pos = healTarget.AboveHUDPos + Vector(0, 4);
--Not placed by AboveHUDPos, since that goes by how the HUD was last drawn, which can't be allowed to change the simulation
cross.Pos = healTarget.Pos + Vector(0, -healTarget.Height * 0.5 - 2);
MovableMan:AddParticle(cross);
end
healTarget:RemoveWounds(self.healStrength);
Expand Down
3 changes: 2 additions & 1 deletion Data/Base.rte/Actors/Shared/Scripts/SelfHeal.lua
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ function Update(self)
end
end
local cross = self.healCrossParticle:Clone();
cross.Pos = self.AboveHUDPos + Vector(0, 4);
--Not placed by AboveHUDPos, since that goes by how the HUD was last drawn, which can't be allowed to change the simulation
cross.Pos = self.Pos + Vector(0, -self.Height * 0.5 - 2);
MovableMan:AddParticle(cross);
else
local timerRatio = self.healing.timer.ElapsedSimTimeMS/self.healing.delay;
Expand Down
3 changes: 2 additions & 1 deletion Data/Base.rte/Devices/Special/Medikit/Medikit.lua
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ function OnFire(self)
MovableMan:AddParticle(part);
end
local cross = CreateMOSParticle("Particle Heal Effect", "Base.rte");
cross.Pos = target.AboveHUDPos + Vector(0, 5);
--Not placed by AboveHUDPos, since that goes by how the HUD was last drawn, which can't be allowed to change the simulation
cross.Pos = target.Pos + Vector(0, -target.Height * 0.5 - 1);
MovableMan:AddParticle(cross);

if target.Status == Actor.DEAD and target.Health == target.MaxHealth and target.WoundCount == 0 then
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,16 @@ function Create(self)

self.overloadExplodeSound = CreateSoundContainer("Yskely Refinery Drill Overload Explode", "Browncoats.rte");
self.overloadSound = CreateSoundContainer("Yskely Refinery Drill Overload Start", "Browncoats.rte");
-- How long the overload sound is, in MS. The drill blows up once that much sim time has gone by, instead of when the audio system is done playing the sound, since that goes by real time.
self.overloadSoundLength = 16165;

self.loopSound = CreateSoundContainer("Yskely Refinery Drill Idle Loop", "Browncoats.rte");
self.loopSound:Play(self.Pos + Vector(0, 200));
end

function ThreadedUpdate(self)
if self.overloading then
if not self.overloadSound:IsBeingPlayed() and not self.overloadExploded then
if self.overloadFXTimer:IsPastSimMS(self.overloadSoundLength) and not self.overloadExploded then
self.overloadExplodeSound:Play(self.Pos + Vector(0, 200));
self.overloadExploded = true;
self.overloadFXTimer:Reset();
Expand Down
5 changes: 4 additions & 1 deletion Data/Coalition.rte/Devices/Weapons/GatlingGun/GatlingGun.lua
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ function Create(self)
self.origActivationDelay = self.ActivationDelay;
self.spinDownTimer = Timer();
self.currentlySpinningDown = false;
self.wasActivated = false;
end

function ThreadedUpdate(self)
if not self.currentlySpinningDown and not self:IsActivated() and not self:IsReloading() and self.ActiveSound:IsBeingPlayed() then
-- The gun starts spinning down when it is let go of after having been activated. It keeps track of that by itself instead of going by whether its ActiveSound is still being played, since that is up to the audio system, which goes by real time.
if not self.currentlySpinningDown and not self:IsActivated() and not self:IsReloading() and self.wasActivated then
self.spinDownTimer:Reset();
self.currentlySpinningDown = true;
self.activationDelay = self.origActivationDelay;
Expand All @@ -16,4 +18,5 @@ function ThreadedUpdate(self)
self.ActivationDelay = self.origActivationDelay * self.spinDownTimer.ElapsedSimTimeMS / self.DeactivationDelay;
self.currentlySpinningDown = false;
end
self.wasActivated = self:IsActivated() and not self:IsReloading();
end
5 changes: 4 additions & 1 deletion Data/Imperatus.rte/Devices/Weapons/Bulldog/Bulldog.lua
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ function Create(self)
self.origActivationDelay = self.ActivationDelay;
self.spinDownTimer = Timer();
self.currentlySpinningDown = false;
self.wasActivated = false;

self.maxRateOfFire = self.RateOfFire;
self.minRateOfFire = self.maxRateOfFire * 0.5;
Expand All @@ -11,7 +12,8 @@ function Create(self)
end

function Update(self)
if not self.currentlySpinningDown and not self:IsActivated() and not self:IsReloading() and self.ActiveSound:IsBeingPlayed() then
-- The gun starts spinning down when it is let go of after having been activated. It keeps track of that by itself instead of going by whether its ActiveSound is still being played, since that is up to the audio system, which goes by real time.
if not self.currentlySpinningDown and not self:IsActivated() and not self:IsReloading() and self.wasActivated then
self.spinDownTimer:Reset();
self.currentlySpinningDown = true;
self.activationDelay = self.origActivationDelay;
Expand All @@ -22,6 +24,7 @@ function Update(self)
self.ActivationDelay = self.origActivationDelay * self.spinDownTimer.ElapsedSimTimeMS / self.DeactivationDelay;
self.currentlySpinningDown = false;
end
self.wasActivated = self:IsActivated() and not self:IsReloading();

if self.FiredFrame then
self.RateOfFire = math.min(self.RateOfFire * self.increasePerShot, self.maxRateOfFire);
Expand Down
3 changes: 2 additions & 1 deletion Data/Ronin.rte/Devices/Tools/MedicalDartGun/MedicalDart.lua
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,8 @@ function OnAttach(self, parent)
parent = parent:GetRootParent();
if IsActor(parent) and MovableMan:IsActor(parent) then
local cross = CreateMOSParticle("Particle Heal Effect", "Base.rte");
cross.Pos = ToActor(parent).AboveHUDPos + Vector(0, 4);
--Not placed by AboveHUDPos, since that goes by how the HUD was last drawn, which can't be allowed to change the simulation
cross.Pos = parent.Pos + Vector(0, -ToActor(parent).Height * 0.5 - 2);
MovableMan:AddParticle(cross);
end
end
Expand Down
8 changes: 4 additions & 4 deletions Data/Techion.rte/Devices/Tools/Nanolyzer/Nanolyzer.lua
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ function Create(self)

--Sounds
self.dissipateSound = CreateSoundContainer("Dissipate Sound", "Techion.rte");
--The pitch is varied by the sound itself instead of rolled when it gets played. That only happens when it isn't already playing, which is up to the audio system, and that can't be what decides how many of the simulation's random numbers get used
self.dissipateSound.PitchVariation = 0.3;
self.disintegrationSound = CreateSoundContainer("Disintegration Sound", "Techion.rte");
end

Expand Down Expand Up @@ -78,8 +80,7 @@ function OnFire(self)
end
end
if gooCount > 0 then
if self.dissipateSound:IsBeingPlayed() then
self.dissipateSound.Pitch = RangeRand(RangeRand(0.7, 1.3));
if not self.dissipateSound:IsBeingPlayed() then
self.dissipateSound:Play(targetMO.Pos);
end
for i = 1, gooCount do
Expand Down Expand Up @@ -166,8 +167,7 @@ function OnFire(self)
glow.Pos = hitPos;
MovableMan:AddParticle(glow);

if self.dissipateSound:IsBeingPlayed() then
self.dissipateSound.Pitch = RangeRand(RangeRand(0.7, 1.3));
if not self.dissipateSound:IsBeingPlayed() then
self.dissipateSound:Play(hitPos);
end
end
Expand Down
5 changes: 4 additions & 1 deletion Data/Techion.rte/Devices/Weapons/GigaPulsar/GigaPulsar.lua
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@ function Create(self)
self.origActivationDelay = self.ActivationDelay;
self.spinDownTimer = Timer();
self.currentlySpinningDown = false;
self.wasActivated = false;

self.dingSound = false;
self.ejectEffect = CreateAEmitter("Techion Giga Pulsar Magazine Eject Effect", "Techion.rte");
end

function Update(self)
if not self.currentlySpinningDown and not self:IsActivated() and not self:IsReloading() and self.ActiveSound:IsBeingPlayed() then
-- The gun starts spinning down when it is let go of after having been activated. It keeps track of that by itself instead of going by whether its ActiveSound is still being played, since that is up to the audio system, which goes by real time.
if not self.currentlySpinningDown and not self:IsActivated() and not self:IsReloading() and self.wasActivated then
self.spinDownTimer:Reset();
self.currentlySpinningDown = true;
self.activationDelay = self.origActivationDelay;
Expand All @@ -19,6 +21,7 @@ function Update(self)
self.ActivationDelay = self.origActivationDelay * self.spinDownTimer.ElapsedSimTimeMS / self.DeactivationDelay;
self.currentlySpinningDown = false;
end
self.wasActivated = self:IsActivated() and not self:IsReloading();

if self.Magazine then
self.lastMag = self.Magazine;
Expand Down
6 changes: 5 additions & 1 deletion Data/Uzira.rte/Devices/Special/Gunsword/Gunsword.lua
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ function Create(self)
self.charge = 0;
self.throwSpeed = 0;
self.swingSound = CreateSoundContainer("Uzira.rte/Swing");
--How long the swing sounds are, in MS. The next swing waits for that much sim time, instead of for the audio system to be done playing the sound, since that goes by real time
self.swingSoundLength = 500;
self.swingSoundTimer = Timer();

self.particle = CreateMOPixel("Base.rte/Smack Particle");
self.particle.IgnoresTeamHits = true;
Expand Down Expand Up @@ -58,7 +61,8 @@ function Update(self)
end

parent.Vel = parent.Vel + Vector(swingSpeed/(5 + self.momentum), 0):RadRotate(parent:GetAimAngle(true));
if not self.swingSound:IsBeingPlayed() then
if self.swingSoundTimer:IsPastSimMS(self.swingSoundLength/self.swingSound.Pitch) then
self.swingSoundTimer:Reset();
self.swingSound.Pos = self.Pos;
self.swingSound:Play();
self:RemoveWounds(1);
Expand Down
2 changes: 2 additions & 0 deletions RTEA.vcxproj
Original file line number Diff line number Diff line change
Expand Up @@ -826,6 +826,7 @@
<ClInclude Include="Source\System\Reader.h" />
<ClInclude Include="Source\System\Serializable.h" />
<ClInclude Include="Source\System\Singleton.h" />
<ClInclude Include="Source\System\DeterminismChecker.h" />
<ClInclude Include="Source\System\System.h" />
<ClInclude Include="Source\System\Timer.h" />
<ClInclude Include="Source\System\Vector.h" />
Expand Down Expand Up @@ -1362,6 +1363,7 @@
</ClCompile>
<ClCompile Include="Source\System\PathFinder.cpp" />
<ClCompile Include="Source\System\Reader.cpp" />
<ClCompile Include="Source\System\DeterminismChecker.cpp" />
<ClCompile Include="Source\System\System.cpp" />
<ClCompile Include="Source\System\Timer.cpp" />
<ClCompile Include="Source\System\Vector.cpp" />
Expand Down
6 changes: 6 additions & 0 deletions RTEA.vcxproj.filters
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,9 @@
<ClInclude Include="Source\System\Singleton.h">
<Filter>System</Filter>
</ClInclude>
<ClInclude Include="Source\System\DeterminismChecker.h">
<Filter>System</Filter>
</ClInclude>
<ClInclude Include="Source\System\System.h">
<Filter>System</Filter>
</ClInclude>
Expand Down Expand Up @@ -685,6 +688,9 @@
<ClCompile Include="Source\System\Reader.cpp">
<Filter>System</Filter>
</ClCompile>
<ClCompile Include="Source\System\DeterminismChecker.cpp">
<Filter>System</Filter>
</ClCompile>
<ClCompile Include="Source\System\System.cpp">
<Filter>System</Filter>
</ClCompile>
Expand Down
5 changes: 3 additions & 2 deletions Source/Entities/ACraft.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -807,9 +807,10 @@ void ACraft::DrawHUD(BITMAP* pTargetBitmap, const Vector& targetPos, int whichSc
continue;
}

Vector exitRadius = RotateOffset(exit->GetVelocity().GetPerpendicular().SetMagnitude(exit->GetRadius()));
// These are without the spread, which gets rolled with the simulation's random numbers. Drawing can't be allowed to use those up.
Vector exitRadius = RotateOffset(exit->GetBaseVelocity().GetPerpendicular().SetMagnitude(exit->GetRadius()));
Vector exitCorner = m_Pos - targetPos + RotateOffset(exit->GetOffset()) + exitRadius;
Vector arrowVec = RotateOffset(exit->GetVelocity().SetMagnitude(exit->GetRange()));
Vector arrowVec = RotateOffset(exit->GetBaseVelocity().SetMagnitude(exit->GetRange()));
g_FrameMan.DrawLine(pTargetBitmap, exitCorner, exitCorner + arrowVec, 120, 120, EXITLINESPACING, m_ExitLinePhase);
exitCorner -= exitRadius * 2;
g_FrameMan.DrawLine(pTargetBitmap, exitCorner, exitCorner + arrowVec, 120, 120, EXITLINESPACING, m_ExitLinePhase);
Expand Down
6 changes: 5 additions & 1 deletion Source/Entities/ACraft.h
Original file line number Diff line number Diff line change
Expand Up @@ -71,10 +71,14 @@ namespace RTE {
/// @return The coordinates relative to the m_Pos of this' ACraft.
Vector GetOffset() const { return m_Offset; }

/// Gets the velocity of anything that exits through this.
/// Gets the velocity of anything that exits through this. Its spread is rolled anew every time, with the simulation's random numbers.
/// @return The velocity vector for anything exiting through this.
Vector GetVelocity() const { return m_Velocity * (1.0F + m_VelSpread * RandomNormalNum()); }

/// Gets the velocity of anything that exits through this as it is before any spread, which is also the way that this faces.
/// @return The velocity vector of this, with no spread to it.
Vector GetBaseVelocity() const { return m_Velocity; }

/// Gets the width from the center tanget created by the velocity vector
/// out from the offet point. This times two gives the total width of the
/// opening.
Expand Down
Loading
Loading