diff --git a/CHANGELOG.md b/CHANGELOG.md index eb6f14a0ea..09e2344586 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 ` and `-save ` skip the menus and launch straight into an Activity or a saved game. + `-sim-updates ` quits after that many simulation updates. + +- New determinism checker, for finding where two runs of the simulation stop matching. The launch argument `-determinism-record ` writes checkpoints of the simulation's state to a trace file as it goes, and `-determinism-verify ` 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. @@ -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. +
Fixed @@ -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.
diff --git a/Data/Base.rte/AI/HumanBehaviors.lua b/Data/Base.rte/AI/HumanBehaviors.lua index ee82b06049..7e04b72349 100644 --- a/Data/Base.rte/AI/HumanBehaviors.lua +++ b/Data/Base.rte/AI/HumanBehaviors.lua @@ -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 @@ -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 diff --git a/Data/Base.rte/Actors/Mecha/AADrone/AADrone.lua b/Data/Base.rte/Actors/Mecha/AADrone/AADrone.lua index dbc595a7b1..b88bdc6582 100644 --- a/Data/Base.rte/Actors/Mecha/AADrone/AADrone.lua +++ b/Data/Base.rte/Actors/Mecha/AADrone/AADrone.lua @@ -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 @@ -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 @@ -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 @@ -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(), @@ -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 diff --git a/Data/Base.rte/Actors/Mecha/Medic/Medic.lua b/Data/Base.rte/Actors/Mecha/Medic/Medic.lua index a6978169e5..1dae09badf 100644 --- a/Data/Base.rte/Actors/Mecha/Medic/Medic.lua +++ b/Data/Base.rte/Actors/Mecha/Medic/Medic.lua @@ -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); diff --git a/Data/Base.rte/Actors/Shared/Scripts/SelfHeal.lua b/Data/Base.rte/Actors/Shared/Scripts/SelfHeal.lua index e3a1773d4a..43601ae3a9 100644 --- a/Data/Base.rte/Actors/Shared/Scripts/SelfHeal.lua +++ b/Data/Base.rte/Actors/Shared/Scripts/SelfHeal.lua @@ -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; diff --git a/Data/Base.rte/Devices/Special/Medikit/Medikit.lua b/Data/Base.rte/Devices/Special/Medikit/Medikit.lua index 93fa9f38ca..84af2b5dd4 100644 --- a/Data/Base.rte/Devices/Special/Medikit/Medikit.lua +++ b/Data/Base.rte/Devices/Special/Medikit/Medikit.lua @@ -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 diff --git a/Data/Browncoats.rte/Scenes/Objects/Breakables/RefineryDrill/RefineryDrill.lua b/Data/Browncoats.rte/Scenes/Objects/Breakables/RefineryDrill/RefineryDrill.lua index 3c6673acab..ad9d376efc 100644 --- a/Data/Browncoats.rte/Scenes/Objects/Breakables/RefineryDrill/RefineryDrill.lua +++ b/Data/Browncoats.rte/Scenes/Objects/Breakables/RefineryDrill/RefineryDrill.lua @@ -12,6 +12,8 @@ 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)); @@ -19,7 +21,7 @@ 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(); diff --git a/Data/Coalition.rte/Devices/Weapons/GatlingGun/GatlingGun.lua b/Data/Coalition.rte/Devices/Weapons/GatlingGun/GatlingGun.lua index f9a375c879..a0d358c795 100644 --- a/Data/Coalition.rte/Devices/Weapons/GatlingGun/GatlingGun.lua +++ b/Data/Coalition.rte/Devices/Weapons/GatlingGun/GatlingGun.lua @@ -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; @@ -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 \ No newline at end of file diff --git a/Data/Imperatus.rte/Devices/Weapons/Bulldog/Bulldog.lua b/Data/Imperatus.rte/Devices/Weapons/Bulldog/Bulldog.lua index 79e82ac8c9..76d26ca275 100644 --- a/Data/Imperatus.rte/Devices/Weapons/Bulldog/Bulldog.lua +++ b/Data/Imperatus.rte/Devices/Weapons/Bulldog/Bulldog.lua @@ -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; @@ -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; @@ -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); diff --git a/Data/Ronin.rte/Devices/Tools/MedicalDartGun/MedicalDart.lua b/Data/Ronin.rte/Devices/Tools/MedicalDartGun/MedicalDart.lua index 81db5e8036..3882d2f3c7 100644 --- a/Data/Ronin.rte/Devices/Tools/MedicalDartGun/MedicalDart.lua +++ b/Data/Ronin.rte/Devices/Tools/MedicalDartGun/MedicalDart.lua @@ -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 diff --git a/Data/Techion.rte/Devices/Tools/Nanolyzer/Nanolyzer.lua b/Data/Techion.rte/Devices/Tools/Nanolyzer/Nanolyzer.lua index eac4444b31..9af49109e9 100644 --- a/Data/Techion.rte/Devices/Tools/Nanolyzer/Nanolyzer.lua +++ b/Data/Techion.rte/Devices/Tools/Nanolyzer/Nanolyzer.lua @@ -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 @@ -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 @@ -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 diff --git a/Data/Techion.rte/Devices/Weapons/GigaPulsar/GigaPulsar.lua b/Data/Techion.rte/Devices/Weapons/GigaPulsar/GigaPulsar.lua index 9bf1441141..c4da6b21a4 100644 --- a/Data/Techion.rte/Devices/Weapons/GigaPulsar/GigaPulsar.lua +++ b/Data/Techion.rte/Devices/Weapons/GigaPulsar/GigaPulsar.lua @@ -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; @@ -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; diff --git a/Data/Uzira.rte/Devices/Special/Gunsword/Gunsword.lua b/Data/Uzira.rte/Devices/Special/Gunsword/Gunsword.lua index bc4ae3f9af..18cccc4022 100644 --- a/Data/Uzira.rte/Devices/Special/Gunsword/Gunsword.lua +++ b/Data/Uzira.rte/Devices/Special/Gunsword/Gunsword.lua @@ -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; @@ -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); diff --git a/RTEA.vcxproj b/RTEA.vcxproj index b774588b95..731c2ad830 100644 --- a/RTEA.vcxproj +++ b/RTEA.vcxproj @@ -826,6 +826,7 @@ + @@ -1362,6 +1363,7 @@ + diff --git a/RTEA.vcxproj.filters b/RTEA.vcxproj.filters index 9459d73c13..e5f13d5197 100644 --- a/RTEA.vcxproj.filters +++ b/RTEA.vcxproj.filters @@ -88,6 +88,9 @@ System + + System + System @@ -685,6 +688,9 @@ System + + System + System diff --git a/Source/Entities/ACraft.cpp b/Source/Entities/ACraft.cpp index f43c42db59..9ae1f7d4f8 100644 --- a/Source/Entities/ACraft.cpp +++ b/Source/Entities/ACraft.cpp @@ -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); diff --git a/Source/Entities/ACraft.h b/Source/Entities/ACraft.h index b276a13bee..dc8fe9238a 100644 --- a/Source/Entities/ACraft.h +++ b/Source/Entities/ACraft.h @@ -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. diff --git a/Source/Entities/AHuman.cpp b/Source/Entities/AHuman.cpp index 4bfecf9edd..22367c3356 100644 --- a/Source/Entities/AHuman.cpp +++ b/Source/Entities/AHuman.cpp @@ -2059,8 +2059,8 @@ void AHuman::PreControllerUpdate() { } } - // Item currently set to be within reach has expired or is now out of range - if (m_pItemInReach && (m_pItemInReach->ToDelete() || !m_pItemInReach->IsPickupableBy(this) || !g_MovableMan.IsDevice(m_pItemInReach) || g_SceneMan.ShortestDistance(reachPoint, m_pItemInReach->GetPos(), g_SceneMan.SceneWrapsX()).MagnitudeIsGreaterThan(reach + m_pItemInReach->GetRadius()))) { + // Item currently set to be within reach has expired or is now out of range. Whether it still exists has to be checked before anything else, since it can't be looked at if it doesn't. + if (m_pItemInReach && (!g_MovableMan.IsDevice(m_pItemInReach) || m_pItemInReach->ToDelete() || !m_pItemInReach->IsPickupableBy(this) || g_SceneMan.ShortestDistance(reachPoint, m_pItemInReach->GetPos(), g_SceneMan.SceneWrapsX()).MagnitudeIsGreaterThan(reach + m_pItemInReach->GetRadius()))) { m_pItemInReach = nullptr; } @@ -2672,7 +2672,7 @@ void AHuman::DrawThrowingReticle(BITMAP* targetBitmap, const Vector& targetPos, points[i].RadRotate(adjustedAimAngle); points[i] += m_pFGArm->GetJointPos(); - g_PostProcessMan.RegisterGlowDotEffect(points[i], YellowDot, RandomNum(63, 127)); + g_PostProcessMan.RegisterGlowDotEffect(points[i], YellowDot, g_DrawRandomGenerator.RandomNum(63, 127)); putpixel(targetBitmap, points[i].GetFloorIntX() - targetPos.GetFloorIntX(), points[i].GetFloorIntY() - targetPos.GetFloorIntY(), g_YellowGlowColor); } } @@ -2933,8 +2933,8 @@ void AHuman::DrawHUD(BITMAP* pTargetBitmap, const Vector& targetPos, int whichSc } // Pickup GUI - if (!m_Controller.IsState(PIE_MENU_ACTIVE) && m_pItemInReach) { - std::snprintf(str, sizeof(str), " %c %s", -49, m_pItemInReach->GetPresetName().c_str()); + if (const HeldDevice* itemInReach = GetItemInReach(); itemInReach && !m_Controller.IsState(PIE_MENU_ACTIVE)) { + std::snprintf(str, sizeof(str), " %c %s", -49, itemInReach->GetPresetName().c_str()); pSmallFont->DrawAligned(&allegroBitmap, drawPos.GetFloorIntX(), drawPos.GetFloorIntY() + m_HUDStack + 3, str, GUIFont::Centre); m_HUDStack -= 9; } @@ -2965,17 +2965,19 @@ void AHuman::SetLimbPathPushForce(MovementState movementState, float newForce) { int AHuman::WhilePieMenuOpenListener(const PieMenu* pieMenu) { int result = Actor::WhilePieMenuOpenListener(pieMenu); + const HeldDevice* itemInReach = GetItemInReach(); + for (PieSlice* pieSlice: GetPieMenu()->GetPieSlices()) { switch (pieSlice->GetType()) { case PieSliceType::Pickup: case PieSliceType::Reload: - pieSlice->SetType(m_pItemInReach ? PieSliceType::Pickup : PieSliceType::Reload); - pieSlice->SetIcon(dynamic_cast(g_PresetMan.GetEntityPreset("Icon", m_pItemInReach ? "Pick Up" : "Refresh")->Clone())); + pieSlice->SetType(itemInReach ? PieSliceType::Pickup : PieSliceType::Reload); + pieSlice->SetIcon(dynamic_cast(g_PresetMan.GetEntityPreset("Icon", itemInReach ? "Pick Up" : "Refresh")->Clone())); if (pieSlice->GetType() == PieSliceType::Pickup) { - if (m_pFGArm || (m_pBGArm && m_pItemInReach->IsOneHanded())) { + if (m_pFGArm || (m_pBGArm && itemInReach->IsOneHanded())) { pieSlice->SetEnabled(m_Status != INACTIVE); - pieSlice->SetDescription("Pick Up " + m_pItemInReach->GetPresetName()); + pieSlice->SetDescription("Pick Up " + itemInReach->GetPresetName()); } else { pieSlice->SetEnabled(false); pieSlice->SetDescription("No Arm"); diff --git a/Source/Entities/Activity.h b/Source/Entities/Activity.h index fa111b4149..e396f44e6b 100644 --- a/Source/Entities/Activity.h +++ b/Source/Entities/Activity.h @@ -556,11 +556,11 @@ namespace RTE { /// Saves a number which will be stored in our ini. /// @param key The key of the saved number. /// @param value The number to save. - void SaveNumber(const std::string& key, float value) { m_SavedValues.SaveNumber(key, value); }; + void SaveNumber(const std::string& key, double value) { m_SavedValues.SaveNumber(key, value); }; /// Loads and returns a previously saved number. /// @param key The key of the string to load. - float LoadNumber(const std::string& key) { return m_SavedValues.LoadNumber(key); }; + double LoadNumber(const std::string& key) { return m_SavedValues.LoadNumber(key); }; #pragma endregion protected: diff --git a/Source/Entities/Actor.cpp b/Source/Entities/Actor.cpp index 7ea36f424b..3bb15246eb 100644 --- a/Source/Entities/Actor.cpp +++ b/Source/Entities/Actor.cpp @@ -505,6 +505,11 @@ float Actor::GetBaseMass() { return m_BaseMass; } +HeldDevice* Actor::GetItemInReach() const { + // The item can get deleted or picked up by someone else at any point after it was found to be in reach, and nothing lets us know when that happens. + return g_MovableMan.IsDevice(m_pItemInReach) ? m_pItemInReach : nullptr; +} + bool Actor::IsPlayerControlled() const { return m_Controller.GetInputMode() == Controller::CIM_PLAYER && m_Controller.GetPlayer() >= 0; } diff --git a/Source/Entities/Actor.h b/Source/Entities/Actor.h index 00be2213e7..2380ab4b3d 100644 --- a/Source/Entities/Actor.h +++ b/Source/Entities/Actor.h @@ -225,7 +225,7 @@ namespace RTE { /// @return A Vector with the absolute position of this' eye or view point. virtual Vector GetEyePos() const { return m_Pos; } - /// Gets the absoltue position of the top of this' HUD stack. + /// Gets the absolute position of the top of this' HUD stack. /// @return A Vector with the absolute position of this' HUD stack top point. Vector GetAboveHUDPos() const override { return m_Pos + Vector(0, m_HUDStack + 6); } @@ -252,8 +252,8 @@ namespace RTE { Vector GetViewPoint() const { return m_ViewPoint.IsZero() ? m_Pos : m_ViewPoint; } /// Gets the item that is within reach of the Actor at this frame, ready to be be picked up. Ownership is NOT transferred! - /// @return A pointer to the item that has been determined to be within reach of this Actor, if any. - HeldDevice* GetItemInReach() const { return m_pItemInReach; } + /// @return A pointer to the item that has been determined to be within reach of this Actor, if any. An item that has since been deleted or picked up doesn't count. + HeldDevice* GetItemInReach() const; /// Gets the direction where this is looking/aiming. /// @return A Vector with the direction in which this is looking along. diff --git a/Source/Entities/AtomGroup.cpp b/Source/Entities/AtomGroup.cpp index 30ff2a91ff..2089f3ba45 100644 --- a/Source/Entities/AtomGroup.cpp +++ b/Source/Entities/AtomGroup.cpp @@ -85,7 +85,7 @@ int AtomGroup::Create(const AtomGroup& reference, bool onlyCopyOwnerAtoms) { atomCopy->SetIgnoreMOIDsByGroup(&m_IgnoreMOIDs); m_Atoms.push_back(atomCopy); - long subgroupID = atomCopy->GetSubID(); + int64_t subgroupID = atomCopy->GetSubID(); if (subgroupID != 0) { if (m_SubGroups.find(subgroupID) == m_SubGroups.end()) { m_SubGroups.insert({subgroupID, std::vector()}); @@ -258,7 +258,7 @@ float AtomGroup::GetMomentOfInertia() { return m_MomentOfInertia; } -void AtomGroup::AddAtoms(const std::vector& atomList, long subgroupID, const Vector& offset, const Matrix& offsetRotation) { +void AtomGroup::AddAtoms(const std::vector& atomList, int64_t subgroupID, const Vector& offset, const Matrix& offsetRotation) { if (m_SubGroups.count(subgroupID) == 0) { m_SubGroups.insert({subgroupID, std::vector()}); } @@ -281,7 +281,7 @@ void AtomGroup::AddAtoms(const std::vector& atomList, long subgroupID, co } } -bool AtomGroup::RemoveAtoms(long removeID) { +bool AtomGroup::RemoveAtoms(int64_t removeID) { std::size_t oldSize = m_Atoms.size(); m_Atoms.erase( @@ -304,7 +304,7 @@ bool AtomGroup::RemoveAtoms(long removeID) { return removedAny; } -bool AtomGroup::UpdateSubAtoms(long subgroupID, const Vector& newOffset, const Matrix& newOffsetRotation) { +bool AtomGroup::UpdateSubAtoms(int64_t subgroupID, const Vector& newOffset, const Matrix& newOffsetRotation) { if (m_SubGroups.empty() || m_SubGroups.count(subgroupID) == 0) { return false; } diff --git a/Source/Entities/AtomGroup.h b/Source/Entities/AtomGroup.h index 935a196436..068c0678e4 100644 --- a/Source/Entities/AtomGroup.h +++ b/Source/Entities/AtomGroup.h @@ -139,7 +139,7 @@ namespace RTE { /// Note, this resets the moment of inertia, which then has to be recalculated. /// @param newAtom A pointer to an Atom that will pushed onto the end of the list. Ownership IS transferred! /// @param subgroupID The subgroup ID that the new Atom will have within the group. - void AddAtom(Atom* newAtom, long subgroupID = 0) { + void AddAtom(Atom* newAtom, int64_t subgroupID = 0) { newAtom->SetSubID(subgroupID); m_Atoms.push_back(newAtom); m_MomentOfInertia = 0.0F; @@ -150,12 +150,12 @@ namespace RTE { /// @param subgroupID The desired subgroup ID for the Atoms being added. /// @param offset An offset that should be applied to all added Atoms. /// @param offsetRotation The rotation of the placed Atoms around the specified offset. - void AddAtoms(const std::vector& atomList, long subgroupID = 0, const Vector& offset = Vector(), const Matrix& offsetRotation = Matrix()); + void AddAtoms(const std::vector& atomList, int64_t subgroupID = 0, const Vector& offset = Vector(), const Matrix& offsetRotation = Matrix()); /// Removes all Atoms of a specific subgroup ID from this AtomGroup. /// @param removeID The ID of the subgroup of Atoms to remove. /// @return Whether any Atoms of that subgroup ID were found and removed. - bool RemoveAtoms(long removeID); + bool RemoveAtoms(int64_t removeID); /// Removes all atoms in this AtomGroup, leaving it empty of Atoms. void RemoveAllAtoms() { @@ -168,14 +168,14 @@ namespace RTE { /// Gets whether the AtomGroup contains a subgroup with the given subgroupID. /// @param subgroupID The subgroupID to check for. /// @return Whether this AtomGroup contains a subgroup with the given subgroupID. - bool ContainsSubGroup(long subgroupID) const { return m_SubGroups.count(subgroupID) != 0; } + bool ContainsSubGroup(int64_t subgroupID) const { return m_SubGroups.count(subgroupID) != 0; } /// Updates the offsets of a subgroup of Atoms in this AtomGroup. This allows repositioning a subgroup to match the position and rotation of the graphical representation of it's owner MOSR. /// @param subgroupID The desired subgroup ID of the Atoms to update offsets for. /// @param newOffset The change in offset for the Atoms of the specified subgroup. /// @param newOffsetRotation The rotation of the updated Atoms around the specified offset. /// @return Whether any Atoms were found and updated for the specified subgroup. - bool UpdateSubAtoms(long subgroupID = 0, const Vector& newOffset = Vector(), const Matrix& newOffsetRotation = Matrix()); + bool UpdateSubAtoms(int64_t subgroupID = 0, const Vector& newOffset = Vector(), const Matrix& newOffsetRotation = Matrix()); #pragma endregion #pragma region Travel @@ -301,7 +301,7 @@ namespace RTE { // TODO: It's probably worth trying out changing this from a list to a vector. m_Atoms is iterated over often and we could probably get some big gainz by doing this swap. // The downside is anytime attachables with atoms get added we may have the cost of resizing the vector but that's an uncommon use case while iterating over atoms happens multiple times per frame. std::vector m_Atoms; //!< List of Atoms that constitute the group. Owned by this. - std::unordered_map> m_SubGroups; //!< Sub groupings of Atoms. Points to Atoms owned in m_Atoms. Not owned. + std::unordered_map> m_SubGroups; //!< Sub groupings of Atoms. Points to Atoms owned in m_Atoms. Not owned. MOSRotating* m_OwnerMOSR; //!< The owner of this AtomGroup. The owner is obviously not owned by this AtomGroup. float m_StoredOwnerMass; //!< The stored mass for the owner MOSR. Used to figure out when the moment of inertia needs to be recalculated due to significant mass changes. diff --git a/Source/Entities/Attachable.h b/Source/Entities/Attachable.h index 0584e494cc..c86f311e89 100644 --- a/Source/Entities/Attachable.h +++ b/Source/Entities/Attachable.h @@ -298,11 +298,11 @@ namespace RTE { #pragma region Collision Management /// Gets the subgroup ID of this' Atoms. /// @return The subgroup ID of this' Atoms. - long GetAtomSubgroupID() const { return m_AtomSubgroupID; } + int64_t GetAtomSubgroupID() const { return m_AtomSubgroupID; } /// Sets the subgroup ID of this' Atoms /// @param newID A long describing the new subgroup id of this' Atoms. - void SetAtomSubgroupID(long subgroupID = 0) { m_AtomSubgroupID = subgroupID; } + void SetAtomSubgroupID(int64_t subgroupID = 0) { m_AtomSubgroupID = subgroupID; } /// Gets whether this Attachable currently has terrain collisions enabled and it's atoms are present in the parent AtomGroup. /// Attachables with Attachable parents that don't collide with terrain will not collide with terrain. This chains up to the root parent. @@ -386,7 +386,7 @@ namespace RTE { /// Removes the Attachable corresponding to the passed in UniqueID and sets its parent to nullptr. Does not add it to MovableMan or add break wounds. /// @param attachableUniqueID The UniqueID of the Attachable to remove. /// @return A pointer to the removed Attachable. Ownership IS transferred! - Attachable* RemoveAttachable(long attachableUniqueID) final { return MOSRotating::RemoveAttachable(attachableUniqueID); } + Attachable* RemoveAttachable(int64_t attachableUniqueID) final { return MOSRotating::RemoveAttachable(attachableUniqueID); } /// Removes the Attachable corresponding to the passed in UniqueID and sets its parent to nullptr. Optionally adds it to MovableMan and/or adds break wounds. /// If the Attachable is not set to delete or delete when removed from its parent, and addToMovableMan is false, the caller must hang onto a pointer to the Attachable ahead of time to avoid memory leaks. @@ -394,7 +394,7 @@ namespace RTE { /// @param addToMovableMan Whether or not to add the Attachable to MovableMan once it has been removed. /// @param addBreakWounds Whether or not to add break wounds to the removed Attachable and this Attachable. /// @return A pointer to the removed Attachable, if it wasn't added to MovableMan or nullptr if it was. Ownership IS transferred! - Attachable* RemoveAttachable(long attachableUniqueID, bool addToMovableMan, bool addBreakWounds) final { return MOSRotating::RemoveAttachable(attachableUniqueID, addToMovableMan, addBreakWounds); } + Attachable* RemoveAttachable(int64_t attachableUniqueID, bool addToMovableMan, bool addBreakWounds) final { return MOSRotating::RemoveAttachable(attachableUniqueID, addToMovableMan, addBreakWounds); } /// Removes the passed in Attachable and sets its parent to nullptr. Does not add it to MovableMan or add break wounds. /// @param attachable The Attachable to remove. @@ -467,7 +467,7 @@ namespace RTE { float m_InheritsVelWhenDetached; //!< How much of the parents velocity this attachable inherits when detached float m_InheritsAngularVelWhenDetached; //!< How much of the parents angular velocity this attachable inherits when detached - long m_AtomSubgroupID; //!< The Atom IDs this' atoms will have when attached and added to a parent's AtomGroup. + int64_t m_AtomSubgroupID; //!< The Atom IDs this' atoms will have when attached and added to a parent's AtomGroup. bool m_CollidesWithTerrainWhileAttached; //!< Whether this attachable currently has terrain collisions enabled while it's attached to a parent. bool m_IgnoresParticlesWhileAttached; //!< Whether this Attachable should ignore collisions with single-atom MOs while attached. diff --git a/Source/Entities/DynamicSong.cpp b/Source/Entities/DynamicSong.cpp index ef71cb244c..688d81ddd4 100644 --- a/Source/Entities/DynamicSong.cpp +++ b/Source/Entities/DynamicSong.cpp @@ -148,12 +148,12 @@ SoundContainer& DynamicSongSection::SelectTransitionSoundContainer() { } } - unsigned int randomIndex = validIndices[RandomNum(0, static_cast(validIndices.size()) - 1)]; + unsigned int randomIndex = validIndices[g_DrawRandomGenerator.RandomNum(0, static_cast(validIndices.size()) - 1)]; m_LastTransitionSoundContainerIndex = randomIndex; return m_TransitionSoundContainers[randomIndex]; } case SHUFFLE: { - unsigned int randomSelection = RandomNum(0, static_cast(m_TransitionShuffleUnplayedIndices.size() - 1)); + unsigned int randomSelection = g_DrawRandomGenerator.RandomNum(0, static_cast(m_TransitionShuffleUnplayedIndices.size() - 1)); unsigned int selectedIndex = m_TransitionShuffleUnplayedIndices[randomSelection]; m_TransitionShuffleUnplayedIndices.erase(m_TransitionShuffleUnplayedIndices.begin() + randomSelection); m_LastTransitionSoundContainerIndex = selectedIndex; @@ -188,12 +188,12 @@ SoundContainer& DynamicSongSection::SelectSoundContainer() { } } - unsigned int randomIndex = validIndices[RandomNum(0, static_cast(validIndices.size()) - 1)]; + unsigned int randomIndex = validIndices[g_DrawRandomGenerator.RandomNum(0, static_cast(validIndices.size()) - 1)]; m_LastSoundContainerIndex = randomIndex; return m_SoundContainers[randomIndex]; } case SHUFFLE: { - unsigned int randomSelection = RandomNum(0, static_cast(m_ShuffleUnplayedIndices.size() - 1)); + unsigned int randomSelection = g_DrawRandomGenerator.RandomNum(0, static_cast(m_ShuffleUnplayedIndices.size() - 1)); unsigned int selectedIndex = m_ShuffleUnplayedIndices[randomSelection]; m_ShuffleUnplayedIndices.erase(m_ShuffleUnplayedIndices.begin() + randomSelection); m_LastSoundContainerIndex = selectedIndex; diff --git a/Source/Entities/HDFirearm.cpp b/Source/Entities/HDFirearm.cpp index dee6346f0f..59c5ac42cb 100644 --- a/Source/Entities/HDFirearm.cpp +++ b/Source/Entities/HDFirearm.cpp @@ -1045,7 +1045,7 @@ void HDFirearm::DrawHUD(BITMAP* pTargetBitmap, const Vector& targetPos, int whic } float sharpLength = std::max(m_MaxSharpLength * m_SharpAim, 20.0F); - int glowStrength = RandomNum(95, 159); + int glowStrength = g_DrawRandomGenerator.RandomNum(95, 159); int pointCount; if (playerControlled && sharpLength > 20.0F) { pointCount = m_SharpAim > 0.5F ? 4 : 3; diff --git a/Source/Entities/MOSRotating.cpp b/Source/Entities/MOSRotating.cpp index fd036c301f..860e6488f1 100644 --- a/Source/Entities/MOSRotating.cpp +++ b/Source/Entities/MOSRotating.cpp @@ -1460,7 +1460,7 @@ bool MOSRotating::AttachableIsHardcoded(const Attachable* attachableToCheck) con return false; } - long attachableUniqueID = attachableToCheck->GetUniqueID(); + int64_t attachableUniqueID = attachableToCheck->GetUniqueID(); return m_HardcodedAttachableUniqueIDsAndRemovers.find(attachableUniqueID) != m_HardcodedAttachableUniqueIDsAndRemovers.end() || m_HardcodedAttachableUniqueIDsAndSetters.find(attachableUniqueID) != m_HardcodedAttachableUniqueIDsAndSetters.end(); } @@ -1484,7 +1484,7 @@ void MOSRotating::AddAttachable(Attachable* attachable, const Vector& parentOffs } } -Attachable* MOSRotating::RemoveAttachable(long attachableUniqueID, bool addToMovableMan, bool addBreakWounds) { +Attachable* MOSRotating::RemoveAttachable(int64_t attachableUniqueID, bool addToMovableMan, bool addBreakWounds) { if (MovableObject* attachableAsMovableObject = g_MovableMan.FindObjectByUniqueID(attachableUniqueID)) { return RemoveAttachable(dynamic_cast(attachableAsMovableObject), addToMovableMan, addBreakWounds); } @@ -1507,7 +1507,7 @@ Attachable* MOSRotating::RemoveAttachable(Attachable* attachable, bool addToMova attachable->SetParent(nullptr); m_AttachableAndWoundMass -= attachable->GetMass(); - std::unordered_map>::iterator hardcodedAttachableMapEntry = m_HardcodedAttachableUniqueIDsAndSetters.find(attachable->GetUniqueID()); + std::unordered_map>::iterator hardcodedAttachableMapEntry = m_HardcodedAttachableUniqueIDsAndSetters.find(attachable->GetUniqueID()); if (hardcodedAttachableMapEntry != m_HardcodedAttachableUniqueIDsAndSetters.end()) { hardcodedAttachableMapEntry->second(this, nullptr); m_HardcodedAttachableUniqueIDsAndSetters.erase(hardcodedAttachableMapEntry); diff --git a/Source/Entities/MOSRotating.h b/Source/Entities/MOSRotating.h index 967af352ca..ef2f5cc791 100644 --- a/Source/Entities/MOSRotating.h +++ b/Source/Entities/MOSRotating.h @@ -251,7 +251,7 @@ namespace RTE { /// Removes the Attachable corresponding to the passed in UniqueID and sets its parent to nullptr. Does not add it to MovableMan or add break wounds. /// @param attachableUniqueID The UniqueID of the Attachable to remove. /// @return A pointer to the removed Attachable. Ownership IS transferred! - virtual Attachable* RemoveAttachable(long attachableUniqueID) { return RemoveAttachable(attachableUniqueID, false, false); } + virtual Attachable* RemoveAttachable(int64_t attachableUniqueID) { return RemoveAttachable(attachableUniqueID, false, false); } /// Removes the Attachable corresponding to the passed in UniqueID and sets its parent to nullptr. Optionally adds it to MovableMan and/or adds break wounds. /// If the Attachable is not set to delete or delete when removed from its parent, and addToMovableMan is false, the caller must hang onto a pointer to the Attachable ahead of time to avoid memory leaks. @@ -259,7 +259,7 @@ namespace RTE { /// @param addToMovableMan Whether or not to add the Attachable to MovableMan once it has been removed. /// @param addBreakWounds Whether or not to add break wounds to the removed Attachable and this MOSRotating. /// @return A pointer to the removed Attachable, if it wasn't added to MovableMan or nullptr if it was. Ownership IS transferred! - virtual Attachable* RemoveAttachable(long attachableUniqueID, bool addToMovableMan, bool addBreakWounds); + virtual Attachable* RemoveAttachable(int64_t attachableUniqueID, bool addToMovableMan, bool addBreakWounds); /// Removes the passed in Attachable and sets its parent to nullptr. Does not add it to MovableMan or add break wounds. /// @param attachable The Attachable to remove. @@ -552,9 +552,9 @@ namespace RTE { bool m_ExitWoundBurstSoundPlayedThisFrame; // The list of Attachables currently attached and Owned by this. std::list m_Attachables; - std::unordered_set m_ReferenceHardcodedAttachableUniqueIDs; //!< An unordered set is filled with the Unique IDs of all of the reference object's hardcoded Attachables when using the copy Create. - std::unordered_map> m_HardcodedAttachableUniqueIDsAndSetters; //!< An unordered map of Unique IDs to setter lambda functions, used to call the appropriate hardcoded Attachable setter when a hardcoded Attachable is removed. - std::unordered_map> m_HardcodedAttachableUniqueIDsAndRemovers; //!< An unordered map of Unique IDs to remove lambda functions, used to call the appropriate hardcoded Attachable remover when a hardcoded Attachable is removed and calling the setter with nullptr won't work. + std::unordered_set m_ReferenceHardcodedAttachableUniqueIDs; //!< An unordered set is filled with the Unique IDs of all of the reference object's hardcoded Attachables when using the copy Create. + std::unordered_map> m_HardcodedAttachableUniqueIDsAndSetters; //!< An unordered map of Unique IDs to setter lambda functions, used to call the appropriate hardcoded Attachable setter when a hardcoded Attachable is removed. + std::unordered_map> m_HardcodedAttachableUniqueIDsAndRemovers; //!< An unordered map of Unique IDs to remove lambda functions, used to call the appropriate hardcoded Attachable remover when a hardcoded Attachable is removed and calling the setter with nullptr won't work. const Attachable* m_RadiusAffectingAttachable; //!< A pointer to the Attachable that is currently affecting the radius. Used for some efficiency benefits. float m_FarthestAttachableDistanceAndRadius; //!< The distance + radius of the radius affecting Attachable. float m_AttachableAndWoundMass; //!< The mass of all Attachables and wounds on this MOSRotating. Used in combination with its actual mass and any other affecting factors to get its total mass. diff --git a/Source/Entities/MovableObject.cpp b/Source/Entities/MovableObject.cpp index 7bd2a05b5a..331086242f 100644 --- a/Source/Entities/MovableObject.cpp +++ b/Source/Entities/MovableObject.cpp @@ -23,9 +23,22 @@ using namespace RTE; AbstractClassInfo(MovableObject, SceneObject); -std::atomic MovableObject::m_UniqueIDCounter = 1; +std::array, 1 << MovableObject::c_UniqueIDThreadBits> MovableObject::m_UniqueIDCounters{}; std::string MovableObject::ms_EmptyString = ""; +int64_t MovableObject::GetNextUniqueID() { + // The main thread is number 0, and the thread that is running a threaded Lua state has the number after that state's. + int64_t threadNumber = 0; + if (const LuaStateWrapper* luaState = g_LuaMan.GetThreadLuaStateOverride(); luaState && luaState != &g_LuaMan.GetMasterScriptState()) { + threadNumber = 1 + (luaState - g_LuaMan.GetThreadedScriptStates().data()); + } + + int64_t count = ++m_UniqueIDCounters[threadNumber]; + RTEAssert(count < (int64_t(1) << c_UniqueIDCountBits), "Ran out of unique IDs to give to MOs!"); + + return (threadNumber << c_UniqueIDCountBits) | count; +} + MovableObject::MovableObject() { Clear(); } @@ -1047,11 +1060,11 @@ void MovableObject::SetHitWhatMOID(MOID id) { m_LastCollisionSimFrameNumber = g_MovableMan.GetSimUpdateFrameNumber(); } -long int MovableObject::HitWhatParticleUniqueID() const { +int64_t MovableObject::HitWhatParticleUniqueID() const { return m_LastCollisionSimFrameNumber == g_MovableMan.GetSimUpdateFrameNumber() ? m_ParticleUniqueIDHit : 0; } -void MovableObject::SetHitWhatParticleUniqueID(long int id) { +void MovableObject::SetHitWhatParticleUniqueID(int64_t id) { m_ParticleUniqueIDHit = id; m_LastCollisionSimFrameNumber = g_MovableMan.GetSimUpdateFrameNumber(); } diff --git a/Source/Entities/MovableObject.h b/Source/Entities/MovableObject.h index b1c7599c78..771cd5a61e 100644 --- a/Source/Entities/MovableObject.h +++ b/Source/Entities/MovableObject.h @@ -13,6 +13,8 @@ #include "Material.h" #include "MovableMan.h" +#include +#include #include struct BITMAP; @@ -982,13 +984,19 @@ namespace RTE { /// @param newRestThreshold New rest threshold value void SetRestThreshold(int newRestThreshold) { m_RestThreshold = newRestThreshold; } + static constexpr int c_UniqueIDBits = 46; //!< How many bits a unique ID can take up. Scripts get unique IDs as Lua numbers, which only come out in full when turned into text if they're 14 digits or less. + static constexpr int c_UniqueIDThreadBits = 7; //!< How many of the upper bits of a unique ID hold the number of the thread it was handed out on. + static constexpr int c_UniqueIDCountBits = c_UniqueIDBits - c_UniqueIDThreadBits; //!< How many bits of a unique ID are left to count with. + /// Returns the next unique id for MO's and increments unique ID counter + /// The upper bits of a unique ID are the number of the thread that it was handed out on, and the rest count up separately for each thread. + /// So when several threads make MOs at the same time, as the threaded Lua states do, each MO still gets the same unique ID every time. /// @return Returns the next unique id. - static long GetNextUniqueID() { return ++m_UniqueIDCounter; } + static int64_t GetNextUniqueID(); /// Returns this MO's unique persistent ID /// @return Returns this MO's unique persistent ID - long GetUniqueID() const { return m_UniqueID; } + int64_t GetUniqueID() const { return m_UniqueID; } /// Gets the preset name and unique ID of this MO, often useful for error messages. /// @return A string containing the unique ID and preset name of this MO. @@ -1058,11 +1066,11 @@ namespace RTE { /// Returns the unique ID of the particle hit at the previously taken Travel /// @return Unique ID of the particle hit at the previously taken Travel - long int HitWhatParticleUniqueID() const; + int64_t HitWhatParticleUniqueID() const; /// Returns the unique ID of the particle hit at the previously taken Travel /// @param id Unique ID of the particle hit at the previously taken Travel. - void SetHitWhatParticleUniqueID(long int id); + void SetHitWhatParticleUniqueID(int64_t id); /// Returns the terrain material the previously taken Tarvel /// hit, if any. @@ -1142,8 +1150,8 @@ namespace RTE { /// @param A MovableObject object which is passed in by reference. // Member variables static Entity::ClassInfo m_sClass; - // Global counter with unique ID's - static std::atomic m_UniqueIDCounter; + // Counters with unique ID's, one for each thread number + static std::array, 1 << c_UniqueIDThreadBits> m_UniqueIDCounters; // The type of MO this is, either Actor, Item, or Particle int m_MOType; float m_Mass; // In metric kilograms (kg). @@ -1277,7 +1285,7 @@ namespace RTE { bool m_PostEffectEnabled; // This object's unique persistent ID - long m_UniqueID; + int64_t m_UniqueID; // In which radis should we look to remove orphaned terrain on terrain penetration, // must not be greater than SceneMan::ORPHANSIZE, or will be truncated int m_RemoveOrphanTerrainRadius; @@ -1300,7 +1308,7 @@ namespace RTE { // Terrain material hit during last travel unsigned char m_TerrainMatHit; // Unique ID of particle hit this MO - long int m_ParticleUniqueIDHit; + int64_t m_ParticleUniqueIDHit; // Number of sim update frame when last collision was detected unsigned int m_LastCollisionSimFrameNumber; int m_SimUpdatesBetweenScriptedUpdates; //!< The number of Sim updates between each scripted update for this MovableObject. diff --git a/Source/Entities/SLBackground.cpp b/Source/Entities/SLBackground.cpp index df0237773b..e01d00b91d 100644 --- a/Source/Entities/SLBackground.cpp +++ b/Source/Entities/SLBackground.cpp @@ -179,7 +179,7 @@ void SLBackground::Update() { break; case SpriteAnimMode::ALWAYSRANDOM: while (m_Frame == prevFrame) { - m_Frame = RandomNum(0, m_FrameCount - 1); + m_Frame = g_DrawRandomGenerator.RandomNum(0, m_FrameCount - 1); } break; case SpriteAnimMode::ALWAYSPINGPONG: diff --git a/Source/Entities/Scene.cpp b/Source/Entities/Scene.cpp index 3c08ece911..53a8bdab42 100644 --- a/Source/Entities/Scene.cpp +++ b/Source/Entities/Scene.cpp @@ -2387,6 +2387,12 @@ void Scene::UpdatePathFinding() { constexpr int nodeUpdatesPerCall = 100; constexpr int maxUnupdatedMaterialAreas = 1000; + // With pathing requests set to complete immediately, they get waited for here as well. + // Putting the update off until none of them are active would have it happen on whichever sim update the pathfinding threads happen to be done by. + if (g_SettingsMan.GetForceImmediatePathingRequestCompletion()) { + BlockUntilAllPathingRequestsComplete(); + } + // If any pathing requests are active, don't update things yet, wait till they're finished // TODO: this can indefinitely block updates if pathing requests are made every frame. Figure out a solution for this // Either force-complete pathing requests occasionally, or delay starting new pathing requests if we've not updated in a while @@ -2487,7 +2493,7 @@ void Scene::Update() { } // Occasionally update pathfinding. There's a tradeoff between how often updates occur vs how big the multithreaded batched node lists to update are. - if (m_PartialPathUpdateTimer.IsPastRealMS(100)) { + if (m_PartialPathUpdateTimer.IsPastSimMS(100)) { UpdatePathFinding(); } } diff --git a/Source/Entities/SoundSet.cpp b/Source/Entities/SoundSet.cpp index 7820637b04..e07183e64d 100644 --- a/Source/Entities/SoundSet.cpp +++ b/Source/Entities/SoundSet.cpp @@ -262,13 +262,13 @@ bool SoundSet::SelectNextSounds() { /// Internal lambda function to pick a random sound that's not the previously played sound. Done to avoid scoping issues inside the switch below. /// auto selectSoundRandom = [&selectedVectorSize, &unselectedVectorSize, this]() { - if (unselectedVectorSize > 0 && (selectedVectorSize == 1 || RandomNum(0, 1) == 1)) { + if (unselectedVectorSize > 0 && (selectedVectorSize == 1 || g_DrawRandomGenerator.RandomNum(0, 1) == 1)) { std::swap(selectedVectorSize, unselectedVectorSize); - m_CurrentSelection = {!m_CurrentSelection.first, RandomNum(0, selectedVectorSize - 1)}; + m_CurrentSelection = {!m_CurrentSelection.first, g_DrawRandomGenerator.RandomNum(0, selectedVectorSize - 1)}; } else { - size_t soundToSelect = RandomNum(0, selectedVectorSize - 1); + size_t soundToSelect = g_DrawRandomGenerator.RandomNum(0, selectedVectorSize - 1); while (soundToSelect == m_CurrentSelection.second) { - soundToSelect = RandomNum(0, selectedVectorSize - 1); + soundToSelect = g_DrawRandomGenerator.RandomNum(0, selectedVectorSize - 1); } m_CurrentSelection.second = soundToSelect; } diff --git a/Source/Lua/LuaBindingsEntities.cpp b/Source/Lua/LuaBindingsEntities.cpp index da0a81547a..cb425407e2 100644 --- a/Source/Lua/LuaBindingsEntities.cpp +++ b/Source/Lua/LuaBindingsEntities.cpp @@ -902,14 +902,14 @@ LuaBindingRegisterFunctionDefinitionForType(EntityLuaBindings, MOSRotating) { .def("EraseFromTerrain", &MOSRotating::EraseFromTerrain) .def("AddAttachable", (void(MOSRotating::*)(Attachable * attachableToAdd)) & MOSRotating::AddAttachable, luabind::adopt(_2)) .def("AddAttachable", (void(MOSRotating::*)(Attachable * attachableToAdd, const Vector& parentOffset)) & MOSRotating::AddAttachable, luabind::adopt(_2)) - .def("RemoveAttachable", (Attachable * (MOSRotating::*)(long uniqueIDOfAttachableToRemove)) & MOSRotating::RemoveAttachable, luabind::adopt(luabind::return_value)) - .def("RemoveAttachable", (Attachable * (MOSRotating::*)(long uniqueIDOfAttachableToRemove, bool addToMovableMan, bool addBreakWounds)) & MOSRotating::RemoveAttachable, luabind::adopt(luabind::return_value)) + .def("RemoveAttachable", (Attachable * (MOSRotating::*)(int64_t uniqueIDOfAttachableToRemove)) & MOSRotating::RemoveAttachable, luabind::adopt(luabind::return_value)) + .def("RemoveAttachable", (Attachable * (MOSRotating::*)(int64_t uniqueIDOfAttachableToRemove, bool addToMovableMan, bool addBreakWounds)) & MOSRotating::RemoveAttachable, luabind::adopt(luabind::return_value)) .def("RemoveAttachable", (Attachable * (MOSRotating::*)(Attachable * attachableToRemove)) & MOSRotating::RemoveAttachable, luabind::adopt(luabind::return_value)) .def("RemoveAttachable", (Attachable * (MOSRotating::*)(Attachable * attachableToRemove, bool addToMovableMan, bool addBreakWounds)) & MOSRotating::RemoveAttachable) .def("AddEmitter", (void(MOSRotating::*)(Attachable * attachableToAdd)) & MOSRotating::AddAttachable, luabind::adopt(_2)) .def("AddEmitter", (void(MOSRotating::*)(Attachable * attachableToAdd, const Vector& parentOffset)) & MOSRotating::AddAttachable, luabind::adopt(_2)) - .def("RemoveEmitter", (Attachable * (MOSRotating::*)(long uniqueIDOfAttachableToRemove)) & MOSRotating::RemoveAttachable, luabind::adopt(luabind::return_value)) - .def("RemoveEmitter", (Attachable * (MOSRotating::*)(long uniqueIDOfAttachableToRemove, bool addToMovableMan, bool addBreakWounds)) & MOSRotating::RemoveAttachable, luabind::adopt(luabind::return_value)) + .def("RemoveEmitter", (Attachable * (MOSRotating::*)(int64_t uniqueIDOfAttachableToRemove)) & MOSRotating::RemoveAttachable, luabind::adopt(luabind::return_value)) + .def("RemoveEmitter", (Attachable * (MOSRotating::*)(int64_t uniqueIDOfAttachableToRemove, bool addToMovableMan, bool addBreakWounds)) & MOSRotating::RemoveAttachable, luabind::adopt(luabind::return_value)) .def("RemoveEmitter", (Attachable * (MOSRotating::*)(Attachable * attachableToRemove)) & MOSRotating::RemoveAttachable, luabind::adopt(luabind::return_value)) .def("RemoveEmitter", (Attachable * (MOSRotating::*)(Attachable * attachableToRemove, bool addToMovableMan, bool addBreakWounds)) & MOSRotating::RemoveAttachable, luabind::adopt(luabind::return_value)) diff --git a/Source/Main.cpp b/Source/Main.cpp index de3fcb7abd..a65537b590 100644 --- a/Source/Main.cpp +++ b/Source/Main.cpp @@ -52,6 +52,7 @@ #include "LuaMan.h" #include "MusicMan.h" #include "System.h" +#include "DeterminismChecker.h" #include "RenderTarget.h" #include "tracy/Tracy.hpp" @@ -68,6 +69,16 @@ FILE __iob_func[3] = {*stdin, *stdout, *stderr}; using namespace RTE; +/// +/// The number of simulation updates to quit after, as set by command-line argument. Negative means to keep going. +/// +static long long s_SimUpdatesToQuitAfter = -1; + +/// +/// Whether to quit once the Activity is over, as set by command-line argument. +/// +static bool s_QuitWhenActivityOver = false; + /// /// Initializes all the essential managers. /// @@ -154,6 +165,29 @@ void DestroyManagers() { #endif } +/// +/// Handling of the command-line arguments that affect how the managers are initialized, so have to be dealt with before that happens. +/// +/// Argument count. +/// Argument values. +void HandleEarlyMainArgs(int argCount, char** argValue) { + // Skip the first argument because it's always the executable path/name + for (int i = 1; i < argCount; ++i) { + std::string currentArg = argValue[i]; + bool lastArg = i + 1 == argCount; + + if (currentArg == "-headless") { + System::EnableHeadlessMode(); + } else if (!lastArg && currentArg == "-determinism-record") { + DeterminismChecker::StartRecording(argValue[++i]); + } else if (!lastArg && currentArg == "-determinism-verify") { + DeterminismChecker::StartVerifying(argValue[++i]); + } else if (!lastArg && currentArg == "-seed") { + SetRNGSeedOffset(static_cast(std::atoll(argValue[++i]))); + } + } +} + /// /// Command-line argument handling. /// @@ -188,10 +222,23 @@ void HandleMainArgs(int argCount, char** argValue) { singleModuleSet = true; } } + if (!lastArg && currentArg == "-sim-updates") { + s_SimUpdatesToQuitAfter = std::atoll(argValue[++i]); + } + if (currentArg == "-quit-when-over") { + s_QuitWhenActivityOver = true; + } if (!launchModeSet) { if (!lastArg && currentArg == "-editor") { g_ActivityMan.SetEditorToLaunch(argValue[++i]); launchModeSet = true; + } else if (!lastArg && currentArg == "-save") { + g_ActivityMan.SetSaveGameToLaunch(argValue[++i]); + launchModeSet = true; + } else if (i + 3 < argCount && currentArg == "-activity") { + g_ActivityMan.SetActivityToLaunch(argValue[i + 1], argValue[i + 2], argValue[i + 3]); + i += 3; + launchModeSet = true; } } ++i; @@ -245,6 +292,12 @@ void PollSDLEvents() { /// Game menus loop. /// void RunMenuLoop() { + // There's no one to use the menus when headless, so ending up here means there's nothing left to do. + if (System::IsInHeadlessMode()) { + System::SetQuit(); + return; + } + g_MenuMan.SetIsInMenuScreen(true); g_UInputMan.DisableKeys(false); g_UInputMan.TrapMousePos(false); @@ -319,7 +372,10 @@ void RunGameLoop() { PollSDLEvents(); g_WindowMan.Update(); - g_WindowMan.ClearBackbuffer(); + // Nothing gets drawn to the backbuffer when headless, so there's nothing to clear from it either. + if (!System::IsInHeadlessMode()) { + g_WindowMan.ClearBackbuffer(); + } g_TimerMan.Update(); @@ -366,6 +422,15 @@ void RunGameLoop() { g_PerformanceMan.StopPerformanceMeasurement(PerformanceMan::SimTotal); g_UInputMan.EndFrame(); + if (s_SimUpdatesToQuitAfter >= 0 && g_TimerMan.GetSimUpdateCount() >= s_SimUpdatesToQuitAfter) { + System::SetQuit(); + break; + } + if (s_QuitWhenActivityOver && g_ActivityMan.GetActivity() && g_ActivityMan.GetActivity()->IsOver()) { + System::SetQuit(); + break; + } + if (!g_ActivityMan.IsInActivity()) { g_TimerMan.PauseSim(true); @@ -392,9 +457,11 @@ void RunGameLoop() { updateTotalTime = updateEndAndDrawStartTime - updateStartTime; drawStartTime = updateEndAndDrawStartTime; - g_FrameMan.Draw(); - g_WindowMan.DrawPostProcessBuffer(); - g_WindowMan.UploadFrame(); + if (!System::IsInHeadlessMode()) { + g_FrameMan.Draw(); + g_WindowMan.DrawPostProcessBuffer(); + g_WindowMan.UploadFrame(); + } drawTotalTime = g_TimerMan.GetAbsoluteTime() - drawStartTime; g_PerformanceMan.UpdateMSPF(updateTotalTime, drawTotalTime); @@ -437,6 +504,10 @@ int main(int argc, char** argv) { // argv[0] actually unreliable for exe path and name, because of course, why would it be, why would anything be simple and make sense. // Just use it anyway until some dumb edge case pops up and it becomes a problem. System::Initialize(argv[0]); + + HandleEarlyMainArgs(argc, argv); + + // This comes after the early arguments because the seed can be set by them, and before the managers because the Lua states get their seeds from it. SeedRNG(); InitializeManagers(); @@ -471,12 +542,14 @@ int main(int argc, char** argv) { g_ThreadMan.GetPriorityThreadPool().wait_for_tasks(); g_ThreadMan.GetBackgroundThreadPool().wait_for_tasks(); + DeterminismChecker::Stop(); + DestroyManagers(); allegro_exit(); SDL_Quit(); - return EXIT_SUCCESS; + return DeterminismChecker::HasDiverged() ? EXIT_FAILURE : EXIT_SUCCESS; } #ifdef _WIN32 diff --git a/Source/Managers/ActivityMan.cpp b/Source/Managers/ActivityMan.cpp index 8c6d7b9b91..a66c97ee7c 100644 --- a/Source/Managers/ActivityMan.cpp +++ b/Source/Managers/ActivityMan.cpp @@ -70,6 +70,10 @@ bool ActivityMan::Initialize() { if (IsSetToLaunchIntoEditor()) { // Evaluate LaunchIntoEditor before LaunchIntoActivity so it takes priority when both are set, otherwise it is ignored and editor is never launched. return SetStartEditorActivitySetToLaunchInto(); + } else if (!m_SaveGameToLaunch.empty()) { + return LoadAndLaunchGame(m_SaveGameToLaunch); + } else if (!m_ActivityNameToLaunch.empty()) { + return SetStartActivitySetToLaunchInto(); } else if (IsSetToLaunchIntoActivity()) { m_ActivityNeedsRestart = true; return true; @@ -457,6 +461,27 @@ bool ActivityMan::SetStartEditorActivitySetToLaunchInto() { } } +bool ActivityMan::SetStartActivitySetToLaunchInto() { + const Activity* activityPreset = dynamic_cast(g_PresetMan.GetEntityPreset(m_ActivityTypeToLaunch, m_ActivityNameToLaunch)); + if (!activityPreset || g_SceneMan.SetSceneToLoad(m_SceneNameToLaunch) < 0) { + g_ConsoleMan.PrintString("ERROR: Invalid Activity or Scene name passed into \"-activity\" argument!"); + g_ConsoleMan.SetEnabled(true); + return false; + } + + Activity* activityToStart = dynamic_cast(activityPreset->Clone()); + if (GameActivity* gameActivityToStart = dynamic_cast(activityToStart)) { + gameActivityToStart->SetStartingGold(gameActivityToStart->GetDefaultGoldMediumDifficulty()); + // Activities come set up with just player one, so give them the CPU to play against, unless the Activity decides that by itself. + if (gameActivityToStart->GetCPUTeam() == Activity::Teams::NoTeam) { + gameActivityToStart->SetCPUTeam(Activity::Teams::TeamTwo); + } + } + SetStartActivity(activityToStart); + m_ActivityNeedsRestart = true; + return true; +} + int ActivityMan::StartActivity(Activity* activity) { RTEAssert(activity, "Trying to start a null activity!"); diff --git a/Source/Managers/ActivityMan.h b/Source/Managers/ActivityMan.h index 4fcf90a3d6..49d87daec9 100644 --- a/Source/Managers/ActivityMan.h +++ b/Source/Managers/ActivityMan.h @@ -113,6 +113,20 @@ namespace RTE { m_LaunchIntoEditor = true; } } + + /// Sets the saved game to launch directly into. + /// @param saveGameName The file name of the saved game, without its extension. + void SetSaveGameToLaunch(const std::string_view& saveGameName) { m_SaveGameToLaunch = saveGameName; } + + /// Sets the Activity and Scene to launch directly into. + /// @param activityType The type name of the Activity. + /// @param activityName The preset name of the Activity. + /// @param sceneName The preset name of the Scene. + void SetActivityToLaunch(const std::string_view& activityType, const std::string_view& activityName, const std::string_view& sceneName) { + m_ActivityTypeToLaunch = activityType; + m_ActivityNameToLaunch = activityName; + m_SceneNameToLaunch = sceneName; + } #pragma endregion #pragma region Saving and Loading @@ -163,6 +177,10 @@ namespace RTE { /// Launch editor Activity specified in command-line argument. /// @return Whether a valid editor name was passed in and set to be launched next time ResetActivity is called. bool SetStartEditorActivitySetToLaunchInto(); + + /// Launch Activity and Scene specified in command-line argument, as player one against the CPU. + /// @return Whether a valid Activity and Scene were passed in and set to be launched next time ResetActivity is called. + bool SetStartActivitySetToLaunchInto(); #pragma endregion #pragma region Concrete Methods @@ -217,6 +235,10 @@ namespace RTE { bool m_LaunchIntoActivity; //!< Whether to skip the intro and main menu and launch directly into the set default Activity instead. bool m_LaunchIntoEditor; //!< Whether to skip the intro and main menu and launch directly into the set editor Activity instead. std::string_view m_EditorToLaunch; //!< The name of the editor Activity to launch directly into. + std::string m_SaveGameToLaunch; //!< The file name of the saved game to launch directly into, if any. + std::string m_ActivityTypeToLaunch; //!< The type name of the Activity to launch directly into, if any. + std::string m_ActivityNameToLaunch; //!< The preset name of the Activity to launch directly into, if any. + std::string m_SceneNameToLaunch; //!< The preset name of the Scene to launch the above Activity in. /// Clears all the member variables of this ActivityMan, effectively resetting the members of this abstraction level only. void Clear(); diff --git a/Source/Managers/AudioMan.cpp b/Source/Managers/AudioMan.cpp index 36ae07bad7..b3d8e210af 100644 --- a/Source/Managers/AudioMan.cpp +++ b/Source/Managers/AudioMan.cpp @@ -8,6 +8,7 @@ #include "SoundContainer.h" #include "WindowMan.h" #include "SoundSet.h" +#include "System.h" #include #include @@ -30,6 +31,7 @@ void AudioMan::Clear() { m_MuteMaster = false; m_MuteMusic = false; m_MuteSounds = false; + m_MuteAudioOnFocusLoss = false; m_MasterVolume = 0.5F; m_MusicVolume = 1.0F; m_SoundsVolume = 1.0F; @@ -59,11 +61,15 @@ bool AudioMan::Initialize() { audioSystemAdvancedSettings.cbSize = sizeof(FMOD_ADVANCEDSETTINGS); audioSystemSetupResult = (audioSystemSetupResult == FMOD_OK) ? m_AudioSystem->getAdvancedSettings(&audioSystemAdvancedSettings) : audioSystemSetupResult; audioSystemAdvancedSettings.vol0virtualvol = 0.001F; - audioSystemAdvancedSettings.randomSeed = RandomNum(0, INT_MAX); + audioSystemAdvancedSettings.randomSeed = g_DrawRandomGenerator.RandomNum(0, INT_MAX); audioSystemSetupResult = (audioSystemSetupResult == FMOD_OK) ? m_AudioSystem->setAdvancedSettings(&audioSystemAdvancedSettings) : audioSystemSetupResult; audioSystemSetupResult = (audioSystemSetupResult == FMOD_OK) ? m_AudioSystem->set3DSettings(1, c_PPM, 1) : audioSystemSetupResult; audioSystemSetupResult = (audioSystemSetupResult == FMOD_OK) ? m_AudioSystem->setSoftwareChannels(c_MaxSoftwareChannels) : audioSystemSetupResult; + if (System::IsInHeadlessMode()) { + // Everything to do with audio still goes on as it otherwise would, it just doesn't get played to anyone. + audioSystemSetupResult = (audioSystemSetupResult == FMOD_OK) ? m_AudioSystem->setOutput(FMOD_OUTPUTTYPE_NOSOUND) : audioSystemSetupResult; + } FMOD_INITFLAGS flags = FMOD_INIT_VOL0_BECOMES_VIRTUAL; #if !RELEASE_BUILD @@ -400,7 +406,7 @@ bool AudioMan::PlaySoundContainer(SoundContainer* soundContainer, int player) { result = (result == FMOD_OK) ? channel->setUserData(soundContainer) : result; result = (result == FMOD_OK) ? channel->setCallback(SoundChannelEndedCallback) : result; result = (result == FMOD_OK) ? channel->setPriority(soundContainer->GetPriority()) : result; - float pitchVariationMultiplier = pitchVariationFactor == 1.0F ? 1.0F : RandomNum(1.0F / pitchVariationFactor, 1.0F * pitchVariationFactor); + float pitchVariationMultiplier = pitchVariationFactor == 1.0F ? 1.0F : g_DrawRandomGenerator.RandomNum(1.0F / pitchVariationFactor, 1.0F * pitchVariationFactor); result = (result == FMOD_OK) ? channel->setPitch(soundContainer->GetPitch() * pitchVariationMultiplier) : result; if (soundContainer->GetCustomPanValue() != 0.0f) { diff --git a/Source/Managers/CameraMan.cpp b/Source/Managers/CameraMan.cpp index e76691b283..e7f57717f5 100644 --- a/Source/Managers/CameraMan.cpp +++ b/Source/Managers/CameraMan.cpp @@ -246,7 +246,7 @@ void CameraMan::Update(int screenId) { const float screenShakeScale = 0.3F; Vector screenShakeOffset(1.0F, 0.0F); - screenShakeOffset.RadRotate(RandomNormalNum() * c_PI); + screenShakeOffset.RadRotate(g_DrawRandomGenerator.RandomNormalNum() * c_PI); screenShakeOffset *= screen.ScreenShakeMagnitude * m_ScreenShakeStrength * screenShakeScale; newOffset += screenShakeOffset; diff --git a/Source/Managers/LuaMan.cpp b/Source/Managers/LuaMan.cpp index f0911901e7..88bab187f0 100644 --- a/Source/Managers/LuaMan.cpp +++ b/Source/Managers/LuaMan.cpp @@ -20,12 +20,17 @@ LuaStateWrapper::~LuaStateWrapper() { Destroy(); } +bool LuaStateWrapper::UniqueIDOrder::operator()(const MovableObject* lhs, const MovableObject* rhs) const { + return lhs->GetUniqueID() < rhs->GetUniqueID(); +} + void LuaStateWrapper::Clear() { m_State = nullptr; m_TempEntity = nullptr; m_TempEntityVector.clear(); m_LastError.clear(); m_CurrentlyRunningScriptPath = ""; + m_RunningAI = false; } void LuaStateWrapper::Initialize() { @@ -244,6 +249,7 @@ void LuaStateWrapper::Initialize() { const uint64_t seed = RandomNum(0, std::numeric_limits::max()); m_RandomGenerator.Seed(seed); + m_AIRandomGenerator.Seed(RandomNum(0, std::numeric_limits::max())); luaL_dostring(m_State, "package.path = package.path .. \";Data/Base.rte/LuaIntegration/?.lua;Data/Base.rte/LuaIntegration/?/?.lua;\"\n" @@ -281,20 +287,29 @@ void LuaStateWrapper::Destroy() { lua_close(m_State); } +void LuaStateWrapper::SetRunningAI(bool runningAI) { + m_RunningAI = runningAI; + SetThreadRandomGenerator(runningAI ? &m_AIRandomGenerator : nullptr); +} + +void LuaStateWrapper::SetRunningThreadedUpdate(bool runningThreadedUpdate) { + SetThreadRandomGenerator(runningThreadedUpdate ? &m_RandomGenerator : nullptr); +} + int LuaStateWrapper::SelectRand(int minInclusive, int maxInclusive) { - return m_RandomGenerator.RandomNum(minInclusive, maxInclusive); + return GetActiveRandomGenerator().RandomNum(minInclusive, maxInclusive); } double LuaStateWrapper::RangeRand(double minInclusive, double maxInclusive) { - return m_RandomGenerator.RandomNum(minInclusive, maxInclusive); + return GetActiveRandomGenerator().RandomNum(minInclusive, maxInclusive); } double LuaStateWrapper::NormalRand() { - return m_RandomGenerator.RandomNormalNum(); + return GetActiveRandomGenerator().RandomNormalNum(); } double LuaStateWrapper::PosRand() { - return m_RandomGenerator.RandomNum(); + return GetActiveRandomGenerator().RandomNum(); } // Passthrough LuaMan Functions @@ -330,6 +345,9 @@ void LuaMan::Initialize() { luaStateCount = g_SettingsMan.GetNumberOfLuaStatesOverride(); } + // Each threaded Lua state hands out unique IDs to the MOs made on its thread from a range of its own, and there's only so many of those ranges, with one going to the main thread. + luaStateCount = std::min(luaStateCount, (1 << MovableObject::c_UniqueIDThreadBits) - 1); + m_ScriptStates = std::vector(luaStateCount); for (LuaStateWrapper& luaState: m_ScriptStates) { luaState.Initialize(); diff --git a/Source/Managers/LuaMan.h b/Source/Managers/LuaMan.h index 1d2b3cbbbc..f7ee1fe8d6 100644 --- a/Source/Managers/LuaMan.h +++ b/Source/Managers/LuaMan.h @@ -8,6 +8,7 @@ #include "BS_thread_pool.hpp" #include +#include #define g_LuaMan LuaMan::Instance() @@ -69,9 +70,28 @@ namespace RTE { /// Gets the currently running script filepath, if applicable. /// @return The currently running script filepath. May return inaccurate values for non-MO scripts due to weirdness in setup. std::string_view GetCurrentlyRunningScriptFilePath() const { return m_CurrentlyRunningScriptPath; } + + /// Gets the random number generator used for this lua state. + /// @return The random number generator used for this lua state. + const RandomGenerator& GetRandomGenerator() const { return m_RandomGenerator; } + + /// Sets whether what is being run on this lua state is AI. While it is, this lua state and the global random functions called from this thread use a random number generator that is only for AI. + /// The AI can be left to do as it pleases this way, without it affecting the random numbers that the rest of the simulation gets. + /// @param runningAI Whether AI is what is being run on this lua state, by this thread. + void SetRunningAI(bool runningAI); + + /// Sets whether what is being run on this lua state is the threaded update of its scripts, which goes on at the same time as that of the other lua states. + /// While it is, the global random functions called from this thread use this lua state's random number generator, since the lua states can't all be using the simulation's one at once. + /// @param runningThreadedUpdate Whether the threaded update of its scripts is what is being run on this lua state, by this thread. + void SetRunningThreadedUpdate(bool runningThreadedUpdate); #pragma endregion #pragma region Script Responsibility Handling + /// Orders MOs by their unique ID. This is the order the MOs registered with us have their scripts run in, since their order in memory is different every time the game is run. + struct UniqueIDOrder { + bool operator()(const MovableObject* lhs, const MovableObject* rhs) const; + }; + /// Registers an MO as using us. /// @param moToRegister The MO to register with us. Ownership is NOT transferred! void RegisterMO(MovableObject* moToRegister) { m_AddedRegisteredMOs.insert(moToRegister); } @@ -85,7 +105,7 @@ namespace RTE { /// Gets a list of the MOs registed as using us. /// @return The MOs registed as using us. - const std::unordered_set& GetRegisteredMOs() const { return m_RegisteredMOs; } + const std::set& GetRegisteredMOs() const { return m_RegisteredMOs; } #pragma endregion #pragma region Script Execution Handling @@ -248,7 +268,7 @@ namespace RTE { /// Clears all the member variables of this LuaStateWrapper, effectively resetting the members of this abstraction level only. void Clear(); - std::unordered_set m_RegisteredMOs; //!< The objects using our lua state. + std::set m_RegisteredMOs; //!< The objects using our lua state. std::unordered_set m_AddedRegisteredMOs; //!< The objects using our lua state that were recently added. lua_State* m_State; @@ -269,6 +289,12 @@ namespace RTE { // For determinism, every Lua state has it's own random number generator. RandomGenerator m_RandomGenerator; //!< The random number generator used for this lua state. + RandomGenerator m_AIRandomGenerator; //!< The random number generator used for this lua state while it is running AI. + bool m_RunningAI; //!< Whether what is being run on this lua state is AI. + + /// Gets the random number generator to use for what is currently being run on this lua state. + /// @return The random number generator for AI if that is what's being run, otherwise the regular one. + RandomGenerator& GetActiveRandomGenerator() { return m_RunningAI ? m_AIRandomGenerator : m_RandomGenerator; } }; typedef std::vector LuaStatesArray; diff --git a/Source/Managers/MovableMan.cpp b/Source/Managers/MovableMan.cpp index ebaba93713..ced2cc53e5 100644 --- a/Source/Managers/MovableMan.cpp +++ b/Source/Managers/MovableMan.cpp @@ -20,6 +20,8 @@ #include "SettingsMan.h" #include "LuaMan.h" #include "ThreadMan.h" +#include "DeterminismChecker.h" +#include "System.h" #include "tracy/Tracy.hpp" @@ -658,6 +660,31 @@ Actor* MovableMan::GetUnassignedBrain(int team) const { return 0; } +void MovableMan::HashState(StateHasher& hasher) const { + auto hashMovableObject = [&hasher](const MovableObject* movableObject) { + hasher.Add(movableObject->GetUniqueID()); + hasher.Add(movableObject->GetPos()); + hasher.Add(movableObject->GetVel()); + hasher.Add(movableObject->GetRotAngle()); + hasher.Add(movableObject->GetAngularVel()); + hasher.Add(movableObject->IsHFlipped()); + }; + + for (const std::deque* actors: {&m_Actors, &m_AddedActors}) { + for (const Actor* actor: *actors) { + hashMovableObject(actor); + hasher.Add(actor->GetTeam()); + hasher.Add(actor->GetStatus()); + hasher.Add(actor->GetHealth()); + } + } + for (const std::deque* movableObjects: {&m_Items, &m_AddedItems, &m_Particles, &m_AddedParticles}) { + for (const MovableObject* movableObject: *movableObjects) { + hashMovableObject(movableObject); + } + } +} + bool MovableMan::AddMO(MovableObject* movableObjectToAdd) { if (!movableObjectToAdd) { return false; @@ -1280,8 +1307,8 @@ void MovableMan::Update() { // ---TEMP --- // These are here for multithreaded AI, but will be unnecessary when multithreaded-sim-and-render is in! - // Clear the MO color layer only if this is a drawn update - if (g_TimerMan.DrawnSimUpdate()) { + // Clear the MO color layer only if this is a drawn update. Nothing gets drawn when headless, so neither is the MO color layer. + if (g_TimerMan.DrawnSimUpdate() && !System::IsInHeadlessMode()) { g_SceneMan.ClearMOColorLayer(); } @@ -1317,6 +1344,8 @@ void MovableMan::Update() { // Finish our Seeing rays from last frame m_ActorsSeeFuture.wait(); + DeterminismCheckSimState("MOs travelled"); + // Prior to controller/AI update, execute lua callbacks g_LuaMan.ExecuteLuaScriptCallbacks(); @@ -1328,6 +1357,8 @@ void MovableMan::Update() { // Updates AI/user input UpdateControllers(); + DeterminismCheckSimState("Controllers updated"); + // Will use some common iterators std::deque::iterator aIt; std::deque::iterator amidIt; @@ -1357,6 +1388,7 @@ void MovableMan::Update() { RTEAssert(start + 1 == end, "Threaded script state being updated across multiple threads!"); LuaStateWrapper& luaState = luaStates[start]; g_LuaMan.SetThreadLuaStateOverride(&luaState); + luaState.SetRunningThreadedUpdate(true); for (MovableObject* mo: luaState.GetRegisteredMOs()) { if (ValidMO(mo->GetRootParent())) { @@ -1364,9 +1396,12 @@ void MovableMan::Update() { } } + luaState.SetRunningThreadedUpdate(false); g_LuaMan.SetThreadLuaStateOverride(nullptr); }) .wait(); + + SortAddedMOs(); } { @@ -1397,6 +1432,8 @@ void MovableMan::Update() { } g_PerformanceMan.StopPerformanceMeasurement(PerformanceMan::ScriptsUpdate); + DeterminismCheckSimState("Threaded scripts updated"); + { { ZoneScopedN("Actors Update"); @@ -1471,6 +1508,8 @@ void MovableMan::Update() { } } // namespace RTE + DeterminismCheckSimState("MOs updated"); + ////////////////////////////////////////////////////////////////////// // TRANSFER ALL MOs ADDED THIS FRAME // All Actors, Items, and Particles added this frame now are officially added @@ -1668,6 +1707,8 @@ void MovableMan::Update() { } } + DeterminismCheckSimState("MOs added and removed"); + // Run seeing rays for all actors m_ActorsSeeFuture = g_ThreadMan.GetPriorityThreadPool().parallelize_loop(m_Actors.size(), [&](int start, int end) { @@ -1686,9 +1727,9 @@ void MovableMan::Update() { }); //////////////////////////////////////////////////////////////////// - // Draw the MO colors ONLY if this is a drawn update! + // Draw the MO colors ONLY if this is a drawn update! Nothing gets drawn when headless, so the MO colors would never be seen. - if (g_TimerMan.DrawnSimUpdate()) { + if (g_TimerMan.DrawnSimUpdate() && !System::IsInHeadlessMode()) { Draw(g_SceneMan.GetMOColorBitmap()); } @@ -1700,6 +1741,12 @@ void MovableMan::Update() { } } +void MovableMan::SortAddedMOs() { + std::sort(m_AddedActors.begin(), m_AddedActors.end(), LuaStateWrapper::UniqueIDOrder()); + std::sort(m_AddedItems.begin(), m_AddedItems.end(), LuaStateWrapper::UniqueIDOrder()); + std::sort(m_AddedParticles.begin(), m_AddedParticles.end(), LuaStateWrapper::UniqueIDOrder()); +} + void MovableMan::Travel() { ZoneScoped; @@ -1763,11 +1810,13 @@ void MovableMan::UpdateControllers() { } g_LuaMan.SetThreadLuaStateOverride(&g_LuaMan.GetMasterScriptState()); + g_LuaMan.GetMasterScriptState().SetRunningAI(true); for (Actor* actor: m_Actors) { if (actor->GetLuaState() == &g_LuaMan.GetMasterScriptState() && actor->GetController()->ShouldUpdateAIThisFrame()) { actor->RunScriptedFunctionInAppropriateScripts("ThreadedUpdateAI", false, true, {}, {}, {}); } } + g_LuaMan.GetMasterScriptState().SetRunningAI(false); g_LuaMan.SetThreadLuaStateOverride(nullptr); LuaStatesArray& luaStates = g_LuaMan.GetThreadedScriptStates(); @@ -1776,18 +1825,25 @@ void MovableMan::UpdateControllers() { RTEAssert(start + 1 == end, "Threaded script state being updated across multiple threads!"); LuaStateWrapper& luaState = luaStates[start]; g_LuaMan.SetThreadLuaStateOverride(&luaState); + luaState.SetRunningAI(true); for (Actor* actor: m_Actors) { if (actor->GetLuaState() == &luaState && actor->GetController()->ShouldUpdateAIThisFrame()) { actor->RunScriptedFunctionInAppropriateScripts("ThreadedUpdateAI", false, true, {}, {}, {}); } } + luaState.SetRunningAI(false); g_LuaMan.SetThreadLuaStateOverride(nullptr); }) .wait(); + SortAddedMOs(); + for (Actor* actor: m_Actors) { - if (actor->GetController()->ShouldUpdateAIThisFrame()) { + LuaStateWrapper* luaState = actor->GetLuaState(); + if (luaState && actor->GetController()->ShouldUpdateAIThisFrame()) { + luaState->SetRunningAI(true); actor->RunScriptedFunctionInAppropriateScripts("UpdateAI", false, true, {}, {}, {}); + luaState->SetRunningAI(false); } } } diff --git a/Source/Managers/MovableMan.h b/Source/Managers/MovableMan.h index 3a4183a5ce..3e672be257 100644 --- a/Source/Managers/MovableMan.h +++ b/Source/Managers/MovableMan.h @@ -30,6 +30,7 @@ namespace RTE { class SceneObject; class Box; class LuabindObjectWrapper; + class StateHasher; /// A struct to keep all data about a an alarming event for the AI Actors. struct AlarmEvent { @@ -253,6 +254,10 @@ namespace RTE { /// @return The number of particles. long GetParticleCount() const { return m_Particles.size(); } + /// Adds the state of all the MovableObjects currently held to a hash, for checking whether two runs of the simulation match. + /// @param hasher The StateHasher to add the state to. + void HashState(StateHasher& hasher) const; + /// Gets the global setting for how much splash MOPixels should be created /// an MO penetrates the terrain deeply. /// @return A float with the global splash amount setting, form 1.0 to 0.0. @@ -487,7 +492,7 @@ namespace RTE { /// Uses a global lookup map to find an object by it's unique id. /// @param id Unique Id to look for. /// @return Object found or 0 if not found any. - MovableObject* FindObjectByUniqueID(long int id) { + MovableObject* FindObjectByUniqueID(int64_t id) { if (m_KnownObjects.count(id) > 0) return m_KnownObjects[id]; else @@ -635,7 +640,7 @@ namespace RTE { unsigned int m_SimUpdateFrameNumber; // Global map which stores all objects so they could be foud by their unique ID - std::map m_KnownObjects; + std::map m_KnownObjects; /// Private member variable and method declarations private: @@ -648,6 +653,10 @@ namespace RTE { /// Travels all of our MOs, updating their location/velocity/physical characteristics. void Travel(); + /// Puts the MOs that are waiting to be added in order of their unique IDs. + /// This has to be done after any point where several threads could have been adding MOs at once, since which thread gets to add its MOs first is different every time. + void SortAddedMOs(); + /// Updates the controllers of all the actors we own. /// This is needed for a tricky reason - we want the controller from the activity to override the normal controller state /// So we need to update the controller state prior to activity, so the changes from activity are layered on top. diff --git a/Source/Managers/PostProcessMan.cpp b/Source/Managers/PostProcessMan.cpp index a89a3bd36e..7afe820782 100644 --- a/Source/Managers/PostProcessMan.cpp +++ b/Source/Managers/PostProcessMan.cpp @@ -389,7 +389,7 @@ void PostProcessMan::DrawDotGlowEffects() { testpixel = _getpixel(g_FrameMan.GetBackBuffer8(), x, y); // YELLOW - if ((testpixel == g_YellowGlowColor && RandomNum() < 0.9F) || testpixel == 98 || (testpixel == 120 && RandomNum() < 0.7F)) { + if ((testpixel == g_YellowGlowColor && g_DrawRandomGenerator.RandomNum() < 0.9F) || testpixel == 98 || (testpixel == 120 && g_DrawRandomGenerator.RandomNum() < 0.7F)) { DrawTexture(yellowGlow, x - yellowGlow.width / 2, y - yellowGlow.height / 2, {255, 255, 255, 255}); } // TODO: Enable and add more colors once we actually have something that needs these. diff --git a/Source/Managers/SceneMan.cpp b/Source/Managers/SceneMan.cpp index 2fd0682046..0f86b10095 100644 --- a/Source/Managers/SceneMan.cpp +++ b/Source/Managers/SceneMan.cpp @@ -560,6 +560,8 @@ bool SceneMan::TryPenetrate(int posX, unsigned char materialID = _getpixel(m_pCurrentScene->GetTerrain()->GetMaterialBitmap(), posX, posY); if (materialID == g_MaterialAir) { // RTEAbort("Why are we penetrating air??"); + // There's nothing here to slow anything down. Callers go on to use this when we return true, so it can't be left as it was. + retardation = 0.0F; return true; } Material const* sceneMat = GetMaterialFromID(materialID); diff --git a/Source/Managers/SettingsMan.cpp b/Source/Managers/SettingsMan.cpp index 39ea1a15ae..f0bfb48b43 100644 --- a/Source/Managers/SettingsMan.cpp +++ b/Source/Managers/SettingsMan.cpp @@ -9,6 +9,7 @@ #include "PerformanceMan.h" #include "UInputMan.h" #include "System.h" +#include "DeterminismChecker.h" using namespace RTE; @@ -104,6 +105,14 @@ void SettingsMan::UpdateSettingsFile() const { g_SettingsMan.Save(settingsWriter); } +int SettingsMan::GetNumberOfLuaStatesOverride() const { + constexpr int luaStatesWhenCheckingDeterminism = 4; + if (m_NumberOfLuaStatesOverride == -1 && DeterminismChecker::IsActive()) { + return luaStatesWhenCheckingDeterminism; + } + return m_NumberOfLuaStatesOverride; +} + int SettingsMan::ReadProperty(const std::string_view& propName, Reader& reader) { StartPropertyList(return Serializable::ReadProperty(propName, reader)); @@ -160,6 +169,7 @@ int SettingsMan::ReadProperty(const std::string_view& propName, Reader& reader) MatchProperty("DefaultSceneName", { reader >> g_SceneMan.m_DefaultSceneName; }); MatchProperty("DisableLuaJIT", { reader >> m_DisableLuaJIT; }); MatchProperty("EnableLuaDebugging", { reader >> m_EnableLuaDebugging; }); + MatchProperty("DisableCrashDialogs", { reader >> RTEError::s_DisableCrashDialogs; }); MatchProperty("RecommendedMOIDCount", { reader >> m_RecommendedMOIDCount; }); MatchProperty("SceneBackgroundAutoScaleMode", { SetSceneBackgroundAutoScaleMode(std::stoi(reader.ReadPropValue())); }); MatchProperty("DisableFactionBuyMenuThemes", { reader >> m_DisableFactionBuyMenuThemes; }); @@ -288,6 +298,7 @@ int SettingsMan::Save(Writer& writer) const { writer.NewLine(false); writer.NewPropertyWithValue("DisableLuaJIT", m_DisableLuaJIT); writer.NewPropertyWithValue("EnableLuaDebugging", m_EnableLuaDebugging); + writer.NewPropertyWithValue("DisableCrashDialogs", RTEError::s_DisableCrashDialogs); writer.NewPropertyWithValue("RecommendedMOIDCount", m_RecommendedMOIDCount); writer.NewPropertyWithValue("SceneBackgroundAutoScaleMode", m_SceneBackgroundAutoScaleMode); writer.NewPropertyWithValue("DisableFactionBuyMenuThemes", m_DisableFactionBuyMenuThemes); diff --git a/Source/Managers/SettingsMan.h b/Source/Managers/SettingsMan.h index 4bb438cdfb..796f1720b3 100644 --- a/Source/Managers/SettingsMan.h +++ b/Source/Managers/SettingsMan.h @@ -98,8 +98,9 @@ namespace RTE { void SetAIUpdateInterval(int newAIUpdateInterval) { m_AIUpdateInterval = newAIUpdateInterval; } /// Gets how many threaded Lua states we'll use. -1 represents no override, which defaults to the maximum number of concurrent hardware threads. + /// There is always an override while determinism is being checked, because scripted objects get spread out over the Lua states, so how many there are can't be left up to the machine. /// @return How many threaded Lua states we'll use. - int GetNumberOfLuaStatesOverride() const { return m_NumberOfLuaStatesOverride; } + int GetNumberOfLuaStatesOverride() const; /// Gets whether pathing requests will be forced to immediately complete for the next frame, or if they can take multiple frames to calculate. /// @return Whether pathing requests will be forced to immediately complete for the next frame diff --git a/Source/Managers/TimerMan.cpp b/Source/Managers/TimerMan.cpp index 4aeaa1cb61..9693d6b035 100644 --- a/Source/Managers/TimerMan.cpp +++ b/Source/Managers/TimerMan.cpp @@ -3,6 +3,7 @@ #include "Constants.h" #include "PerformanceMan.h" #include "SettingsMan.h" +#include "System.h" #include #include @@ -78,7 +79,12 @@ void TimerMan::UpdateSim() { void TimerMan::Update() { long long prevTime = m_RealTimeTicks; - m_RealTimeTicks = std::chrono::duration_cast(std::chrono::steady_clock::now() - m_StartTime).count(); + if (System::IsInHeadlessMode()) { + // With nothing being presented there's no point in waiting on real time, so act as if exactly one sim update's worth of it passes between each update. + m_RealTimeTicks += m_DeltaTime; + } else { + m_RealTimeTicks = std::chrono::duration_cast(std::chrono::steady_clock::now() - m_StartTime).count(); + } // Cap timeIncrease if too long (as when the app went out of focus), to c_RealToSimCap. long long timeIncrease = std::min(m_RealTimeTicks - prevTime, static_cast(c_RealToSimCap * m_TicksPerSecond)); diff --git a/Source/Managers/WindowMan.cpp b/Source/Managers/WindowMan.cpp index 0e09efecd6..21d4f62799 100644 --- a/Source/Managers/WindowMan.cpp +++ b/Source/Managers/WindowMan.cpp @@ -10,6 +10,7 @@ #include "PostProcessMan.h" #include "RenderTarget.h" #include "GLResourceMan.h" +#include "System.h" #include "GLCheck.h" #include @@ -185,6 +186,7 @@ void WindowMan::CreatePrimaryWindow() { SDL_SetBooleanProperty(windowProps, SDL_PROP_WINDOW_CREATE_RESIZABLE_BOOLEAN, true); SDL_SetBooleanProperty(windowProps, SDL_PROP_WINDOW_CREATE_OPENGL_BOOLEAN, true); SDL_SetBooleanProperty(windowProps, SDL_PROP_WINDOW_CREATE_FULLSCREEN_BOOLEAN, m_Fullscreen); + SDL_SetBooleanProperty(windowProps, SDL_PROP_WINDOW_CREATE_HIDDEN_BOOLEAN, System::IsInHeadlessMode()); SDL_SetBooleanProperty(windowProps, SDL_PROP_WINDOW_CREATE_MINIMIZED_BOOLEAN, m_LaunchMinimised); SDL_SetNumberProperty(windowProps, SDL_PROP_WINDOW_CREATE_X_NUMBER, windowPosX); SDL_SetNumberProperty(windowProps, SDL_PROP_WINDOW_CREATE_Y_NUMBER, windowPosY); diff --git a/Source/Menus/MetagameGUI.cpp b/Source/Menus/MetagameGUI.cpp index 3884a63a30..4d9606b115 100644 --- a/Source/Menus/MetagameGUI.cpp +++ b/Source/Menus/MetagameGUI.cpp @@ -1674,7 +1674,7 @@ void MetagameGUI::Draw(BITMAP* drawBitmap) { // Transparency effect on the scene dots and lines drawing_mode(DRAW_MODE_TRANS, 0, 0, 0); // Screen blend the dots and lines, with some flickering in its intensity - int blendAmount = 130 + RandomNum(-45, 45); + int blendAmount = 130 + g_DrawRandomGenerator.RandomNum(-45, 45); set_screen_blender(blendAmount, blendAmount, blendAmount, blendAmount); // Draw the scene location dots @@ -1699,11 +1699,11 @@ void MetagameGUI::Draw(BITMAP* drawBitmap) { // Ownership not known, so place nondescript dot instead else { // Make it flicker more if it's currently being fought over - blendAmount = 95 + (battleSite ? RandomNum(-25, 25) : RandomNum(-15, 15)); + blendAmount = 95 + (battleSite ? g_DrawRandomGenerator.RandomNum(-25, 25) : g_DrawRandomGenerator.RandomNum(-15, 15)); set_screen_blender(blendAmount, blendAmount, blendAmount, blendAmount); circlefill(drawBitmap, screenLocation.m_X, screenLocation.m_Y, 4, c_GUIColorYellow); circlefill(drawBitmap, screenLocation.m_X, screenLocation.m_Y, 2, c_GUIColorYellow); - blendAmount = 210 + RandomNum(-45, 45); + blendAmount = 210 + g_DrawRandomGenerator.RandomNum(-45, 45); set_screen_blender(blendAmount, blendAmount, blendAmount, blendAmount); circlefill(drawBitmap, screenLocation.m_X, screenLocation.m_Y, 1, c_GUIColorYellow); } @@ -5713,7 +5713,7 @@ void MetagameGUI::UpdatePlayerLineRatios(std::vector& lineList, int me } void MetagameGUI::DrawGlowLine(BITMAP* drawBitmap, const Vector& start, const Vector& end, int color) { - int blendAmount = 210 + RandomNum(-15, 15); + int blendAmount = 210 + g_DrawRandomGenerator.RandomNum(-15, 15); set_screen_blender(blendAmount, blendAmount, blendAmount, blendAmount); line(drawBitmap, start.m_X, start.m_Y, end.m_X, end.m_Y, color); /* Looks like ass @@ -5729,7 +5729,7 @@ void MetagameGUI::DrawGlowLine(BITMAP* drawBitmap, const Vector& start, const Ve line(drawBitmap, start.m_X - 1, start.m_Y, end.m_X - 1, end.m_Y, color); } */ - blendAmount = 45 + RandomNum(-25, 25); + blendAmount = 45 + g_DrawRandomGenerator.RandomNum(-25, 25); set_screen_blender(blendAmount, blendAmount, blendAmount, blendAmount); line(drawBitmap, start.m_X + 1, start.m_Y, end.m_X + 1, end.m_Y, color); line(drawBitmap, start.m_X - 1, start.m_Y, end.m_X - 1, end.m_Y, color); @@ -5830,7 +5830,7 @@ bool MetagameGUI::DrawScreenLineToSitePoint(BITMAP* drawBitmap, // Draw a circle around the site target if (!(drawnFirstSegments++ >= onlyFirstSegments || lastSegmentsToDraw-- > onlyLastSegments)) { - int blendAmount = 225 + RandomNum(-20, 20); + int blendAmount = 225 + g_DrawRandomGenerator.RandomNum(-20, 20); set_screen_blender(blendAmount, blendAmount, blendAmount, blendAmount); // If specified, draw a squareSite instead (with chamfered corners) @@ -5976,7 +5976,7 @@ bool MetagameGUI::DrawPlayerLineToSitePoint(BITMAP* drawBitmap, // Draw a circle around the site target if (!(drawnFirstSegments++ >= onlyFirstSegments || lastSegmentsToDraw-- > onlyLastSegments)) { - int blendAmount = 225 + RandomNum(-20, 20); + int blendAmount = 225 + g_DrawRandomGenerator.RandomNum(-20, 20); set_screen_blender(blendAmount, blendAmount, blendAmount, blendAmount); // If specified, draw a squareSite instead (with chamfered corners) diff --git a/Source/Menus/ScenarioGUI.cpp b/Source/Menus/ScenarioGUI.cpp index 59258d3bbe..5f1ddee06a 100644 --- a/Source/Menus/ScenarioGUI.cpp +++ b/Source/Menus/ScenarioGUI.cpp @@ -544,12 +544,12 @@ void ScenarioGUI::DrawSitePoints(BITMAP* drawBitmap) const { int sitePosX = sitePos.GetFloorIntX(); int sitePosY = sitePos.GetFloorIntY(); - blendAmount = 70 + RandomNum(0, 40); + blendAmount = 70 + g_DrawRandomGenerator.RandomNum(0, 40); set_screen_blender(blendAmount, blendAmount, blendAmount, blendAmount); circlefill(drawBitmap, sitePosX, sitePosY, 4, drawColor); circlefill(drawBitmap, sitePosX, sitePosY, 2, drawColor); - blendAmount = 145 + RandomNum(0, 110); + blendAmount = 145 + g_DrawRandomGenerator.RandomNum(0, 110); set_screen_blender(blendAmount, blendAmount, blendAmount, blendAmount); circlefill(drawBitmap, sitePosX, sitePosY, 1, drawColor); } @@ -566,11 +566,11 @@ void ScenarioGUI::DrawLinesToSitePoint(BITMAP* drawBitmap) const { int lineEndX = m_LineToSitePoints.at(i + 1).GetFloorIntX(); int lineEndY = m_LineToSitePoints.at(i + 1).GetFloorIntY(); - blendAmount = 195 + RandomNum(0, 30); + blendAmount = 195 + g_DrawRandomGenerator.RandomNum(0, 30); set_screen_blender(blendAmount, blendAmount, blendAmount, blendAmount); line(drawBitmap, lineStartX, lineStartY, lineEndX, lineEndY, drawColor); - blendAmount = 30 + RandomNum(0, 50); + blendAmount = 30 + g_DrawRandomGenerator.RandomNum(0, 50); set_screen_blender(blendAmount, blendAmount, blendAmount, blendAmount); line(drawBitmap, lineStartX + 1, lineStartY, lineEndX + 1, lineEndY, drawColor); line(drawBitmap, lineStartX - 1, lineStartY, lineEndX - 1, lineEndY, drawColor); @@ -583,11 +583,11 @@ void ScenarioGUI::DrawLinesToSitePoint(BITMAP* drawBitmap) const { int sitePosY = sitePos.GetFloorIntY(); int circleRadius = 8; - blendAmount = 195 + RandomNum(0, 30); + blendAmount = 195 + g_DrawRandomGenerator.RandomNum(0, 30); set_screen_blender(blendAmount, blendAmount, blendAmount, blendAmount); circle(drawBitmap, sitePosX, sitePosY, circleRadius, drawColor); - blendAmount = 120 + RandomNum(0, 50); + blendAmount = 120 + g_DrawRandomGenerator.RandomNum(0, 50); set_screen_blender(blendAmount, blendAmount, blendAmount, blendAmount); circle(drawBitmap, sitePosX, sitePosY, circleRadius - 1, drawColor); } diff --git a/Source/Menus/TitleScreen.cpp b/Source/Menus/TitleScreen.cpp index 906aabd09b..5f58a62fa8 100644 --- a/Source/Menus/TitleScreen.cpp +++ b/Source/Menus/TitleScreen.cpp @@ -129,20 +129,20 @@ void TitleScreen::CreateTitleElements() { int starCount = (g_WindowMan.GetResX() * m_Nebula.GetBitmap()->h) / 1000; for (int i = 0; i < starCount; ++i) { Star newStar; - if (RandomNum() < 0.95F) { + if (g_DrawRandomGenerator.RandomNum() < 0.95F) { newStar.Size = Star::StarSize::StarSmall; - newStar.Bitmap = starSmallBitmaps.at(RandomNum(0, starSmallBitmapCount - 1)); - newStar.Intensity = RandomNum(0, 92); - } else if (RandomNum() < 0.85F) { + newStar.Bitmap = starSmallBitmaps.at(g_DrawRandomGenerator.RandomNum(0, starSmallBitmapCount - 1)); + newStar.Intensity = g_DrawRandomGenerator.RandomNum(0, 92); + } else if (g_DrawRandomGenerator.RandomNum() < 0.85F) { newStar.Size = Star::StarSize::StarLarge; - newStar.Bitmap = starLargeBitmaps.at(RandomNum(0, starLargeBitmapCount - 1)); - newStar.Intensity = RandomNum(111, 185); + newStar.Bitmap = starLargeBitmaps.at(g_DrawRandomGenerator.RandomNum(0, starLargeBitmapCount - 1)); + newStar.Intensity = g_DrawRandomGenerator.RandomNum(111, 185); } else { newStar.Size = Star::StarSize::StarHuge; - newStar.Bitmap = starHugeBitmaps.at(RandomNum(0, starHugeBitmapCount - 1)); - newStar.Intensity = RandomNum(166, 185); + newStar.Bitmap = starHugeBitmaps.at(g_DrawRandomGenerator.RandomNum(0, starHugeBitmapCount - 1)); + newStar.Intensity = g_DrawRandomGenerator.RandomNum(166, 185); } - newStar.Position = Vector(RandomNum(0.0F, static_cast(g_WindowMan.GetResX())), RandomNum(-100.0F, static_cast(m_Nebula.GetBitmap()->h))); + newStar.Position = Vector(g_DrawRandomGenerator.RandomNum(0.0F, static_cast(g_WindowMan.GetResX())), g_DrawRandomGenerator.RandomNum(-100.0F, static_cast(m_Nebula.GetBitmap()->h))); m_BackdropStars.emplace_back(newStar); } @@ -562,7 +562,7 @@ void TitleScreen::Draw() { DrawSlideshowSlide(); } else if (m_IntroSequenceState == IntroSequence::SlideshowEnd) { m_PreGameLogoText.Draw(g_FrameMan.GetBackBuffer32()); - int blendAmount = 220 + RandomNum(-35, 35); + int blendAmount = 220 + g_DrawRandomGenerator.RandomNum(-35, 35); set_screen_blender(blendAmount, blendAmount, blendAmount, blendAmount); m_PreGameLogoTextGlow.Draw(g_FrameMan.GetBackBuffer32(), Vector(), DrawMode::g_DrawTrans); } @@ -596,7 +596,7 @@ void TitleScreen::DrawTitleScreenScene() { rlSetBlendMode(RL_BLEND_CUSTOM_SEPARATE); for (const Star& star: m_BackdropStars) { - int intensity = star.Intensity + RandomNum(0, (star.Size == Star::StarSize::StarSmall) ? 35 : 70); + int intensity = star.Intensity + g_DrawRandomGenerator.RandomNum(0, (star.Size == Star::StarSize::StarSmall) ? 35 : 70); // set_screen_blender(intensity, intensity, intensity, intensity); int starPosY = static_cast(star.Position.GetY() - (m_ScrollOffset.GetY() * (m_Nebula.GetScrollRatio().GetY() * ((star.Size == Star::StarSize::StarSmall) ? 0.8F : 1.0F)))); DrawTexture(g_GLResourceMan.GetStaticTextureFromBitmap(star.Bitmap), star.Position.m_X, starPosY, RLColor(intensity, intensity, intensity, intensity)); @@ -628,7 +628,7 @@ void TitleScreen::DrawGameLogo() { rlEnableColorBlend(); rlSetBlendFactorsSeparate(GL_ONE, GL_ONE_MINUS_SRC_COLOR, GL_ONE, GL_ONE_MINUS_SRC_ALPHA, GL_FUNC_ADD, GL_FUNC_ADD); rlSetBlendMode(RL_BLEND_CUSTOM_SEPARATE); - int glowIntensity = 220 + RandomNum(-35, 35); + int glowIntensity = 220 + g_DrawRandomGenerator.RandomNum(-35, 35); // set_screen_blender(glowIntensity, glowIntensity, glowIntensity, glowIntensity); // m_GameLogoGlow.Draw(g_FrameMan.GetBackBuffer32(), Vector(), DrawMode::g_DrawTrans); DrawTextureV(g_GLResourceMan.GetStaticTextureFromBitmap(m_GameLogoGlow.GetSpriteFrame(0)), m_GameLogoGlow.GetPos() + m_GameLogoGlow.GetSpriteOffset(), RLColor(glowIntensity, glowIntensity, glowIntensity, glowIntensity)); diff --git a/Source/System/Atom.cpp b/Source/System/Atom.cpp index bfda1676b1..d66433bf77 100644 --- a/Source/System/Atom.cpp +++ b/Source/System/Atom.cpp @@ -1058,7 +1058,7 @@ int Atom::Travel(float travelTime, bool autoTravel) { Vector topLeftExtent = Vector(trailPoints[0].first, trailPoints[0].second); Vector bottomRightExtent = topLeftExtent + Vector(1.0F, 1.0F); - int length = static_cast(static_cast(m_TrailLength) * RandomNum(1.0F - m_TrailLengthVariation, 1.0F)); + int length = static_cast(static_cast(m_TrailLength) * g_DrawRandomGenerator.RandomNum(1.0F - m_TrailLengthVariation, 1.0F)); for (size_t i = trailPoints.size() - std::min(length, static_cast(trailPoints.size())); i < trailPoints.size(); ++i) { putpixel(trailBitmap, trailPoints[i].first, trailPoints[i].second, m_TrailColor.GetIndex()); diff --git a/Source/System/Atom.h b/Source/System/Atom.h index ea5f3d2d03..118aacb26b 100644 --- a/Source/System/Atom.h +++ b/Source/System/Atom.h @@ -137,11 +137,11 @@ namespace RTE { /// Gets the group ID of this Atom. /// @return The group ID of this Atom. - long GetSubID() const { return m_SubgroupID; } + int64_t GetSubID() const { return m_SubgroupID; } /// Sets the subgroup ID of this Atom. /// @param newID The new subgroup ID of this Atom. - void SetSubID(long newID = 0) { m_SubgroupID = newID; } + void SetSubID(int64_t newID = 0) { m_SubgroupID = newID; } /// Gets the material of this Atom. /// @return The material of this Atom. @@ -356,7 +356,7 @@ namespace RTE { Vector m_OriginalOffset; //!< This offset is before altering the m_Offset for use in composite groups. Vector m_Normal; //!< The current normalized surface normal Vector of this Atom. Material const* m_Material; //!< The material this Atom is made of. - int m_SubgroupID; //!< Identifying ID for adding and removing atoms from AtomGroups. + int64_t m_SubgroupID; //!< Identifying ID for adding and removing atoms from AtomGroups. bool m_StepWasTaken; //!< Whether the last call to StepForward actually resulted in a step or not. float m_StepRatio; //!< The normalized ratio of how many steps are actually taken to how many calls to TakeStep are made. diff --git a/Source/System/DeterminismChecker.cpp b/Source/System/DeterminismChecker.cpp new file mode 100644 index 0000000000..819eff9362 --- /dev/null +++ b/Source/System/DeterminismChecker.cpp @@ -0,0 +1,153 @@ +#include "DeterminismChecker.h" + +#include "ActivityMan.h" +#include "ConsoleMan.h" +#include "LuaMan.h" +#include "MovableMan.h" +#include "SceneMan.h" +#include "TimerMan.h" + +#include "Activity.h" +#include "Scene.h" +#include "SLTerrain.h" + +#include "RTEError.h" +#include "RTETools.h" +#include "System.h" + +#include "allegro.h" + +#include +#include + +using namespace RTE; + +DeterminismChecker::Mode DeterminismChecker::s_Mode = Mode::Inactive; +bool DeterminismChecker::s_Diverged = false; +std::string DeterminismChecker::s_TraceFilePath; +std::fstream DeterminismChecker::s_TraceFile; +std::thread::id DeterminismChecker::s_MainThreadID; + +void DeterminismChecker::StartRecording(const std::string& traceFilePath) { + OpenTraceFile(traceFilePath, std::ios::out | std::ios::trunc); + s_Mode = Mode::Recording; +} + +void DeterminismChecker::StartVerifying(const std::string& traceFilePath) { + OpenTraceFile(traceFilePath, std::ios::in); + s_Mode = Mode::Verifying; +} + +void DeterminismChecker::OpenTraceFile(const std::string& traceFilePath, std::ios::openmode openMode) { + s_TraceFilePath = traceFilePath; + s_TraceFile.open(traceFilePath, openMode); + if (!s_TraceFile.is_open()) { + // This happens before there are any managers around to report through, and carrying on without checking anything would look just like a run that passed. + RTEError::ShowMessageBox("Failed to open the determinism trace file \"" + traceFilePath + "\"!"); + std::exit(EXIT_FAILURE); + } + s_MainThreadID = std::this_thread::get_id(); +} + +void DeterminismChecker::Stop() { + if (!IsActive()) { + return; + } + + if (g_SceneMan.GetScene()) { + CheckpointSimState("End of run", true); + } + + std::string remainingCheckpoint; + if (s_Mode == Mode::Verifying && !s_Diverged && std::getline(s_TraceFile, remainingCheckpoint)) { + ReportDivergence(remainingCheckpoint, "Nothing, this run ended before the recorded one did."); + } + + s_TraceFile.close(); + s_Mode = Mode::Inactive; +} + +void DeterminismChecker::SubmitCheckpoint(std::string_view label, uint64_t hash) { + RTEAssert(std::this_thread::get_id() == s_MainThreadID, "Determinism checkpoints can only be made from the main thread, as there's no telling which order they would get made in otherwise!"); + + std::string checkpoint = std::format("{} {:016x} {}", g_TimerMan.GetSimUpdateCount(), hash, label); + + if (s_Mode == Mode::Recording) { + s_TraceFile << checkpoint << "\n"; + } else if (!s_Diverged) { + std::string expectedCheckpoint; + if (!std::getline(s_TraceFile, expectedCheckpoint)) { + ReportDivergence("Nothing, the recorded run ended before this one did.", checkpoint); + } else if (checkpoint != expectedCheckpoint) { + ReportDivergence(expectedCheckpoint, checkpoint); + } + } +} + +void DeterminismChecker::CheckpointSimState(std::string_view label) { + CheckpointSimState(label, g_TimerMan.GetSimUpdateCount() % c_SceneCheckInterval == 0); +} + +void DeterminismChecker::CheckpointSimState(std::string_view label, bool checkSceneLayers) { + Checkpoint(std::format("{} - RNG", label), g_RandomGenerator.PeekNext()); + + Checkpoint(std::format("{} - Master Lua state RNG", label), g_LuaMan.GetMasterScriptState().GetRandomGenerator().PeekNext()); + const LuaStatesArray& luaStates = g_LuaMan.GetThreadedScriptStates(); + for (size_t i = 0; i < luaStates.size(); ++i) { + Checkpoint(std::format("{} - Lua state {} RNG", label, i), luaStates[i].GetRandomGenerator().PeekNext()); + } + + StateHasher movableObjectsHasher; + g_MovableMan.HashState(movableObjectsHasher); + Checkpoint(std::format("{} - MovableObjects", label), movableObjectsHasher.GetHash()); + + if (const Activity* activity = g_ActivityMan.GetActivity()) { + StateHasher activityHasher; + activityHasher.Add(activity->GetActivityState()); + for (int team = Activity::Teams::TeamOne; team < Activity::Teams::MaxTeamCount; ++team) { + activityHasher.Add(activity->GetTeamFunds(team)); + } + Checkpoint(std::format("{} - Activity", label), activityHasher.GetHash()); + } + + if (checkSceneLayers) { + CheckpointSceneLayers(label); + } +} + +void DeterminismChecker::CheckpointSceneLayers(std::string_view label) { + Scene* scene = g_SceneMan.GetScene(); + if (!scene) { + return; + } + + auto hashBitmap = [](BITMAP* bitmap) { + StateHasher hasher; + size_t bytesPerLine = static_cast(bitmap->w) * (bitmap_color_depth(bitmap) / 8); + for (int y = 0; y < bitmap->h; ++y) { + hasher.AddBytes(bitmap->line[y], bytesPerLine); + } + return hasher.GetHash(); + }; + + Checkpoint(std::format("{} - Terrain material", label), hashBitmap(scene->GetTerrain()->GetMaterialBitmap())); + + for (int team = Activity::Teams::TeamOne; team < Activity::Teams::MaxTeamCount; ++team) { + if (const SceneLayer* unseenLayer = scene->GetUnseenLayer(team)) { + Checkpoint(std::format("{} - Team {} unseen layer", label, team + 1), hashBitmap(unseenLayer->GetBitmap())); + } + } +} + +void DeterminismChecker::ReportDivergence(const std::string& expectedCheckpoint, const std::string& actualCheckpoint) { + s_Diverged = true; + + // Checkpoints are written out as the sim update they were made in, the hash of the checked values, and the label. + std::string report = "Determinism break! This run diverged from the one recorded in \"" + s_TraceFilePath + "\".\nExpected: " + expectedCheckpoint + "\nGot: " + actualCheckpoint; + + g_ConsoleMan.PrintString("ERROR: " + report); + // The console can't be seen from the outside, so also report it in a way that whatever launched this run can pick up. + std::cerr << report << std::endl; + + System::SetQuit(); +} diff --git a/Source/System/DeterminismChecker.h b/Source/System/DeterminismChecker.h new file mode 100644 index 0000000000..c666bb529b --- /dev/null +++ b/Source/System/DeterminismChecker.h @@ -0,0 +1,145 @@ +#pragma once + +#include "Vector.h" + +#include +#include +#include +#include +#include +#include + +namespace RTE { + + /// Accumulates a hash of the values added to it. Adding the same values in the same order always results in the same hash. + class StateHasher { + + public: + /// Adds a block of memory to the hash. + /// @param data The start of the block. + /// @param size The size of the block, in bytes. + void AddBytes(const void* data, size_t size) { + const unsigned char* bytes = static_cast(data); + for (size_t i = 0; i < size; ++i) { + m_Hash = (m_Hash ^ bytes[i]) * c_FNVPrime; + } + } + + /// Adds a number or enum value to the hash. Pointers are deliberately not accepted, since they differ from run to run. + /// @param value The value to add. + template + requires std::is_arithmetic_v || std::is_enum_v + void Add(ValueType value) { AddBytes(&value, sizeof(value)); } + + /// Adds a Vector to the hash. + /// @param value The Vector to add. + void Add(const Vector& value) { + Add(value.m_X); + Add(value.m_Y); + } + + /// Adds a string to the hash. + /// @param value The string to add. + void Add(std::string_view value) { AddBytes(value.data(), value.size()); } + + /// Gets the hash of everything added so far. + /// @return The hash. + uint64_t GetHash() const { return m_Hash; } + + private: + // This is the 64-bit FNV-1a hash. + static constexpr uint64_t c_FNVOffsetBasis = 14695981039346656037ULL; + static constexpr uint64_t c_FNVPrime = 1099511628211ULL; + + uint64_t m_Hash = c_FNVOffsetBasis; //!< The hash of everything added so far. + }; + + /// Finds out whether, and where, two runs of the simulation diverge. + /// The simulation makes checkpoints as it goes, each a label and a hash of some of its state. One run records these to a trace file, which later runs verify their own checkpoints against. + class DeterminismChecker { + + public: + /// Starts recording all checkpoints to a trace file. + /// @param traceFilePath The path of the trace file to write. + static void StartRecording(const std::string& traceFilePath); + + /// Starts verifying all checkpoints against a previously recorded trace file. The first checkpoint that doesn't match gets reported, and quits the program. + /// @param traceFilePath The path of the trace file to verify against. + static void StartVerifying(const std::string& traceFilePath); + + /// Makes a last checkpoint of the simulation state, and stops recording or verifying. + static void Stop(); + + /// Gets whether checkpoints are being recorded or verified. + /// @return Whether checkpoints are being recorded or verified. + static bool IsActive() { return s_Mode != Mode::Inactive; } + + /// Gets whether a checkpoint that didn't match the trace being verified against was found. + /// @return Whether this run diverged from the recorded one. + static bool HasDiverged() { return s_Diverged; } + + /// Makes a checkpoint out of the passed in values. Can only be done from the main thread, since the checkpoints have to be made in the same order each run. + /// @param label A description of what is being checked, to tell the checkpoints apart. + /// @param values The values to check. See the StateHasher for which types can be used. + template + static void Checkpoint(std::string_view label, const ValueTypes&... values) { + StateHasher hasher; + (hasher.Add(values), ...); + SubmitCheckpoint(label, hasher.GetHash()); + } + + /// Makes checkpoints out of the state of the simulation as a whole. The layers of the Scene are big, so they are only included every once in a while. + /// @param label A description of the point in the simulation update this is done at. + static void CheckpointSimState(std::string_view label); + + private: + enum class Mode { + Inactive, + Recording, + Verifying + }; + + static constexpr int c_SceneCheckInterval = 60; //!< How many sim updates to go between checking the layers of the Scene. + + static Mode s_Mode; //!< Whether checkpoints are being recorded, verified or neither. + static bool s_Diverged; //!< Whether a checkpoint that didn't match the trace being verified against was found. + static std::string s_TraceFilePath; //!< The path of the trace file being recorded to or verified against. + static std::fstream s_TraceFile; //!< The trace file being recorded to or verified against. + static std::thread::id s_MainThreadID; //!< The thread that checkpoints are allowed to be made from. + + /// Opens the trace file, and quits the program if that fails. + /// @param traceFilePath The path of the trace file. + /// @param openMode Whether to open the file for reading or writing. + static void OpenTraceFile(const std::string& traceFilePath, std::ios::openmode openMode); + + /// Records the checkpoint, or verifies it against the next one in the trace file. + /// @param label A description of what is being checked. + /// @param hash The hash of the values being checked. + static void SubmitCheckpoint(std::string_view label, uint64_t hash); + + /// Makes checkpoints out of the state of the simulation as a whole. + /// @param label A description of the point in the simulation update this is done at. + /// @param checkSceneLayers Whether to include the layers of the Scene. + static void CheckpointSimState(std::string_view label, bool checkSceneLayers); + + /// Makes checkpoints out of the layers of the Scene that the simulation depends on. + /// @param label A description of the point in the simulation update this is done at. + static void CheckpointSceneLayers(std::string_view label); + + /// Reports that this run doesn't match the trace being verified against, and quits the program. + /// @param expectedCheckpoint The checkpoint from the trace file. + /// @param actualCheckpoint The checkpoint that this run made instead. + static void ReportDivergence(const std::string& expectedCheckpoint, const std::string& actualCheckpoint); + }; +} // namespace RTE + +// Use these to make checkpoints rather than calling the DeterminismChecker directly, so that nothing gets evaluated unless it is active. +#define DeterminismCheck(label, ...) \ + if (RTE::DeterminismChecker::IsActive()) { \ + RTE::DeterminismChecker::Checkpoint(label, __VA_ARGS__); \ + } + +#define DeterminismCheckSimState(label) \ + if (RTE::DeterminismChecker::IsActive()) { \ + RTE::DeterminismChecker::CheckpointSimState(label); \ + } diff --git a/Source/System/GenericSavedData.cpp b/Source/System/GenericSavedData.cpp index b7fd66cfa7..a9609b33b3 100644 --- a/Source/System/GenericSavedData.cpp +++ b/Source/System/GenericSavedData.cpp @@ -106,7 +106,7 @@ int GenericSavedData::GenericSavedStrings::Save(Writer& writer) const { } int GenericSavedData::GenericSavedNumbers::ReadProperty(const std::string_view& propName, Reader& reader) { - float value; + double value; reader >> value; m_Data[std::string(propName)] = value; // until we get P0919R2. return 0; diff --git a/Source/System/GenericSavedData.h b/Source/System/GenericSavedData.h index db9ba911d7..ba815e72f5 100644 --- a/Source/System/GenericSavedData.h +++ b/Source/System/GenericSavedData.h @@ -61,7 +61,7 @@ namespace RTE { /// @param reference A reference to the GenericSavedNumbers to deep copy. GenericSavedNumbers(const GenericSavedNumbers& reference) = default; - std::unordered_map m_Data; //!< Stored number data. + std::unordered_map m_Data; //!< Stored number data. private: static const std::string c_ClassName; //!< A string with the friendly formatted type name of this object. @@ -81,8 +81,8 @@ namespace RTE { void SaveString(const std::string& key, const std::string& value); const std::string& LoadString(const std::string& key); - void SaveNumber(const std::string& key, float value) { m_SavedNumbers.m_Data[key] = value; }; - float LoadNumber(const std::string& key) { return m_SavedNumbers.m_Data[key]; }; + void SaveNumber(const std::string& key, double value) { m_SavedNumbers.m_Data[key] = value; }; + double LoadNumber(const std::string& key) { return m_SavedNumbers.m_Data[key]; }; GenericSavedEncodedStrings m_SavedEncodedStrings; //!< Stored encoded string data. GenericSavedStrings m_SavedStrings; //!< Stored string data. diff --git a/Source/System/PathFinder.cpp b/Source/System/PathFinder.cpp index c62653b9a1..9b86f612d4 100644 --- a/Source/System/PathFinder.cpp +++ b/Source/System/PathFinder.cpp @@ -151,8 +151,6 @@ MicroPather* PathFinder::GetPather() { int PathFinder::CalculatePath(Vector start, Vector end, std::list& pathResult, float& totalCostResult, float jumpHeight, float digStrength) { ZoneScoped; - ++m_CurrentPathingRequests; - // Make sure start and end are within scene bounds. g_SceneMan.ForceBounds(start); g_SceneMan.ForceBounds(end); @@ -221,8 +219,6 @@ int PathFinder::CalculatePath(Vector start, Vector end, std::list& pathR pathResult.push_back(end); } - --m_CurrentPathingRequests; - // TODO: Clean up the path, remove series of nodes in the same direction etc? return result; } @@ -233,6 +229,10 @@ std::shared_ptr PathFinder::CalculatePathAsync(Vector star const_cast(pathRequest->startPos) = start; const_cast(pathRequest->targetPos) = end; + // The request counts as active from when it gets queued up until it has been marked as complete, + // so that whatever waits for the requests to be done doesn't miss the ones that haven't been started on yet, or that aren't marked as complete yet. + ++m_CurrentPathingRequests; + g_ThreadMan.GetBackgroundThreadPool().push_task( [this, start, end, jumpHeight, digStrength, callback](std::shared_ptr volRequest) { // Cast away the volatile-ness - only matters outside (and complicates the API otherwise) @@ -250,6 +250,8 @@ std::shared_ptr PathFinder::CalculatePathAsync(Vector star // Have to set to complete after the callback, so anything that blocks on it knows that the callback will have been called by now // This has the awkward side-effect that the complete flag is actually false during the callback - but that's fine, if it's called we know it's complete anyways request.complete = true; + + --m_CurrentPathingRequests; }, pathRequest); diff --git a/Source/System/RTEError.cpp b/Source/System/RTEError.cpp index b643bfbf80..182a32b535 100644 --- a/Source/System/RTEError.cpp +++ b/Source/System/RTEError.cpp @@ -48,6 +48,7 @@ using namespace RTE; bool RTEError::s_CurrentlyAborting = false; bool RTEError::s_IgnoreAllAsserts = false; +bool RTEError::s_DisableCrashDialogs = false; std::string RTEError::s_LastIgnoredAssertDescription = ""; std::source_location RTEError::s_LastIgnoredAssertLocation = {}; @@ -214,6 +215,11 @@ void RTEError::SetExceptionHandlers() { } void RTEError::ShowMessageBox(const std::string& message) { + // There's no one around to dismiss a message box when headless, so it would just stall the program. + if (System::IsInHeadlessMode()) { + System::PrintToCLI(message); + return; + } if (!IsOnAppMainThread()) { std::fprintf(stderr, "RTE Warning (from worker thread): %s\n", message.c_str()); return; @@ -222,6 +228,9 @@ void RTEError::ShowMessageBox(const std::string& message) { } bool RTEError::ShowAbortMessageBox(const std::string& message) { + if (System::IsInHeadlessMode() || s_DisableCrashDialogs) { + return false; + } if (!IsOnAppMainThread()) { std::fprintf(stderr, "RTE Abort (from worker thread): %s\n", message.c_str()); return false; @@ -259,6 +268,10 @@ bool RTEError::ShowAbortMessageBox(const std::string& message) { } bool RTEError::ShowAssertMessageBox(const std::string& message) { + // There's no one around to choose to ignore the assert when headless, and no asking them when crash dialogs are disabled, so always abort. + if (System::IsInHeadlessMode() || s_DisableCrashDialogs) { + return true; + } if (!IsOnAppMainThread()) { // Return false (Ignore-once) so the worker can unwind; the main thread sees the assert on its next pass. std::fprintf(stderr, "RTE Assert (from worker thread): %s\n", message.c_str()); @@ -343,7 +356,9 @@ void RTEError::UnhandledExceptionFunc(const std::string& description, const std: SDL_SetWindowFullscreen(g_WindowMan.GetWindow(), 0); } - SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, "RTE CATASTROPHIC ERROR!!! (X_X)", exceptionMessage.c_str(), nullptr); + if (!System::IsInHeadlessMode() && !s_DisableCrashDialogs) { + SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, "RTE CATASTROPHIC ERROR!!! (X_X)", exceptionMessage.c_str(), nullptr); + } AbortAction; } @@ -607,6 +622,11 @@ bool RTEError::DumpAbortScreen() { } bool RTEError::DumpAbortSave() { + // Headless runs are automated, so they shouldn't get to overwrite an abort save from an actual play session. + if (System::IsInHeadlessMode()) { + return false; + } + bool success = false; if (g_ActivityMan.GetActivity() && g_ActivityMan.GetActivity()->CanBeUserSaved()) { success = g_ActivityMan.SaveCurrentGame("AbortSave"); diff --git a/Source/System/RTEError.h b/Source/System/RTEError.h index d06c8b49be..595d49b627 100644 --- a/Source/System/RTEError.h +++ b/Source/System/RTEError.h @@ -26,6 +26,7 @@ namespace RTE { public: static bool s_CurrentlyAborting; //!< Flag to prevent a potential recursive fault while attempting to save the game when aborting. static bool s_IgnoreAllAsserts; //!< Whether to skip the assert dialog and just let everything burn at whatever point that happens. + static bool s_DisableCrashDialogs; //!< Whether to not show a dialog when the game aborts, runs into an unhandled exception or fails an assert, and just log it and quit. static std::string s_LastIgnoredAssertDescription; //!< The last ignored assert message. static std::source_location s_LastIgnoredAssertLocation; //!< The last ignored assert call site. diff --git a/Source/System/RTETools.cpp b/Source/System/RTETools.cpp index 4c99c80bec..0cfdb1ce0d 100644 --- a/Source/System/RTETools.cpp +++ b/Source/System/RTETools.cpp @@ -9,6 +9,19 @@ namespace RTE { RandomGenerator g_RandomGenerator; + RandomGenerator g_DrawRandomGenerator; + + thread_local RandomGenerator* s_ThreadRandomGenerator = nullptr; + + static uint32_t s_SeedOffset = 0; + + void SetThreadRandomGenerator(RandomGenerator* randomGenerator) { + s_ThreadRandomGenerator = randomGenerator; + } + + RandomGenerator& GetThreadRandomGenerator() { + return s_ThreadRandomGenerator ? *s_ThreadRandomGenerator : g_RandomGenerator; + } void SeedRNG() { // Use a constant seed for determinism. @@ -28,7 +41,12 @@ namespace RTE { return static_cast(seedResult); }(); - g_RandomGenerator.Seed(constSeed); + g_RandomGenerator.Seed(constSeed + s_SeedOffset); + g_DrawRandomGenerator.Seed(constSeed); + } + + void SetRNGSeedOffset(uint32_t seedOffset) { + s_SeedOffset = seedOffset; } float Lerp(float scaleStart, float scaleEnd, float startValue, float endValue, float progressScalar) { diff --git a/Source/System/RTETools.h b/Source/System/RTETools.h index 0ce5e77a77..b40578168a 100644 --- a/Source/System/RTETools.h +++ b/Source/System/RTETools.h @@ -23,6 +23,10 @@ namespace RTE { /// Seed the random number generator. void Seed(uint64_t seed) { m_RNG.seed(seed); }; + /// Gets the next raw number the generator will produce, without advancing it. This can be used to check whether two generators are in the same state. + /// @return The next raw number the generator will produce. + uint32_t PeekNext() const { return static_cast(std::mt19937(m_RNG)()); } + /// Function template which returns a uniformly distributed random number in the range [-1, 1]. /// @return Uniformly distributed random number in the range [-1, 1]. template @@ -77,40 +81,54 @@ namespace RTE { }; extern RandomGenerator g_RandomGenerator; //!< The global random number generator used in our simulation thread. + extern RandomGenerator g_DrawRandomGenerator; //!< The random number generator for drawing and audio code. How much of either goes on is not up to the simulation, so they can't be allowed to use up the simulation's random numbers. /// Seed global the global random number generators. void SeedRNG(); - // TODO: Maybe remove these passthrough functions and force the user to manually specify if they want the simulation thread random, - // Or, in future, a render-thread random, as right now determinism isn't viable because framerate affects sim updates per draw + /// Sets a number that SeedRNG adds to the seed of the simulation's random number generator, so that the simulation gets a different stream of random numbers. + /// @param seedOffset The number to add to the seed. With 0, the simulation gets its usual random numbers. + void SetRNGSeedOffset(uint32_t seedOffset); + + /// Sets a random number generator for the global random functions to use when called from this thread, instead of the global one. + /// This is for keeping whatever is being run from affecting the random numbers that the rest of the simulation gets. + /// @param randomGenerator The random number generator to use, or nullptr to go back to the global one. Ownership is NOT transferred! + void SetThreadRandomGenerator(RandomGenerator* randomGenerator); + + /// Gets the random number generator that the global random functions use when called from this thread. + /// @return The random number generator set for this thread, or the global one if there is none. + RandomGenerator& GetThreadRandomGenerator(); + + // These all use the simulation's random number generator, so drawing and audio code have to use g_DrawRandomGenerator instead of them. + // TODO: Maybe remove these passthrough functions and force the user to manually specify if they want the simulation random or the draw random. template typename std::enable_if::value, floatType>::type RandomNormalNum() { - return g_RandomGenerator.RandomNormalNum(); + return GetThreadRandomGenerator().RandomNormalNum(); } template typename std::enable_if::value, intType>::type RandomNormalNum() { - return g_RandomGenerator.RandomNormalNum(); + return GetThreadRandomGenerator().RandomNormalNum(); } template typename std::enable_if::value, floatType>::type RandomNum() { - return g_RandomGenerator.RandomNum(); + return GetThreadRandomGenerator().RandomNum(); } template typename std::enable_if::value, intType>::type RandomNum() { - return g_RandomGenerator.RandomNum(); + return GetThreadRandomGenerator().RandomNum(); } template typename std::enable_if::value, floatType>::type RandomNum(floatType min, floatType max) { - return g_RandomGenerator.RandomNum(min, max); + return GetThreadRandomGenerator().RandomNum(min, max); } template typename std::enable_if::value, intType>::type RandomNum(intType min, intType max) { - return g_RandomGenerator.RandomNum(min, max); + return GetThreadRandomGenerator().RandomNum(min, max); } #pragma endregion diff --git a/Source/System/System.cpp b/Source/System/System.cpp index cb41eb6f16..9668cd69d9 100644 --- a/Source/System/System.cpp +++ b/Source/System/System.cpp @@ -38,6 +38,7 @@ using namespace RTE; bool System::s_Quit = false; bool System::s_LogToCLI = false; bool System::s_ExternalModuleValidation = false; +bool System::s_Headless = false; std::string System::s_ThisExePathAndName = ""; std::string System::s_WorkingDirectory = "."; std::vector System::s_WorkingTree; diff --git a/Source/System/System.h b/Source/System/System.h index 379ba4761b..398653fbd1 100644 --- a/Source/System/System.h +++ b/Source/System/System.h @@ -116,6 +116,15 @@ namespace RTE { static void EnableExternalModuleValidationMode() { s_ExternalModuleValidation = true; } #pragma endregion +#pragma region Headless Mode + /// Gets whether the program is running in headless mode, where nothing is presented to the user and the simulation runs as fast as it can instead of in real time. + /// @return Whether the program is running in headless mode. + static bool IsInHeadlessMode() { return s_Headless; } + + /// Sets the program to run in headless mode. This has to be done before the managers are initialized. + static void EnableHeadlessMode() { s_Headless = true; } +#pragma endregion + #pragma region Misc /// Fires up the default browser for the current OS on a specific URL. /// @param goToURL A string with the URL to send the browser to. @@ -132,6 +141,7 @@ namespace RTE { static bool s_Quit; //!< Whether the user requested program termination through GUI or the window close button. static bool s_LogToCLI; //!< Bool to tell whether to print the loading log and anything specified with PrintToCLI to command-line or not. static bool s_ExternalModuleValidation; //!< Whether to run the program in a special mode where it will immediately quit without any messages after either successful loading of all modules or aborting during loading. For use by an external tool. + static bool s_Headless; //!< Whether to run the program without presenting anything to the user, and with the simulation running as fast as it can instead of in real time. static std::string s_ThisExePathAndName; //!< String containing the absolute path to this executable. Used for relaunching via abort message. static std::string s_WorkingDirectory; //!< String containing the absolute path to current working directory. static std::vector s_WorkingTree; //!< Vector of the hashes of all file paths in the working directory. diff --git a/Source/System/Writer.cpp b/Source/System/Writer.cpp index 31ca56c0f3..68f157597e 100644 --- a/Source/System/Writer.cpp +++ b/Source/System/Writer.cpp @@ -1,7 +1,6 @@ #include "Writer.h" #include "System.h" -#include #include using namespace RTE; @@ -36,9 +35,7 @@ int Writer::Create(const std::string& fileName, bool append, bool createDir) { System::MakeDirectory(System::GetWorkingDirectory() + m_FolderPath); } - auto ofStream = std::make_unique(fileName, append ? (std::ios::out | std::ios::app | std::ios::ate) : (std::ios::out | std::ios::trunc)); - *ofStream << std::fixed << std::setprecision(6); - return Create(std::move(ofStream)); + return Create(std::make_unique(fileName, append ? (std::ios::out | std::ios::app | std::ios::ate) : (std::ios::out | std::ios::trunc))); } int Writer::Create(std::unique_ptr&& stream) { diff --git a/Source/System/Writer.h b/Source/System/Writer.h index c85e83c5d4..351b7b4b09 100644 --- a/Source/System/Writer.h +++ b/Source/System/Writer.h @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include @@ -171,12 +172,13 @@ namespace RTE { *m_Stream << var; return *this; } + // Floating point numbers get written with as many digits as it takes for them to be read back as exactly the same number, and no more. Writer& operator<<(const float& var) { - *m_Stream << var; + *m_Stream << std::format("{}", var); return *this; } Writer& operator<<(const double& var) { - *m_Stream << var; + *m_Stream << std::format("{}", var); return *this; } Writer& operator<<(const char* var) { diff --git a/Source/System/meson.build b/Source/System/meson.build index 8128bb4645..39488f4ffc 100644 --- a/Source/System/meson.build +++ b/Source/System/meson.build @@ -19,6 +19,7 @@ sources += files( 'InputScheme.cpp', 'RTETools.cpp', 'System.cpp', +'DeterminismChecker.cpp', 'InputMapping.cpp', 'PathFinder.cpp', 'DataModule.cpp', diff --git a/Tools/determinism_test.py b/Tools/determinism_test.py new file mode 100644 index 0000000000..137013046d --- /dev/null +++ b/Tools/determinism_test.py @@ -0,0 +1,142 @@ +#!/usr/bin/env python3 +"""Checks that the simulation is deterministic, by running scenarios several times and comparing the runs. + +Each scenario is first run to record a trace of determinism checkpoints, then run again to verify against that trace. +The runs are headless unless asked otherwise. A run that doesn't match reports the first checkpoint it diverged at, as the sim update it was made in, +the hash of the checked state, and a label that says what was checked and where in the update. +The runs use a copy of the game's settings with what they need set in it, so the game's own settings are left alone. +A scenario can be run with several seeds, each of which has it play out differently, to cover more of what can happen in it. +A scenario whose Activity ends without a player, such as the AI Battle, can be run until it does. +Several scenarios can be run at the same time, to get through many of them sooner. + +Run this from the game's working directory. For example: + python Tools/determinism_test.py + python Tools/determinism_test.py --activity GAScripted "Wave Defense" "Grasslands" --updates 3000 --verify-windowed + python Tools/determinism_test.py --seeds 0 1 2 3 + python Tools/determinism_test.py --activity GAScripted "AI Battle" "Ketanot Hills" --until-over + python Tools/determinism_test.py --seeds 0 1 2 3 4 5 6 7 --jobs 4 +""" + +import argparse +import os +import subprocess +import sys +import tempfile +from concurrent.futures import ThreadPoolExecutor +from pathlib import Path + +# What gets run when no scenarios are asked for. Each is the type name and preset name of an Activity, and the preset name of a Scene. +# These are missions where things happen on their own. Most other Activities start out waiting for the player to place their brain. +DEFAULT_ACTIVITIES = [ + ("GAScripted", "Dummy Assault", "Dummy Assault"), + ("GAScripted", "Decision Day", "Decision Day"), +] + +# The settings that the runs need, whatever the game's own settings say. +TEST_SETTINGS = { + # Pathing requests otherwise complete after however many sim updates the pathfinding threads happen to take. + "ForceImmediatePathingRequestCompletion": "1", + # A windowed run that crashes otherwise waits for someone to click its crash dialog away. + "DisableCrashDialogs": "1", +} + + +def write_test_settings(path): + """Writes a copy of the game's settings to the path, with the settings that the runs need.""" + lines = Path("Userdata/Settings.ini").read_text().splitlines() + lines = [line for line in lines if line.split("=")[0].strip() not in TEST_SETTINGS] + # The first line is the class name, which the settings have to come after. + lines[1:1] = [f"\t{name} = {value}" for name, value in TEST_SETTINGS.items()] + path.write_text("\n".join(lines) + "\n") + + +def run_game(exe, scenario_args, checker_args, headless, settings, timeout): + """Runs the game through one scenario. Returns whether it finished properly, and what it reported if not.""" + args = [exe, *scenario_args, *checker_args] + if headless: + args.append("-headless") + # This is where the game looks for its settings file, when set. + environment = {**os.environ, "CCCP_SETTINGSPATH": str(settings)} + try: + # The game reports a determinism break on stderr. + result = subprocess.run(args, timeout=timeout, stderr=subprocess.PIPE, text=True, env=environment) + except subprocess.TimeoutExpired: + return False, f"It was still going after {timeout} seconds." + return result.returncode == 0, result.stderr.strip() or f"It exited with code {result.returncode}." + + +def test_scenario(name, scenario_args, options, trace_dir): + """Records a trace of the scenario and verifies further runs against it. Returns whether all the runs matched.""" + file_name = name.replace(" ", "_").replace("/", "_") + trace = trace_dir / (file_name + ".trace") + # The game can write to its settings file, so each scenario gets one of its own in case several are running at the same time. + settings = trace_dir / (file_name + ".Settings.ini") + write_test_settings(settings) + + mode = "windowed" if options.windowed else "headless" + + print(f"{name}: recording, {mode}...", flush=True) + finished, report = run_game(options.exe, scenario_args, ["-determinism-record", str(trace)], mode == "headless", settings, options.timeout) + if not finished: + print(f"{name}: FAILED, the recording run didn't finish properly\n{report}") + return False + # A scenario that fails to launch still exits normally, it just doesn't make any checkpoints. + if trace.stat().st_size == 0: + print(f"{name}: FAILED, the scenario didn't start") + return False + + # A windowed run runs in real time and so takes the longest to fail, which is why the extra one goes last. + verify_runs = [mode] * options.runs + (["windowed"] if options.verify_windowed else []) + for run, mode in enumerate(verify_runs, start=1): + print(f"{name}: verifying, {mode} run {run} of {len(verify_runs)}...", flush=True) + finished, report = run_game(options.exe, scenario_args, ["-determinism-verify", str(trace)], mode == "headless", settings, options.timeout) + if not finished: + print(f"{name}: FAILED\n{report}") + return False + + print(f"{name}: passed") + return True + + +def main(): + parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) + parser.add_argument("--exe", default="Cortex Command.exe", help="the game executable to test (default: %(default)s)") + parser.add_argument("--updates", type=int, help="how many sim updates to run each scenario for (default: 1000, or as many as it takes with --until-over)") + parser.add_argument("--until-over", action="store_true", help="stop running a scenario once its Activity is over, for Activities that get there without a player") + parser.add_argument("--seeds", type=int, nargs="+", default=[0], metavar="SEED", help="the seeds to run each scenario with, each giving the sim different random numbers (default: 0, which gives it its usual ones)") + parser.add_argument("--runs", type=int, default=2, help="how many runs to verify against the recorded one (default: %(default)s)") + parser.add_argument("--jobs", type=int, default=1, help="how many scenarios to run at the same time (default: %(default)s)") + parser.add_argument("--windowed", action="store_true", help="do the runs with the game window, audio and real time pacing, instead of headless") + parser.add_argument("--verify-windowed", action="store_true", help="also verify a windowed run against the recorded one, to check that the sim doesn't depend on drawing, audio or frame pacing") + parser.add_argument("--save", action="append", default=[], metavar="NAME", help="a saved game to run, can be given several times") + parser.add_argument("--activity", action="append", default=[], nargs=3, metavar=("TYPE", "ACTIVITY", "SCENE"), help="an Activity and Scene to run, can be given several times") + parser.add_argument("--trace-dir", type=Path, help="where to keep the recorded traces, instead of a temporary directory") + parser.add_argument("--timeout", type=int, default=1800, help="how many seconds a single run can take before it is given up on (default: %(default)s)") + options = parser.parse_args() + options.exe = str(Path(options.exe).resolve()) + + scenarios =[(f"Save {save}", ["-save", save]) for save in options.save] + scenarios += [(f"{activity} on {scene}", ["-activity", activity_type, activity, scene]) for activity_type, activity, scene in options.activity or ([] if options.save else DEFAULT_ACTIVITIES)] + + scenarios = [(f"{name}, seed {seed}", [*scenario_args, "-seed", str(seed)]) for name, scenario_args in scenarios for seed in options.seeds] + + if options.updates is None and not options.until_over: + options.updates = 1000 + length_args = (["-sim-updates", str(options.updates)] if options.updates is not None else []) + (["-quit-when-over"] if options.until_over else []) + scenarios = [(name, [*scenario_args, *length_args]) for name, scenario_args in scenarios] + + with tempfile.TemporaryDirectory() as temp_dir: + trace_dir = (options.trace_dir or Path(temp_dir)).resolve() + trace_dir.mkdir(parents=True, exist_ok=True) + with ThreadPoolExecutor(options.jobs) as pool: + results = list(pool.map(lambda scenario: test_scenario(*scenario, options, trace_dir), scenarios)) + failed = [name for (name, scenario_args), passed in zip(scenarios, results) if not passed] + + print(f"\n{len(scenarios) - len(failed)} of {len(scenarios)} scenarios are deterministic.") + for name in failed: + print(f" FAILED: {name}") + return 1 if failed else 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/external/sources/LuaJIT-2.1/libluajit.vcxproj b/external/sources/LuaJIT-2.1/libluajit.vcxproj index aec315f63b..6685f9f205 100644 --- a/external/sources/LuaJIT-2.1/libluajit.vcxproj +++ b/external/sources/LuaJIT-2.1/libluajit.vcxproj @@ -80,7 +80,7 @@ _FORTIFY_SOURCE false Level3 - _ITERATOR_DEBUG_LEVEL=0;WIN32;_WINDOWS;_CRT_SECURE_NO_WARNINGS;LUAJIT_OS=LUAJIT_OS_WINDOWS;LUAJIT_UNWIND_EXTERNAL;LUA_MULTILIB="lib";%(PreprocessorDefinitions) + _ITERATOR_DEBUG_LEVEL=0;WIN32;_WINDOWS;_CRT_SECURE_NO_WARNINGS;LUAJIT_OS=LUAJIT_OS_WINDOWS;LUAJIT_UNWIND_EXTERNAL;LUAJIT_SECURITY_PRNG=0;LUAJIT_SECURITY_STRID=0;LUA_MULTILIB="lib";%(PreprocessorDefinitions) $(IntDir) /opt:ref /opt:icf %(AdditionalOptions) false @@ -118,7 +118,7 @@ _FORTIFY_SOURCE false Level3 - _ITERATOR_DEBUG_LEVEL=0;WIN32;_WINDOWS;_CRT_SECURE_NO_WARNINGS;NDEBUG;LUAJIT_OS=LUAJIT_OS_WINDOWS;LUAJIT_UNWIND_EXTERNAL;LUA_MULTILIB="lib";%(PreprocessorDefinitions) + _ITERATOR_DEBUG_LEVEL=0;WIN32;_WINDOWS;_CRT_SECURE_NO_WARNINGS;NDEBUG;LUAJIT_OS=LUAJIT_OS_WINDOWS;LUAJIT_UNWIND_EXTERNAL;LUAJIT_SECURITY_PRNG=0;LUAJIT_SECURITY_STRID=0;LUA_MULTILIB="lib";%(PreprocessorDefinitions) $(IntDir) @@ -159,7 +159,7 @@ _FORTIFY_SOURCE false Level3 - _ITERATOR_DEBUG_LEVEL=0;WIN32;_WINDOWS;_CRT_SECURE_NO_WARNINGS;NDEBUG;LUAJIT_OS=LUAJIT_OS_WINDOWS;LUAJIT_UNWIND_EXTERNAL;LUA_MULTILIB="lib";%(PreprocessorDefinitions) + _ITERATOR_DEBUG_LEVEL=0;WIN32;_WINDOWS;_CRT_SECURE_NO_WARNINGS;NDEBUG;LUAJIT_OS=LUAJIT_OS_WINDOWS;LUAJIT_UNWIND_EXTERNAL;LUAJIT_SECURITY_PRNG=0;LUAJIT_SECURITY_STRID=0;LUA_MULTILIB="lib";%(PreprocessorDefinitions) $(IntDir) false true diff --git a/external/sources/LuaJIT-2.1/src/lj_arch.h b/external/sources/LuaJIT-2.1/src/lj_arch.h index 3c4e3f9b0b..91019e8470 100644 --- a/external/sources/LuaJIT-2.1/src/lj_arch.h +++ b/external/sources/LuaJIT-2.1/src/lj_arch.h @@ -600,6 +600,18 @@ #define LJ_GC64 0 #endif +/* +** Hash tables, functions, userdata and threads by an ID that's kept in them +** instead of by their address, so that a table with them for keys gets +** iterated in the same order every run. The ID goes in padding they only +** have with 64 bit GC references, and the JIT backend has to hash it too. +*/ +#if LJ_GC64 && (LJ_TARGET_X64 || LJ_TARGET_ARM64) +#define LJ_HASHID 1 +#else +#define LJ_HASHID 0 +#endif + /* 2-slot frame info. */ #if LJ_GC64 #define LJ_FR2 1 diff --git a/external/sources/LuaJIT-2.1/src/lj_asm.c b/external/sources/LuaJIT-2.1/src/lj_asm.c index 4818c68dae..b784164b12 100644 --- a/external/sources/LuaJIT-2.1/src/lj_asm.c +++ b/external/sources/LuaJIT-2.1/src/lj_asm.c @@ -1121,6 +1121,10 @@ static uint32_t ir_khash(ASMState *as, IRIns *ir) } else if (irt_ispri(ir->t)) { lj_assertA(!irt_isnil(ir->t), "hash of nil key"); return irt_type(ir->t)-IRT_FALSE; +#if LJ_HASHID + } else if (irt_ishashid(ir->t)) { + return ir_kgc(ir)->gch.hid; +#endif } else { lj_assertA(irt_isgcv(ir->t), "hash of bad IR type %d", irt_type(ir->t)); lo = u32ptr(ir_kgc(ir)); diff --git a/external/sources/LuaJIT-2.1/src/lj_asm_arm64.h b/external/sources/LuaJIT-2.1/src/lj_asm_arm64.h index dfc5490fa9..fd2539b472 100644 --- a/external/sources/LuaJIT-2.1/src/lj_asm_arm64.h +++ b/external/sources/LuaJIT-2.1/src/lj_asm_arm64.h @@ -884,7 +884,8 @@ static void asm_href(ASMState *as, IRIns *ir, IROp merge) Reg tmphash = ra_allock(as, khash, allow); emit_dnm(as, A64I_ANDw, dest, dest, tmphash); emit_lso(as, A64I_LDRw, dest, tab, offsetof(GCtab, hmask)); - } else if (irt_isstr(kt)) { + } else if (irt_isstr(kt) || irt_ishashid(kt)) { + /* A hash ID is at the same offset as the sid of a string. */ emit_dnm(as, A64I_ANDw, dest, dest, tmp); emit_lso(as, A64I_LDRw, tmp, key, offsetof(GCstr, sid)); emit_lso(as, A64I_LDRw, dest, tab, offsetof(GCtab, hmask)); diff --git a/external/sources/LuaJIT-2.1/src/lj_asm_x86.h b/external/sources/LuaJIT-2.1/src/lj_asm_x86.h index 3d68baefdc..c045d13506 100644 --- a/external/sources/LuaJIT-2.1/src/lj_asm_x86.h +++ b/external/sources/LuaJIT-2.1/src/lj_asm_x86.h @@ -1276,7 +1276,8 @@ static void asm_href(ASMState *as, IRIns *ir, IROp merge) if (isk) { emit_gri(as, XG_ARITHi(XOg_AND), dest, (int32_t)khash); emit_rmro(as, XO_MOV, dest, tab, offsetof(GCtab, hmask)); - } else if (irt_isstr(kt)) { + } else if (irt_isstr(kt) || irt_ishashid(kt)) { + /* A hash ID is at the same offset as the sid of a string. */ emit_rmro(as, XO_ARITH(XOg_AND), dest, key, offsetof(GCstr, sid)); emit_rmro(as, XO_MOV, dest, tab, offsetof(GCtab, hmask)); } else { /* Must match with hashrot() in lj_tab.c. */ diff --git a/external/sources/LuaJIT-2.1/src/lj_func.c b/external/sources/LuaJIT-2.1/src/lj_func.c index e9dd36d869..c9bb4dc1f8 100644 --- a/external/sources/LuaJIT-2.1/src/lj_func.c +++ b/external/sources/LuaJIT-2.1/src/lj_func.c @@ -12,6 +12,7 @@ #include "lj_obj.h" #include "lj_gc.h" #include "lj_func.h" +#include "lj_tab.h" #include "lj_trace.h" #include "lj_vm.h" @@ -113,6 +114,7 @@ GCfunc *lj_func_newC(lua_State *L, MSize nelems, GCtab *env) GCfunc *fn = (GCfunc *)lj_mem_newgco(L, sizeCfunc(nelems)); fn->c.gct = ~LJ_TFUNC; fn->c.ffid = FF_C; + lj_tab_sethid(G(L), &fn->c); fn->c.nupvalues = (uint8_t)nelems; /* NOBARRIER: The GCfunc is new (marked white). */ setmref(fn->c.pc, &G(L)->bc_cfunc_ext); @@ -126,6 +128,7 @@ static GCfunc *func_newL(lua_State *L, GCproto *pt, GCtab *env) GCfunc *fn = (GCfunc *)lj_mem_newgco(L, sizeLfunc((MSize)pt->sizeuv)); fn->l.gct = ~LJ_TFUNC; fn->l.ffid = FF_LUA; + lj_tab_sethid(G(L), &fn->l); fn->l.nupvalues = 0; /* Set to zero until upvalues are initialized. */ /* NOBARRIER: Really a setgcref. But the GCfunc is new (marked white). */ setmref(fn->l.pc, proto_bc(pt)); diff --git a/external/sources/LuaJIT-2.1/src/lj_ir.h b/external/sources/LuaJIT-2.1/src/lj_ir.h index b9f5e2c13e..5a761578cc 100644 --- a/external/sources/LuaJIT-2.1/src/lj_ir.h +++ b/external/sources/LuaJIT-2.1/src/lj_ir.h @@ -373,6 +373,13 @@ typedef struct IRType1 { uint8_t irt; } IRType1; #define irt_isstr(t) (irt_type(t) == IRT_STR) #define irt_istab(t) (irt_type(t) == IRT_TAB) #define irt_iscdata(t) (irt_type(t) == IRT_CDATA) +#if LJ_HASHID +#define irt_ishashid(t) \ + (irt_istab(t) || irt_type(t) == IRT_FUNC || irt_type(t) == IRT_UDATA || \ + irt_type(t) == IRT_THREAD) +#else +#define irt_ishashid(t) 0 +#endif #define irt_isfloat(t) (irt_type(t) == IRT_FLOAT) #define irt_isnum(t) (irt_type(t) == IRT_NUM) #define irt_isint(t) (irt_type(t) == IRT_INT) diff --git a/external/sources/LuaJIT-2.1/src/lj_obj.h b/external/sources/LuaJIT-2.1/src/lj_obj.h index c3417fd150..ccae506c3b 100644 --- a/external/sources/LuaJIT-2.1/src/lj_obj.h +++ b/external/sources/LuaJIT-2.1/src/lj_obj.h @@ -324,6 +324,9 @@ typedef struct GCudata { GCHeader; uint8_t udtype; /* Userdata type. */ uint8_t unused2; +#if LJ_HASHID + uint32_t hid; /* What tables hash this by. Same offset as GCstr.sid. */ +#endif GCRef env; /* Should be at same offset in GCfunc. */ MSize len; /* Size of payload. */ #if LJ_GC64 @@ -454,9 +457,15 @@ typedef struct GCupval { /* -- Function object (closures) ------------------------------------------ */ /* Common header for functions. env should be at same offset in GCudata. */ +#if LJ_HASHID +#define GCfuncHeader \ + GCHeader; uint8_t ffid; uint8_t nupvalues; uint32_t hid; \ + GCRef env; GCRef gclist; MRef pc +#else #define GCfuncHeader \ GCHeader; uint8_t ffid; uint8_t nupvalues; \ GCRef env; GCRef gclist; MRef pc +#endif typedef struct GCfuncC { GCfuncHeader; @@ -502,6 +511,9 @@ typedef struct GCtab { GCHeader; uint8_t nomm; /* Negative cache for fast metamethods. */ int8_t colo; /* Array colocation. */ +#if LJ_HASHID + uint32_t hid; /* What tables hash this by. Same offset as GCstr.sid. */ +#endif MRef array; /* Array part. */ GCRef gclist; GCRef metatable; /* Must be at same offset in GCudata. */ @@ -663,6 +675,9 @@ typedef struct global_State { MRef jit_base; /* Current JIT code L->base or NULL. */ MRef ctype_state; /* Pointer to C type state. */ PRNGState prng; /* Global PRNG state. */ +#if LJ_HASHID + uint32_t hidcount; /* How many objects have been given a hash ID. */ +#endif GCRef gcroot[GCROOT_MAX]; /* GC roots. */ } global_State; @@ -695,6 +710,9 @@ struct lua_State { GCHeader; uint8_t dummy_ffid; /* Fake FF_C for curr_funcisL() on dummy frames. */ uint8_t status; /* Thread status. */ +#if LJ_HASHID + uint32_t hid; /* What tables hash this by. Same offset as GCstr.sid. */ +#endif MRef glref; /* Link to global state. */ GCRef gclist; /* GC chain. */ TValue *base; /* Base of currently executing function. */ @@ -735,6 +753,9 @@ typedef struct GChead { GCHeader; uint8_t unused1; uint8_t unused2; +#if LJ_HASHID + uint32_t hid; /* What tables hash this by. Same offset as GCstr.sid. */ +#endif GCRef env; GCRef gclist; GCRef metatable; diff --git a/external/sources/LuaJIT-2.1/src/lj_state.c b/external/sources/LuaJIT-2.1/src/lj_state.c index 0c2c075052..c0eaab7445 100644 --- a/external/sources/LuaJIT-2.1/src/lj_state.c +++ b/external/sources/LuaJIT-2.1/src/lj_state.c @@ -273,6 +273,7 @@ LUA_API lua_State *lua_newstate(lua_Alloc allocf, void *allocd) L->gct = ~LJ_TTHREAD; L->marked = LJ_GC_WHITE0 | LJ_GC_FIXED | LJ_GC_SFIXED; /* Prevent free. */ L->dummy_ffid = FF_C; + lj_tab_sethid(g, L); setmref(L->glref, g); g->gc.currentwhite = LJ_GC_WHITE0 | LJ_GC_FIXED; g->strempty.marked = LJ_GC_WHITE0; @@ -359,6 +360,7 @@ lua_State *lj_state_new(lua_State *L) lua_State *L1 = lj_mem_newobj(L, lua_State); L1->gct = ~LJ_TTHREAD; L1->dummy_ffid = FF_C; + lj_tab_sethid(G(L), L1); L1->status = LUA_OK; L1->stacksize = 0; setmref(L1->stack, NULL); diff --git a/external/sources/LuaJIT-2.1/src/lj_tab.c b/external/sources/LuaJIT-2.1/src/lj_tab.c index c63117fa42..925766810e 100644 --- a/external/sources/LuaJIT-2.1/src/lj_tab.c +++ b/external/sources/LuaJIT-2.1/src/lj_tab.c @@ -17,6 +17,15 @@ /* -- Object hashing ------------------------------------------------------ */ /* Hash an arbitrary key and return its anchor position in the hash table. */ +#if LJ_HASHID +/* The backends load the hash ID of any of these from where the sid of a string is. */ +LJ_STATIC_ASSERT(offsetof(GChead, hid) == offsetof(GCstr, sid)); +LJ_STATIC_ASSERT(offsetof(GCtab, hid) == offsetof(GChead, hid)); +LJ_STATIC_ASSERT(offsetof(GCfuncC, hid) == offsetof(GChead, hid)); +LJ_STATIC_ASSERT(offsetof(GCudata, hid) == offsetof(GChead, hid)); +LJ_STATIC_ASSERT(offsetof(lua_State, hid) == offsetof(GChead, hid)); +#endif + static Node *hashkey(const GCtab *t, cTValue *key) { lj_assertX(!tvisint(key), "attempt to hash integer"); @@ -26,6 +35,10 @@ static Node *hashkey(const GCtab *t, cTValue *key) return hashnum(t, key); else if (tvisbool(key)) return hashmask(t, boolV(key)); +#if LJ_HASHID + else if (tvistab(key) || tvisfunc(key) || tvisudata(key) || tvisthread(key)) + return hashmask(t, gcval(key)->gch.hid); +#endif else return hashgcref(t, key->gcr); /* Only hash 32 bits of lightuserdata on a 64 bit CPU. Good enough? */ @@ -89,6 +102,7 @@ static GCtab *newtab(lua_State *L, uint32_t asize, uint32_t hbits) t->gct = ~LJ_TTAB; t->nomm = (uint8_t)~0; t->colo = (int8_t)asize; + lj_tab_sethid(G(L), t); setmref(t->array, (TValue *)((char *)t + sizeof(GCtab))); setgcrefnull(t->metatable); t->asize = asize; @@ -104,6 +118,7 @@ static GCtab *newtab(lua_State *L, uint32_t asize, uint32_t hbits) t->gct = ~LJ_TTAB; t->nomm = (uint8_t)~0; t->colo = 0; + lj_tab_sethid(G(L), t); setmref(t->array, NULL); setgcrefnull(t->metatable); t->asize = 0; /* In case the array allocation fails. */ diff --git a/external/sources/LuaJIT-2.1/src/lj_tab.h b/external/sources/LuaJIT-2.1/src/lj_tab.h index f1712e45f8..535e6aca3d 100644 --- a/external/sources/LuaJIT-2.1/src/lj_tab.h +++ b/external/sources/LuaJIT-2.1/src/lj_tab.h @@ -41,6 +41,18 @@ static LJ_AINLINE Node *hashmask(const GCtab *t, uint32_t hash) /* String IDs are generated when a string is interned. */ #define hashstr(t, s) hashmask(t, (s)->sid) +#if LJ_HASHID +/* The hash ID for a new GC object. It's a count of them that's been scrambled, since a count on its own makes for a poor hash. */ +static LJ_AINLINE uint32_t lj_tab_newhid(global_State *g) +{ + uint32_t count = ++g->hidcount; + return hashrot(count, count + HASH_BIAS); +} +#define lj_tab_sethid(g, o) ((o)->hid = lj_tab_newhid(g)) +#else +#define lj_tab_sethid(g, o) UNUSED(o) +#endif + #define hashlohi(t, lo, hi) hashmask((t), hashrot((lo), (hi))) #define hashnum(t, o) hashlohi((t), (o)->u32.lo, ((o)->u32.hi << 1)) #if LJ_GC64 diff --git a/external/sources/LuaJIT-2.1/src/lj_udata.c b/external/sources/LuaJIT-2.1/src/lj_udata.c index 1275f4f209..d26d2d94bc 100644 --- a/external/sources/LuaJIT-2.1/src/lj_udata.c +++ b/external/sources/LuaJIT-2.1/src/lj_udata.c @@ -10,6 +10,7 @@ #include "lj_gc.h" #include "lj_err.h" #include "lj_udata.h" +#include "lj_tab.h" GCudata *lj_udata_new(lua_State *L, MSize sz, GCtab *env) { @@ -18,6 +19,7 @@ GCudata *lj_udata_new(lua_State *L, MSize sz, GCtab *env) newwhite(g, ud); /* Not finalized. */ ud->gct = ~LJ_TUDATA; ud->udtype = UDTYPE_USERDATA; + lj_tab_sethid(g, ud); ud->len = sz; /* NOBARRIER: The GCudata is new (marked white). */ setgcrefnull(ud->metatable); diff --git a/external/sources/LuaJIT-2.1/src/meson.build b/external/sources/LuaJIT-2.1/src/meson.build index f8acbb2348..83f0fab613 100644 --- a/external/sources/LuaJIT-2.1/src/meson.build +++ b/external/sources/LuaJIT-2.1/src/meson.build @@ -42,7 +42,12 @@ lj_defines = [ '-D_LARGEFILE_SOURCE', '-U_FORTIFY_SOURCE', '-DLUA_MULTILIB="lib"', - '-DLUA_ROOT="' + get_option('prefix') + '"' + '-DLUA_ROOT="' + get_option('prefix') + '"', + # Seed LuaJIT's own PRNG with a fixed value instead of from the OS, and have strings get their IDs in the order they are made in instead of from that PRNG. + # Strings are hashed by their IDs, so those decide the order pairs() goes through a table in, which would otherwise differ between runs of the game. + # The fixed seed doesn't do it by itself, because LuaJIT also draws from the PRNG when it looks for memory to put compiled code in, and how many times it does that isn't up to us. + '-DLUAJIT_SECURITY_PRNG=0', + '-DLUAJIT_SECURITY_STRID=0' ] if host_machine.system() == 'darwin' diff --git a/external/sources/luabind-0.7.1/luabind/detail/policy.hpp b/external/sources/luabind-0.7.1/luabind/detail/policy.hpp index e1d62d18a0..7e033f50f6 100644 --- a/external/sources/luabind-0.7.1/luabind/detail/policy.hpp +++ b/external/sources/luabind-0.7.1/luabind/detail/policy.hpp @@ -224,6 +224,7 @@ namespace luabind { namespace detail LUABIND_INTEGER_TYPE(short) LUABIND_INTEGER_TYPE(int) LUABIND_INTEGER_TYPE(long) + LUABIND_INTEGER_TYPE(long long) template<> struct is_primitive : boost::mpl::true_ {}; \ template<> struct is_primitive : boost::mpl::true_ {}; \ @@ -275,6 +276,8 @@ namespace luabind { namespace detail void apply(lua_State* L, unsigned short v) { lua_pushnumber(L, (lua_Number)v); } void apply(lua_State* L, unsigned char v) { lua_pushnumber(L, (lua_Number)v); } void apply(lua_State* L, unsigned long v) { lua_pushnumber(L, (lua_Number)v); } + void apply(lua_State* L, long long v) { lua_pushnumber(L, (lua_Number)v); } + void apply(lua_State* L, unsigned long long v) { lua_pushnumber(L, (lua_Number)v); } void apply(lua_State* L, float v) { lua_pushnumber(L, (lua_Number)v); } void apply(lua_State* L, double v) { lua_pushnumber(L, (lua_Number)v); } void apply(lua_State* L, long double v) { lua_pushnumber(L, (lua_Number)v); } @@ -340,6 +343,12 @@ namespace luabind { namespace detail PRIMITIVE_CONVERTER(unsigned long) { return static_cast(lua_tonumber(L, index)); } PRIMITIVE_MATCHER(unsigned long) { if (lua_type(L, index) == LUA_TNUMBER) return 0; else return -1; } + PRIMITIVE_CONVERTER(long long) { return static_cast(lua_tonumber(L, index)); } + PRIMITIVE_MATCHER(long long) { if (lua_type(L, index) == LUA_TNUMBER) return 0; else return -1; } + + PRIMITIVE_CONVERTER(unsigned long long) { return static_cast(lua_tonumber(L, index)); } + PRIMITIVE_MATCHER(unsigned long long) { if (lua_type(L, index) == LUA_TNUMBER) return 0; else return -1; } + PRIMITIVE_CONVERTER(float) { return static_cast(lua_tonumber(L, index)); } PRIMITIVE_MATCHER(float) { if (lua_type(L, index) == LUA_TNUMBER) return 0; else return -1; }