111 Commits

Author SHA1 Message Date
NotAlwaysTrue f9ad542029 Merge branch 'CBT' into dev_itemrefactor 2026-04-30 22:15:38 +08:00
NotAlwaysTrue b5b25a2ccb oops... 2026-04-30 22:12:12 +08:00
NotAlwaysTrue 25683dcf39 Reapply "OBT1.1.0 Merge branch 'dev_pte' into dev"
This reverts commit 046483b9da.
2026-04-30 21:59:54 +08:00
NotAlwaysTrue 02689d0d86 Refactor single-thread worker
Re-parallel Hull Update
Use new gap shaffle algorithm
2026-04-30 20:05:23 +08:00
NotAlwaysTrue 099d664731 Fixed issues bring by update 2026-04-25 13:21:25 +08:00
NotAlwaysTrue 1f7d695bba Removed LuaSafeUserData to sync with upstream 2026-04-25 13:11:05 +08:00
NotAlwaysTrue 50327a4d83 Merge branch 'heads/upstream' into OBT/1.2.0(SpringUpdate) 2026-04-25 13:08:16 +08:00
NotAlwaysTrue 9b35f6b23f Sync with upstream
* Update bug-reports.yml

* Fix modifyChatMessage hook

* Add LuaCsSetup.Lua back for compatibility

* Fix Game.AssignOnExecute having command arguments be passed as varargs instead of a table

* Actually use the PackageId const everywhere we need to refer to our content package

* Load languages files even if the package is disabled

* Fix Hook.Remove not being implemented properly

* - Changed event aliases to be case insensitive.

* - Fixed assembly logging style.
- Fixed double logging during execution.

* Fix garbage network data being read by the game when reading LuaCs network messages

* PackageId -> PackageName

* Added caching toggle to PluginManagementService

* Fix LuaCs initializing too late for singleplayer campaigns and rework the C# prompt to only show when enabling mods/joining server

* Oops, fix NRE crash

* Fix hide username in logs config not doing anything

* Fix Cs prompt showing up more than one between rounds

* Fix server host being prompted twice with the C# popup

* Ignore our workshop packages from the game's dependency thing since it doesn't really make sense

* Load console commands after executing and possible fix for the not console command permitted

* Added fallback friendly name resolution for ModConfig assembly contents.

* Register Voronoi2 stuff

* Added configinfo null check to SettingBase.cs

* Add safety check so this stops crashing when we look at it the wrong way

* Fixed "Folder" attribute files not being found.

* Keep the LuaCsConfig class laying around for compatibility, not sure anywhere in our code base (and shouldn't be)

* Added fallback compilation for UseInternalsAwareAssembly if the publicized script compilation fails.

* Added legacy overload of AddCommand for mod compat.

* Added LoggerService to Lua env. Made ILoggerService compliant with LuaCsLogger API.

* Changed csharp script compilation algorithm to be best effort.

* Added "RunUnrestricted" mode for lua scripts that need to run outside of sandbox.

* - Fixed networking sync vars failing to sync initially.
- Fixed lua failing to differentiate overloads ISettingBase.

* Add alias for human.CPRSuccess and human.CPRFailed

* - Fixed up the settings menu.
- Made SettingEntry throw an error if "Value" attribute is not found in XML.
- Fixed saved values for settings sometimes not reloading after disabling and re-enabling a package.

* Fix LuaCs net messages received during connection initialization to be read incorrectly, happened because we would reset the BitPosition in our harmony patch which would cause the message to be read incorrectly later

* Allow reloadlua to force the state to running

* New icon for settings and make the top left text more user friendly

* Fix client.packages hook sending normal packages

* Fixed OnUpdate() not passing in deltaTime instead of totalTime.

* Missing diffs from bb21a09244

* Added networking tests for configs.

* Added missing diffs for f61f852a25.

* Some tweaks to the text

* Remove missing Value error, it should just use the default value if it's not specified

* Fix UseInternalAccessName

* Always purge cashes for plugin content on unloading.

* Fix texture not multiple of 4

* v1.12.7.0 (Spring Update 2026 Hotfix 1)

---------

Co-authored-by: Joonas Rikkonen <poe.regalis@gmail.com>
Co-authored-by: Evil Factory <36804725+evilfactory@users.noreply.github.com>
Co-authored-by: MapleWheels <njainanan@hotmail.com>
2026-04-25 12:10:24 +08:00
NotAlwaysTrue 5207b381b7 OBT/1.1.1 (#51)
Reverted an outdated change which is no longer in use
Removed CL-EP install command due to partical system issues
2026-03-13 13:34:21 +08:00
NotAlwaysTrue 679ffd380b OBT/1.1.0 (#50)
Finished CL-EP
Restored early Parallelism setting
2026-02-24 15:16:41 +08:00
NotAlwaysTrue de1f1c599f OBT/1.0.17
Removed Client Kick to see if this will fix some of "Received an update for an entity that doesn't exist"
Revert some change from pervious updates due to performance issue
Sync with upstream
2026-02-20 14:54:04 +08:00
NotAlwaysTrue d16707a2f3 OBT/1.0.16
Fixed the GapList bug again to see if this will work

P L E A S E   W O R K
2026-02-13 15:02:17 +08:00
NotAlwaysTrue 740ace88f0 OBT/1.0.15
* Removed PF support again
Fixed Object reference not set to an instance of an object exception in EnemyAIController.cs UpdateLimbAttack()

* Reverted pervious fix on EnemyAIController bcuz the fix will not fix anything
Reduced MaxDegreeOfParallelism by 1 bcuz already a task there

* Re-removed all PF related stuff to fix issue
Removed all RUN_PHYSICS_IN_SEPARATE_THREAD related code because these codes are no longer functional
Removed a duplicated loop in GameScreen
Removed mulitple unnessary parallel operations

* Potentially fixed #44 and #43

* added a ToList for Gap.GapList
2026-02-09 15:34:07 +08:00
NotAlwaysTrue d5d14e9684 oops... 2026-01-16 17:31:15 +08:00
NotAlwaysTrue 086f45510f Removed PF support on Character 2026-01-16 17:21:39 +08:00
NotAlwaysTrue e24024cbb2 Fixed #41/2
Removed min hard cap for MaxDegreeOfParallelism
2026-01-16 17:15:57 +08:00
NotAlwaysTrue e7e444e9b2 Fixed multiple LINQ using shared resources and cause crashes
Added an null check in AIObjectiveManager.cs to avoid accessing removed resources
Use shuffledGaps instead of gapList to ensure update order requirement(already in master)
Updated parallelism count
2026-01-09 18:09:49 +08:00
NotAlwaysTrue 8bfe8a2c37 OBT/1.0.14
Fixed parallelism count issue
Added SingleThreadWorker to handle single-thread related works
Fixed incorrect order when updating gaps
Potentially Fixed #39
2026-01-08 12:40:30 +08:00
Eero caec44c57d Fix concurrent access issues with ConnectedClients
Replaced direct access to GameMain.Server.ConnectedClients with array snapshots in multiple server-side classes to prevent concurrent modification issues during parallel updates. Also updated PhysicsBody and LevelTrigger to avoid static/shared state in parallel contexts, improving thread safety and reliability.
2026-01-08 00:26:29 +08:00
Eero f4a0d149ca CBT2.0.3 #33 2026-01-04 00:23:09 +08:00
NotAlwaysTrue f3c22315a1 OBT/1.0.13
Fixed another crash(Object reference not set to an instance of an object.)
2026-01-02 18:34:12 +08:00
NotAlwaysTrue 59da9211f6 OBT/1.0.2 (#35)
* Fixed? the cause of gap crashes. May require further monitor and check :(

* Fixed a bug that will cause the server stop responding

* Fixed (?) an issue causing a SetPosition() will crash the game
Removed CL release, Added manual dispatcher inputs
2026-01-01 14:14:40 +08:00
NotAlwaysTrue 82d26b5bb8 OBT/1.0.11
Fixed a bug that caused the server to stop responding
Fixed an issue in gap.Update that may cause the server to crash
2025-12-30 17:41:50 +08:00
Eero 7c61859840 CBT2.0.2 Add Lua converters for thread-safe and immutable collections
Implemented custom Lua converters for various thread-safe lists, ImmutableList, ImmutableHashSet, and ImmutableDictionary types. This allows seamless conversion between C# collections and Lua tables, improving Lua scripting integration with these collection types.
2025-12-30 03:14:54 +08:00
NotAlwaysTrue 4c151a4cf1 OBT/1.0.10
Reverted a fixed bug that caused a rare crash but the fix itself will do impact on server performance
2025-12-29 22:52:29 +08:00
NotAlwaysTrue 85b71f1dd6 Revert a change made in 1.0.9 due to large-scale performence impact.
Note : This may cause server crash
2025-12-29 22:36:38 +08:00
Eero 9474f7654c CBT2.0.1 Fix event reset and temp cell clearing logic
Changed ResetReceivedEvents from partial to regular method in EntitySpawner to ensure proper event queue clearing. Updated Level.cs to clear tempCellsLocal instead of tempCells, addressing potential issues with thread-local storage.
2025-12-29 18:37:13 +08:00
Eero 854d7bea1f CBT2.0 Make Hull and Level methods thread-safe using ThreadLocal
Replaced instance fields with ThreadLocal collections in Hull.GetConnectedHulls and Level.GetCells to ensure thread safety during parallel updates. Methods now return copies of the collections to prevent concurrent modification issues.
2025-12-29 18:22:02 +08:00
Eero 7b8275100d Improve thread safety and performance in core systems
Refactors event, entity, and physics management to use thread-safe and lock-free data structures (Immutable collections, ConcurrentQueue, ConcurrentDictionary, Channel) for improved concurrency and performance. Replaces O(n) queue lookups with O(1) set/dictionary checks, ensures atomic updates for shared state, and optimizes queue draining and deferred action processing. Updates related code to use new APIs and patterns, and adds documentation for thread safety and workflow.
2025-12-29 16:47:10 +08:00
NotAlwaysTrue 24484496d2 OBT/1.0.9
Fixed an issue causing gap.update crashes the game(engine issue)
Fixed an potential issue that on MacOS we cannot get core count and cause MaxDegreeOfParallelism will be set to 0. Now if we cant get that number we simply use a fixed 16 instead
2025-12-29 16:33:37 +08:00
NotAlwaysTrue 190c98d8f2 Fixed 2 issues
Fixed an issue causing gap.update crashes the game(engine issue)
Fixed an potential issue that on MacOS we cannot get core count and cause MaxDegreeOfParallelism will be set to 0. Now if we cant get that number we simply use a fixed 16 instead
2025-12-29 16:28:34 +08:00
Eero 046483b9da Revert "OBT1.1.0 Merge branch 'dev_pte' into dev"
This reverts commit 177cf89756, reversing
changes made to 42ba733cd4.
2025-12-29 11:18:11 +08:00
Eero 177cf89756 OBT1.1.0 Merge branch 'dev_pte' into dev 2025-12-29 11:15:41 +08:00
Eero e167a34f32 Make entity lists thread-safe with copy-on-write wrappers
Replaced static entity lists (e.g., HullList, GapList, MapEntityList, etc.) with thread-safe copy-on-write wrappers to improve concurrency and prevent race conditions. Updated usages and related methods to support the new thread-safe collections, ensuring atomic operations and lock-free reads throughout the codebase.
2025-12-28 21:59:03 +08:00
Eero bd1e624eb1 Remove unnecessary thread-safety code from entity spawning
Eliminated redundant locks and related comments in EntitySpawner and Entity classes, simplifying the spawn and remove queue handling. Also removed outdated comments in GameScreen regarding thread safety. These changes assume entity spawning and removal are no longer performed from multiple threads, improving code clarity and maintainability.
2025-12-28 17:45:51 +08:00
Eero 1db14631df Defer physics transforms to ensure thread safety
Refactored multiple components to defer Farseer physics transform operations using PhysicsBodyQueue, preventing unsafe calls from parallel contexts. This change addresses thread safety issues with Farseer's DynamicTree and ensures transforms are executed in a safe context. Also increased the spawn amount limit in DebugConsole from 100 to 100000.
2025-12-28 17:14:16 +08:00
Eero 59bf2749dd Improve thread safety in sound and physics systems
Refactored SoundChannel and SoundManager to use explicit locking for OpenAL operations and channel assignment, preventing race conditions during parallel sound playback. Added thread-local stacks in DynamicTree to ensure thread safety during parallel physics queries and raycasts. These changes address concurrency issues when sounds or physics queries are triggered from multiple threads.
2025-12-28 16:18:49 +08:00
Eero ad0bcddaa4 CL_Fix Use TryAdd when adding afflictions to dictionary
Replaces afflictions.Add with afflictions.TryAdd to prevent exceptions if the affliction already exists in the dictionary.
2025-12-28 15:11:56 +08:00
Eero f485583621 Unstable 0.2 Defer physics operations during parallel updates
Introduces a thread-safe queue for deferring physics operations (such as body creation and transforms) to the main thread, ensuring Farseer Physics is not accessed from parallel contexts. Updates Holdable, Item, MapEntity, and GameScreen to use the new PhysicsBodyQueue for safe physics operations during parallel updates, and refactors PhysicsBodyQueue to support general deferred physics actions.
2025-12-28 15:10:06 +08:00
Eero 49355fe32b Unstable Add thread-safe queue for deferred physics body creation
Introduces PhysicsBodyQueue to safely defer physics body creation to the main thread, addressing thread-safety issues with Farseer Physics during parallel updates. Updates LevelResource, TriggerComponent, BallastFloraBehavior, and MapEntity to use the queue for all physics body creation and refresh operations, ensuring they are processed outside of parallel loops. Also adds cleanup of the queue at round end.
2025-12-28 14:42:17 +08:00
NotAlwaysTrue 356ae6cf74 OBT/1.0.8
Fixed an mod conflict due to execute order
2025-12-28 14:36:09 +08:00
NotAlwaysTrue 770f76a658 Fixed a potential conflict with mods 2025-12-28 14:21:32 +08:00
NotAlwaysTrue 42ba733cd4 Fixed a potential conflict with mods 2025-12-28 14:17:01 +08:00
Eero 45312af297 WIP Make static collections thread-safe using ThreadStatic and ThreadLocal
Refactored various static and instance collections to use [ThreadStatic], ThreadLocal, or local variables to prevent concurrent modification issues during parallel updates. This affects status effect targets, affliction lists, damage modifiers, and cached data in Character, CharacterHealth, Limb, Explosion, Hull, Submarine, and ToolBox classes. Also replaced Dictionary caches with ConcurrentDictionary where appropriate for thread safety.
2025-12-28 14:14:53 +08:00
NotAlwaysTrue baee73e132 OBT/1.0.7
Fixed #7
Fixed #22/#25
Fixed a potential cause to teleport/etc stuff
Added a Client counter in PM
Fixed typo
2025-12-28 14:01:44 +08:00
NotAlwaysTrue 279f40c82e Merge branch 'master' into master-1.0.6 2025-12-28 14:00:03 +08:00
NotAlwaysTrue aaf0763e09 Removed a potential issue 2025-12-28 13:54:29 +08:00
Eero c5fa49405f WIP Make networking code thread-safe and refactor update ID
Replaces direct increments of LastClientListUpdateID with a thread-safe IncrementLastClientListUpdateID method and uses Interlocked for atomic operations. Refactors EntitySpawner to lock access to the spawn/remove queue for thread safety. Updates INetSerializableStruct to use concurrent collections for cached variables and type behaviors, improving thread safety in networking code.
2025-12-28 13:10:17 +08:00
NotAlwaysTrue 3d96e4adb6 Fixed #22 2025-12-28 13:05:56 +08:00
NotAlwaysTrue 42af6f2ec0 Fixed #22 2025-12-28 13:02:31 +08:00
Eero 31812d524d Make collections thread-safe for AI and character systems
Refactored various collections (lists, dictionaries, queues, and caches) in AI, character, animation, and item component systems to use thread-safe patterns and concurrent data structures. This includes introducing copy-on-write wrappers, ConcurrentDictionary, ConcurrentQueue, and ThreadLocal where appropriate to ensure safe concurrent access and mutation, improving stability in multi-threaded scenarios.
2025-12-28 12:53:10 +08:00
NotAlwaysTrue 21a2863a1a Fixed a typo using e instead of a :( 2025-12-28 11:45:55 +08:00
NotAlwaysTrue bd643503b3 Added ClientCount for PerformenceMonitor
Add a marker to help distinguish EP from other SV Executables
Fixed a "Failed to copy object. Source is null." introduced by last update
Uses dynamic ThreadCount instead of fixed 16
Re-Removed most PF support
Re-Parallelzed Level update and Character Update(a conflict warning will be issued
2025-12-28 11:45:21 +08:00
NotAlwaysTrue 51a1fb1235 Re-applied multiple fixs 2025-12-28 11:43:49 +08:00
NotAlwaysTrue 8f0eec7031 Fixed #7 (re-applied) 2025-12-28 11:43:15 +08:00
Evil Factory 6dd36a1575 Fix memory leak that happens when you press retry in singleplayer 2025-12-28 11:42:03 +08:00
Eero 46595b1399 WIP Make collections thread-safe and add safe iteration
Replaced static lists and dictionaries with thread-safe ConcurrentDictionary or ThreadLocal collections for various item components and systems. Updated all relevant code to use snapshots (ToArray, ToList) for safe iteration, and added helper methods for marking and clearing changed connections. These changes improve thread safety and prevent potential concurrency issues in multi-threaded scenarios.
2025-12-28 04:59:56 +08:00
Eero 90962b2328 Refactor Item collections for thread safety and performance
Replaces static Item.ItemList and related collections with thread-safe data structures using ConcurrentDictionary and ImmutableHashSet. Adds thread-safe helpers for marking items for deconstruction and managing item lists. Updates all usages of Item.ItemList and DeconstructItems to use new APIs, improving performance and safety in multi-threaded contexts. Also refactors MeleeWeapon and Projectile impact queues to use ConcurrentQueue, and updates related logic throughout the codebase.
2025-12-28 03:57:04 +08:00
NotAlwaysTrue edd50ef181 Update publish-release.yml 2025-12-27 20:54:06 +08:00
NotAlwaysTrue ced6dca7a3 OBT/1.0.6
Fixed a typo using "e" instead of "a" in word "performance"
2025-12-27 20:33:51 +08:00
NotAlwaysTrue 559aeb3c3f Fixed a typo using e instead of a :( 2025-12-27 18:47:50 +08:00
NotAlwaysTrue 16131e0acc Fixed a typo 2025-12-27 18:45:51 +08:00
NotAlwaysTrue e8f377c20d Sync with Main
Sync with Main
2025-12-27 16:08:15 +08:00
NotAlwaysTrue 27896d53ef OBT/1.0.5
OBT/1.0.5
2025-12-27 16:06:23 +08:00
NotAlwaysTrue a44d89f953 Revert "Revert back to 1.0.3" 2025-12-27 16:05:20 +08:00
NotAlwaysTrue 75465f2f21 OBT/1.0.5
Same as 1.0.4
Fixed a crash introduced by 1.0.4
2025-12-27 16:02:38 +08:00
NotAlwaysTrue 3aadff7a3c Improved thread safety for Gap.update() 2025-12-27 15:55:50 +08:00
NotAlwaysTrue c65101537d Merge pull request #17 from NotAlwaysTrue/revert-16-master-fix
Revert back to 1.0.3
2025-12-27 15:35:07 +08:00
NotAlwaysTrue b35eee5561 Revert "OBT/1.0.4" 2025-12-27 15:32:16 +08:00
NotAlwaysTrue cdfe87c81b OBT/1.0.4
Add a marker to help distinguish EP from other SV Executables
Fixed a "Failed to copy object. Source is null." introduced by last update
Uses dynamic ThreadCount instead of fixed 16
Re-Removed most PF support
Re-Parallelzed Level update and Character Update(a conflict warning will be issued
2025-12-27 13:26:30 +08:00
NotAlwaysTrue 7e899d900a Added ClientCount for PerformenceMonitor
Add a marker to help distinguish EP from other SV Executables
Fixed a "Failed to copy object. Source is null." introduced by last update
Uses dynamic ThreadCount instead of fixed 16
Re-Removed most PF support
Re-Parallelzed Level update and Character Update(a conflict warning will be issued
2025-12-27 13:19:36 +08:00
NotAlwaysTrue a037e616bf OBT/1.0.3
Restored multiple removed parallel stuff
Restored PF support(why)
Fixed #7
2025-12-27 02:12:25 +08:00
NotAlwaysTrue f7650bd6df Re-applied multiple fixs 2025-12-27 02:06:04 +08:00
NotAlwaysTrue 716a35701c Revision 6032010
Removed a potential issue causing the server to stuck in GameScreen.cs (Internal reports)
Added an Warning message to SEEM
2025-12-27 01:59:17 +08:00
Eero 6032010847 Improve parallelization in map and game screen updates
Refactored update logic in MapEntity and GameScreen to use more granular and conditional parallelization, reducing unnecessary allocations and improving performance. Updates to hulls, structures, items, and physics bodies are now executed in parallel where safe, and item updates are only performed when necessary. Also parallelized submarine and physics body transform updates.
2025-12-26 21:04:07 +08:00
NotAlwaysTrue 5446795196 Fixed #7 (re-applied) 2025-12-26 11:07:54 +08:00
NotAlwaysTrue 31ed540939 OBT/1.0.2
Fixed #10
Fixed #12
Sync with upstream
2025-12-26 01:19:04 +08:00
NotAlwaysTrue e715fdc835 Fixed #10
Fixed #12
2025-12-26 01:16:05 +08:00
Evil Factory 81f44969ee Fix memory leak that happens when you press retry in singleplayer 2025-12-25 23:42:55 +08:00
NotAlwaysTrue fc35d888fc Merge pull request #6 from NotAlwaysTrue/master-dev 2025-12-23 23:12:38 +08:00
NotAlwaysTrue eb01597dd3 Try-catch Sever Entity Event Manager thread-safety issue 2025-12-23 22:38:06 +08:00
Eero 572430cc2b Add server-only usage warning to README
Added prominent warnings in both English and Chinese indicating that this release is intended for server-side use only and should not be run on the client. Users are advised to thoroughly test compatibility before deployment.
2025-12-23 13:19:10 +08:00
eero 7b263676e0 Reapply "Refactor ServerEntityEventManager event processing"
This reverts commit bdd6c52e4e.
2025-12-23 00:36:47 +08:00
NotAlwaysTrue c8123383d5 Add another try-catch to avoid Destination array was not long enough. Check the destination index, length, and the array's lower bounds issue 2025-12-23 00:17:45 +08:00
eero bdd6c52e4e Revert "Refactor ServerEntityEventManager event processing"
This reverts commit de65cd88bc.
2025-12-23 00:16:02 +08:00
eero de65cd88bc Refactor ServerEntityEventManager event processing
Introduces async event processing with cancellation support using SemaphoreSlim and CancellationTokenSource. Improves client event handling logic, separates in-game and mid-round sync clients, and adds proper disposal of resources. Enhances robustness and maintainability of event management.
2025-12-23 00:07:47 +08:00
NotAlwaysTrue 83ca51a99b Fixed compile errors :( 2025-12-22 22:58:22 +08:00
NotAlwaysTrue 1def7b7b8d add catch code for previous commit :( 2025-12-22 22:54:33 +08:00
NotAlwaysTrue dadd6c598f try-finally multiple UniqueEvents.ToList() to avoid issues(Destination array was not long enough. Check the destination index, length, and the array's lower bounds) 2025-12-22 22:43:02 +08:00
NotAlwaysTrue b891ec7793 Add a try-finally in HumanPrefab to fix an issue causing issue(Destination array was not long enough. )
Fixed another Collection was modified in SubmarineBody.Update()
2025-12-22 21:51:07 +08:00
eero 5b823d8684 Clarify thread safety in update methods
Updated comments in GameScreen.cs to indicate that StatusEffect.UpdateAll and Character.UpdateAnimAll are not thread-safe and must be executed on the main thread. Also removed a non-English comment from MapEntity.cs for consistency.
2025-12-22 18:07:17 +08:00
eero b146fa326d Refactor MapEntity.UpdateAll for improved parallelism
Rewrites MapEntity.UpdateAll to use more granular parallelism, reducing contention and improving performance by parallelizing hull, structure, and gap resets, while keeping order-dependent and non-thread-safe updates sequential. Updates GameScreen to pass ParallelOptions to UpdateAll.
2025-12-22 17:48:14 +08:00
NotAlwaysTrue bab02fca8a Tried to fix all errors realted to UniqueEvent and Events(Collection was modified) 2025-12-22 17:16:49 +08:00
NotAlwaysTrue 0b6882c37f Reduced max threadcount for main thread to avoid issue 2025-12-22 17:00:03 +08:00
NotAlwaysTrue b1072b7a50 Merge branch 'Dev' of https://github.com/NotAlwaysTrue/LuaCsForBarotraumaEP into Dev 2025-12-22 16:58:00 +08:00
NotAlwaysTrue 70a039da83 Added a command to show server perfomence (ShowServerPerf)
Removed unwanted PhysicsTask that was accideltally readded via revert action
Removed a parallel operation for SE.UpdateAll and ME.UpdateAll.
Cancelled roll back for ServerEntityEventManager
2025-12-22 16:57:58 +08:00
eero 7efb112058 Add contributors and Chinese documentation to README
Added a Main Contributors section and acknowledgements to the README. Also included a full Chinese translation of the project introduction, contributors, acknowledgements, and setup instructions.
2025-12-22 16:57:43 +08:00
NotAlwaysTrue 9c2f300325 Rolled back ServerEntityEventManager 2025-12-22 15:56:29 +08:00
eero 4d4f1f8351 Parallelize game update loops for performance
Refactored GameScreen update logic to use Parallel.Invoke and Parallel.ForEach for physics bodies, submarines, particles, level, characters, map entities, and status effects. This change aims to improve performance by leveraging multi-core processing. Also removed a debug Console.WriteLine from GameMain.
2025-12-22 15:47:15 +08:00
eero 9dc98192f4 Merge branch 'Dev' of https://github.com/NotAlwaysTrue/LuaCsForBarotraumaEP into Dev 2025-12-22 10:08:24 +08:00
eero b1a9757b40 Revert "Enable and improve threaded physics execution"
This reverts commit 7d9642a5b1.
2025-12-22 10:07:24 +08:00
NotAlwaysTrue 522f58511c (Temp) Fixed an issue causing SV to crash due to PhysicsThread(Cannot modify the transform of a body when the World is locked) 2025-12-22 09:14:27 +08:00
NotAlwaysTrue d98f9de5d4 Updated PM, uses 60s average to match message update time, modified message to display Server Running Time(Total Time Elapsed) in h:m:s.ms format
Readded perviously removed update interval condition
2025-12-22 00:24:33 +08:00
eero 7d9642a5b1 Enable and improve threaded physics execution
Activated the RUN_PHYSICS_IN_SEPARATE_THREAD directive and refactored the physics threading logic. Added cancellation support, improved error handling, and used synchronization primitives to coordinate physics updates. These changes aim to improve performance and stability when running physics in a separate thread.
2025-12-22 00:16:25 +08:00
NotAlwaysTrue dc08b6b92a Merge branch 'Dev' of https://github.com/NotAlwaysTrue/LuaCsForBarotraumaEP into Dev 2025-12-22 00:07:04 +08:00
NotAlwaysTrue 220ccb2eac Updated CreateEvent() to make it thread safe 2025-12-22 00:07:01 +08:00
eero eb8c59efb5 Add .vscode/settings.json to .gitignore
Exclude VS Code workspace settings from version control to prevent local configuration from being committed.
2025-12-22 00:02:02 +08:00
NotAlwaysTrue f87e943a1a Move a lot of thing around to fix collection was modified.
Note: For now I can only adjust orders and cannot make some real thing. It will take time to check and refactor everything necessary.
2025-12-20 14:16:51 +08:00
NotAlwaysTrue 2f845b40ca Moved Item related stuff to the end of MapEntity.UpdateAll to avoid issues
Added a threadlock to avoid some issue(someday i will take care of)
Add a function that automatically log server performence every 60s
2025-12-20 00:08:42 +08:00
NotAlwaysTrue d0a5d13a0e Update README.md
Use same stuff in main branch
2025-12-19 13:59:32 +08:00
NotAlwaysTrue fff157d5ca Added a Performence Monitor for debug
Many multi-threading work in ServerSource
2025-12-19 13:43:12 +08:00
184 changed files with 5155 additions and 1705 deletions
+1 -1
View File
@@ -73,7 +73,7 @@ body:
label: Version
description: Which version of the game did the bug happen in? You can see the current version number in the bottom left corner of your screen in the main menu.
options:
- v1.11.5.0 (Winter Update 2025 Hotfix 1)
- v1.12.6.2 (Spring Update 2026)
- Other
validations:
required: true
+19 -5
View File
@@ -4,6 +4,20 @@ name: Publish release
on:
workflow_dispatch:
inputs:
target:
description: "The git ref to checkout, build from and release"
required: true
type: string
tag:
description: "The tag of the release"
required: true
type: string
prerelease:
description: "Prerelease"
required: false
default: false
type: boolean
workflow_call:
inputs:
target:
@@ -23,13 +37,13 @@ on:
env:
CI_DIR: 2049ef39-42a2-46d2-b513-ee6d2e3a7b15
RELEASES: |
windows:client:Windows/Client
windows:server:Windows/Server
linux:client:Linux/Client
linux:server:Linux/Server
mac:client:Mac/Client/Barotrauma.app/Contents/MacOS
mac:server:Mac/Server
ARCHIVE_BASE_NAME: luacsforbarotrauma
windows:client:Windows/Client
linux:client:Linux/Client
mac:client:Mac/Client/Barotrauma.app/Contents/MacOS
ARCHIVE_BASE_NAME: luacsforbarotraumaEP
# XXX: these file names are subject to shell expansion.
# Be careful when using special characters.
ARCHIVE_FILES_SERVER: |
@@ -215,4 +229,4 @@ jobs:
files: |
${{ env.CI_DIR }}/archives/${{ env.ARCHIVE_BASE_NAME }}_{build,patch}_{windows,linux,mac}_{client,server}.zip
${{ env.CI_DIR }}/archives/${{ env.ARCHIVE_BASE_NAME }}_{build,patch}_linux_{client,server}.tar.gz
${{ env.CI_DIR }}/archives/${{ env.ARCHIVE_BASE_NAME }}_refs.zip
${{ env.CI_DIR }}/archives/${{ env.ARCHIVE_BASE_NAME }}_refs.zip
+3
View File
@@ -60,3 +60,6 @@ Deploy/DeployAll/PrivateKey.*
#Rider
*.DotSettings.user
.vscode/settings.json
.vscode/launch.json
.vscode/tasks.json
@@ -2143,7 +2143,7 @@ namespace Barotrauma
if (existingAffliction == null)
{
existingAffliction = afflictionPrefab.Instantiate(strength);
afflictions.Add(existingAffliction, limb);
afflictions.TryAdd(existingAffliction, limb);
newAdded = true;
}
existingAffliction.SetStrength(strength);
@@ -1388,12 +1388,12 @@ namespace Barotrauma
if (me.SimPosition.Length() > 2000.0f)
{
NewMessage("Removed " + me.Name + " (simposition " + me.SimPosition + ")", Color.Orange);
MapEntity.MapEntityList.RemoveAt(i);
MapEntity.MapEntityList.Remove(me);
}
else if (!me.ShouldBeSaved)
{
NewMessage("Removed " + me.Name + " (!ShouldBeSaved)", Color.Orange);
MapEntity.MapEntityList.RemoveAt(i);
MapEntity.MapEntityList.Remove(me);
}
else if (me is Item)
{
@@ -3544,6 +3544,11 @@ namespace Barotrauma
ContentPackageManager.RegularPackages.Select(p => p.Name).ToArray()
}));
commands.Add(new Command("ShowServerPerf", "Immediately log server performance info", (string[] args) =>
{
// TODO: Not yet :)
}));
#if WINDOWS
commands.Add(new Command("startdedicatedserver", "", (string[] args) =>
{
@@ -3577,6 +3582,14 @@ namespace Barotrauma
}
}));*/
AssignOnClientExecute(
"ShowServerPerf",
(string[] args) =>
{
GameMain.Client?.SendConsoleCommand("ShowServerPerf");
}
);
AssignOnClientExecute(
"giveperm",
(string[] args) =>
@@ -28,7 +28,7 @@ namespace Barotrauma
public void DebugDraw(SpriteBatch spriteBatch)
{
foreach (Event ev in activeEvents)
foreach (Event ev in _activeEvents)
{
Vector2 drawPos = ev.DebugDrawPos;
drawPos.Y = -drawPos.Y;
@@ -41,7 +41,7 @@ namespace Barotrauma
public void DebugDrawHUD(SpriteBatch spriteBatch, float y)
{
foreach (ScriptedEvent scriptedEvent in activeEvents.Where(ev => !ev.IsFinished && ev is ScriptedEvent).Cast<ScriptedEvent>())
foreach (ScriptedEvent scriptedEvent in _activeEvents.Where(ev => !ev.IsFinished && ev is ScriptedEvent).Cast<ScriptedEvent>())
{
DrawEventTargetTags(spriteBatch, scriptedEvent);
}
@@ -156,7 +156,7 @@ namespace Barotrauma
{
if (isGraphHovered || isGraphSelected)
{
foreach (var timeStamp in timeStamps)
foreach (var timeStamp in _timeStamps)
{
int t = (int)Math.Abs(Math.Round((timeStamp.Time - lastIntensityUpdate) / intensityGraphUpdateInterval));
if (t == order)
@@ -205,7 +205,7 @@ namespace Barotrauma
}
adjustedYStep = GUI.AdjustForTextScale(12);
foreach (EventSet eventSet in pendingEventSets)
foreach (EventSet eventSet in _pendingEventSets)
{
if (Submarine.MainSub == null) { break; }
@@ -263,7 +263,7 @@ namespace Barotrauma
y += yStep;
adjustedYStep = GUI.AdjustForTextScale(18);
foreach (Event ev in activeEvents.Where(ev => !ev.IsFinished || PlayerInput.IsShiftDown()))
foreach (Event ev in _activeEvents.Where(ev => !ev.IsFinished || PlayerInput.IsShiftDown()))
{
GUI.DrawString(spriteBatch, new Vector2(x + 5, y), ev.ToString(), (!ev.IsFinished ? Color.White : Color.Red) * 0.8f, null, 0, GUIStyle.SmallFont);
@@ -752,4 +752,4 @@ namespace Barotrauma
entry.CanBeCompleted);
}
}
}
}
@@ -867,7 +867,7 @@ namespace Barotrauma
{
foreach (var stackedItem in item.GetStackedItems())
{
Item.DeconstructItems.Add(stackedItem);
Item.MarkForDeconstruction(stackedItem);
}
HintManager.OnItemMarkedForDeconstruction(order.OrderGiver);
}
@@ -875,7 +875,7 @@ namespace Barotrauma
{
foreach (var stackedItem in item.GetStackedItems())
{
Item.DeconstructItems.Remove(stackedItem);
Item.UnmarkForDeconstruction(stackedItem);
}
}
}
@@ -1933,7 +1933,7 @@ namespace Barotrauma.Items.Components
void CalculateDistance()
{
pathFinder ??= new PathFinder(WayPoint.WayPointList, false);
pathFinder ??= new PathFinder(WayPoint.WayPointList.ToList(), false);
var path = pathFinder.FindPath(ConvertUnits.ToSimUnits(transducerPosition), ConvertUnits.ToSimUnits(worldPosition));
if (!path.Unreachable)
{
@@ -1892,7 +1892,7 @@ namespace Barotrauma
}
}
else if (Item.DeconstructItems.Contains(item) &&
else if (Item.IsMarkedForDeconstruction(item) &&
OrderPrefab.Prefabs.TryGet(Tags.DeconstructThis, out OrderPrefab deconstructOrder))
{
DrawSideIcon(deconstructOrder.SymbolSprite, Direction.Right, TextManager.Get("tooltip.markedfordeconstruction"), GUIStyle.Red, out bool mouseOn);
@@ -24,7 +24,7 @@ public sealed class SettingControl : SettingBase, ISettingControl
public SettingControl(IConfigInfo configInfo, Func<OneOf<string, XElement, object>, bool> valueChangePredicate) : base(configInfo)
{
_valueChangePredicate = valueChangePredicate;
TrySetValue(configInfo.Element);
TrySetSerializedValue(configInfo.Element);
}
protected override void OnDispose()
@@ -37,7 +37,7 @@ public sealed class SettingControl : SettingBase, ISettingControl
public override string GetStringValue() => Value.ToString();
public override string GetDefaultStringValue() => new KeyOrMouse(Keys.NumLock).ToString();
public override bool TrySetValue(OneOf<string, XElement> value)
public override bool TrySetSerializedValue(OneOf<string, XElement> value)
{
var newVal = value.Match<KeyOrMouse>(
(string v) => GetKeyOrMouse(v),
@@ -1,106 +1,47 @@
using System;
using Barotrauma.CharacterEditor;
using Barotrauma.Extensions;
using Barotrauma.LuaCs;
using Barotrauma.LuaCs.Data;
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using System.Linq;
using System.Text;
using Barotrauma.CharacterEditor;
using Barotrauma.LuaCs;
using Barotrauma.LuaCs.Data;
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
using static System.Collections.Specialized.BitVector32;
// ReSharper disable ObjectCreationAsStatement
namespace Barotrauma
{
partial class LuaCsSetup
{
private bool _isClientPromptActive;
private bool _isCsEnabledForSession = false;
public void CheckRunConditionalHostingCsEnabled(Action onReadyToRun)
{
public void PromptCSharpMods(Action<bool> onSelection, bool joiningServer)
{
var res = ReadyToRunNoPrompt();
if (res.ShouldRun)
{
onReadyToRun?.Invoke();
return;
}
DisplayCsModsPromptClient(res.Item2, (selectedYes) =>
{
if (selectedYes)
{
onReadyToRun?.Invoke();
}
});
}
private (bool ShouldRun, ImmutableArray<ContentPackage> PromptPackages) ReadyToRunNoPrompt()
{
if (this.IsCsEnabled)
{
return (true, ImmutableArray<ContentPackage>.Empty);
}
if (!ShouldPromptForCs)
{
return (true, ImmutableArray<ContentPackage>.Empty);
}
ImmutableArray<ContentPackage> contentPackages = PackageManagementService.GetLoadedAssemblyPackages()
.Where(p => p.Name != PackageId)
ImmutableArray<ContentPackage> contentPackages = PackageManagementService.GetLoadedUnrestrictedPackages()
.Where(p => p.Name != PackageName)
.ToImmutableArray();
return (contentPackages.IsEmpty, contentPackages);
}
partial void CheckReadyToRun(Action onReadyToRun)
{
var res = ReadyToRunNoPrompt();
if (res.ShouldRun)
if (_csRunPolicy?.Value is "Enabled")
{
onReadyToRun?.Invoke();
IsCsEnabledForSession = true;
onSelection(true);
return;
}
if (GameMain.Client?.ClientPeer is P2POwnerPeer)
else if (_csRunPolicy?.Value is "Disabled")
{
SetCsPolicyAndContinue(true);
IsCsEnabledForSession = false;
onSelection(false);
return;
}
DisplayCsModsPromptClient(res.PromptPackages, (selectedYes) =>
if (contentPackages.None())
{
SetCsPolicyAndContinue(selectedYes);
onSelection(true);
return;
});
void SetCsPolicyAndContinue(bool csSessionExecutionPolicy)
{
var prevRunState = this.CurrentRunState;
if (CurrentRunState >= RunState.Running)
{
SetRunState(RunState.LoadedNoExec);
}
this._isCsEnabledForSession = csSessionExecutionPolicy;
CoroutineManager.Invoke(() =>
{
if (CurrentRunState != prevRunState)
{
SetRunState(prevRunState);
}
onReadyToRun?.Invoke();
}, 0f);
}
}
void DisplayCsModsPromptClient(ImmutableArray<ContentPackage> contentPackages, Action<bool> onSelection)
{
if (_isClientPromptActive) { return; }
_isClientPromptActive = true;
GUIMessageBox messageBox = new GUIMessageBox(
TextManager.Get("warning"),
@@ -115,7 +56,7 @@ namespace Barotrauma
Stretch = true
};
new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), msgBoxLayout.RectTransform), "The following mods contain CSharp code",
new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), msgBoxLayout.RectTransform), "The following mods contain CSharp code OR Unsandboxed Lua Code",
font: GUIStyle.SubHeadingFont, wrap: true, textAlignment: Alignment.Center);
GUIListBox packageListBox = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.4f), msgBoxLayout.RectTransform))
@@ -126,22 +67,39 @@ namespace Barotrauma
foreach (ContentPackage package in contentPackages)
{
GUIFrame packageFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.15f), packageListBox.Content.RectTransform), style: "ListBoxElement");
new GUITextBlock(new RectTransform(new Vector2(1f, 1f), packageFrame.RectTransform), package.Name);
GUILayoutGroup packageLayout = new GUILayoutGroup(new RectTransform(Vector2.One, packageFrame.RectTransform), true, Anchor.CenterLeft);
new GUITextBlock(new RectTransform(new Vector2(0.7f, 1f), packageLayout.RectTransform), package.Name);
new GUIButton(new RectTransform(new Vector2(0.3f, 1f), packageLayout.RectTransform, Anchor.CenterRight), "Open Folder", style: "GUIButtonSmall")
{
OnClicked = (GUIButton button, object obj) =>
{
string directory = package.Dir;
if (string.IsNullOrEmpty(directory)) { return false; }
ToolBox.OpenFileWithShell(directory);
return true;
}
};
}
new GUITextBlock(new RectTransform(new Vector2(1.0f, 0f), msgBoxLayout.RectTransform), "C# mods are not sandboxed, meaning that they have unrestrictive access to your computer, please make sure you trust these mods before you continue. If you are not hosting a server, selecting cancel will only run Lua mods.", wrap: true)
string bodyText =
joiningServer ?
"You are joining a server that includes mods with C# code OR unrestricted Lua code. These mods are not sandboxed and may access your computer without restrictions. If you trust these mods, select 'Enable C# for this session'. Otherwise, select 'Cancel' to run only Lua mods."
: "You have enabled mods that include C# code. These mods are not sandboxed and may access your computer without restrictions. If you trust these mods, select 'Enable C# for this session'. Otherwise, select 'Cancel' to run only Sandboxed Lua mods.";
new GUITextBlock(new RectTransform(new Vector2(1.0f, 0f), msgBoxLayout.RectTransform), bodyText, wrap: true)
{
Wrap = true
};
GUILayoutGroup buttonLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.25f), messageBox.Content.RectTransform, Anchor.BottomCenter), isHorizontal: false, childAnchor: Anchor.TopCenter);
new GUIButton(new RectTransform(new Vector2(0.8f, 0.0f), buttonLayout.RectTransform), "Continue")
new GUIButton(new RectTransform(new Vector2(0.8f, 0.0f), buttonLayout.RectTransform), "Enable C# for this session")
{
TextBlock = { AutoScaleHorizontal = true },
OnClicked = (btn, userdata) =>
{
_isClientPromptActive = false;
IsCsEnabledForSession = true;
onSelection(true);
messageBox.Close();
return true;
@@ -152,7 +110,7 @@ namespace Barotrauma
{
OnClicked = (btn, userdata) =>
{
_isClientPromptActive = false;
IsCsEnabledForSession = false;
onSelection(false);
messageBox.Close();
return true;
@@ -201,10 +159,18 @@ namespace Barotrauma
case SpriteEditorScreen:
case SubEditorScreen:
case TestScreen: // notes: TestScreen is a Linux edge case editor screen and is deprecated.
CheckReadyToRun(() =>
if (screen is NetLobbyScreen && CurrentRunState != RunState.Running && GameMain.Client?.ClientPeer is not P2POwnerPeer)
{
PromptCSharpMods(selection =>
{
SetRunState(RunState.Running);
}, joiningServer: true);
}
else
{
SetRunState(RunState.Running);
});
}
break;
default:
Logger.LogError(
@@ -32,11 +32,11 @@ partial class NetworkingService : INetworkingService, IEventServerConnected, IEv
}
}
public void OnReceivedServerNetMessage(IReadMessage netMessage, ServerPacketHeader serverPacketHeader)
public bool? OnReceivedServerNetMessage(IReadMessage netMessage, ServerPacketHeader serverPacketHeader)
{
if (serverPacketHeader != ServerHeader)
{
return;
return null;
}
ServerToClient luaCsHeader = (ServerToClient)netMessage.ReadByte();
@@ -55,6 +55,8 @@ partial class NetworkingService : INetworkingService, IEventServerConnected, IEv
ReadIds(netMessage);
break;
}
return true;
}
private void SendSyncMessage()
@@ -1,5 +1,6 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.Immutable;
using Microsoft.Xna.Framework;
using System.Linq;
@@ -20,23 +21,87 @@ internal sealed class ModsGameplaySettingsMenu : ModsSettingsMenuBase
private string _selectedSearchQuery = string.Empty;
private ContentPackage _selectedContentPackage;
private string _selectedCategory = string.Empty;
private ImmutableArray<ISettingBase> _currentlyDisplayedSettings;
private ILoggerService _loggerService;
private bool _promptOpen = false;
// Note: "static" instead of "const" for Hot Reload and to allow changing at runtime.
// ReSharper disable FieldCanBeMadeReadOnly.Local
// --- UI controls ---
private static float MenuTitleHeight = 0.06f; // (ContentDisplayAreaHeightContainer + MenuTitleHeight) < 1f
private static float ContentDisplayAreaHeightContainer = 0.93f;
private static float ContentDisplayAreaHeightInnerCategories = 0.99f;
private static float ContentDisplayAreaHeightInnerSettings = 0.97f;
private static float ContentLeftRightSplitPosition = 0.3f;
// Search Bar
private static float SearchBarLayoutHeight = 0.06f;
private static float SearchBarLabelWidth = 0.1f;
private static float SearchBarLabelBoxSpacing = 0.05f;
private static float SearchBarTextBoxWidth = 1f - SearchBarLabelWidth - SearchBarLabelBoxSpacing;
// Categories, Packages Display Area
private static float CategoriesDisplayListHeight = 0.945f;
private static float CategoryButtonHeightRelative = 0.122f;
private static float PackageSelectionButtonHeight = 0.07f;
private static Color CategoryButtonHoverSelectColor = new Color(50, 50, 50, 255);
private static Color CategoryButtonTextColor = Color.PeachPuff;
private static Color CategoryButtonTextColorSelected = Color.White;
private static Color CategoryButtonColorPressed = Color.TransparentBlack;
// Settings Display Area
private static float SettingLabelWidth = 0.6f;
private static float SettingControlWidth = 0.4f;
private static float SettingHeight = 0.05625f/ContentDisplayAreaHeightContainer/ContentDisplayAreaHeightInnerSettings;
private static Color SettingEntryLabelTextColor = Color.PeachPuff;
private static string SettingGUIFrameStyle = "";
private static Color? SettingGUIFrameColor = null;
// settings reset
private static Vector2 SettingsResetButtonTopSpacer = new Vector2(0f, 0.02f);
private static Vector2 SettingsResetButtonDimensions = new Vector2(0.3f, 0.05f);
private static string SettingsResetButtonStyle = "GUIButtonSmall";
private static Color SettingsResetButtonColor = Color.DarkOliveGreen;
private static Color SettingsResetButtonHoverColor = Color.Olive;
private static Color SettingsResetButtonTextColor = Color.PeachPuff;
private static Color SettingsResetButtonTextColorSelected = Color.White;
private static Vector2 ResetConfirmationPromptDimensions = new Vector2(0.15f, 0.2f);
// ReSharper restore FieldCanBeMadeReadOnly.Local
private const string SettingsResetButtonText = "LuaCsForBarotrauma.SettingsMenu.ResetVisibleSettings";
private const string SettingsResetPromptTitle = "LuaCsForBarotrauma.SettingsMenu.ResetPrompt.Title";
private const string SettingsResetPromptContents = "LuaCsForBarotrauma.SettingsMenu.ResetPrompt.Message";
private const string SettingsResetPromptYesText = "LuaCsForBarotrauma.SettingsMenu.ResetPrompt.Yes";
private const string SettingsResetPromptNoText = "LuaCsForBarotrauma.SettingsMenu.ResetPrompt.No";
private event Action OnApplyInstalledModsChanges;
public ModsGameplaySettingsMenu(GUIFrame contentFrame,
IPackageManagementService packageManagementService,
IConfigService configService,
ILoggerService loggerService,
SettingsMenu settingsMenuInstance) : base(contentFrame, packageManagementService, configService, settingsMenuInstance)
{
_settingsInstancesGameplay = configService.GetDisplayableConfigs()
.ToImmutableArray();
_loggerService = loggerService;
var mainLayoutGroup = new GUILayoutGroup(new RectTransform(new Vector2(1f, 1f), contentFrame.RectTransform, Anchor.Center), false, Anchor.TopLeft);
// page title
var menuTitleLayoutGroup = new GUILayoutGroup(
new RectTransform(new Vector2(1f, 0.06f), mainLayoutGroup.RectTransform, Anchor.TopLeft), true, Anchor.TopLeft);
GUIUtil.Label(menuTitleLayoutGroup, "Mods Gameplay Settings", GUIStyle.LargeFont, new Vector2(1f, 1f));
new RectTransform(new Vector2(1f, MenuTitleHeight), mainLayoutGroup.RectTransform, Anchor.TopLeft), true, Anchor.TopLeft);
GUIUtil.Label(menuTitleLayoutGroup,
GetLocalizedString("LuaCsForBarotrauma.SettingsMenu.ModGameplayButton", "Mod Gameplay Settings"),
GUIStyle.LargeFont, new Vector2(1f, 1f));
// page contents
var contentAreaLayoutGroup = new GUILayoutGroup(
@@ -44,10 +109,10 @@ internal sealed class ModsGameplaySettingsMenu : ModsSettingsMenuBase
Anchor.TopLeft);
var searchBarLayoutGroup = new GUILayoutGroup(
new RectTransform(new Vector2(1f, 0.06f), contentAreaLayoutGroup.RectTransform, Anchor.TopCenter), true, Anchor.CenterLeft);
GUIUtil.Label(searchBarLayoutGroup, "Search: ", GUIStyle.SubHeadingFont, new Vector2(0.1f, 1f));
new RectTransform(new Vector2(1f, SearchBarLayoutHeight), contentAreaLayoutGroup.RectTransform, Anchor.TopCenter), true, Anchor.CenterLeft);
GUIUtil.Label(searchBarLayoutGroup, "Search: ", GUIStyle.SubHeadingFont, new Vector2(SearchBarLabelWidth, 1f));
var searchBar = new GUITextBox(
new RectTransform(new Vector2(0.85f, 0.1f), searchBarLayoutGroup.RectTransform, Anchor.TopLeft),
new RectTransform(new Vector2(SearchBarTextBoxWidth, 0.1f), searchBarLayoutGroup.RectTransform, Anchor.TopLeft),
createClearButton: true)
{
OnTextChangedDelegate = (btn, txt) =>
@@ -56,12 +121,13 @@ internal sealed class ModsGameplaySettingsMenu : ModsSettingsMenuBase
return true;
}
};
// main display area
var settingsContentAreaGroup = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.90f), contentAreaLayoutGroup.RectTransform, Anchor.BottomCenter));
var settingsContentAreaGroup = new GUILayoutGroup(new RectTransform(new Vector2(1f, ContentDisplayAreaHeightContainer), contentAreaLayoutGroup.RectTransform, Anchor.BottomCenter));
GUIUtil.Spacer(settingsContentAreaGroup, Vector2.One);
(_modCategoryDisplayGroup, _settingsDisplayGroup) = GUIUtil.CreateSidebars(settingsContentAreaGroup, true);
_modCategoryDisplayGroup.RectTransform.RelativeSize = new Vector2(0.3f, 1f);
_settingsDisplayGroup.RectTransform.RelativeSize = new Vector2(0.7f, 1f);
_modCategoryDisplayGroup.RectTransform.RelativeSize = new Vector2(ContentLeftRightSplitPosition, ContentDisplayAreaHeightInnerCategories);
_settingsDisplayGroup.RectTransform.RelativeSize = new Vector2(1f-ContentLeftRightSplitPosition, ContentDisplayAreaHeightInnerSettings);
// default category
_selectedCategory = "All";
@@ -202,10 +268,11 @@ internal sealed class ModsGameplaySettingsMenu : ModsSettingsMenuBase
_selectedCategory = string.Empty;
GenerateCategoryListDisplay(_modCategoryDisplayGroup, GetTargetPackagesList(), GetDisplayCategoriesList());
GenerateSettingsListDisplay(_settingsDisplayGroup, GetDisplaySettingsList());
}, new Vector2(1f, 0.07f));
var containerBox = new GUIListBox(new RectTransform(new Vector2(1f, 0.945f), layoutGroup.RectTransform));
const float entryHeight = 0.122f;
float sizeY = MathF.Max(categories.Length * entryHeight, 1f);
}, new Vector2(1f, PackageSelectionButtonHeight));
var containerBox = new GUIListBox(new RectTransform(new Vector2(1f, CategoriesDisplayListHeight), layoutGroup.RectTransform));
float sizeY = MathF.Max(categories.Length * CategoryButtonHeightRelative, 1f);
var displayedCategoriesFrame = new GUIFrame(new RectTransform(new Vector2(1f, sizeY), containerBox.Content.RectTransform), style: null, color: Color.Black)
{
CanBeFocused = false
@@ -214,17 +281,18 @@ internal sealed class ModsGameplaySettingsMenu : ModsSettingsMenuBase
foreach (var category in categories)
{
var btn = new GUIButton(new RectTransform(new Vector2(1f, entryHeight), displayCategoriesLayout.RectTransform),
var btn = new GUIButton(new RectTransform(new Vector2(1f, CategoryButtonHeightRelative), displayCategoriesLayout.RectTransform),
text: category, color: Color.TransparentBlack)
{
CanBeFocused = true,
CanBeSelected = true,
TextColor = Color.PeachPuff,
HoverColor = new Color(50, 50, 50, 255),
HoverTextColor = Color.White,
SelectedColor = new Color(50, 50, 50, 255),
SelectedTextColor = Color.White,
OnPressed = () =>
TextColor = CategoryButtonTextColor,
HoverColor = CategoryButtonHoverSelectColor,
HoverTextColor = CategoryButtonTextColorSelected,
PressedColor = CategoryButtonColorPressed,
SelectedColor = CategoryButtonHoverSelectColor,
SelectedTextColor = CategoryButtonHoverSelectColor,
OnClicked = (btn, obj) =>
{
_selectedCategory = category;
GenerateSettingsListDisplay(_settingsDisplayGroup, GetDisplaySettingsList());
@@ -237,26 +305,47 @@ internal sealed class ModsGameplaySettingsMenu : ModsSettingsMenuBase
void GenerateSettingsListDisplay(GUILayoutGroup layoutGroup, ImmutableArray<ISettingBase> settings)
{
layoutGroup.ClearChildren();
const float settingHeight = 0.0625f;
_currentlyDisplayedSettings = settings;
var containerBox = new GUIListBox(new RectTransform(new Vector2(1f, 1f), layoutGroup.RectTransform));
var containerBox = new GUIListBox(new RectTransform(new Vector2(1f, 1f-SettingsResetButtonDimensions.Y), layoutGroup.RectTransform));
foreach (var setting in settings)
{
var entry = AddSettingToDisplay(
setting,
containerBox.Content.RectTransform,
settingHeight: settingHeight,
labelSize: new Vector2(0.6f, 1f),
controlSize: new Vector2(0.4f, 1f));
settingHeight: SettingHeight,
labelSize: new Vector2(SettingLabelWidth, 1f),
controlSize: new Vector2(SettingControlWidth, 1f));
}
}
(GUIFrame entryFrame, GUILayoutGroup entryLayoutGroup) AddSettingToDisplay(ISettingBase setting,
RectTransform parent, float settingHeight, Vector2 labelSize, Vector2 controlSize)
var spacer = new GUIFrame(new RectTransform(SettingsResetButtonTopSpacer, layoutGroup.RectTransform),
style: null, color: Color.TransparentBlack);
var resetSettingsButton = new GUIButton(
new RectTransform(SettingsResetButtonDimensions, layoutGroup.RectTransform),
GetLocalizedString(SettingsResetButtonText, "Reset Visible Settings"),
style: SettingsResetButtonStyle)
{
CanBeSelected = true,
CanBeFocused = true,
Color = SettingsResetButtonColor,
HoverColor = SettingsResetButtonHoverColor,
SelectedColor = SettingsResetButtonHoverColor,
SelectedTextColor = SettingsResetButtonTextColorSelected,
TextColor = SettingsResetButtonTextColor,
OnClicked = (btn, obj) =>
{
DisplayResetConfirmationPrompt(settings);
return true;
}
};
}
(GUIFrame entryFrame, GUILayoutGroup entryLayoutGroup)
AddSettingToDisplay(ISettingBase setting, RectTransform parent, float settingHeight, Vector2 labelSize, Vector2 controlSize)
{
GUIFrame entryFrame = new GUIFrame(new RectTransform(new Vector2(1f, settingHeight), parent))
GUIFrame entryFrame = new GUIFrame(new RectTransform(new Vector2(1f, settingHeight), parent),
style: SettingGUIFrameStyle, color: SettingGUIFrameColor)
{
Color = Color.DarkGray
};
@@ -266,9 +355,10 @@ internal sealed class ModsGameplaySettingsMenu : ModsSettingsMenuBase
new GUIFrame(new RectTransform(new Vector2(0.02f, 1f), entryLayoutGroup.RectTransform),
color: Color.TransparentBlack);
// setting label
new GUITextBlock(new RectTransform(labelSize - new Vector2(0.05f, 0f), entryLayoutGroup.RectTransform),
GetLocalizedString(setting.GetDisplayInfo().DisplayName, setting.GetDisplayInfo().DisplayName),
textColor: Color.PeachPuff,
textColor: SettingEntryLabelTextColor,
font: GUIStyle.SmallFont,
textAlignment: Alignment.Left)
{
@@ -281,6 +371,58 @@ internal sealed class ModsGameplaySettingsMenu : ModsSettingsMenuBase
});
return (entryFrame, entryLayoutGroup);
}
void DisplayResetConfirmationPrompt(ImmutableArray<ISettingBase> settings)
{
if (_promptOpen)
{
return;
}
_promptOpen = true;
var msgBox = new GUIMessageBox(GetLocalizedString(SettingsResetPromptTitle, "Reset Visible Settings"),
GetLocalizedString(SettingsResetPromptContents,
"Are you sure you want to reset the values for currently displayed settings?"),
new LocalizedString[]
{
GetLocalizedString(SettingsResetPromptYesText, "Yes"),
GetLocalizedString(SettingsResetPromptNoText, "No")
}, ResetConfirmationPromptDimensions);
msgBox.Buttons[0].OnClicked = (btn, obj) =>
{
ResetValuesForDisplayedSettings(settings);
btn.Visible = false;
_promptOpen = false;
msgBox.Close();
return true;
};
msgBox.Buttons[1].OnClicked = (btn, obj) =>
{
btn.Visible = false;
_promptOpen = false;
msgBox.Close();
return true;
};
}
void ResetValuesForDisplayedSettings(ImmutableArray<ISettingBase> settings)
{
if (settings.IsDefaultOrEmpty)
{
return;
}
NewValuesCache.Clear();
foreach (var setting in settings)
{
var str = setting.GetDefaultStringValue();
NewValuesCache[setting] = str;
loggerService.LogDebug($"Resetting value for {setting.InternalName} to '{str}'");
}
ApplyInstalledModChanges();
}
}
@@ -303,8 +445,12 @@ internal sealed class ModsGameplaySettingsMenu : ModsSettingsMenuBase
continue;
}
kvp.Key.TrySetValue(kvp.Value);
ConfigService.SaveConfigValue(kvp.Key);
var success = kvp.Key.TrySetSerializedValue(kvp.Value);
if (success)
{
ConfigService.SaveConfigValue(kvp.Key);
_loggerService.LogDebug($"Applied save value for {kvp.Key.InternalName} of {kvp.Value.ToString()}");
}
}
NewValuesCache.Clear();
OnApplyInstalledModsChanges?.Invoke();
@@ -1,8 +1,10 @@
using System;
using System.Collections.Concurrent;
using System.Xml.Linq;
using Barotrauma.Extensions;
using Barotrauma.LuaCs.Data;
using Microsoft.Xna.Framework;
using OneOf;
namespace Barotrauma.LuaCs;
@@ -12,7 +14,7 @@ internal abstract class ModsSettingsMenuBase : IDisposable
protected IPackageManagementService PackageManagementService { get; private set; }
protected IConfigService ConfigService { get; private set; }
protected SettingsMenu SettingsMenuInstance { get; private set; }
protected readonly ConcurrentDictionary<ISettingBase, string> NewValuesCache = new();
protected readonly ConcurrentDictionary<ISettingBase, OneOf<string, XElement>> NewValuesCache = new();
protected ModsSettingsMenuBase(GUIFrame contentFrame,
IPackageManagementService packageManagementService,
@@ -18,12 +18,14 @@ public class SettingsMenuSystem : ISettingsMenuSystem
private readonly Harmony _harmony;
private readonly IPackageManagementService _packageManagementService;
private readonly IConfigService _configService;
private readonly ILoggerService _loggerService;
private static SettingsMenuSystem SystemInstance;
public SettingsMenuSystem(IPackageManagementService packageManagementService, IConfigService configService)
public SettingsMenuSystem(IPackageManagementService packageManagementService, IConfigService configService, ILoggerService loggerService)
{
_packageManagementService = packageManagementService;
_configService = configService;
_loggerService = loggerService;
SystemInstance = this;
_harmony = Harmony.CreateAndPatchAll(typeof(SettingsMenuSystem));
}
@@ -43,13 +45,14 @@ public class SettingsMenuSystem : ISettingsMenuSystem
var tabGameplayIndex = (SettingsMenu.Tab)tabCount;
var tabControlsIndex = (SettingsMenu.Tab)tabCount+1;
_gameplayContentFrame = CreateNewContentTab(tabGameplayIndex, __instance,
"SettingsMenuTab.Mods", "LuaCsForBarotrauma.SettingsMenu.ModGameplayButton");
_gameplayContentFrame = CreateNewContentTab(tabGameplayIndex, __instance,
GUIStyle.ComponentStyles.ContainsKey("SettingsMenuTab.LuaCsSettings") ? "SettingsMenuTab.LuaCsSettings" : "SettingsMenuTab.Mods",
"LuaCsForBarotrauma.SettingsMenu.ModGameplayButton");
/*_controlsContentFrame = CreateNewContentTab(tabControlsIndex, __instance,
"SettingsMenuTab.Controls", "LuaCsForBarotrauma.SettingsMenu.ModControlsButton");
*/
_gameplayMenuInstance = new ModsGameplaySettingsMenu(_gameplayContentFrame, _packageManagementService, _configService, __instance);
_gameplayMenuInstance = new ModsGameplaySettingsMenu(_gameplayContentFrame, _packageManagementService, _configService, _loggerService, __instance);
//_controlsMenuInstance = new ModsControlsSettingsMenu(_controlsContentFrame, _packageManagementService, _configService, __instance);
}
@@ -471,11 +471,11 @@ namespace Barotrauma
if (item0 == null && item1 != null)
{
item0 = Item.ItemList.Find(it => it.GetComponent<ConnectionPanel>()?.DisconnectedWires.Contains(wire) ?? false);
item0 = Item.ItemList.FirstOrDefault(it => it.GetComponent<ConnectionPanel>()?.DisconnectedWires.Contains(wire) ?? false);
}
else if (item0 != null && item1 == null)
{
item1 = Item.ItemList.Find(it => it.GetComponent<ConnectionPanel>()?.DisconnectedWires.Contains(wire) ?? false);
item1 = Item.ItemList.FirstOrDefault(it => it.GetComponent<ConnectionPanel>()?.DisconnectedWires.Contains(wire) ?? false);
}
if (item0 != null && item1 != null && SelectedList.Contains(item0) && SelectedList.Contains(item1))
{
@@ -105,7 +105,7 @@ namespace Barotrauma
public static void Draw(SpriteBatch spriteBatch, bool editing = false)
{
var entitiesToRender = !editing && visibleEntities != null ? visibleEntities : MapEntity.MapEntityList;
var entitiesToRender = !editing && visibleEntities != null ? visibleEntities : MapEntity.MapEntityList.ToList();
foreach (MapEntity e in entitiesToRender)
{
@@ -115,7 +115,7 @@ namespace Barotrauma
public static void DrawFront(SpriteBatch spriteBatch, bool editing = false, Predicate<MapEntity> predicate = null)
{
var entitiesToRender = !editing && visibleEntities != null ? visibleEntities : MapEntity.MapEntityList;
var entitiesToRender = !editing && visibleEntities != null ? visibleEntities : MapEntity.MapEntityList.ToList();
foreach (MapEntity e in entitiesToRender)
{
@@ -164,7 +164,7 @@ namespace Barotrauma
public static void DrawDamageable(SpriteBatch spriteBatch, Effect damageEffect, bool editing = false, Predicate<MapEntity> predicate = null)
{
var entitiesToRender = !editing && visibleEntities != null ? visibleEntities : MapEntity.MapEntityList;
var entitiesToRender = !editing && visibleEntities != null ? visibleEntities : MapEntity.MapEntityList.ToList();
depthSortedDamageable.Clear();
@@ -197,7 +197,7 @@ namespace Barotrauma
public static void DrawPaintedColors(SpriteBatch spriteBatch, bool editing = false, Predicate<MapEntity> predicate = null)
{
var entitiesToRender = !editing && visibleEntities != null ? visibleEntities : MapEntity.MapEntityList;
var entitiesToRender = !editing && visibleEntities != null ? visibleEntities : MapEntity.MapEntityList.ToList();
foreach (MapEntity e in entitiesToRender)
{
@@ -217,7 +217,7 @@ namespace Barotrauma
public static void DrawBack(SpriteBatch spriteBatch, bool editing = false, Predicate<MapEntity> predicate = null)
{
var entitiesToRender = !editing && visibleEntities != null ? visibleEntities : MapEntity.MapEntityList;
var entitiesToRender = !editing && visibleEntities != null ? visibleEntities : MapEntity.MapEntityList.ToList();
foreach (MapEntity e in entitiesToRender)
{
@@ -1,12 +1,32 @@
using Barotrauma.Items.Components;
using Barotrauma.Networking;
using System.Collections.Concurrent;
using System.Collections.Generic;
namespace Barotrauma
{
partial class EntitySpawner : Entity, IServerSerializable
{
public readonly List<(Entity entity, bool isRemoval)> receivedEvents = new List<(Entity entity, bool isRemoval)>();
/// <summary>
/// Thread-safe queue for received entity spawn/remove events from the server.
/// </summary>
private readonly ConcurrentQueue<(Entity entity, bool isRemoval)> receivedEventsQueue = new ConcurrentQueue<(Entity entity, bool isRemoval)>();
/// <summary>
/// Gets a thread-safe snapshot of received events.
/// </summary>
public IEnumerable<(Entity entity, bool isRemoval)> GetReceivedEventsSnapshot()
{
return receivedEventsQueue.ToArray();
}
/// <summary>
/// Clears all received events from the queue.
/// </summary>
void ResetReceivedEvents()
{
while (receivedEventsQueue.TryDequeue(out _)) { }
}
public void ClientEventRead(IReadMessage message, float sendingTime)
{
@@ -34,7 +54,7 @@ namespace Barotrauma
{
DebugConsole.Log("Received entity removal message for ID " + entityId + ". Entity with a matching ID not found.");
}
receivedEvents.Add((entity, true));
receivedEventsQueue.Enqueue((entity, true));
}
else
{
@@ -57,7 +77,7 @@ namespace Barotrauma
GameAnalyticsManager.AddDesignEvent("ItemFabricated:" + (GameMain.GameSession?.GameMode?.Preset.Identifier ?? "none".ToIdentifier()) + ":" + newItem.Prefab.Identifier);
}
}
receivedEvents.Add((newItem, false));
receivedEventsQueue.Enqueue((newItem, false));
}
break;
case (byte)SpawnableType.Character:
@@ -68,7 +88,7 @@ namespace Barotrauma
}
else
{
receivedEvents.Add((character, false));
receivedEventsQueue.Enqueue((character, false));
}
break;
default:
@@ -3916,7 +3916,7 @@ namespace Barotrauma.Networking
{
errorLines.Add("");
errorLines.Add("EntitySpawner events:");
foreach ((Entity entity, bool isRemoval) in Entity.Spawner.receivedEvents)
foreach ((Entity entity, bool isRemoval) in Entity.Spawner.GetReceivedEventsSnapshot())
{
errorLines.Add(
(isRemoval ? "Remove " : "Create ") +
@@ -1015,25 +1015,20 @@ namespace Barotrauma
private void TryStartServer()
{
LuaCsSetup.Instance.CheckRunConditionalHostingCsEnabled(() =>
if (SubmarineInfo.SavedSubmarines.Any(s => s.CalculatingHash))
{
if (SubmarineInfo.SavedSubmarines.Any(s => s.CalculatingHash))
var waitBox = new GUIMessageBox(TextManager.Get("pleasewait"), TextManager.Get("waitforsubmarinehashcalculations"), new LocalizedString[] { TextManager.Get("cancel") });
var waitCoroutine = CoroutineManager.StartCoroutine(WaitForSubmarineHashCalculations(waitBox), "WaitForSubmarineHashCalculations");
waitBox.Buttons[0].OnClicked += (btn, userdata) =>
{
var waitBox = new GUIMessageBox(TextManager.Get("pleasewait"), TextManager.Get("waitforsubmarinehashcalculations"), new LocalizedString[] { TextManager.Get("cancel") });
var waitCoroutine = CoroutineManager.StartCoroutine(WaitForSubmarineHashCalculations(waitBox), "WaitForSubmarineHashCalculations");
waitBox.Buttons[0].OnClicked += (btn, userdata) =>
{
CoroutineManager.StopCoroutines(waitCoroutine);
return true;
};
}
else
{
StartServer();
}
});
CoroutineManager.StopCoroutines(waitCoroutine);
return true;
};
}
else
{
StartServer();
}
}
private IEnumerable<CoroutineStatus> WaitForSubmarineHashCalculations(GUIMessageBox messageBox)
@@ -174,9 +174,9 @@ namespace Barotrauma
public static RectTransform NewItemRectT(GUILayoutGroup parent)
=> new RectTransform((1.0f, 0.06f), parent.RectTransform, Anchor.CenterLeft);
public static void Spacer(GUILayoutGroup parent)
public static void Spacer(GUILayoutGroup parent, float height = 0.03f)
{
new GUIFrame(new RectTransform((1.0f, 0.03f), parent.RectTransform, Anchor.CenterLeft), style: null);
new GUIFrame(new RectTransform((1.0f, height), parent.RectTransform, Anchor.CenterLeft), style: null);
}
public static GUITextBlock Label(GUILayoutGroup parent, LocalizedString str, GUIFont font)
@@ -507,6 +507,47 @@ namespace Barotrauma
return true;
}
};
#if OSX
Spacer(voiceChat, 0.003f);
// On macOS, microphone permission can apparently sometimes end up in a broken state when the app binary changes (eg. after a Steam update).
// The device seems to be there, but won't receive anything, even if the mic permission is fine.
// This button lets the user reset it and reboot the game, so the mic permission check will be retriggered on next run.
new GUIButton(new RectTransform(new Vector2(1.0f, 1.0f), voiceChat.RectTransform),
text: TextManager.Get("MacResetMicPermissions"),
style: "GUIButtonSmall")
{
ToolTip = TextManager.Get("MacResetMicPermissionsToolTip"),
OnClicked = (btn, obj) =>
{
var confirmBox = new GUIMessageBox(
TextManager.Get("MacResetMicPermissions"),
TextManager.Get("MacResetMicPermissionsConfirm"),
[TextManager.Get("OK"), TextManager.Get("Cancel")]);
confirmBox.Buttons[0].OnClicked = (_, _) =>
{
try
{
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo
{
FileName = "tccutil",
Arguments = "reset Microphone com.FakeFish.Barotrauma",
UseShellExecute = false
});
}
catch (Exception e)
{
DebugConsole.NewMessage($"Failed to reset microphone permission: {e.Message}", Color.Orange);
}
GameMain.Instance.Exit();
confirmBox.Close();
return true;
};
confirmBox.Buttons[1].OnClicked = confirmBox.Close;
return true;
}
};
#endif
Spacer(voiceChat);
Label(voiceChat, TextManager.Get("VCInputMode"), GUIStyle.SubHeadingFont);
@@ -503,86 +503,102 @@ namespace Barotrauma.Sounds
mutex = new object();
}
// Use the playingChannels lock to protect both channel assignment AND OpenAL operations.
// This prevents race conditions when multiple threads try to play sounds simultaneously
// (e.g., during Parallel.ForEach in MapEntity.UpdateAll).
int poolIndex = (int)sound.SourcePoolIndex;
object channelsLock = sound.Owner.GetPlayingChannelsLock(sound.SourcePoolIndex);
#if !DEBUG
try
{
#endif
if (mutex != null) { Monitor.Enter(mutex); }
if (sound.Owner.CountPlayingInstances(sound) < sound.MaxSimultaneousInstances)
lock (channelsLock)
{
ALSourceIndex = sound.Owner.AssignFreeSourceToChannel(this);
}
if (ALSourceIndex >= 0)
{
if (!IsStream)
if (mutex != null) { Monitor.Enter(mutex); }
try
{
Al.Sourcei(sound.Owner.GetSourceFromIndex(Sound.SourcePoolIndex, ALSourceIndex), Al.Buffer, 0);
int alError = Al.GetError();
if (alError != Al.NoError)
if (sound.Owner.CountPlayingInstancesUnsafe(sound, poolIndex) < sound.MaxSimultaneousInstances)
{
throw new Exception("Failed to reset source buffer: " + debugName + ", " + Al.GetErrorString(alError));
ALSourceIndex = sound.Owner.AssignFreeSourceToChannelUnsafe(this, poolIndex);
}
Sound.FillAlBuffers();
if (Sound.Buffers is not { AlBuffer: not 0, AlMuffledBuffer: not 0 }) { return; }
uint alBuffer = sound.Owner.GetCategoryMuffle(category) || muffled ? Sound.Buffers.AlMuffledBuffer : Sound.Buffers.AlBuffer;
Al.Sourcei(sound.Owner.GetSourceFromIndex(Sound.SourcePoolIndex, ALSourceIndex), Al.Buffer, (int)alBuffer);
alError = Al.GetError();
if (alError != Al.NoError)
if (ALSourceIndex >= 0)
{
throw new Exception("Failed to bind buffer to source (" + ALSourceIndex.ToString() + ":" + sound.Owner.GetSourceFromIndex(Sound.SourcePoolIndex, ALSourceIndex) + "," + alBuffer.ToString() + "): " + debugName + ", " + Al.GetErrorString(alError));
}
if (!IsStream)
{
Al.Sourcei(sound.Owner.GetSourceFromIndex(Sound.SourcePoolIndex, ALSourceIndex), Al.Buffer, 0);
int alError = Al.GetError();
if (alError != Al.NoError)
{
throw new Exception("Failed to reset source buffer: " + debugName + ", " + Al.GetErrorString(alError));
}
SetProperties();
Sound.FillAlBuffers();
if (Sound.Buffers is not { AlBuffer: not 0, AlMuffledBuffer: not 0 }) { return; }
Al.SourcePlay(sound.Owner.GetSourceFromIndex(Sound.SourcePoolIndex, ALSourceIndex));
alError = Al.GetError();
if (alError != Al.NoError)
{
throw new Exception("Failed to play source: " + debugName + ", " + Al.GetErrorString(alError));
uint alBuffer = sound.Owner.GetCategoryMuffle(category) || muffled ? Sound.Buffers.AlMuffledBuffer : Sound.Buffers.AlBuffer;
Al.Sourcei(sound.Owner.GetSourceFromIndex(Sound.SourcePoolIndex, ALSourceIndex), Al.Buffer, (int)alBuffer);
alError = Al.GetError();
if (alError != Al.NoError)
{
throw new Exception("Failed to bind buffer to source (" + ALSourceIndex.ToString() + ":" + sound.Owner.GetSourceFromIndex(Sound.SourcePoolIndex, ALSourceIndex) + "," + alBuffer.ToString() + "): " + debugName + ", " + Al.GetErrorString(alError));
}
SetProperties();
Al.SourcePlay(sound.Owner.GetSourceFromIndex(Sound.SourcePoolIndex, ALSourceIndex));
alError = Al.GetError();
if (alError != Al.NoError)
{
throw new Exception("Failed to play source: " + debugName + ", " + Al.GetErrorString(alError));
}
}
else
{
uint alBuffer = 0;
Al.Sourcei(sound.Owner.GetSourceFromIndex(Sound.SourcePoolIndex, ALSourceIndex), Al.Buffer, (int)alBuffer);
int alError = Al.GetError();
if (alError != Al.NoError)
{
throw new Exception("Failed to reset source buffer: " + debugName + ", " + Al.GetErrorString(alError));
}
Al.Sourcei(sound.Owner.GetSourceFromIndex(Sound.SourcePoolIndex, ALSourceIndex), Al.Looping, Al.False);
alError = Al.GetError();
if (alError != Al.NoError)
{
throw new Exception("Failed to set stream looping state: " + debugName + ", " + Al.GetErrorString(alError));
}
streamShortBuffer = new short[STREAM_BUFFER_SIZE];
streamBuffers = new uint[4];
unqueuedBuffers = new uint[4];
streamBufferAmplitudes = new float[4];
for (int i = 0; i < 4; i++)
{
Al.GenBuffer(out streamBuffers[i]);
alError = Al.GetError();
if (alError != Al.NoError)
{
throw new Exception("Failed to generate stream buffers: " + debugName + ", " + Al.GetErrorString(alError));
}
if (!Al.IsBuffer(streamBuffers[i]))
{
throw new Exception("Generated streamBuffer[" + i.ToString() + "] is invalid! " + debugName);
}
}
Sound.Owner.InitUpdateChannelThread();
SetProperties();
}
}
}
else
finally
{
uint alBuffer = 0;
Al.Sourcei(sound.Owner.GetSourceFromIndex(Sound.SourcePoolIndex, ALSourceIndex), Al.Buffer, (int)alBuffer);
int alError = Al.GetError();
if (alError != Al.NoError)
{
throw new Exception("Failed to reset source buffer: " + debugName + ", " + Al.GetErrorString(alError));
}
Al.Sourcei(sound.Owner.GetSourceFromIndex(Sound.SourcePoolIndex, ALSourceIndex), Al.Looping, Al.False);
alError = Al.GetError();
if (alError != Al.NoError)
{
throw new Exception("Failed to set stream looping state: " + debugName + ", " + Al.GetErrorString(alError));
}
streamShortBuffer = new short[STREAM_BUFFER_SIZE];
streamBuffers = new uint[4];
unqueuedBuffers = new uint[4];
streamBufferAmplitudes = new float[4];
for (int i = 0; i < 4; i++)
{
Al.GenBuffer(out streamBuffers[i]);
alError = Al.GetError();
if (alError != Al.NoError)
{
throw new Exception("Failed to generate stream buffers: " + debugName + ", " + Al.GetErrorString(alError));
}
if (!Al.IsBuffer(streamBuffers[i]))
{
throw new Exception("Generated streamBuffer[" + i.ToString() + "] is invalid! " + debugName);
}
}
Sound.Owner.InitUpdateChannelThread();
SetProperties();
if (mutex != null) { Monitor.Exit(mutex); }
}
}
#if !DEBUG
@@ -591,12 +607,6 @@ namespace Barotrauma.Sounds
{
throw;
}
finally
{
#endif
if (mutex != null) { Monitor.Exit(mutex); }
#if !DEBUG
}
#endif
void SetProperties()
@@ -417,6 +417,15 @@ namespace Barotrauma.Sounds
return sourcePools[(int)poolIndex].ALSources[srcInd];
}
/// <summary>
/// Gets the lock object for the playing channels array for a specific pool.
/// Used to protect OpenAL operations that need to be atomic with channel assignment.
/// </summary>
public object GetPlayingChannelsLock(SourcePoolIndex poolIndex)
{
return playingChannels[(int)poolIndex];
}
public int AssignFreeSourceToChannel(SoundChannel newChannel)
{
if (Disabled) { return -1; }
@@ -427,14 +436,25 @@ namespace Barotrauma.Sounds
lock (playingChannels[poolIndex])
{
for (int i = 0; i < playingChannels[poolIndex].Length; i++)
return AssignFreeSourceToChannelUnsafe(newChannel, poolIndex);
}
}
/// <summary>
/// Assigns a free source to a channel without locking.
/// Caller MUST hold the playingChannels[poolIndex] lock before calling this method.
/// </summary>
public int AssignFreeSourceToChannelUnsafe(SoundChannel newChannel, int poolIndex)
{
if (Disabled) { return -1; }
for (int i = 0; i < playingChannels[poolIndex].Length; i++)
{
if (playingChannels[poolIndex][i] == null || !playingChannels[poolIndex][i].IsPlaying)
{
if (playingChannels[poolIndex][i] == null || !playingChannels[poolIndex][i].IsPlaying)
{
if (playingChannels[poolIndex][i] != null) { playingChannels[poolIndex][i].Dispose(); }
playingChannels[poolIndex][i] = newChannel;
return i;
}
if (playingChannels[poolIndex][i] != null) { playingChannels[poolIndex][i].Dispose(); }
playingChannels[poolIndex][i] = newChannel;
return i;
}
}
@@ -476,13 +496,25 @@ namespace Barotrauma.Sounds
int count = 0;
lock (playingChannels[(int)sound.SourcePoolIndex])
{
for (int i = 0; i < playingChannels[(int)sound.SourcePoolIndex].Length; i++)
count = CountPlayingInstancesUnsafe(sound, (int)sound.SourcePoolIndex);
}
return count;
}
/// <summary>
/// Counts playing instances without locking.
/// Caller MUST hold the playingChannels[poolIndex] lock before calling this method.
/// </summary>
public int CountPlayingInstancesUnsafe(Sound sound, int poolIndex)
{
if (Disabled) { return 0; }
int count = 0;
for (int i = 0; i < playingChannels[poolIndex].Length; i++)
{
if (playingChannels[poolIndex][i] != null &&
playingChannels[poolIndex][i].Sound.Filename == sound.Filename)
{
if (playingChannels[(int)sound.SourcePoolIndex][i] != null &&
playingChannels[(int)sound.SourcePoolIndex][i].Sound.Filename == sound.Filename)
{
if (playingChannels[(int)sound.SourcePoolIndex][i].IsPlaying) { count++; };
}
if (playingChannels[poolIndex][i].IsPlaying) { count++; };
}
}
return count;
@@ -18,6 +18,10 @@ namespace Barotrauma.Steam
{
public const int MaxThumbnailSize = 1024 * 1024;
/// <summary>
/// Tags the players can choose for their workshop items. These must match the ones defined in the Steamworks backend. They're case insensitive, but must otherwise match exactly for the tag filtering to work correctly.
/// The localized names for these are fetched from the loca files with the identifier "workshop.contenttag.{tag.RemoveWhitespace()}".
/// </summary>
public static readonly ImmutableArray<Identifier> Tags = new []
{
"submarine",
@@ -25,7 +29,7 @@ namespace Barotrauma.Steam
"monster",
"mission",
"outpost",
"beaconstation",
"beacon station",
"wreck",
"ruin",
"weapons",
@@ -34,14 +38,14 @@ namespace Barotrauma.Steam
"art",
"event set",
"total conversion",
"gamemode",
"gameplaymechanics",
"game mode",
"gameplay mechanics",
"environment",
"item assembly",
"language",
"qol",
"clientside",
"serverside",
"client-side",
"server-side",
"outdated",
"library"
}.ToIdentifiers().ToImmutableArray();
@@ -6,7 +6,7 @@
<RootNamespace>Barotrauma</RootNamespace>
<Authors>FakeFish, Undertow Games</Authors>
<Product>Barotrauma</Product>
<Version>1.12.6.2</Version>
<Version>1.12.7.0</Version>
<Copyright>Copyright © FakeFish 2018-2024</Copyright>
<Platforms>AnyCPU;x64</Platforms>
<AssemblyName>Barotrauma</AssemblyName>
+1 -1
View File
@@ -6,7 +6,7 @@
<RootNamespace>Barotrauma</RootNamespace>
<Authors>FakeFish, Undertow Games</Authors>
<Product>Barotrauma</Product>
<Version>1.12.6.2</Version>
<Version>1.12.7.0</Version>
<Copyright>Copyright © FakeFish 2018-2024</Copyright>
<Platforms>AnyCPU;x64</Platforms>
<AssemblyName>Barotrauma</AssemblyName>
@@ -6,7 +6,7 @@
<RootNamespace>Barotrauma</RootNamespace>
<Authors>FakeFish, Undertow Games</Authors>
<Product>Barotrauma</Product>
<Version>1.12.6.2</Version>
<Version>1.12.7.0</Version>
<Copyright>Copyright © FakeFish 2018-2024</Copyright>
<Platforms>AnyCPU;x64</Platforms>
<AssemblyName>Barotrauma</AssemblyName>
@@ -6,7 +6,7 @@
<RootNamespace>Barotrauma</RootNamespace>
<Authors>FakeFish, Undertow Games</Authors>
<Product>Barotrauma Dedicated Server</Product>
<Version>1.12.6.2</Version>
<Version>1.12.7.0</Version>
<Copyright>Copyright © FakeFish 2018-2023</Copyright>
<Platforms>AnyCPU;x64</Platforms>
<AssemblyName>DedicatedServer</AssemblyName>
+1 -1
View File
@@ -6,7 +6,7 @@
<RootNamespace>Barotrauma</RootNamespace>
<Authors>FakeFish, Undertow Games</Authors>
<Product>Barotrauma Dedicated Server</Product>
<Version>1.12.6.2</Version>
<Version>1.12.7.0</Version>
<Copyright>Copyright © FakeFish 2018-2023</Copyright>
<Platforms>AnyCPU;x64</Platforms>
<AssemblyName>DedicatedServer</AssemblyName>
@@ -28,11 +28,14 @@ namespace Barotrauma
}
}
// Create snapshot to avoid concurrent access issues during parallel updates
var clients = GameMain.Server.ConnectedClients.ToArray();
if (GameMain.Server is { ServerSettings.RespawnMode: RespawnMode.Permadeath } &&
GameMain.GameSession?.Campaign is MultiPlayerCampaign mpCampaign &&
causeOfDeath != CauseOfDeathType.Disconnected)
{
Client ownerClient = GameMain.Server.ConnectedClients.FirstOrDefault(c => c.Character == this);
Client ownerClient = clients.FirstOrDefault(c => c.Character == this);
if (ownerClient != null)
{
ownerClient.SpectateOnly = true;
@@ -51,7 +54,7 @@ namespace Barotrauma
if (HasAbilityFlag(AbilityFlags.RetainExperienceForNewCharacter))
{
var ownerClient = GameMain.Server.ConnectedClients.Find(c => c.Character == this);
var ownerClient = clients.FirstOrDefault(c => c.Character == this);
if (ownerClient != null)
{
(GameMain.GameSession?.GameMode as MultiPlayerCampaign)?.SaveExperiencePoints(ownerClient);
@@ -62,7 +65,7 @@ namespace Barotrauma
if (CauseOfDeath.Killer != null && CauseOfDeath.Killer.IsTraitor && CauseOfDeath.Killer != this)
{
var owner = GameMain.Server.ConnectedClients.Find(c => c.Character == this);
var owner = clients.FirstOrDefault(c => c.Character == this);
if (owner != null)
{
if (!LuaCsSetup.Instance.Game.overrideTraitors)
@@ -71,11 +74,11 @@ namespace Barotrauma
}
}
}
foreach (Client client in GameMain.Server.ConnectedClients)
foreach (Client client in clients)
{
if (client.InGame)
{
client.PendingPositionUpdates.Enqueue(this);
client.TryEnqueuePositionUpdate(this);
}
}
}
@@ -486,7 +486,9 @@ namespace Barotrauma
case ControlEventData controlEventData:
Client owner = controlEventData.Owner;
msg.WriteBoolean(owner == c && owner.Character == this);
msg.WriteByte(owner != null && owner.Character == this && GameMain.Server.ConnectedClients.Contains(owner) ? owner.SessionId : (byte)0);
// Create snapshot to avoid concurrent access issues during parallel updates
var connectedClients = GameMain.Server.ConnectedClients.ToArray();
msg.WriteByte(owner != null && owner.Character == this && connectedClients.Contains(owner) ? owner.SessionId : (byte)0);
msg.WriteBoolean(info is { RenamingEnabled: true });
break;
case CharacterStatusEventData statusEventData:
@@ -742,7 +744,9 @@ namespace Barotrauma
return;
}
Client ownerClient = GameMain.Server.ConnectedClients.Find(c => c.Character == this && (!c.SpectateOnly || !GameMain.Server.ServerSettings.AllowSpectating));
// Create snapshot to avoid concurrent access issues during parallel updates
var clients = GameMain.Server.ConnectedClients.ToArray();
Client ownerClient = clients.FirstOrDefault(c => c.Character == this && (!c.SpectateOnly || !GameMain.Server.ServerSettings.AllowSpectating));
if (ownerClient != null)
{
msg.WriteBoolean(true);
@@ -2735,6 +2735,19 @@ namespace Barotrauma
}
);
commands.Add(new Command("ShowServerPerf", "Immediately log server performance info in ServerMessage", (string[] args) =>
{
GameServer.Log(PerformanceMonitor.PM.ToString(), ServerLog.MessageType.ServerMessage);
}));
AssignOnClientRequestExecute(
"ShowServerPerf",
(senderClient, cursorWorldPos, args) =>
{
GameMain.Server.SendConsoleMessage(PerformanceMonitor.PM.ToString(), senderClient);
}
);
#if DEBUG
commands.Add(new Command("spamevents", "A debug command that creates a ton of entity events.", (string[] args) =>
{
@@ -82,10 +82,13 @@ namespace Barotrauma
private bool IsBlockedByAnotherConversation(IEnumerable<Entity> targets, float duration)
{
// Create snapshot to avoid concurrent access issues during parallel updates
var clients = GameMain.Server.ConnectedClients.ToArray();
if (targets == null || targets.None())
{
//if the action doesn't target anyone in specific, it's shown to every client
foreach (var client in GameMain.Server.ConnectedClients)
foreach (var client in clients)
{
if (IsBlockedByAnotherConversation(client, duration)) { return true; }
}
@@ -95,7 +98,7 @@ namespace Barotrauma
foreach (Entity e in targets)
{
if (e is not Character character || !character.IsRemotePlayer) { continue; }
Client targetClient = GameMain.Server.ConnectedClients.Find(c => c.Character == character);
Client targetClient = clients.FirstOrDefault(c => c.Character == character);
if (targetClient != null && IsBlockedByAnotherConversation(targetClient, duration)) { return true; }
}
}
@@ -117,13 +120,16 @@ namespace Barotrauma
partial void ShowDialog(Character speaker, Character targetCharacter)
{
targetClients.Clear();
// Create snapshot to avoid concurrent access issues during parallel updates
var clients = GameMain.Server.ConnectedClients.ToArray();
if (!TargetTag.IsEmpty)
{
IEnumerable<Entity> entities = ParentEvent.GetTargets(TargetTag);
foreach (Entity e in entities)
{
if (e is not Character character || !character.IsRemotePlayer) { continue; }
Client targetClient = GameMain.Server.ConnectedClients.Find(c => c.Character == character);
Client targetClient = clients.FirstOrDefault(c => c.Character == character);
if (targetClient != null)
{
targetClients.Add(targetClient);
@@ -135,7 +141,7 @@ namespace Barotrauma
}
else
{
foreach (Client c in GameMain.Server.ConnectedClients)
foreach (Client c in clients)
{
if (CanClientReceive(c))
{
@@ -9,48 +9,52 @@ namespace Barotrauma;
partial class EventLogAction : EventAction
{
partial void AddEntryProjSpecific(EventLog? eventLog, string displayText)
partial void AddEntryProjSpecific(EventLog? eventLog, string displayText)
{
if (eventLog == null) { return; }
// Create snapshot to avoid concurrent access issues during parallel updates
var clients = GameMain.Server.ConnectedClients.ToArray();
if (!TargetTag.IsEmpty)
{
if (eventLog == null) { return; }
if (!TargetTag.IsEmpty)
List<Client> targetClients = new List<Client>();
foreach (var target in ParentEvent.GetTargets(TargetTag))
{
List<Client> targetClients = new List<Client>();
foreach (var target in ParentEvent.GetTargets(TargetTag))
if (target is Character character)
{
if (target is Character character)
var ownerClient = clients.FirstOrDefault(c => c.Character == character);
if (ownerClient != null)
{
var ownerClient = GameMain.Server.ConnectedClients.Find(c => c.Character == character);
if (ownerClient != null)
{
targetClients.Add(ownerClient);
}
}
else
{
DebugConsole.AddWarning($"{target} is not a valid target for an EventLogAction. The target should be a character.",
ParentEvent.Prefab.ContentPackage);
targetClients.Add(ownerClient);
}
}
if (eventLog!.TryAddEntry(ParentEvent.Prefab.Identifier, Id, displayText, targetClients) && ShowInServerLog)
else
{
Log(targetClients);
DebugConsole.AddWarning($"{target} is not a valid target for an EventLogAction. The target should be a character.",
ParentEvent.Prefab.ContentPackage);
}
}
else
if (eventLog!.TryAddEntry(ParentEvent.Prefab.Identifier, Id, displayText, targetClients) && ShowInServerLog)
{
if (eventLog.TryAddEntry(ParentEvent.Prefab.Identifier, Id, displayText, GameMain.Server.ConnectedClients) && ShowInServerLog)
{
Log(targetClients: null);
}
}
void Log(List<Client>? targetClients)
{
string clientStr = targetClients == null || targetClients.None() ?
string.Empty :
$" ({string.Join(", ", targetClients.Select(c => NetworkMember.ClientLogName(c)))})";
GameServer.Log($"Event \"{ParentEvent.Prefab.Name}\"{clientStr}: " + displayText,
ParentEvent is TraitorEvent ? ServerLog.MessageType.Traitors : ServerLog.MessageType.Chat);
Log(targetClients);
}
}
else
{
if (eventLog.TryAddEntry(ParentEvent.Prefab.Identifier, Id, displayText, clients) && ShowInServerLog)
{
Log(targetClients: null);
}
}
void Log(List<Client>? targetClients)
{
string clientStr = targetClients == null || targetClients.None() ?
string.Empty :
$" ({string.Join(", ", targetClients.Select(c => NetworkMember.ClientLogName(c)))})";
GameServer.Log($"Event \"{ParentEvent.Prefab.Name}\"{clientStr}: " + displayText,
ParentEvent is TraitorEvent ? ServerLog.MessageType.Traitors : ServerLog.MessageType.Chat);
}
}
}
@@ -1,3 +1,5 @@
using System.Linq;
namespace Barotrauma
{
partial class EventObjectiveAction : EventAction
@@ -13,9 +15,12 @@ namespace Barotrauma
ParentObjectiveId,
CanBeCompleted);
// Create snapshot to avoid concurrent access issues during parallel updates
var clients = GameMain.Server.ConnectedClients.ToArray();
if (TargetTag.IsEmpty)
{
foreach (var client in GameMain.Server.ConnectedClients)
foreach (var client in clients)
{
if (client.Character == null) { continue; }
EventManager.ServerWriteObjective(client, objective);
@@ -26,11 +31,11 @@ namespace Barotrauma
foreach (var target in ParentEvent.GetTargets(TargetTag))
{
if (target is not Character character) { continue; }
var ownerClient = GameMain.Server.ConnectedClients.Find(c => c.Character == character);
var ownerClient = clients.FirstOrDefault(c => c.Character == character);
if (ownerClient == null) { continue; }
EventManager.ServerWriteObjective(ownerClient, objective);
}
}
}
}
}
}
@@ -14,8 +14,10 @@ partial class HighlightAction : EventAction
IEnumerable<Client>? targetClients = null;
if (targetCharacters != null)
{
// Create snapshot to avoid concurrent access issues during parallel updates
var clients = GameMain.Server.ConnectedClients.ToArray();
targetClients = targetCharacters
.Select(c => GameMain.Server.ConnectedClients.FirstOrDefault(client => client.Character == c))
.Select(c => clients.FirstOrDefault(client => client.Character == c))
.Where(c => c != null)!;
}
GameMain.Server?.CreateEntityEvent(item, new Item.SetHighlightEventData(State, highlightColor, targetClients));
@@ -1,5 +1,6 @@
using Barotrauma.Networking;
using Barotrauma.Networking;
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma
{
@@ -22,7 +23,9 @@ namespace Barotrauma
private static void NotifyMissionUnlock(Mission mission)
{
foreach (Client client in GameMain.Server.ConnectedClients)
// Create snapshot to avoid concurrent access issues during parallel updates
var clients = GameMain.Server.ConnectedClients.ToArray();
foreach (Client client in clients)
{
NotifyMissionUnlock(mission, client);
}
@@ -40,4 +43,4 @@ namespace Barotrauma
GameMain.Server.ServerPeer.Send(outmsg, client.Connection, DeliveryMethod.Reliable);
}
}
}
}
@@ -25,4 +25,4 @@ namespace Barotrauma
}
}
}
}
}
@@ -33,7 +33,7 @@ namespace Barotrauma
byte selectedOption = inc.ReadByte();
bool isIgnore = selectedOption == byte.MaxValue;
foreach (Event ev in activeEvents)
foreach (Event ev in _activeEvents)
{
if (ev is not ScriptedEvent scriptedEvent) { continue; }
@@ -13,6 +13,7 @@ using System.Xml.Linq;
using MoonSharp.Interpreter;
using System.Net;
using Barotrauma.Extensions;
using System.Threading.Tasks;
using Barotrauma.LuaCs.Events;
namespace Barotrauma
@@ -329,8 +330,10 @@ namespace Barotrauma
}
Stopwatch performanceCounterTimer = Stopwatch.StartNew();
stopwatch = Stopwatch.StartNew();
PerformanceMonitor PM = new PerformanceMonitor();
long prevTicks = stopwatch.ElapsedTicks;
while (ShouldRun)
{
@@ -380,6 +383,7 @@ namespace Barotrauma
Timing.Accumulator -= Timing.Step;
updateCount++;
PM.Update();
}
#if !DEBUG
@@ -427,6 +431,9 @@ namespace Barotrauma
updateCount = 0;
}
}
PerformanceMonitor.PM.Dispose();
stopwatch.Stop();
CloseServer();
@@ -76,7 +76,9 @@ namespace Barotrauma.Items.Components
{
var (msg, deliveryMethod) = PrepareToSend(opcode, data);
foreach (Client client in GameMain.Server.ConnectedClients)
// Create snapshot to avoid concurrent access issues during parallel updates
var clients = GameMain.Server.ConnectedClients.ToArray();
foreach (Client client in clients)
{
if (predicate is not null && !predicate(client)) { continue; }
@@ -391,4 +393,4 @@ namespace Barotrauma.Items.Components
CreateServerEvent(new CircuitBoxServerUpdateSelection(nodes, wires, ios, labels));
}
}
}
}
@@ -211,9 +211,9 @@ namespace Barotrauma
#if DEBUG || UNSTABLE
DebugConsole.NewMessage($"Client {sender.Name} failed to put \"{item}\" in the inventory of {Owner} (parent inventory: {item.ParentInventory?.Owner.ToString() ?? "null"}). No access.", Color.Yellow);
#endif
if (item.body != null && !sender.PendingPositionUpdates.Contains(item))
if (item.body != null)
{
sender.PendingPositionUpdates.Enqueue(item);
sender.TryEnqueuePositionUpdate(item);
}
item.PositionUpdateInterval = 0.0f;
continue;
@@ -2,6 +2,7 @@
using System;
using System.IO;
using System.Linq;
using Barotrauma.LuaCs;
namespace Barotrauma
{
@@ -13,7 +14,7 @@ namespace Barotrauma
if (luaPackage == null)
{
GameMain.Server.SendChatMessage("Couldn't find the LuaCsForBarotrauma content package.", ChatMessageType.ServerMessageBox);
GameMain.Server.SendChatMessage("Couldn't find the ProjectEP package.", ChatMessageType.ServerMessageBox);
return;
}
@@ -51,7 +52,7 @@ namespace Barotrauma
}
catch (UnauthorizedAccessException e)
{
LuaCsLogger.LogError($"Unauthorized file access exception. This usually means you already have LuaCs installed. ${e}", LuaCsMessageOrigin.LuaCs);
LuaCsLogger.LogError($"Unauthorized file access exception. This usually means you already have ProjectEP installed. ${e}", LuaCsMessageOrigin.LuaCs);
return;
}
@@ -62,7 +63,7 @@ namespace Barotrauma
return;
}
GameMain.Server.SendChatMessage("Client-Side LuaCs installed, restart your game to apply changes.", ChatMessageType.ServerMessageBox);
GameMain.Server.SendChatMessage("Client-Side ProjectEP installed, restart your game to apply changes.", ChatMessageType.ServerMessageBox);
}
}
}
@@ -35,11 +35,11 @@ partial class NetworkingService : INetworkingService, IEventClientRawNetMessageR
return message;
}
public void OnReceivedClientNetMessage(IReadMessage netMessage, ClientPacketHeader clientPacketHeader, NetworkConnection sender)
public bool? OnReceivedClientNetMessage(IReadMessage netMessage, ClientPacketHeader clientPacketHeader, NetworkConnection sender)
{
if (clientPacketHeader != ClientHeader)
{
return;
return null;
}
Client client = GameMain.Server.ConnectedClients.First(c => c.Connection == sender);
@@ -64,6 +64,8 @@ partial class NetworkingService : INetworkingService, IEventClientRawNetMessageR
RequestIdSingle(netMessage, client);
break;
}
return true;
}
private void HandleNetMessageId(IReadMessage netMessage, Client client = null)
@@ -29,7 +29,9 @@ namespace Barotrauma
//don't create updates if all clients are very far from the hull
float hullUpdateDistanceSqr = NetConfig.HullUpdateDistance * NetConfig.HullUpdateDistance;
if (!GameMain.Server.ConnectedClients.Any(c =>
// Create snapshot to avoid concurrent access issues during parallel updates
var clients = GameMain.Server.ConnectedClients.ToArray();
if (!clients.Any(c =>
(c.Character != null && Vector2.DistanceSquared(c.Character.WorldPosition, WorldPosition) < hullUpdateDistanceSqr) ||
(c.SpectatePos != null && Vector2.DistanceSquared(c.SpectatePos.Value, WorldPosition) < hullUpdateDistanceSqr)) )
{
@@ -64,6 +64,32 @@ namespace Barotrauma.Networking
// key = entity, value = NetTime.Now when sending
public readonly Dictionary<Entity, float> PositionUpdateLastSent = new Dictionary<Entity, float>();
public readonly Queue<Entity> PendingPositionUpdates = new Queue<Entity>();
private readonly HashSet<Entity> pendingPositionUpdatesSet = new HashSet<Entity>();
/// <summary>
/// Attempts to enqueue a position update for the given entity. Returns true if the entity was added, false if it was already in the queue.
/// Uses HashSet for O(1) lookup instead of Queue.Contains() which is O(n).
/// </summary>
public bool TryEnqueuePositionUpdate(Entity entity)
{
if (pendingPositionUpdatesSet.Add(entity))
{
PendingPositionUpdates.Enqueue(entity);
return true;
}
return false;
}
/// <summary>
/// Dequeues a position update and removes it from the HashSet tracking.
/// </summary>
public Entity DequeuePositionUpdate()
{
if (PendingPositionUpdates.Count == 0) { return null; }
var entity = PendingPositionUpdates.Dequeue();
pendingPositionUpdatesSet.Remove(entity);
return entity;
}
public bool ReadyToStart;
@@ -126,7 +152,7 @@ namespace Barotrauma.Networking
if (!MathUtils.NearlyEqual(karma, syncedKarma, 10.0f))
{
syncedKarma = karma;
GameMain.NetworkMember.LastClientListUpdateID++;
GameMain.NetworkMember.IncrementLastClientListUpdateID();
}
}
}
@@ -353,6 +379,7 @@ namespace Barotrauma.Networking
{
NeedsMidRoundSync = false;
PendingPositionUpdates.Clear();
pendingPositionUpdatesSet.Clear();
EntityEventLastSent.Clear();
LastSentEntityEventID = 0;
LastRecvEntityEventID = 0;
@@ -174,7 +174,7 @@ namespace Barotrauma.Networking
StartTime = DateTime.Now;
OnStarted(transfer);
GameMain.Server.LastClientListUpdateID++;
GameMain.Server.IncrementLastClientListUpdateID();
return transfer;
}
@@ -204,7 +204,7 @@ namespace Barotrauma.Networking
if (numRemoved > 0 || endedTransfers.Count > 0)
{
GameMain.Server.LastClientListUpdateID++;
GameMain.Server.IncrementLastClientListUpdateID();
}
}
@@ -327,7 +327,7 @@ namespace Barotrauma.Networking
}
}
LastClientListUpdateID++;
IncrementLastClientListUpdateID();
if (newClient.Connection == OwnerConnection && OwnerConnection != null)
{
@@ -742,11 +742,6 @@ namespace Barotrauma.Networking
{
errorMsg += "\nInner exception: " + e.InnerException.Message + "\n" + e.InnerException.StackTrace.CleanupStackTrace();
}
GameAnalyticsManager.AddErrorEventOnce(
"GameServer.Update:ClientWriteFailed" + e.StackTrace.CleanupStackTrace(),
GameAnalyticsManager.ErrorSeverity.Error,
errorMsg);
}
}
@@ -1150,7 +1145,9 @@ namespace Barotrauma.Networking
}
else
{
KickClient(c, errorStr);
//Is it necessary to kick a client for a non-existing entity?
//there are plenty of things have been done if received an non-existing entity update.
//KickClient(c, errorStr);
}
}
@@ -1214,18 +1211,25 @@ namespace Barotrauma.Networking
errorLines.Add("");
errorLines.Add("EntitySpawner events:");
foreach (var entityEvent in entityEventManager.UniqueEvents)
try
{
if (entityEvent.Entity is EntitySpawner)
foreach (var entityEvent in entityEventManager.UniqueEvents.ToList())
{
var spawnData = entityEvent.Data as EntitySpawner.SpawnOrRemove;
errorLines.Add(
entityEvent.ID + ": " +
(spawnData is EntitySpawner.RemoveEntity ? "Remove " : "Create ") +
spawnData.Entity.ToString() +
" (" + spawnData.ID + ", " + spawnData.Entity.ID + ")");
if (entityEvent.Entity is EntitySpawner)
{
var spawnData = entityEvent.Data as EntitySpawner.SpawnOrRemove;
errorLines.Add(
entityEvent.ID + ": " +
(spawnData is EntitySpawner.RemoveEntity ? "Remove " : "Create ") +
spawnData.Entity.ToString() +
" (" + spawnData.ID + ", " + spawnData.Entity.ID + ")");
}
}
}
catch
{
errorLines.Add("Failed to write EntitySpawner events.");
}
errorLines.Add("");
errorLines.Add("Last debug messages:");
@@ -2136,7 +2140,7 @@ namespace Barotrauma.Networking
{
if (lastSent > NetTime.Now - updateInterval) { continue; }
}
if (!c.PendingPositionUpdates.Contains(otherCharacter)) { c.PendingPositionUpdates.Enqueue(otherCharacter); }
c.TryEnqueuePositionUpdate(otherCharacter);
}
foreach (Submarine sub in Submarine.Loaded)
@@ -2145,7 +2149,7 @@ namespace Barotrauma.Networking
// (= update is only sent for the docked sub that has the smallest ID, doesn't matter if it's the main sub or a shuttle)
if (sub.Info.IsOutpost || sub.DockedTo.Any(s => s.ID < sub.ID)) { continue; }
if (sub.PhysicsBody == null || sub.PhysicsBody.BodyType == FarseerPhysics.BodyType.Static) { continue; }
if (!c.PendingPositionUpdates.Contains(sub)) { c.PendingPositionUpdates.Enqueue(sub); }
c.TryEnqueuePositionUpdate(sub);
}
foreach (Item item in Item.ItemList)
@@ -2162,7 +2166,7 @@ namespace Barotrauma.Networking
{
if (lastSent > NetTime.Now - updateInterval) { continue; }
}
if (!c.PendingPositionUpdates.Contains(item)) { c.PendingPositionUpdates.Enqueue(item); }
c.TryEnqueuePositionUpdate(item);
}
}
@@ -2206,7 +2210,7 @@ namespace Barotrauma.Networking
entity.Removed ||
(entity is Item item && float.IsInfinity(item.PositionUpdateInterval)))
{
c.PendingPositionUpdates.Dequeue();
c.DequeuePositionUpdate();
continue;
}
@@ -2228,7 +2232,7 @@ namespace Barotrauma.Networking
outmsg.WritePadBits();
c.PositionUpdateLastSent[entity] = (float)NetTime.Now;
c.PendingPositionUpdates.Dequeue();
c.DequeuePositionUpdate();
}
positionUpdateBytes = outmsg.LengthBytes - positionUpdateBytes;
@@ -3203,7 +3207,7 @@ namespace Barotrauma.Networking
initiatedStartGame = false;
GameMain.ResetFrameTime();
LastClientListUpdateID++;
IncrementLastClientListUpdateID();
roundStartTime = DateTime.Now;
@@ -3502,7 +3506,7 @@ namespace Barotrauma.Networking
{
var coolDownRemaining = Client.NameChangeCoolDown - timeSinceNameChange;
SendDirectChatMessage($"ServerMessage.NameChangeFailedCooldownActive~[seconds]={(int)coolDownRemaining.TotalSeconds}", c);
LastClientListUpdateID++;
IncrementLastClientListUpdateID();
//increment the ID to make sure the current server-side name is treated as the "latest",
//and the client correctly reverts back to the old name
c.NameId++;
@@ -3516,7 +3520,7 @@ namespace Barotrauma.Networking
if (result != null)
{
LastClientListUpdateID++;
IncrementLastClientListUpdateID();
return result.Value;
}
@@ -3533,14 +3537,14 @@ namespace Barotrauma.Networking
c.Name = newName;
c.RejectedName = string.Empty;
SendChatMessage($"ServerMessage.NameChangeSuccessful~[oldname]={oldName}~[newname]={newName}", ChatMessageType.Server);
LastClientListUpdateID++;
IncrementLastClientListUpdateID();
return true;
}
else
{
//update client list even if the name cannot be changed to the one sent by the client,
//so the client will be informed what their actual name is
LastClientListUpdateID++;
IncrementLastClientListUpdateID();
return false;
}
}
@@ -4779,7 +4783,9 @@ namespace Barotrauma.Networking
public static string CharacterLogName(Character character)
{
if (character == null) { return "[NULL]"; }
Client client = GameMain.Server.ConnectedClients.Find(c => c.Character == character);
// Create snapshot to avoid concurrent access issues during parallel updates
var clients = GameMain.Server.ConnectedClients.ToArray();
Client client = clients.FirstOrDefault(c => c.Character == character);
return ClientLogName(client, character.LogName);
}
@@ -4790,8 +4796,8 @@ namespace Barotrauma.Networking
LuaCsSetup.Instance?.EventService.PublishEvent<IEventServerLog>(x => x.OnServerLog(line, messageType));
GameMain.Server.ServerSettings.ServerLog.WriteLine(line, messageType);
foreach (Client client in GameMain.Server.ConnectedClients)
var clients = GameMain.Server.ConnectedClients.ToArray();
foreach (Client client in clients)
{
if (!client.HasPermission(ClientPermissions.ServerLog)) continue;
//use sendername as the message type
@@ -4832,7 +4838,7 @@ namespace Barotrauma.Networking
private void UpdateClientLobbies()
{
// Triggers a call to WriteClientList(), which causes clients to call GameClient.ReadClientList()
LastClientListUpdateID++;
IncrementLastClientListUpdateID();
}
private List<Client> GetPlayingClients()
@@ -163,7 +163,7 @@ namespace Barotrauma
{
client.Character.CharacterHealth.ApplyAffliction(null, new Affliction(herpesAffliction, herpesStrength));
GameServer.Log($"{GameServer.ClientLogName(client)} has contracted space herpes due to low karma.", ServerLog.MessageType.Karma);
GameMain.NetworkMember.LastClientListUpdateID++;
GameMain.NetworkMember.IncrementLastClientListUpdateID();
}
else if (existingAffliction != null)
{
@@ -1,15 +1,22 @@
using Barotrauma.Extensions;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using static Barotrauma.EosInterface.Ownership;
// DO NOT TOUCH ANYTHING HERE
// OR EVERYTHING WILL FAIL
namespace Barotrauma.Networking
{
class ServerEntityEvent : NetEntityEvent
{
private IServerSerializable serializable;
#if DEBUG
public string StackTrace;
#endif
@@ -44,6 +51,8 @@ namespace Barotrauma.Networking
class ServerEntityEventManager : NetEntityEventManager
{
static public ServerEntityEventManager SEM;
private readonly List<ServerEntityEvent> events;
//list of unique events (i.e. !IsDuplicate) created during the round
@@ -102,60 +111,137 @@ namespace Barotrauma.Networking
private readonly GameServer server;
private double lastEventCountHighWarning;
public ServerEntityEventManager(GameServer server)
private class PendingCreateEvent
{
events = new List<ServerEntityEvent>();
this.server = server;
bufferedEvents = new List<BufferedEvent>();
uniqueEvents = new List<ServerEntityEvent>();
lastWarningTime = -10.0;
public IServerSerializable Entity;
public NetEntityEvent.IData Data;
public PendingCreateEvent(IServerSerializable entity, NetEntityEvent.IData data)
{
Entity = entity;
Data = data;
}
}
private readonly ConcurrentQueue<PendingCreateEvent> pendingCreateQueue;
private readonly Task createEventTask;
private readonly CancellationTokenSource cancellationTokenSource = new CancellationTokenSource();
private readonly SemaphoreSlim eventSignal = new SemaphoreSlim(0);
public ServerEntityEventManager(GameServer server)
{
events = new List<ServerEntityEvent>();
this.server = server;
bufferedEvents = new List<BufferedEvent>();
uniqueEvents = new List<ServerEntityEvent>();
pendingCreateQueue = new ConcurrentQueue<PendingCreateEvent>();
lastWarningTime = -10.0;
SEM = this;
createEventTask = Task.Run(() => CreateEventProcessorLoop(cancellationTokenSource.Token));
}
private async Task CreateEventProcessorLoop(CancellationToken token)
{
while (!token.IsCancellationRequested)
{
try
{
await eventSignal.WaitAsync(100, token);
ProcessPendingCreateEvents();
}
catch (OperationCanceledException)
{
break;
}
}
}
private void ProcessPendingCreateEvents()
{
// Dequeue and process all pending events currently in the queue.
// Use a lock to synchronize modifications to shared lists / ID.
while (pendingCreateQueue.TryDequeue(out PendingCreateEvent pending))
{
// The original CreateEvent logic (mostly unchanged) but executed under a lock
if (pending == null || pending.Entity == null) { continue; }
var entity = pending.Entity;
var extraData = pending.Data;
var newEvent = new ServerEntityEvent(entity, (UInt16)(ID + 1));
if (extraData != null) newEvent.SetData(extraData);
bool inGameClientsPresent = server.ConnectedClients.Count(c => c.InGame) > 0;
//remove old events that have been sent to all clients, they are redundant now
// keep at least one event in the list (lastSentToAll == e.ID) so we can use it to keep track of the latest ID
// and events less than 15 seconds old to give disconnected clients a bit of time to reconnect without getting desynced
if (GameMain.GameSession.RoundDuration > server.ServerSettings.RoundStartSyncDuration)
{
events.RemoveAll(e =>
(NetIdUtils.IdMoreRecent(lastSentToAll, e.ID) || !inGameClientsPresent) &&
e.CreateTime < Timing.TotalTime - server.ServerSettings.EventRemovalTime);
}
bool duplicateFound = false;
for (int i = events.Count - 1; i >= 0; i--)
{
//we already have an identical event that's waiting to be sent
// -> no need to add a new one
if (events[i].IsDuplicate(newEvent) && !events[i].Sent)
{
duplicateFound = true;
break;
}
}
if (duplicateFound) { continue; }
ID++;
events.Add(newEvent);
if (!uniqueEvents.Any(e => e.IsDuplicate(newEvent)))
{
//create a copy of the event and give it a new ID
var uniqueEvent = new ServerEntityEvent(entity, (UInt16)(uniqueEvents.Count + 1));
uniqueEvent.SetData(extraData);
uniqueEvents.Add(uniqueEvent);
}
}
}
public void CreateEvent(IServerSerializable entity, NetEntityEvent.IData extraData = null)
{
if (!ValidateEntity(entity)) { return; }
var newEvent = new ServerEntityEvent(entity, (UInt16)(ID + 1));
if (extraData != null) newEvent.SetData(extraData);
bool inGameClientsPresent = server.ConnectedClients.Count(c => c.InGame) > 0;
//remove old events that have been sent to all clients, they are redundant now
// keep at least one event in the list (lastSentToAll == e.ID) so we can use it to keep track of the latest ID
// and events less than 15 seconds old to give disconnected clients a bit of time to reconnect without getting desynced
if (GameMain.GameSession.RoundDuration > server.ServerSettings.RoundStartSyncDuration)
{
events.RemoveAll(e =>
(NetIdUtils.IdMoreRecent(lastSentToAll, e.ID) || !inGameClientsPresent) &&
e.CreateTime < Timing.TotalTime - server.ServerSettings.EventRemovalTime);
}
// enqueue and let background task handle the rest
pendingCreateQueue.Enqueue(new PendingCreateEvent(entity, extraData));
for (int i = events.Count - 1; i >= 0; i--)
if (eventSignal.CurrentCount == 0)
{
//we already have an identical event that's waiting to be sent
// -> no need to add a new one
if (events[i].IsDuplicate(newEvent) && !events[i].Sent) return;
}
ID++;
events.Add(newEvent);
if (!uniqueEvents.Any(e => e.IsDuplicate(newEvent)))
{
//create a copy of the event and give it a new ID
var uniqueEvent = new ServerEntityEvent(entity, (UInt16)(uniqueEvents.Count + 1));
uniqueEvent.SetData(extraData);
uniqueEvents.Add(uniqueEvent);
eventSignal.Release();
}
}
public void Dispose()
{
cancellationTokenSource.Cancel();
eventSignal.Release();
try
{
createEventTask?.Wait(2000);
}
catch (AggregateException) { }
finally
{
cancellationTokenSource.Dispose();
eventSignal.Dispose();
}
}
// Due to intensive access demend and time it takes to refactor, we use try-catch when facing thread-safety issue to skip to next update :(
public void Update(List<Client> clients)
{
foreach (BufferedEvent bufferedEvent in bufferedEvents)
@@ -203,29 +289,58 @@ namespace Barotrauma.Networking
bufferedEvent.IsProcessed = true;
}
var inGameClients = clients.FindAll(c => c.InGame && !c.NeedsMidRoundSync);
if (inGameClients.Count > 0)
List<Client> inGameClients = null;
List<Client> midRoundSyncClients = null;
Client ownerClient = null;
foreach (var c in clients)
{
lastSentToAnyone = inGameClients[0].LastRecvEntityEventID;
lastSentToAll = inGameClients[0].LastRecvEntityEventID;
if (server.OwnerConnection != null)
if (c.InGame)
{
var owner = clients.Find(c => c.Connection == server.OwnerConnection);
if (owner != null)
if (c.NeedsMidRoundSync)
{
lastSentToAll = owner.LastRecvEntityEventID;
(midRoundSyncClients ??= new List<Client>()).Add(c);
}
else
{
(inGameClients ??= new List<Client>()).Add(c);
}
}
inGameClients.ForEach(c =>
if (server.OwnerConnection != null && c.Connection == server.OwnerConnection)
{
if (NetIdUtils.IdMoreRecent(lastSentToAll, c.LastRecvEntityEventID)) { lastSentToAll = c.LastRecvEntityEventID; }
if (NetIdUtils.IdMoreRecent(c.LastRecvEntityEventID, lastSentToAnyone)) { lastSentToAnyone = c.LastRecvEntityEventID; }
});
lastSentToAnyoneTime = events.Find(e => e.ID == lastSentToAnyone)?.CreateTime ?? Timing.TotalTime;
ownerClient = c;
}
}
if (Timing.TotalTime - lastWarningTime > 5.0 &&
Timing.TotalTime - lastSentToAnyoneTime > 10.0 &&
if (inGameClients != null && inGameClients.Count > 0)
{
lastSentToAnyone = inGameClients[0].LastRecvEntityEventID;
lastSentToAll = ownerClient?.LastRecvEntityEventID ?? inGameClients[0].LastRecvEntityEventID;
foreach (var c in inGameClients)
{
if (NetIdUtils.IdMoreRecent(lastSentToAll, c.LastRecvEntityEventID))
{
lastSentToAll = c.LastRecvEntityEventID;
}
if (NetIdUtils.IdMoreRecent(c.LastRecvEntityEventID, lastSentToAnyone))
{
lastSentToAnyone = c.LastRecvEntityEventID;
}
}
try
{
lastSentToAnyoneTime = events.ToList().Find(e => e.ID == lastSentToAnyone)?.CreateTime ?? Timing.TotalTime;
}
catch
{
lastSentToAnyoneTime = Timing.TotalTime;
}
if (Timing.TotalTime - lastWarningTime > 5.0 &&
Timing.TotalTime - lastSentToAnyoneTime > 10.0 &&
GameMain.GameSession.RoundDuration > server.ServerSettings.RoundStartSyncDuration)
{
lastWarningTime = Timing.TotalTime;
@@ -235,10 +350,19 @@ namespace Barotrauma.Networking
events.ForEach(e => e.ResetCreateTime());
//TODO: reset clients if this happens, maybe do it if a majority are behind rather than all of them?
}
clients.Where(c => c.NeedsMidRoundSync).ForEach(c => { if (NetIdUtils.IdMoreRecent(lastSentToAll, c.FirstNewEventID)) lastSentToAll = (ushort)(c.FirstNewEventID - 1); });
ServerEntityEvent firstEventToResend = events.Find(e => e.ID == (ushort)(lastSentToAll + 1));
ServerEntityEvent firstEventToResend;
try
{
firstEventToResend = events.Find(e => e.ID == (ushort)(lastSentToAll + 1));
}
catch
{
firstEventToResend = null;
}
if (firstEventToResend != null &&
GameMain.GameSession.RoundDuration > server.ServerSettings.RoundStartSyncDuration &&
((lastSentToAnyoneTime - firstEventToResend.CreateTime) > server.ServerSettings.OldReceivedEventKickTime || (Timing.TotalTime - firstEventToResend.CreateTime) > server.ServerSettings.OldEventKickTime))
@@ -247,19 +371,19 @@ namespace Barotrauma.Networking
// kick everyone that hasn't received it yet, this is way too old
// UNLESS the event was created when the client was still midround syncing,
// in which case we'll wait until the timeout runs out before kicking the client
List<Client> toKick = inGameClients.FindAll(c =>
List<Client> toKick = inGameClients.FindAll(c =>
NetIdUtils.IdMoreRecent((UInt16)(lastSentToAll + 1), c.LastRecvEntityEventID) &&
(firstEventToResend.CreateTime > c.MidRoundSyncTimeOut || lastSentToAnyoneTime > c.MidRoundSyncTimeOut || Timing.TotalTime > c.MidRoundSyncTimeOut + 10.0));
toKick.ForEach(c =>
{
DebugConsole.NewMessage(c.Name + " was kicked because they were expecting a very old network event (" + (c.LastRecvEntityEventID + 1).ToString() + ")", Color.Red);
GameServer.Log(GameServer.ClientLogName(c) + " was kicked because they were expecting a very old network event ("
+ (c.LastRecvEntityEventID + 1).ToString() +
" (created " + (Timing.TotalTime - firstEventToResend.CreateTime).ToString("0.##") + " s ago, " +
(lastSentToAnyoneTime - firstEventToResend.CreateTime).ToString("0.##") + " s older than last event sent to anyone)" +
" Events queued: " + events.Count + ", last sent to all: " + lastSentToAll, ServerLog.MessageType.Error);
server.DisconnectClient(c, PeerDisconnectPacket.WithReason(DisconnectReason.ExcessiveDesyncOldEvent));
}
{
DebugConsole.NewMessage(c.Name + " was kicked because they were expecting a very old network event (" + (c.LastRecvEntityEventID + 1).ToString() + ")", Color.Red);
GameServer.Log(GameServer.ClientLogName(c) + " was kicked because they were expecting a very old network event ("
+ (c.LastRecvEntityEventID + 1).ToString() +
" (created " + (Timing.TotalTime - firstEventToResend.CreateTime).ToString("0.##") + " s ago, " +
(lastSentToAnyoneTime - firstEventToResend.CreateTime).ToString("0.##") + " s older than last event sent to anyone)" +
" Events queued: " + events.Count + ", last sent to all: " + lastSentToAll, ServerLog.MessageType.Error);
server.DisconnectClient(c, PeerDisconnectPacket.WithReason(DisconnectReason.ExcessiveDesyncOldEvent));
}
);
}
@@ -277,11 +401,21 @@ namespace Barotrauma.Networking
}
}
var timedOutClients = clients.FindAll(c => c.Connection != GameMain.Server.OwnerConnection && c.InGame && c.NeedsMidRoundSync && Timing.TotalTime > c.MidRoundSyncTimeOut);
foreach (Client timedOutClient in timedOutClients)
if (midRoundSyncClients != null)
{
GameServer.Log("Disconnecting client " + GameServer.ClientLogName(timedOutClient) + ". Syncing the client with the server took too long.", ServerLog.MessageType.Error);
GameMain.Server.DisconnectClient(timedOutClient, PeerDisconnectPacket.WithReason(DisconnectReason.SyncTimeout));
foreach (var c in midRoundSyncClients)
{
if (NetIdUtils.IdMoreRecent(lastSentToAll, c.FirstNewEventID))
{
lastSentToAll = (ushort)(c.FirstNewEventID - 1);
}
if (c.Connection != GameMain.Server.OwnerConnection && Timing.TotalTime > c.MidRoundSyncTimeOut)
{
GameServer.Log("Disconnecting client " + GameServer.ClientLogName(c) + ". Syncing took too long.", ServerLog.MessageType.Error);
GameMain.Server.DisconnectClient(c, PeerDisconnectPacket.WithReason(DisconnectReason.SyncTimeout));
}
}
}
bufferedEvents.RemoveAll(b => b.IsProcessed);
@@ -346,7 +480,7 @@ namespace Barotrauma.Networking
if (client.NeedsMidRoundSync)
{
segmentTable.StartNewSegment(ServerNetSegment.EntityEventInitial);
segmentTable.StartNewSegment(ServerNetSegment.EntityEventInitial);
msg.WriteUInt16(client.UnreceivedEntityEventCount);
msg.WriteUInt16(client.FirstNewEventID);
@@ -553,10 +687,10 @@ namespace Barotrauma.Networking
{
var clientEntity = entity as IClientSerializable;
if (clientEntity == null) return;
clientEntity.ServerEventRead(buffer, sender);
}
public void Clear()
{
ID = 0;
@@ -279,7 +279,7 @@ namespace Barotrauma.Networking
var shuttleGaps = Gap.GapList.FindAll(g => RespawnShuttles.Contains(g.Submarine) && g.ConnectedWall != null);
shuttleGaps.ForEach(g => Spawner.AddEntityToRemoveQueue(g));
var dockingPorts = Item.ItemList.FindAll(i => RespawnShuttles.Contains(i.Submarine) && i.GetComponent<DockingPort>() != null);
var dockingPorts = Item.ItemList.Where(i => RespawnShuttles.Contains(i.Submarine) && i.GetComponent<DockingPort>() != null).ToList();
dockingPorts.ForEach(d => d.GetComponent<DockingPort>().Undock());
if (!IsShuttleInsideLevel || DateTime.Now > teamSpecificState.DespawnTime)
@@ -0,0 +1,180 @@
using Barotrauma.Networking;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
namespace Barotrauma
{
public class PerformanceMonitor
{
static public PerformanceMonitor PM;
private Stopwatch PMStopwatch = new Stopwatch();
private double tickratetimer = 0;
private double tickrate60stimer = 0;
private static Queue<double> tickrate60s = new Queue<double>(61);
public int ItemCount
{
get{ return Item.ItemList.Count; }
}
public int CharacterCount
{
get { return Character.CharacterList.Count; }
}
public int PhysicsBodyCount
{
get { return PhysicsBody.List.Count; }
}
public int ConnectClients
{
get { return GameMain.Server.ConnectedClients.Count; }
}
public double RealTickRate
{
get; set;
}
public long TotalTicks
{
get;set;
}
public int LastSecondTicks
{
get; set;
} = 0;
public float AverageTickRate
{
get
{
return TotalTicks / (float)TotalTimeElapsed * 1000;
}
}
public double AverageTickRate10s
{
get
{
return tickrate60s.Count > 0 ? tickrate60s.Average() : 60;
}
}
public double TotalTimeElapsed
{
get
{
return PMStopwatch.Elapsed.TotalMilliseconds;
}
}
public TimeSpan TimeElapsed
{
get
{
return TimeSpan.FromMilliseconds(TotalTimeElapsed);
}
}
public float MemoryUsage
{
get
{
Process proc = Process.GetCurrentProcess();
float memory = MathF.Round(proc.PrivateMemorySize64 / (1024 * 1024), 2);
proc.Dispose();
return memory;
}
}
public double TickRateLow
{
get; set;
}
public double TickRateHigh
{
get; set;
}
public PerformanceMonitor()
{
PM = this;
RealTickRate = 60;
TotalTicks = 0;
LastSecondTicks = 60;
TickRateLow = 60;
TickRateHigh = 60;
PMStopwatch.Start();
}
public void Update()
{
TotalTicks += 1;
LastSecondTicks += 1;
if (tickrate60s.Count > 60)
{
tickrate60s.Dequeue();
}
if (TotalTimeElapsed - 1000 >= tickratetimer)
{
RealTickRate = LastSecondTicks / (TotalTimeElapsed - tickratetimer) * 1000;
tickrate60s.Enqueue(RealTickRate);
tickratetimer = TotalTimeElapsed;
LastSecondTicks = 0;
}
if (TotalTimeElapsed - 60000 >= tickrate60stimer)
{
GameServer.Log(PM.ToString(), ServerLog.MessageType.ServerMessage);
TickRateLow = 60;
TickRateHigh = 60;
tickrate60stimer = TotalTimeElapsed;
}
if (RealTickRate > TickRateHigh)
{
TickRateHigh = RealTickRate;
}
if (RealTickRate < TickRateLow)
{
TickRateLow = RealTickRate;
}
}
public void Dispose()
{
PMStopwatch.Reset();
PM = null;
}
override public string ToString()
{
return $"Server Performance Info \n" +
$"Item Count: {ItemCount}\n" +
$"Character Count: {CharacterCount}\n" +
$"Clients Count {ConnectClients}\n " +
$"PhysicsBody Count: {PhysicsBodyCount}\n" +
$"Tick Rate: {RealTickRate}\n" +
$"Min Tick Rate: {TickRateLow}\n" +
$"Max Tick Rate: {TickRateHigh}\n" +
$"Total Ticks: {TotalTicks}\n" +
$"All time Average Tick Rate: {AverageTickRate}\n" +
$"60s Average Tick Rate: {AverageTickRate10s}\n" +
$"Server Run Time: {TimeElapsed}\n" +
$"Memory Usage: {MemoryUsage}\n";
}
}
}
@@ -64,7 +64,7 @@ namespace Barotrauma
GameMain.ShouldRun = false;
};
#endif
Console.WriteLine("Barotrauma Dedicated Server " + GameMain.Version +
Console.WriteLine("Barotrauma Dedicated Server(EP) " + GameMain.Version +
" (" + AssemblyInfo.BuildString + ", branch " + AssemblyInfo.GitBranch + ", revision " + AssemblyInfo.GitRevision + ")");
if (Console.IsOutputRedirected)
{
@@ -93,6 +93,7 @@ namespace Barotrauma
private static bool hasShutDown = false;
private static void ShutDown()
{
SingleThreadWorker.Instance.Dispose();
if (hasShutDown) { return; }
hasShutDown = true;
@@ -6,7 +6,7 @@
<RootNamespace>Barotrauma</RootNamespace>
<Authors>FakeFish, Undertow Games</Authors>
<Product>Barotrauma Dedicated Server</Product>
<Version>1.12.6.2</Version>
<Version>1.12.7.0</Version>
<Copyright>Copyright © FakeFish 2018-2023</Copyright>
<Platforms>AnyCPU;x64</Platforms>
<AssemblyName>DedicatedServer</AssemblyName>
@@ -10,6 +10,6 @@
</Values>
</Setting>
<Setting Name="UseCaching" Type="bool" Value="true" AllowChangesWhileExecuting="false"/>
<Setting Name="CsRunPolicySession" Type="bool" AllowChangesWhileExecuting="false" ShowInMenus="false"/>
<Setting Name="IsCsEnabledForSession" Type="bool" AllowChangesWhileExecuting="false" ShowInMenus="false" Value="false"/>
</Settings>
</Configuration>
@@ -35,44 +35,4 @@ if not CSActive then
end
end
if SERVER then
Networking.Receive("_luastart", function (message, client)
local num = message.ReadUInt16()
local packages = {}
for i = 1, num, 1 do
table.insert(packages, {
Name = message.ReadString(),
Version = message.ReadString(),
Id = message.ReadUInt64(),
Hash = message.ReadString()
})
end
Hook.Call("client.packages", client, packages)
end)
elseif Game.IsMultiplayer then
local message = Networking.Start("_luastart")
local packageCount = 0
for package in ContentPackageManager.EnabledPackages.All do packageCount = packageCount + 1 end
message.WriteUInt16(packageCount)
for package in ContentPackageManager.EnabledPackages.All do
local id = package.UgcId
local hash = package.Hash and package.Hash.StringRepresentation or ""
if id == nil then id = 0 end
message.WriteString(package.Name)
message.WriteString(package.ModVersion)
message.WriteUInt64(UInt64(id))
message.WriteString(hash)
end
Networking.Send(message)
end
LuaSetup = nil
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<style>
<SettingsMenuTab.LuaCsSettings color="169,212,187,255" hovercolor="220,220,220,255" selectedcolor="255,255,255,255" pressedcolor="100,100,100,255" disabledcolor="125,125,125,125">
<Sprite name="LuaCsSettings" texture="%ModDir%/LuaCsSettingsIcon.png" sourcerect="0,0,64,64" tile="false" maintainaspectratio="true" origin="0.5,0.5"/>
</SettingsMenuTab.LuaCsSettings>
</style>
@@ -2,6 +2,11 @@
<infotexts language="English" nowhitespace="false" translatedname="English">
<LuaCsForBarotrauma.SettingsMenu.ModControlsButton>Mod Controls Settings</LuaCsForBarotrauma.SettingsMenu.ModControlsButton>
<LuaCsForBarotrauma.SettingsMenu.ModGameplayButton>Mod Gameplay Settings</LuaCsForBarotrauma.SettingsMenu.ModGameplayButton>
<LuaCsForBarotrauma.SettingsMenu.ResetVisibleSettings>Reset Displayed Settings</LuaCsForBarotrauma.SettingsMenu.ResetVisibleSettings>
<LuaCsForBarotrauma.SettingsMenu.ResetPrompt.Title>Reset Visible Settings</LuaCsForBarotrauma.SettingsMenu.ResetPrompt.Title>
<LuaCsForBarotrauma.SettingsMenu.ResetPrompt.Message>Are you sure you want to reset the values for currently displayed settings?</LuaCsForBarotrauma.SettingsMenu.ResetPrompt.Message>
<LuaCsForBarotrauma.SettingsMenu.ResetPrompt.Yes>Yes</LuaCsForBarotrauma.SettingsMenu.ResetPrompt.Yes>
<LuaCsForBarotrauma.SettingsMenu.ResetPrompt.No>No</LuaCsForBarotrauma.SettingsMenu.ResetPrompt.No>
<!-- Settings -->
<!-- Is Cs Enabled-->
<LuaCsForBarotrauma.CsRunPolicy.DisplayName>Are C# Mods Allowed</LuaCsForBarotrauma.CsRunPolicy.DisplayName>
@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<contentpackage name="LuaCsForBarotrauma">
<Text file="%ModDir%/Texts/English.xml"/>
<UIStyle file="%ModDir%/Style.xml" />
<!--<Text file="%ModDir%/Texts/Portuguese.xml"/>-->
</contentpackage>
@@ -14,7 +14,7 @@
AttachedByDefault="true" DisallowAttachingOverTags="container,planter,refuelableitem" DisallowAttachingOverSize="115,130">
</Holdable>
<ItemContainer hideitems="false" drawinventory="true" ItemsUseInventoryPlacement="true" capacity="1" maxstacksize="1" canbeselected="true" itempos="-25,-20" iteminterval="0,0" itemrotation="0" msg="ItemMsgOxygenRefill" containedspritedepth="0.1">
<ItemContainer hideitems="false" drawinventory="true" ItemsUseInventoryPlacement="true" capacity="1" maxstacksize="1" canbeselected="true" itempos="32,-83" iteminterval="0,0" itemrotation="0" msg="ItemMsgOxygenRefill" containedspritedepth="0.1">
<GuiFrame relativesize="0.2,0.25" anchor="Center" minsize="140,170" maxsize="280,280" style="ItemUI" />
<SlotIcon slotindex="0" texture="Content/UI/StatusMonitorUI.png" sourcerect="64,448,64,64" origin="0.5,0.5" />
<Containable items="oxygensource" />
@@ -24,29 +24,52 @@ Hook.Add("missionsEnded", "test", function()
print("missionsEnded")
end)
-- cfg tests
local str = "CLIENT: "
if SERVER then
str = "SERVER: "
end
function OnChanged(cfg)
print(str, "cfg value for ", cfg.InternalName, " changed to ", cfg.Value)
end
local failed, package = trygetpackage("[DebugOnlyTest]TestLuaMod")
print("packageFailed=", failed)
print("package", package.Name)
local success, config = ConfigService.TryGetConfig(SettingBase.Single, package, "TestFloat")
local success, config = ConfigService.TryGetConfig(SettingBase.Int32, package, "TestSynchroServer")
local success2, config2 = ConfigService.TryGetConfig(SettingBase.Int32, package, "TestSynchroClient")
local success2, config2 = ConfigService.TryGetConfig(SettingBase.Int32, package, "TestSynchroServer")
local success3, config3 = ConfigService.TryGetConfig(SettingBase.Int32, package, "TestSynchroClient")
if not success or not success2 then
print("Failed to get configs.")
return
end
print("config ", success, " ", config.Value)
print("config testsynchrosrv", success2, " ", config2.Value)
print("config testsynchrocli", success3, " ", config3.Value)
config.OnValueChanged.add(OnChanged)
config2.OnValueChanged.add(OnChanged)
local lastTime = 0
print(str, " testsynchroclient=", config2.Value)
print(str, " testsynchroserver=", config.Value)
-- The server should keep updating the value and it should show up on the client.
-- The client should try updating and it should fail.
local lastTime = Timer.Time + 30 -- give time to join
Hook.Add("think", "printconfig", function()
if lastTime > Timer.Time then return end
if lastTime > Timer.Time then return end
lastTime = Timer.Time + 10
lastTime = Timer.Time + 10
print(config.Value)
if SERVER then
config.TrySetValue(config.Value + 1)
end
if SERVER then
local succ = config.TrySetValue(config.Value + 1)
print("Success of setting value on server for '", config.InternalName,"': ", succ)
end
if CLIENT then
local succ = config.TrySetValue(config.Value + 1)
print("Success of setting value on client for '", config.InternalName,"': ", succ, " | This should fail if permissions are not set for client.")
end
end)
@@ -2,4 +2,6 @@
<ModConfig>
<Lua File="%ModDir%/Lua/init.lua" IsAutorun="true" />
<Config File="%ModDir%/Settings.xml"/>
<Config File="%ModDir%/SettingsClient.xml" Target="Client"/>
<Config File="%ModDir%/SettingsServer.xml" Target="Server"/>
</ModConfig>
@@ -1,14 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<Configuration>
<Settings>
<Setting Name="TestTickbox" Type="bool"/>
<Setting Name="TestSynchroClient" Type="int" NetSync="TwoWay" />
<Setting Name="TestSynchroServer" Type="int" NetSync="ServerAuthority" />
<Setting Name="TestFloat" Type="float" NetSync="ServerAuthority" />
<Setting Name="TestHidden" Type="bool" ShowInMenus="false"/>
<Setting Name="TestRangeFloat" Type="rangeFloat" Min="0" Max="25" Steps="11"/>
<Setting Name="TestRangeInt" Type="rangeInt" Min="0" Max="10" Steps="11"/>
<Setting Name="TestString" Type="string" />
<Setting Name="TestTickbox" Type="bool" Value="true"/>
<Setting Name="TestSynchroClient" Type="int" NetSync="TwoWay" Value="40"/>
<Setting Name="TestSynchroServer" Type="int" NetSync="ServerAuthority" Value="25"/>
<Setting Name="TestFloat" Type="float" Value="3498"/>
<Setting Name="TestHidden" Type="bool" ShowInMenus="false" Value="false"/>
<Setting Name="TestRangeFloat" Type="rangeFloat" Min="0" Max="25" Steps="11" Value="4.5"/>
<Setting Name="TestRangeInt" Type="rangeInt" Min="0" Max="10" Steps="11" Value="4"/>
<Setting Name="TestString" Type="string" Value="ok"/>
<Setting Name="TestControl" Type="control" Value="A"/>
<Setting Name="TestDropdownList" Type="listString" Value="Hi">
<Values>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<Configuration>
<!-- Should match 'SettingsServer'. We define these on the client and server separately to give different values. -->
<Settings>
<Setting Name="TestSynchroClient" Type="int" NetSync="TwoWay" Value="3545" ShowInMenus="false"/>
<Setting Name="TestSynchroServer" Type="int" NetSync="ServerAuthority" Value="577" ShowInMenus="false"/>
</Settings>
</Configuration>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<Configuration>
<!-- Should match 'SettingsClient'. We define these on the client and server separately to give different values. -->
<Settings>
<Setting Name="TestSynchroClient" Type="int" NetSync="TwoWay" Value="40" ShowInMenus="false"/>
<Setting Name="TestSynchroServer" Type="int" NetSync="ServerAuthority" Value="25" ShowInMenus="false"/>
</Settings>
</Configuration>
@@ -156,7 +156,8 @@ namespace Barotrauma
Reactor reactor = item.GetComponent<Reactor>();
if (reactor != null && reactor.Item.Condition > 0.0f) { roundData.Reactors.Add(reactor); }
}
pathFinder = new PathFinder(WayPoint.WayPointList, false);
pathFinder = new PathFinder(WayPoint.WayPointList.ToList(), false);
cachedDistances.Clear();
#if CLIENT
@@ -323,7 +324,7 @@ namespace Barotrauma
static CachedDistance CalculateNewCachedDistance(Character c)
{
pathFinder ??= new PathFinder(WayPoint.WayPointList, false);
pathFinder ??= new PathFinder(WayPoint.WayPointList.ToList(), false);
var path = pathFinder.FindPath(ConvertUnits.ToSimUnits(c.WorldPosition), ConvertUnits.ToSimUnits(Submarine.MainSub.WorldPosition));
if (path.Unreachable) { return null; }
return new CachedDistance(c.WorldPosition, Submarine.MainSub.WorldPosition, path.TotalLength, Timing.TotalTime + Rand.Range(1.0f, 5.0f));
@@ -1,13 +1,67 @@
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Xml.Linq;
namespace Barotrauma
{
/// <summary>
/// Thread-safe wrapper for AITarget list operations.
/// Uses copy-on-write pattern for lock-free reads.
/// </summary>
class ThreadSafeAITargetList : IEnumerable<AITarget>
{
private volatile List<AITarget> _list = new List<AITarget>();
private readonly object _writeLock = new object();
public int Count => _list.Count;
public void Add(AITarget target)
{
lock (_writeLock)
{
var newList = new List<AITarget>(_list) { target };
Interlocked.Exchange(ref _list, newList);
}
}
public bool Remove(AITarget target)
{
lock (_writeLock)
{
var newList = new List<AITarget>(_list);
bool removed = newList.Remove(target);
if (removed)
{
Interlocked.Exchange(ref _list, newList);
}
return removed;
}
}
public void Clear()
{
Interlocked.Exchange(ref _list, new List<AITarget>());
}
public bool Contains(AITarget target) => _list.Contains(target);
public AITarget this[int index] => _list[index];
public IEnumerator<AITarget> GetEnumerator() => _list.GetEnumerator();
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator();
public List<AITarget> ToList() => new List<AITarget>(_list);
public AITarget FirstOrDefault(Func<AITarget, bool> predicate) => _list.FirstOrDefault(predicate);
public IEnumerable<AITarget> Where(Func<AITarget, bool> predicate) => _list.Where(predicate);
public bool Any(Func<AITarget, bool> predicate) => _list.Any(predicate);
}
partial class AITarget
{
public static List<AITarget> List = new List<AITarget>();
public static ThreadSafeAITargetList List = new ThreadSafeAITargetList();
private Entity entity;
public Entity Entity
@@ -242,10 +242,13 @@ namespace Barotrauma
}
/// <summary>
/// The monster won't try to damage these submarines
/// The monster won't try to damage these submarines. Applies to hulls, structures and static items (items without a physics body) belonging to these submarines. Does not apply to non-static items, e.g. flares or other provocative items.
/// </summary>
private readonly HashSet<Submarine> unattackableSubmarines = [];
/// <summary>
/// Set the submarine(s) the monster won't attack. Applies to hulls, structures and static items (items without a physics body) belonging to these submarines. Does not apply to non-static items, e.g. flares or other provocative items.
/// </summary>
private readonly HashSet<Submarine> unattackableSubmarines = new HashSet<Submarine>();
public void SetUnattackableSubmarines(Submarine submarine, bool includeOwnSub = true, bool includeConnectedSubs = true, bool clearExisting = true)
{
if (clearExisting)
@@ -3075,11 +3078,17 @@ namespace Barotrauma
}
else
{
// Ignore all structures, items, and hulls inside these subs.
if (aiTarget.Entity.Submarine != null)
if (aiTarget.Entity.Submarine != null)
{
//ignore all items, structures and hulls in wrecks and beacon stations
//(we don't want monsters to be distracted by them during missions,
//nor have monsters inside them attack "their home" rather than the player)
if (aiTarget.Entity.Submarine.Info.IsWreck ||
aiTarget.Entity.Submarine.Info.IsBeacon ||
aiTarget.Entity.Submarine.Info.IsBeacon)
{
continue;
}
if (aiTarget.Entity is Structure or Hull or Item { body: null } &&
unattackableSubmarines.Contains(aiTarget.Entity.Submarine))
{
continue;
@@ -5,6 +5,7 @@ using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
namespace Barotrauma
{
@@ -381,8 +382,13 @@ namespace Barotrauma
}
steeringBuffer = Math.Clamp(steeringBuffer, minSteeringBuffer, maxSteeringBuffer);
AnimController.Crouching = shouldCrouch;
CheckCrouching(deltaTime);
// in case of somehow AnimController was a null, eg. something removed AnimController in the middle of an update
if (AnimController != null)
{
AnimController.Crouching = shouldCrouch;
CheckCrouching(deltaTime);
}
Character.ClearInputs();
if (SortTimer > 0.0f)
@@ -1637,7 +1643,8 @@ namespace Barotrauma
if (mode == AIObjectiveCombat.CombatMode.None) { return; }
if (Character.IsDead || Character.IsIncapacitated || Character.Removed) { return; }
if (!Character.IsBot) { return; }
if (ObjectiveManager.Objectives.FirstOrDefault(o => o is AIObjectiveCombat) is AIObjectiveCombat combatObjective)
List<AIObjective> ObjectivesLocal = ObjectiveManager.Objectives;
if (ObjectivesLocal.FirstOrDefault(o => o is AIObjectiveCombat) is AIObjectiveCombat combatObjective)
{
// Don't replace offensive mode with something else
if (combatObjective.Mode == AIObjectiveCombat.CombatMode.Offensive && mode != AIObjectiveCombat.CombatMode.Offensive) { return; }
@@ -1817,7 +1824,9 @@ namespace Barotrauma
public static bool HasDivingMask(Character character, float conditionPercentage = 0, bool requireOxygenTank = true)
=> HasItem(character, Tags.LightDivingGear, out _, requireOxygenTank ? Tags.OxygenSource : Identifier.Empty, conditionPercentage, requireEquipped: true);
private static List<Item> matchingItems = new List<Item>();
// ThreadLocal to ensure thread safety - each thread gets its own list instance
private static readonly ThreadLocal<List<Item>> matchingItemsLocal = new ThreadLocal<List<Item>>(() => new List<Item>());
private static List<Item> matchingItems => matchingItemsLocal.Value;
/// <summary>
/// Note: uses a single list for matching items. The item is reused each time when the method is called. So if you use the method twice, and then refer to the first items, you'll actually get the second.
@@ -1825,15 +1834,16 @@ namespace Barotrauma
/// </summary>
public static bool HasItem(Character character, Identifier tagOrIdentifier, out IEnumerable<Item> items, Identifier containedTag = default, float conditionPercentage = 0, bool requireEquipped = false, bool recursive = true, Func<Item, bool> predicate = null)
{
matchingItems.Clear();
items = matchingItems;
var localMatchingItems = matchingItems;
localMatchingItems.Clear();
items = localMatchingItems;
if (character?.Inventory == null) { return false; }
matchingItems = character.Inventory.FindAllItems(i => (i.Prefab.Identifier == tagOrIdentifier || i.HasTag(tagOrIdentifier)) &&
character.Inventory.FindAllItems(i => (i.Prefab.Identifier == tagOrIdentifier || i.HasTag(tagOrIdentifier)) &&
i.ConditionPercentage >= conditionPercentage &&
(!requireEquipped || character.HasEquippedItem(i)) &&
(predicate == null || predicate(i)), recursive, matchingItems);
items = matchingItems;
foreach (var item in matchingItems)
(predicate == null || predicate(i)), recursive, localMatchingItems);
items = localMatchingItems;
foreach (var item in localMatchingItems)
{
if (item == null) { continue; }
@@ -1,4 +1,5 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using Barotrauma.IO;
using System.Linq;
@@ -9,7 +10,8 @@ namespace Barotrauma
{
class NPCConversationCollection : Prefab
{
public static readonly Dictionary<LanguageIdentifier, PrefabCollection<NPCConversationCollection>> Collections = new Dictionary<LanguageIdentifier, PrefabCollection<NPCConversationCollection>>();
// Thread-safe dictionary for language-based collections
public static readonly ConcurrentDictionary<LanguageIdentifier, PrefabCollection<NPCConversationCollection>> Collections = new ConcurrentDictionary<LanguageIdentifier, PrefabCollection<NPCConversationCollection>>();
public readonly LanguageIdentifier Language;
@@ -160,7 +162,24 @@ namespace Barotrauma
return currentFlags;
}
private static readonly List<NPCConversation> previousConversations = new List<NPCConversation>();
// Thread-safe previous conversations tracking using copy-on-write pattern
private static volatile List<NPCConversation> _previousConversations = new List<NPCConversation>();
private static readonly object _previousConversationsLock = new object();
private static List<NPCConversation> previousConversations => _previousConversations;
private static void AddToPreviousConversations(NPCConversation conversation)
{
lock (_previousConversationsLock)
{
var newList = new List<NPCConversation>(_previousConversations);
newList.Insert(0, conversation);
if (newList.Count > MaxPreviousConversations)
{
newList.RemoveAt(MaxPreviousConversations);
}
_previousConversations = newList;
}
}
public static List<(Character speaker, string line)> CreateRandom(List<Character> availableSpeakers)
{
@@ -281,8 +300,7 @@ namespace Barotrauma
if (baseConversation == null)
{
previousConversations.Insert(0, selectedConversation);
if (previousConversations.Count > MaxPreviousConversations) previousConversations.RemoveAt(MaxPreviousConversations);
AddToPreviousConversations(selectedConversation);
}
lineList.Add((speaker, selectedConversation.Line));
CreateConversation(availableSpeakers, assignedSpeakers, selectedConversation, lineList, availableConversations);
@@ -119,7 +119,7 @@ namespace Barotrauma
protected override bool CheckObjectiveState()
{
if (item.IgnoreByAI(character) || Item.DeconstructItems.Contains(item))
if (item.IgnoreByAI(character) || Item.IsMarkedForDeconstruction(item))
{
Abandon = true;
}
@@ -114,7 +114,7 @@ namespace Barotrauma
if (!allowUnloading) { return false; }
if (requireValidContainer && !IsValidContainer(item.Container, character)) { return false; }
}
if (ignoreItemsMarkedForDeconstruction && Item.DeconstructItems.Contains(item)) { return false; }
if (ignoreItemsMarkedForDeconstruction && Item.IsMarkedForDeconstruction(item)) { return false; }
if (!item.HasAccess(character)) { return false; }
if (character != null && !IsItemInsideValidSubmarine(item, character)) { return false; }
if (item.HasBallastFloraInHull) { return false; }
@@ -1,5 +1,6 @@
#nullable enable
using Microsoft.Xna.Framework;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using Barotrauma.Items.Components;
@@ -68,8 +69,9 @@ namespace Barotrauma
/// <summary>
/// When did the character last inspect whether some other character has stolen items on them?
/// Thread-safe dictionary for concurrent access.
/// </summary>
private static readonly Dictionary<Character, double> lastInspectionTimes = new Dictionary<Character, double>();
private static readonly ConcurrentDictionary<Character, double> lastInspectionTimes = new ConcurrentDictionary<Character, double>();
private const float NormalInspectionInterval = 120.0f;
private const float CriminalInspectionInterval = 30.0f;
@@ -122,7 +122,7 @@ namespace Barotrauma
}
else
{
Objectives.RemoveAll(o => o.GetType() == type);
Objectives.RemoveAll(o => o?.GetType() == type);
}
Objectives.Add(objective);
}
@@ -440,7 +440,7 @@ namespace Barotrauma
if (Identifier == Tags.DeconstructThis && item.AllowDeconstruct)
{
if (item.AllowDeconstruct && !Item.DeconstructItems.Contains(item) &&
if (item.AllowDeconstruct && !Item.IsMarkedForDeconstruction(item) &&
//only allow deconstructing if there are no deconstruction recipes (= deconstructing yields nothing), or deconstruction recipes that
(item.Prefab.DeconstructItems.None() ||
item.Prefab.DeconstructItems.Any(deconstructItem =>
@@ -454,7 +454,7 @@ namespace Barotrauma
}
else if (Identifier == Tags.DontDeconstructThis)
{
if (Item.DeconstructItems.Contains(item)) { return true; }
if (Item.IsMarkedForDeconstruction(item)) { return true; }
}
ImmutableArray<Identifier> targetItems = GetTargetItems(option);
@@ -8,8 +8,10 @@ using FarseerPhysics.Dynamics.Joints;
using Microsoft.Xna.Framework;
using MoonSharp.Interpreter;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Xml.Linq;
using JointParams = Barotrauma.RagdollParams.JointParams;
using LimbParams = Barotrauma.RagdollParams.LimbParams;
@@ -26,7 +28,33 @@ namespace Barotrauma
/// </summary>
const float MaxImpactDamage = 0.1f;
private static readonly List<Ragdoll> list = new List<Ragdoll>();
// Thread-safe list using copy-on-write pattern (ConcurrentBag doesn't support indexer/Remove)
private static volatile List<Ragdoll> _list = new List<Ragdoll>();
private static readonly object _listLock = new object();
private static List<Ragdoll> list => _list;
private static void ListAdd(Ragdoll ragdoll)
{
lock (_listLock)
{
var newList = new List<Ragdoll>(_list) { ragdoll };
Interlocked.Exchange(ref _list, newList);
}
}
private static bool ListRemove(Ragdoll ragdoll)
{
lock (_listLock)
{
var newList = new List<Ragdoll>(_list);
bool removed = newList.Remove(ragdoll);
if (removed)
{
Interlocked.Exchange(ref _list, newList);
}
return removed;
}
}
struct Impact
{
@@ -47,7 +75,8 @@ namespace Barotrauma
}
}
private readonly Queue<Impact> impactQueue = new Queue<Impact>();
// Thread-safe queue for physics collision callbacks
private readonly ConcurrentQueue<Impact> impactQueue = new ConcurrentQueue<Impact>();
protected Hull currentHull;
@@ -469,7 +498,7 @@ namespace Barotrauma
public Ragdoll(Character character, string seed, RagdollParams ragdollParams = null)
{
list.Add(this);
ListAdd(this);
this.character = character;
Recreate(ragdollParams ?? RagdollParams);
}
@@ -746,10 +775,7 @@ namespace Barotrauma
{
if (!f2.IsSensor)
{
lock (impactQueue)
{
impactQueue.Enqueue(new Impact(f1, f2, contact, velocity));
}
impactQueue.Enqueue(new Impact(f1, f2, contact, velocity));
}
return true;
}
@@ -821,10 +847,7 @@ namespace Barotrauma
}
}
lock (impactQueue)
{
impactQueue.Enqueue(new Impact(f1, f2, contact, velocity));
}
impactQueue.Enqueue(new Impact(f1, f2, contact, velocity));
return true;
}
@@ -1193,6 +1216,10 @@ namespace Barotrauma
public void Teleport(Vector2 moveAmount, Vector2 velocityChange, bool detachProjectiles = true)
{
// Hopefully this will fix some crashes :(
// If Collider was null then no need to procced: nothing is there already
if (Collider == null) { return; }
foreach (Limb limb in Limbs)
{
if (limb.IsSevered) { continue; }
@@ -1214,6 +1241,7 @@ namespace Barotrauma
character.DisableImpactDamageTimer = 0.25f;
// Why they did null check below but didn't do it here????
SetPosition(Collider.SimPosition + moveAmount);
character.CursorPosition += moveAmount;
@@ -1286,10 +1314,9 @@ namespace Barotrauma
{
if (!character.Enabled || character.Removed || Frozen || Invalid || Collider == null || Collider.Removed) { return; }
while (impactQueue.Count > 0)
while (impactQueue.TryDequeue(out var impact))
{
var impact = impactQueue.Dequeue();
ApplyImpact(impact.F1, impact.F2, impact.WorldNormal, impact.ImpactPos, impact.Velocity);
ApplyImpact(impact.F1, impact.F2, impact.LocalNormal, impact.ImpactPos, impact.Velocity);
}
CheckValidity();
@@ -2235,8 +2262,9 @@ namespace Barotrauma
if (limb == null)
{
// Didn't seek or find a (valid) limb of the matching type. If there's multiple limbs of the same type, check the other limbs.
foreach (var l in limbs)
foreach (var l in Limbs)
{
if (l == null) { continue; }
if (l.Removed) { continue; }
if (useSecondaryType)
{
@@ -2362,7 +2390,7 @@ namespace Barotrauma
LimbJoints = null;
}
list.Remove(this);
ListRemove(this);
}
public static void RemoveAll()
@@ -7,10 +7,12 @@ using FarseerPhysics;
using FarseerPhysics.Dynamics;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Xml.Linq;
#if SERVER
using System.Text;
@@ -28,12 +30,70 @@ namespace Barotrauma
public readonly record struct TalentResistanceIdentifier(Identifier ResistanceIdentifier, Identifier TalentIdentifier);
/// <summary>
/// Thread-safe wrapper for character list operations.
/// Provides lock-free read operations and synchronized write operations.
/// </summary>
class ThreadSafeCharacterList : IEnumerable<Character>
{
private volatile List<Character> _list = new List<Character>();
private readonly object _writeLock = new object();
public int Count => _list.Count;
public void Add(Character character)
{
lock (_writeLock)
{
var newList = new List<Character>(_list) { character };
Interlocked.Exchange(ref _list, newList);
}
}
public bool Remove(Character character)
{
lock (_writeLock)
{
var newList = new List<Character>(_list);
bool removed = newList.Remove(character);
if (removed)
{
Interlocked.Exchange(ref _list, newList);
}
return removed;
}
}
public void Clear()
{
Interlocked.Exchange(ref _list, new List<Character>());
}
public bool Contains(Character character) => _list.Contains(character);
public Character this[int index] => _list[index];
public IEnumerator<Character> GetEnumerator() => _list.GetEnumerator();
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator();
// LINQ-friendly snapshot for complex queries
public List<Character> ToList() => new List<Character>(_list);
public Character FirstOrDefault(Func<Character, bool> predicate) => _list.FirstOrDefault(predicate);
public Character Find(Predicate<Character> predicate) => _list.Find(predicate);
public List<Character> FindAll(Predicate<Character> predicate) => _list.FindAll(predicate);
public IEnumerable<Character> Where(Func<Character, bool> predicate) => _list.Where(predicate);
public bool Any(Func<Character, bool> predicate) => _list.Any(predicate);
public bool None(Func<Character, bool> predicate) => !_list.Any(predicate);
public int CountWhere(Func<Character, bool> predicate) => _list.Count(predicate);
}
partial class Character : Entity, IDamageable, ISerializableEntity, IClientSerializable, IServerPositionSync
{
public static readonly List<Character> CharacterList = new List<Character>();
public static readonly ThreadSafeCharacterList CharacterList = new ThreadSafeCharacterList();
public static int CharacterUpdateInterval = 1;
private static int characterUpdateTick = 1;
private static volatile int characterUpdateTick = 1;
public const float MaxHighlightDistance = 150.0f;
public const float MaxDragDistance = 200.0f;
@@ -2793,10 +2853,11 @@ namespace Barotrauma
}
int itemsPerFrame = IsOnPlayerTeam ? 100 : 10;
int checkedItemCount = 0;
for (int i = 0; i < itemsPerFrame && itemIndex < Item.ItemList.Count; i++, itemIndex++)
var cachedItems = Item.GetCachedItemList();
for (int i = 0; i < itemsPerFrame && itemIndex < cachedItems.Count; i++, itemIndex++)
{
checkedItemCount++;
var item = Item.ItemList[itemIndex];
var item = cachedItems[itemIndex];
if (!item.IsInteractable(this)) { continue; }
if (ignoredItems != null && ignoredItems.Contains(item)) { continue; }
if (item.Submarine == null) { continue; }
@@ -2832,10 +2893,10 @@ namespace Barotrauma
}
}
targetItem = _foundItem;
bool completed = itemIndex >= Item.ItemList.Count - 1;
bool completed = itemIndex >= cachedItems.Count - 1;
if (HumanAIController.DebugAI && checkedItemCount > 0 && targetItem != null && StopWatch.ElapsedMilliseconds > 1)
{
var msg = $"Went through {checkedItemCount} of total {Item.ItemList.Count} items. Found item {targetItem.Name} in {StopWatch.ElapsedMilliseconds} ms. Completed: {completed}";
var msg = $"Went through {checkedItemCount} of total {cachedItems.Count} items. Found item {targetItem.Name} in {StopWatch.ElapsedMilliseconds} ms. Completed: {completed}";
if (StopWatch.ElapsedMilliseconds > 5)
{
DebugConsole.ThrowError(msg);
@@ -3432,21 +3493,9 @@ namespace Barotrauma
characterUpdateTick++;
if (characterUpdateTick % CharacterUpdateInterval == 0)
for (int i = 0; i < CharacterList.Count; i++)
{
for (int i = 0; i < CharacterList.Count; i++)
{
if (LuaCsSetup.Instance.Game.UpdatePriorityCharacters.Contains(CharacterList[i])) continue;
CharacterList[i].Update(deltaTime * CharacterUpdateInterval, cam);
}
}
foreach (Character character in LuaCsSetup.Instance.Game.UpdatePriorityCharacters)
{
if (character.Removed) { continue; }
Debug.Assert(character is { Removed: false });
character.Update(deltaTime, cam);
CharacterList[i].Update(deltaTime, cam);
}
#if CLIENT
@@ -4922,7 +4971,11 @@ namespace Barotrauma
HealthUpdateInterval = 0.0f;
}
private readonly List<ISerializableEntity> targets = new List<ISerializableEntity>();
// Thread-static to avoid concurrent modification in parallel item updates
[ThreadStatic]
private static List<ISerializableEntity> t_statusEffectTargets;
private static List<ISerializableEntity> StatusEffectTargets => t_statusEffectTargets ??= new List<ISerializableEntity>();
public void ApplyStatusEffects(ActionType actionType, float deltaTime)
{
if (actionType == ActionType.OnEating)
@@ -4951,6 +5004,7 @@ namespace Barotrauma
if (statusEffect.HasTargetType(StatusEffect.TargetType.NearbyItems) ||
statusEffect.HasTargetType(StatusEffect.TargetType.NearbyCharacters))
{
var targets = StatusEffectTargets;
targets.Clear();
statusEffect.AddNearbyTargets(WorldPosition, targets);
statusEffect.Apply(actionType, deltaTime, this, targets);
@@ -8,12 +8,14 @@ using Barotrauma.Networking;
using Microsoft.Xna.Framework;
using MoonSharp.Interpreter;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization;
using System.Globalization;
using System.Linq;
using System.Threading;
using System.Xml.Linq;
using static OneOf.Types.TrueFalseOrNull;
using MoonSharp.Interpreter;
namespace Barotrauma
{
@@ -134,8 +136,9 @@ namespace Barotrauma
private readonly List<LimbHealth> limbHealths = new List<LimbHealth>();
private readonly Dictionary<Affliction, LimbHealth> afflictions = new Dictionary<Affliction, LimbHealth>();
private readonly HashSet<Affliction> irremovableAfflictions = new HashSet<Affliction>();
// Thread-safe afflictions dictionary for concurrent access
private readonly ConcurrentDictionary<Affliction, LimbHealth> afflictions = new ConcurrentDictionary<Affliction, LimbHealth>();
private readonly ConcurrentDictionary<Affliction, byte> irremovableAfflictions = new ConcurrentDictionary<Affliction, byte>();
private Affliction bloodlossAffliction;
private Affliction oxygenLowAffliction;
private Affliction pressureAffliction;
@@ -326,13 +329,13 @@ namespace Barotrauma
private void InitIrremovableAfflictions()
{
irremovableAfflictions.Add(bloodlossAffliction = new Affliction(AfflictionPrefab.Bloodloss, 0.0f));
irremovableAfflictions.Add(stunAffliction = new Affliction(AfflictionPrefab.Stun, 0.0f));
irremovableAfflictions.Add(pressureAffliction = new Affliction(AfflictionPrefab.Pressure, 0.0f));
irremovableAfflictions.Add(oxygenLowAffliction = new Affliction(AfflictionPrefab.OxygenLow, 0.0f));
foreach (Affliction affliction in irremovableAfflictions)
irremovableAfflictions.TryAdd(bloodlossAffliction = new Affliction(AfflictionPrefab.Bloodloss, 0.0f), 0);
irremovableAfflictions.TryAdd(stunAffliction = new Affliction(AfflictionPrefab.Stun, 0.0f), 0);
irremovableAfflictions.TryAdd(pressureAffliction = new Affliction(AfflictionPrefab.Pressure, 0.0f), 0);
irremovableAfflictions.TryAdd(oxygenLowAffliction = new Affliction(AfflictionPrefab.OxygenLow, 0.0f), 0);
foreach (Affliction affliction in irremovableAfflictions.Keys)
{
afflictions.Add(affliction, null);
afflictions.TryAdd(affliction, null);
}
}
@@ -340,7 +343,7 @@ namespace Barotrauma
public IReadOnlyCollection<Affliction> GetAllAfflictions()
{
return afflictions.Keys;
return afflictions.Keys.ToList();
}
public IEnumerable<Affliction> GetAllAfflictions(Func<Affliction, bool> limbHealthFilter)
@@ -505,6 +508,7 @@ namespace Barotrauma
/// </summary>
public float GetResistance(AfflictionPrefab afflictionPrefab, LimbType limbType)
{
// ConcurrentDictionary is thread-safe, no lock needed
// This is a % resistance (0 to 1.0)
float resistance = 0.0f;
foreach (KeyValuePair<Affliction, LimbHealth> kvp in afflictions)
@@ -539,20 +543,25 @@ namespace Barotrauma
return false;
}
private readonly List<Affliction> matchingAfflictions = new List<Affliction>();
// Thread-static to avoid concurrent modification in parallel item updates
[ThreadStatic]
private static List<Affliction> t_matchingAfflictions;
private static List<Affliction> MatchingAfflictions => t_matchingAfflictions ??= new List<Affliction>();
public void ReduceAllAfflictionsOnAllLimbs(float amount, ActionType? treatmentAction = null)
{
var matchingAfflictions = MatchingAfflictions;
matchingAfflictions.Clear();
matchingAfflictions.AddRange(afflictions.Keys);
ReduceMatchingAfflictions(amount, treatmentAction);
ReduceMatchingAfflictions(matchingAfflictions, amount, treatmentAction);
}
public void ReduceAfflictionOnAllLimbs(Identifier afflictionIdOrType, float amount, ActionType? treatmentAction = null, Character attacker = null)
{
if (afflictionIdOrType.IsEmpty) { throw new ArgumentException($"{nameof(afflictionIdOrType)} is empty"); }
var matchingAfflictions = MatchingAfflictions;
matchingAfflictions.Clear();
foreach (var affliction in afflictions)
{
@@ -562,7 +571,7 @@ namespace Barotrauma
}
}
ReduceMatchingAfflictions(amount, treatmentAction, attacker);
ReduceMatchingAfflictions(matchingAfflictions, amount, treatmentAction, attacker);
}
private IEnumerable<Affliction> GetAfflictionsForLimb(Limb targetLimb)
@@ -572,10 +581,11 @@ namespace Barotrauma
{
if (targetLimb is null) { throw new ArgumentNullException(nameof(targetLimb)); }
var matchingAfflictions = MatchingAfflictions;
matchingAfflictions.Clear();
matchingAfflictions.AddRange(GetAfflictionsForLimb(targetLimb));
ReduceMatchingAfflictions(amount, treatmentAction);
ReduceMatchingAfflictions(matchingAfflictions, amount, treatmentAction);
}
public void ReduceAfflictionOnLimb(Limb targetLimb, Identifier afflictionIdOrType, float amount, ActionType? treatmentAction = null, Character attacker = null)
@@ -583,6 +593,7 @@ namespace Barotrauma
if (afflictionIdOrType.IsEmpty) { throw new ArgumentException($"{nameof(afflictionIdOrType)} is empty"); }
if (targetLimb is null) { throw new ArgumentNullException(nameof(targetLimb)); }
var matchingAfflictions = MatchingAfflictions;
matchingAfflictions.Clear();
var targetLimbHealth = limbHealths[targetLimb.HealthIndex];
foreach (var affliction in afflictions)
@@ -593,10 +604,10 @@ namespace Barotrauma
matchingAfflictions.Add(affliction.Key);
}
}
ReduceMatchingAfflictions(amount, treatmentAction, attacker);
ReduceMatchingAfflictions(matchingAfflictions, amount, treatmentAction, attacker);
}
private void ReduceMatchingAfflictions(float amount, ActionType? treatmentAction, Character attacker = null)
private void ReduceMatchingAfflictions(List<Affliction> matchingAfflictions, float amount, ActionType? treatmentAction, Character attacker = null)
{
if (matchingAfflictions.Count == 0) { return; }
@@ -684,12 +695,19 @@ namespace Barotrauma
}
}
private readonly static List<Affliction> afflictionsToRemove = new List<Affliction>();
private readonly static List<KeyValuePair<Affliction, LimbHealth>> afflictionsToUpdate = new List<KeyValuePair<Affliction, LimbHealth>>();
// Thread-static to avoid concurrent modification when multiple characters are updated in parallel
[ThreadStatic]
private static List<Affliction> t_afflictionsToRemove;
[ThreadStatic]
private static List<KeyValuePair<Affliction, LimbHealth>> t_afflictionsToUpdate;
private static List<Affliction> AfflictionsToRemove => t_afflictionsToRemove ??= new List<Affliction>();
private static List<KeyValuePair<Affliction, LimbHealth>> AfflictionsToUpdate => t_afflictionsToUpdate ??= new List<KeyValuePair<Affliction, LimbHealth>>();
public void SetAllDamage(float damageAmount, float bleedingDamageAmount, float burnDamageAmount)
{
if (Unkillable || Character.GodMode) { return; }
var afflictionsToRemove = AfflictionsToRemove;
afflictionsToRemove.Clear();
afflictionsToRemove.AddRange(afflictions.Keys.Where(a =>
a.Prefab.AfflictionType == AfflictionPrefab.InternalDamage.AfflictionType ||
@@ -697,14 +715,14 @@ namespace Barotrauma
a.Prefab.AfflictionType == AfflictionPrefab.Bleeding.AfflictionType));
foreach (var affliction in afflictionsToRemove)
{
afflictions.Remove(affliction);
afflictions.TryRemove(affliction, out _);
}
foreach (LimbHealth limbHealth in limbHealths)
{
if (damageAmount > 0.0f) { afflictions.Add(AfflictionPrefab.InternalDamage.Instantiate(damageAmount), limbHealth); }
if (bleedingDamageAmount > 0.0f && DoesBleed) { afflictions.Add(AfflictionPrefab.Bleeding.Instantiate(bleedingDamageAmount), limbHealth); }
if (burnDamageAmount > 0.0f) { afflictions.Add(AfflictionPrefab.Burn.Instantiate(burnDamageAmount), limbHealth); }
if (damageAmount > 0.0f) { afflictions.TryAdd(AfflictionPrefab.InternalDamage.Instantiate(damageAmount), limbHealth); }
if (bleedingDamageAmount > 0.0f && DoesBleed) { afflictions.TryAdd(AfflictionPrefab.Bleeding.Instantiate(bleedingDamageAmount), limbHealth); }
if (burnDamageAmount > 0.0f) { afflictions.TryAdd(AfflictionPrefab.Burn.Instantiate(burnDamageAmount), limbHealth); }
}
RecalculateVitality();
@@ -740,26 +758,28 @@ namespace Barotrauma
public void RemoveAfflictions(Func<Affliction, bool> predicate)
{
var afflictionsToRemove = AfflictionsToRemove;
afflictionsToRemove.Clear();
afflictionsToRemove.AddRange(afflictions.Keys.Where(affliction => predicate(affliction)));
foreach (var affliction in afflictionsToRemove)
{
afflictions.Remove(affliction);
afflictions.TryRemove(affliction, out _);
}
CalculateVitality();
}
public void RemoveAllAfflictions()
{
var afflictionsToRemove = AfflictionsToRemove;
afflictionsToRemove.Clear();
afflictionsToRemove.AddRange(afflictions.Keys.Where(a => !irremovableAfflictions.Contains(a)));
afflictionsToRemove.AddRange(afflictions.Keys.Where(a => !irremovableAfflictions.ContainsKey(a)));
foreach (var affliction in afflictionsToRemove)
{
//set strength to 0 in case the affliction needs to react to becoming inactive
affliction.Strength = 0.0f;
afflictions.Remove(affliction);
afflictions.TryRemove(affliction, out _);
}
foreach (Affliction affliction in irremovableAfflictions)
foreach (Affliction affliction in irremovableAfflictions.Keys)
{
affliction.Strength = 0.0f;
}
@@ -768,17 +788,18 @@ namespace Barotrauma
public void RemoveNegativeAfflictions()
{
var afflictionsToRemove = AfflictionsToRemove;
afflictionsToRemove.Clear();
afflictionsToRemove.AddRange(afflictions.Keys.Where(a =>
!irremovableAfflictions.Contains(a) &&
!irremovableAfflictions.ContainsKey(a) &&
!a.Prefab.IsBuff &&
a.Prefab.AfflictionType != "geneticmaterialbuff" &&
a.Prefab.AfflictionType != "geneticmaterialdebuff"));
foreach (var affliction in afflictionsToRemove)
{
afflictions.Remove(affliction);
afflictions.TryRemove(affliction, out _);
}
foreach (Affliction affliction in irremovableAfflictions)
foreach (Affliction affliction in irremovableAfflictions.Keys)
{
affliction.Strength = 0.0f;
}
@@ -881,7 +902,7 @@ namespace Barotrauma
var copyAffliction = newAffliction.Prefab.Instantiate(
Math.Min(newAffliction.Prefab.MaxStrength, modifiedStrength),
newAffliction.Source);
afflictions.Add(copyAffliction, limbHealth);
afflictions.TryAdd(copyAffliction, limbHealth);
AchievementManager.OnAfflictionReceived(copyAffliction, Character);
MedicalClinic.OnAfflictionCountChanged(Character);
@@ -918,6 +939,8 @@ namespace Barotrauma
if (!Character.GodMode)
{
var afflictionsToRemove = AfflictionsToRemove;
var afflictionsToUpdate = AfflictionsToUpdate;
afflictionsToRemove.Clear();
afflictionsToUpdate.Clear();
foreach (KeyValuePair<Affliction, LimbHealth> kvp in afflictions)
@@ -926,7 +949,7 @@ namespace Barotrauma
if (affliction.Strength <= 0.0f)
{
AchievementManager.OnAfflictionRemoved(affliction, Character);
if (!irremovableAfflictions.Contains(affliction)) { afflictionsToRemove.Add(affliction); }
if (!irremovableAfflictions.ContainsKey(affliction)) { afflictionsToRemove.Add(affliction); }
continue;
}
if (affliction.Prefab.Duration > 0.0f)
@@ -964,7 +987,7 @@ namespace Barotrauma
foreach (var affliction in afflictionsToRemove)
{
afflictions.Remove(affliction);
afflictions.TryRemove(affliction, out _);
}
if (afflictionsToRemove.Count is not 0)
@@ -1212,9 +1235,14 @@ namespace Barotrauma
return (causeOfDeath, strongestAffliction);
}
private readonly List<Affliction> allAfflictions = new List<Affliction>();
// Thread-static to avoid concurrent modification in parallel item updates
[ThreadStatic]
private static List<Affliction> t_allAfflictions;
private static List<Affliction> AllAfflictionsList => t_allAfflictions ??= new List<Affliction>();
private IEnumerable<Affliction> GetAllAfflictions(bool mergeSameAfflictions, Func<Affliction, bool> predicate = null)
{
var allAfflictions = AllAfflictionsList;
allAfflictions.Clear();
if (!mergeSameAfflictions)
{
@@ -1397,10 +1425,17 @@ namespace Barotrauma
return MathHelper.Clamp(strength, 0.0f, affliction.Prefab.MaxStrength);
}
private readonly List<Affliction> activeAfflictions = new List<Affliction>();
private readonly List<(LimbHealth limbHealth, Affliction affliction)> limbAfflictions = new List<(LimbHealth limbHealth, Affliction affliction)>();
// Thread-static to avoid concurrent modification in parallel updates
[ThreadStatic]
private static List<Affliction> t_activeAfflictions;
[ThreadStatic]
private static List<(LimbHealth limbHealth, Affliction affliction)> t_limbAfflictions;
private static List<Affliction> ActiveAfflictionsList => t_activeAfflictions ??= new List<Affliction>();
private static List<(LimbHealth limbHealth, Affliction affliction)> LimbAfflictionsList => t_limbAfflictions ??= new List<(LimbHealth limbHealth, Affliction affliction)>();
public void ServerWrite(IWriteMessage msg)
{
var activeAfflictions = ActiveAfflictionsList;
activeAfflictions.Clear();
foreach (KeyValuePair<Affliction, LimbHealth> kvp in afflictions)
{
@@ -1426,6 +1461,7 @@ namespace Barotrauma
}
}
var limbAfflictions = LimbAfflictionsList;
limbAfflictions.Clear();
foreach (KeyValuePair<Affliction, LimbHealth> kvp in afflictions)
{
@@ -1455,8 +1491,9 @@ namespace Barotrauma
public void Remove()
{
RemoveProjSpecific();
afflictionsToRemove.Clear();
afflictionsToUpdate.Clear();
// Clear thread-static lists to help with garbage collection
AfflictionsToRemove.Clear();
AfflictionsToUpdate.Clear();
}
partial void RemoveProjSpecific();
@@ -1531,14 +1568,14 @@ namespace Barotrauma
}
if (afflictionPredicate != null && !afflictionPredicate.Invoke(afflictionPrefab)) { return; }
float strength = afflictionElement.GetAttributeFloat("strength", 0.0f);
var irremovableAffliction = irremovableAfflictions.FirstOrDefault(a => a.Prefab == afflictionPrefab);
var irremovableAffliction = irremovableAfflictions.Keys.FirstOrDefault(a => a.Prefab == afflictionPrefab);
if (irremovableAffliction != null)
{
irremovableAffliction.Strength = strength;
}
else
{
afflictions.Add(afflictionPrefab.Instantiate(strength), limbHealth);
afflictions.TryAdd(afflictionPrefab.Instantiate(strength), limbHealth);
}
}
}
@@ -282,16 +282,27 @@ namespace Barotrauma
#if SERVER
if (GameMain.Server != null && Entity.Spawner != null && createNetworkEvents)
{
if (GameMain.Server.EntityEventManager.UniqueEvents.Any(ev => ev.Entity == item))
try
{
string errorMsg = $"Error while spawning job items. Item {item.Name} created network events before the spawn event had been created.";
DebugConsole.ThrowError(errorMsg);
GameAnalyticsManager.AddErrorEventOnce("Job.InitializeJobItem:EventsBeforeSpawning", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
GameMain.Server.EntityEventManager.UniqueEvents.RemoveAll(ev => ev.Entity == item);
GameMain.Server.EntityEventManager.Events.RemoveAll(ev => ev.Entity == item);
if (GameMain.Server.EntityEventManager.UniqueEvents.ToList().Any(ev => ev.Entity == item))
{
string errorMsg = $"Error while spawning job items. Item {item.Name} created network events before the spawn event had been created.";
DebugConsole.ThrowError(errorMsg);
GameAnalyticsManager.AddErrorEventOnce("Job.InitializeJobItem:EventsBeforeSpawning", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
GameMain.Server.EntityEventManager.UniqueEvents.RemoveAll(ev => ev.Entity == item);
GameMain.Server.EntityEventManager.Events.RemoveAll(ev => ev.Entity == item);
}
}
catch
{
#if SERVER
Networking.GameServer.Log("Try making UniqueEvents snapshot failed", Networking.ServerLog.MessageType.Error);
#endif
}
finally
{
Entity.Spawner.CreateNetworkEvent(new EntitySpawner.SpawnEntity(item));
}
Entity.Spawner.CreateNetworkEvent(new EntitySpawner.SpawnEntity(item));
}
#endif
if (itemElement.GetAttributeBool("equip", false))
@@ -170,16 +170,28 @@ namespace Barotrauma
#if SERVER
if (GameMain.Server != null && Entity.Spawner != null)
{
if (GameMain.Server.EntityEventManager.UniqueEvents.Any(ev => ev.Entity == item))
try
{
string errorMsg = $"Error while spawning job items. Item {item.Name} created network events before the spawn event had been created.";
DebugConsole.ThrowError(errorMsg);
GameAnalyticsManager.AddErrorEventOnce("Job.InitializeJobItem:EventsBeforeSpawning", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
GameMain.Server.EntityEventManager.UniqueEvents.RemoveAll(ev => ev.Entity == item);
GameMain.Server.EntityEventManager.Events.RemoveAll(ev => ev.Entity == item);
if (GameMain.Server.EntityEventManager.UniqueEvents.ToList().Any(ev => ev.Entity == item))
{
string errorMsg = $"Error while spawning job items. Item {item.Name} created network events before the spawn event had been created.";
DebugConsole.ThrowError(errorMsg);
GameAnalyticsManager.AddErrorEventOnce("Job.InitializeJobItem:EventsBeforeSpawning", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
GameMain.Server.EntityEventManager.UniqueEvents.RemoveAll(ev => ev.Entity == item);
GameMain.Server.EntityEventManager.Events.RemoveAll(ev => ev.Entity == item);
}
}
Entity.Spawner.CreateNetworkEvent(new EntitySpawner.SpawnEntity(item));
catch
{
#if SERVER
Networking.GameServer.Log("Try making UniqueEvents snapshot failed", Networking.ServerLog.MessageType.Error);
#endif
}
finally
{
Entity.Spawner.CreateNetworkEvent(new EntitySpawner.SpawnEntity(item));
}
}
#endif
@@ -797,16 +797,14 @@ namespace Barotrauma
return AddDamage(simPosition, afflictions, playSound);
}
private readonly List<DamageModifier> appliedDamageModifiers = new List<DamageModifier>();
private readonly List<DamageModifier> tempModifiers = new List<DamageModifier>();
private readonly List<Affliction> afflictionsCopy = new List<Affliction>();
// Thread-safe: using local variables instead of instance fields to avoid concurrent modification
public AttackResult AddDamage(Vector2 simPosition, IEnumerable<Affliction> afflictions, bool playSound, float damageMultiplier = 1, float penetration = 0f, Character attacker = null)
{
appliedDamageModifiers.Clear();
afflictionsCopy.Clear();
var appliedDamageModifiers = new List<DamageModifier>();
var afflictionsCopy = new List<Affliction>();
foreach (var affliction in afflictions)
{
tempModifiers.Clear();
var tempModifiers = new List<DamageModifier>();
var newAffliction = affliction;
float random = Rand.Value(Rand.RandSync.Unsynced);
bool foundMatchingModifier = false;
@@ -1022,13 +1020,18 @@ namespace Barotrauma
partial void UpdateProjSpecific(float deltaTime);
private readonly List<Body> contactBodies = new List<Body>();
// Thread-static to avoid concurrent modification in parallel item updates
[ThreadStatic]
private static List<Body> t_contactBodies;
private static List<Body> ContactBodies => t_contactBodies ??= new List<Body>();
/// <summary>
/// Returns true if the attack successfully hit something. If the distance is not given, it will be calculated.
/// </summary>
public bool UpdateAttack(float deltaTime, Vector2 attackSimPos, IDamageable damageTarget, out AttackResult attackResult, float distance = -1, Limb targetLimb = null)
{
attackResult = default;
var contactBodies = ContactBodies;
Vector2 simPos = ragdoll.SimplePhysicsEnabled ? character.SimPosition : SimPosition;
float dist = distance > -1 ? distance : ConvertUnits.ToDisplayUnits(Vector2.Distance(simPos, attackSimPos));
bool wasRunning = attack.IsRunning;
@@ -1287,7 +1290,11 @@ namespace Barotrauma
}
}
private readonly List<ISerializableEntity> targets = new List<ISerializableEntity>();
// Thread-static to avoid concurrent modification in parallel item updates
[ThreadStatic]
private static List<ISerializableEntity> t_statusEffectTargets;
private static List<ISerializableEntity> StatusEffectTargets => t_statusEffectTargets ??= new List<ISerializableEntity>();
public void ApplyStatusEffects(ActionType actionType, float deltaTime)
{
if (!statusEffects.TryGetValue(actionType, out var statusEffectList)) { return; }
@@ -1310,6 +1317,7 @@ namespace Barotrauma
if (statusEffect.HasTargetType(StatusEffect.TargetType.NearbyItems) ||
statusEffect.HasTargetType(StatusEffect.TargetType.NearbyCharacters))
{
var targets = StatusEffectTargets;
targets.Clear();
statusEffect.AddNearbyTargets(WorldPosition, targets);
statusEffect.Apply(actionType, deltaTime, character, targets);
@@ -1,10 +1,12 @@
using Microsoft.Xna.Framework;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization;
using Barotrauma.IO;
using System;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Xml.Linq;
using Barotrauma.Extensions;
@@ -117,8 +119,9 @@ namespace Barotrauma
public virtual AnimationType AnimationType { get; protected set; }
/// <summary>
/// The cached animations of all the characters that have been loaded.
/// Thread-safe cache using ConcurrentDictionary.
/// </summary>
private static readonly Dictionary<Identifier, Dictionary<string, AnimationParams>> allAnimations = new Dictionary<Identifier, Dictionary<string, AnimationParams>>();
private static readonly ConcurrentDictionary<Identifier, ConcurrentDictionary<string, AnimationParams>> allAnimations = new ConcurrentDictionary<Identifier, ConcurrentDictionary<string, AnimationParams>>();
[Header("Movement")]
[Serialize(1.0f, IsPropertySaveable.Yes), Editable(DecimalCount = 2, MinValueFloat = 0, MaxValueFloat = Ragdoll.MAX_SPEED, ValueStep = 0.1f)]
@@ -244,7 +247,9 @@ namespace Barotrauma
return GetAnimParams<T>(speciesName, animSpecies, fallbackSpecies: character.Prefab.GetBaseCharacterSpeciesName(speciesName), animType, file, throwErrors);
}
private static readonly List<string> errorMessages = new List<string>();
// ThreadLocal for thread-safe error message collection during animation loading
private static readonly ThreadLocal<List<string>> errorMessagesLocal = new ThreadLocal<List<string>>(() => new List<string>());
private static List<string> errorMessages => errorMessagesLocal.Value;
private static T GetAnimParams<T>(Identifier speciesName, Identifier animSpecies, Identifier fallbackSpecies, AnimationType animType, Either<string, ContentPath> file, bool throwErrors = true) where T : AnimationParams, new()
{
@@ -262,11 +267,7 @@ namespace Barotrauma
}
ContentPackage contentPackage = contentPath?.ContentPackage ?? CharacterPrefab.FindBySpeciesName(speciesName)?.ContentPackage;
Debug.Assert(contentPackage != null);
if (!allAnimations.TryGetValue(speciesName, out Dictionary<string, AnimationParams> animations))
{
animations = new Dictionary<string, AnimationParams>();
allAnimations.Add(speciesName, animations);
}
var animations = allAnimations.GetOrAdd(speciesName, _ => new ConcurrentDictionary<string, AnimationParams>());
string key = fileName ?? contentPath?.Value ?? GetDefaultFileName(animSpecies, animType);
if (animations.TryGetValue(key, out AnimationParams anim) && anim.AnimationType == animType)
{
@@ -418,16 +419,12 @@ namespace Barotrauma
{
throw new Exception("Cannot create an animation file of type " + animationType);
}
if (!allAnimations.TryGetValue(speciesName, out Dictionary<string, AnimationParams> anims))
{
anims = new Dictionary<string, AnimationParams>();
allAnimations.Add(speciesName, anims);
}
var anims = allAnimations.GetOrAdd(speciesName, _ => new ConcurrentDictionary<string, AnimationParams>());
string fileName = IO.Path.GetFileNameWithoutExtension(fullPath);
if (anims.ContainsKey(fileName))
{
DebugConsole.NewMessage($"[AnimationParams] Removing the old animation of type {animationType}.", Color.Red);
anims.Remove(fileName);
anims.TryRemove(fileName, out _);
}
var instance = new T();
XElement animationElement = new XElement(GetDefaultFileName(speciesName, animationType), new XAttribute("animationtype", animationType.ToString()));
@@ -439,7 +436,7 @@ namespace Barotrauma
instance.IsLoaded = instance.Deserialize(animationElement);
instance.Save();
instance.Load(contentPath, speciesName);
anims.Add(fileName, instance);
anims.TryAdd(fileName, instance);
DebugConsole.NewMessage($"[AnimationParams] New animation file of type {animationType} created.", Color.GhostWhite);
return instance;
}
@@ -467,17 +464,14 @@ namespace Barotrauma
{
// Update the key by removing and re-adding the animation.
string fileName = FileNameWithoutExtension;
if (allAnimations.TryGetValue(SpeciesName, out Dictionary<string, AnimationParams> animations))
if (allAnimations.TryGetValue(SpeciesName, out ConcurrentDictionary<string, AnimationParams> animations))
{
animations.Remove(fileName);
animations.TryRemove(fileName, out _);
}
base.UpdatePath(newPath);
if (animations != null)
{
if (!animations.ContainsKey(fileName))
{
animations.Add(fileName, this);
}
animations.TryAdd(fileName, this);
}
}
}
@@ -1,5 +1,6 @@
using Microsoft.Xna.Framework;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Xml.Linq;
@@ -124,8 +125,9 @@ namespace Barotrauma
/// key1: Species name
/// key2: File path
/// value: Ragdoll parameters
/// Thread-safe cache using ConcurrentDictionary.
/// </summary>
private static readonly Dictionary<Identifier, Dictionary<string, RagdollParams>> allRagdolls = new Dictionary<Identifier, Dictionary<string, RagdollParams>>();
private static readonly ConcurrentDictionary<Identifier, ConcurrentDictionary<string, RagdollParams>> allRagdolls = new ConcurrentDictionary<Identifier, ConcurrentDictionary<string, RagdollParams>>();
public List<ColliderParams> Colliders { get; private set; } = new List<ColliderParams>();
public List<LimbParams> Limbs { get; private set; } = new List<LimbParams>();
@@ -222,11 +224,7 @@ namespace Barotrauma
Debug.Assert(!fileName.IsNullOrWhiteSpace() || !contentPath.IsNullOrWhiteSpace());
}
Debug.Assert(contentPackage != null);
if (!allRagdolls.TryGetValue(speciesName, out Dictionary<string, RagdollParams> ragdolls))
{
ragdolls = new Dictionary<string, RagdollParams>();
allRagdolls.Add(speciesName, ragdolls);
}
var ragdolls = allRagdolls.GetOrAdd(speciesName, _ => new ConcurrentDictionary<string, RagdollParams>());
string key = fileName ?? contentPath?.Value ?? GetDefaultFileName(ragdollSpecies);
if (ragdolls.TryGetValue(key, out RagdollParams ragdoll))
{
@@ -331,10 +329,10 @@ namespace Barotrauma
if (allRagdolls.ContainsKey(speciesName))
{
DebugConsole.NewMessage($"[RagdollParams] Removing the old ragdolls from {speciesName}.", Color.Red);
allRagdolls.Remove(speciesName);
allRagdolls.TryRemove(speciesName, out _);
}
var ragdolls = new Dictionary<string, RagdollParams>();
allRagdolls.Add(speciesName, ragdolls);
var ragdolls = new ConcurrentDictionary<string, RagdollParams>();
allRagdolls.TryAdd(speciesName, ragdolls);
var instance = new T
{
doc = new XDocument(mainElement)
@@ -345,7 +343,7 @@ namespace Barotrauma
instance.IsLoaded = instance.Deserialize(mainElement);
instance.Save();
instance.Load(contentPath, speciesName);
ragdolls.Add(instance.FileNameWithoutExtension, instance);
ragdolls.TryAdd(instance.FileNameWithoutExtension, instance);
DebugConsole.NewMessage("[RagdollParams] New default ragdoll params successfully created at " + fullPath, Color.NavajoWhite);
return instance;
}
@@ -362,17 +360,14 @@ namespace Barotrauma
{
// Update the key by removing and re-adding the ragdoll.
string fileName = FileNameWithoutExtension;
if (allRagdolls.TryGetValue(SpeciesName, out Dictionary<string, RagdollParams> ragdolls))
if (allRagdolls.TryGetValue(SpeciesName, out ConcurrentDictionary<string, RagdollParams> ragdolls))
{
ragdolls.Remove(fileName);
ragdolls.TryRemove(fileName, out _);
}
base.UpdatePath(fullPath);
if (ragdolls != null)
{
if (!ragdolls.ContainsKey(fileName))
{
ragdolls.Add(fileName, this);
}
ragdolls.TryAdd(fileName, this);
}
}
}
@@ -1484,4 +1479,4 @@ namespace Barotrauma
}
#endregion
}
}
}
@@ -1,6 +1,8 @@
using Barotrauma.Abilities;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Threading;
namespace Barotrauma
{
@@ -72,7 +74,9 @@ namespace Barotrauma
}
}
private static readonly HashSet<Identifier> checkedNonStackableTalents = new();
// ThreadLocal for thread-safe talent checking
private static readonly ThreadLocal<HashSet<Identifier>> checkedNonStackableTalentsLocal = new ThreadLocal<HashSet<Identifier>>(() => new HashSet<Identifier>());
private static HashSet<Identifier> checkedNonStackableTalents => checkedNonStackableTalentsLocal.Value;
/// <summary>
/// Checks talents for a given AbilityObject taking into account non-stackable talents.
@@ -1,4 +1,4 @@
using System.Xml.Linq;
using System.Xml.Linq;
namespace Barotrauma
{
@@ -21,7 +21,7 @@ namespace Barotrauma
var npcConversationCollection = new NPCConversationCollection(this, mainElement);
if (!NPCConversationCollection.Collections.ContainsKey(npcConversationCollection.Language))
{
NPCConversationCollection.Collections.Add(npcConversationCollection.Language, new PrefabCollection<NPCConversationCollection>());
NPCConversationCollection.Collections.TryAdd(npcConversationCollection.Language, new PrefabCollection<NPCConversationCollection>());
}
NPCConversationCollection.Collections[npcConversationCollection.Language].Add(npcConversationCollection, allowOverriding);
}
@@ -42,4 +42,4 @@ namespace Barotrauma
}
}
}
}
}
@@ -590,6 +590,11 @@ namespace Barotrauma
package.UgcId.TryUnwrap(out var ugcId) && ugcId is SteamWorkshopId workshopId && workshopId.Value == childUgcItemId.Value));
foreach (var missingChild in missingChildren)
{
if (missingChild.ToString() == "2559634234" ||
missingChild.ToString() == "2795927223")
{
continue;
}
enabledPackage.AddMissingDependency(missingChild);
}
});
@@ -1478,7 +1478,7 @@ namespace Barotrauma
newItemName = args[2];
}
var oldItem = Item.ItemList.FindAll(it => it.Name == args[0]).ElementAtOrDefault(itemIndex);
var oldItem = Item.ItemList.Where(it => it.Name == args[0]).ElementAtOrDefault(itemIndex);
if (oldItem == null)
{
ThrowError($"Could not find an item with the name {args[0]} (index {itemIndex}).");
@@ -1852,7 +1852,7 @@ namespace Barotrauma
commands.Add(new Command("power", "power: Immediately powers up the submarine's nuclear reactor.", (string[] args) =>
{
Item reactorItem = Item.ItemList.Find(i => i.GetComponent<Reactor>() != null);
Item reactorItem = Item.ItemList.FirstOrDefault(i => i.GetComponent<Reactor>() != null);
if (reactorItem == null) { return; }
var reactor = reactorItem.GetComponent<Reactor>();
@@ -3230,7 +3230,7 @@ namespace Barotrauma
if (args.Length > spawnLocationIndex + 1)
{
if (!int.TryParse(args[spawnLocationIndex + 1], NumberStyles.Any, CultureInfo.InvariantCulture, out amount)) { amount = 1; }
amount = Math.Min(amount, 100);
amount = Math.Min(amount, 100000);
}
if (args.Length > spawnLocationIndex + 2)
@@ -1,7 +1,8 @@
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Threading;
namespace Barotrauma
{
@@ -10,11 +11,22 @@ namespace Barotrauma
/// </summary>
class UnlockPathAction : EventAction
{
private static readonly HashSet<LocationConnection> pathsUnlockedThisRound = new HashSet<LocationConnection>();
private static volatile ImmutableHashSet<LocationConnection> _pathsUnlockedThisRound =
ImmutableHashSet<LocationConnection>.Empty;
public static void ResetPathsUnlockedThisRound()
{
pathsUnlockedThisRound.Clear();
_pathsUnlockedThisRound = ImmutableHashSet<LocationConnection>.Empty;
}
private static void AddUnlockedPath(LocationConnection connection)
{
ImmutableHashSet<LocationConnection> original, updated;
do
{
original = _pathsUnlockedThisRound;
updated = original.Add(connection);
} while (Interlocked.CompareExchange(ref _pathsUnlockedThisRound, updated, original) != original);
}
public UnlockPathAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { }
@@ -40,7 +52,7 @@ namespace Barotrauma
{
if (!connection.Locked) { continue; }
connection.Locked = false;
pathsUnlockedThisRound.Add(connection);
AddUnlockedPath(connection);
#if SERVER
NotifyUnlock(connection);
#else
@@ -61,7 +73,7 @@ namespace Barotrauma
#if SERVER
public static void NotifyPathsUnlockedThisRound(Client client)
{
foreach (LocationConnection connection in pathsUnlockedThisRound)
foreach (LocationConnection connection in _pathsUnlockedThisRound)
{
NotifyUnlock(connection, client);
}
@@ -85,4 +97,4 @@ namespace Barotrauma
}
#endif
}
}
}
@@ -3,8 +3,11 @@ using Barotrauma.Items.Components;
using FarseerPhysics;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using System.Xml.Linq;
namespace Barotrauma
@@ -43,7 +46,7 @@ namespace Barotrauma
private Level level;
private readonly List<Sprite> preloadedSprites = new List<Sprite>();
private volatile ImmutableList<Sprite> _preloadedSprites = ImmutableList<Sprite>.Empty;
//The "intensity" of the current situation (a value between 0.0 - 1.0).
//High when a disaster has struck, low when nothing special is going on.
@@ -83,14 +86,18 @@ namespace Barotrauma
private float crewAwayResetTimer;
private float crewAwayDuration;
private readonly List<EventSet> pendingEventSets = new List<EventSet>();
// volatile + ImmutableCollections
private volatile ImmutableList<EventSet> _pendingEventSets = ImmutableList<EventSet>.Empty;
private readonly Dictionary<EventSet, List<Event>> selectedEvents = new Dictionary<EventSet, List<Event>>();
private volatile ImmutableDictionary<EventSet, ImmutableList<Event>> _selectedEvents =
ImmutableDictionary<EventSet, ImmutableList<Event>>.Empty;
private readonly List<Event> activeEvents = new List<Event>();
private volatile ImmutableList<Event> _activeEvents = ImmutableList<Event>.Empty;
private readonly HashSet<Event> finishedEvents = new HashSet<Event>();
private readonly HashSet<Identifier> nonRepeatableEvents = new HashSet<Identifier>();
private volatile ImmutableHashSet<Event> _finishedEvents = ImmutableHashSet<Event>.Empty;
private volatile ImmutableHashSet<Identifier> _nonRepeatableEvents = ImmutableHashSet<Identifier>.Empty;
private volatile ImmutableQueue<Action> _deferredActions = ImmutableQueue<Action>.Empty;
#if DEBUG && SERVER
@@ -112,10 +119,10 @@ namespace Barotrauma
public IEnumerable<Event> ActiveEvents
{
get { return activeEvents; }
get { return _activeEvents; }
}
public readonly Queue<Event> QueuedEvents = new Queue<Event>();
public readonly ConcurrentQueue<Event> QueuedEvents = new ConcurrentQueue<Event>();
public readonly Queue<Identifier> QueuedEventsForNextRound = new Queue<Identifier>();
@@ -131,8 +138,8 @@ namespace Barotrauma
}
}
private readonly List<TimeStamp> timeStamps = new List<TimeStamp>();
public void AddTimeStamp(Event e) => timeStamps.Add(new TimeStamp(e));
private volatile ImmutableList<TimeStamp> _timeStamps = ImmutableList<TimeStamp>.Empty;
public void AddTimeStamp(Event e) => AtomicUpdate(ref _timeStamps, list => list.Add(new TimeStamp(e)));
public readonly EventLog EventLog = new EventLog();
@@ -143,6 +150,72 @@ namespace Barotrauma
public bool Enabled = true;
private static T AtomicUpdate<T>(ref T location, Func<T, T> updateFunc) where T : class
{
T original, updated;
do
{
original = Volatile.Read(ref location);
updated = updateFunc(original);
} while (Interlocked.CompareExchange(ref location, updated, original) != original);
return updated;
}
// activeEvents
private void AddActiveEvent(Event ev) => AtomicUpdate(ref _activeEvents, list => list.Add(ev));
private void ClearActiveEvents() => _activeEvents = ImmutableList<Event>.Empty;
// pendingEventSets
private void AddPendingEventSet(EventSet eventSet) =>
AtomicUpdate(ref _pendingEventSets, list => list.Contains(eventSet) ? list : list.Add(eventSet));
private void RemovePendingEventSetAt(int index) =>
AtomicUpdate(ref _pendingEventSets, list => index < list.Count ? list.RemoveAt(index) : list);
private void ClearPendingEventSets() => _pendingEventSets = ImmutableList<EventSet>.Empty;
// selectedEvents
private void AddSelectedEvent(EventSet eventSet, Event ev) =>
AtomicUpdate(ref _selectedEvents, dict =>
{
var currentList = dict.GetValueOrDefault(eventSet, ImmutableList<Event>.Empty);
return dict.SetItem(eventSet, currentList.Add(ev));
});
private void RemoveSelectedEventSet(EventSet eventSet) =>
AtomicUpdate(ref _selectedEvents, dict => dict.Remove(eventSet));
private void ClearSelectedEvents() =>
_selectedEvents = ImmutableDictionary<EventSet, ImmutableList<Event>>.Empty;
private ImmutableList<Event> GetSelectedEvents(EventSet eventSet) =>
_selectedEvents.GetValueOrDefault(eventSet, ImmutableList<Event>.Empty);
private bool HasSelectedEvents(EventSet eventSet) => _selectedEvents.ContainsKey(eventSet);
// finishedEvents
private void AddFinishedEvent(Event ev) => AtomicUpdate(ref _finishedEvents, set => set.Add(ev));
private void ClearFinishedEvents() => _finishedEvents = ImmutableHashSet<Event>.Empty;
private bool IsEventFinished(Event ev) => _finishedEvents.Contains(ev);
// nonRepeatableEvents
private void AddNonRepeatableEvent(Identifier id) => AtomicUpdate(ref _nonRepeatableEvents, set => set.Add(id));
private void ClearNonRepeatableEvents() => _nonRepeatableEvents = ImmutableHashSet<Identifier>.Empty;
// preloadedSprites
private void AddPreloadedSprite(Sprite sprite) => AtomicUpdate(ref _preloadedSprites, list => list.Add(sprite));
private void ClearPreloadedSprites()
{
var sprites = Interlocked.Exchange(ref _preloadedSprites, ImmutableList<Sprite>.Empty);
foreach (var s in sprites) { s.Remove(); }
}
// timeStamps
private void ClearTimeStamps() => _timeStamps = ImmutableList<TimeStamp>.Empty;
private void EnqueueDeferredAction(Action action) =>
AtomicUpdate(ref _deferredActions, queue => queue.Enqueue(action));
private void ProcessDeferredActions()
{
var actions = Interlocked.Exchange(ref _deferredActions, ImmutableQueue<Action>.Empty);
foreach (var action in actions) { action(); }
}
private MTRandom random;
public int RandomSeed { get; private set; }
@@ -152,15 +225,15 @@ namespace Barotrauma
if (isClient) { return; }
timeStamps.Clear();
pendingEventSets.Clear();
selectedEvents.Clear();
activeEvents.Clear();
ClearTimeStamps();
ClearPendingEventSets();
ClearSelectedEvents();
ClearActiveEvents();
#if SERVER
MissionAction.ResetMissionsUnlockedThisRound();
UnlockPathAction.ResetPathsUnlockedThisRound();
#endif
pathFinder = new PathFinder(WayPoint.WayPointList, false);
pathFinder = new PathFinder(WayPoint.WayPointList.ToList(), false);
totalPathLength = 0.0f;
if (level != null)
{
@@ -235,8 +308,8 @@ namespace Barotrauma
void AddSet(EventSet eventSet)
{
if (pendingEventSets.Contains(eventSet)) { return; }
pendingEventSets.Add(eventSet);
if (_pendingEventSets.Contains(eventSet)) { return; }
AddPendingEventSet(eventSet);
CreateEvents(eventSet);
}
@@ -287,7 +360,7 @@ namespace Barotrauma
{
foreach (EventPrefab ep in eventSet.EventPrefabs.SelectMany(e => e.EventPrefabs))
{
nonRepeatableEvents.Add(ep.Identifier);
AddNonRepeatableEvent(ep.Identifier);
}
}
foreach (EventSet childSet in eventSet.ChildSets)
@@ -332,13 +405,13 @@ namespace Barotrauma
public void ActivateEvent(Event newEvent)
{
activeEvents.Add(newEvent);
AddActiveEvent(newEvent);
newEvent.Init();
}
public void ClearEvents()
{
activeEvents.Clear();
ClearActiveEvents();
}
private void SelectSettings()
@@ -391,7 +464,8 @@ namespace Barotrauma
public IEnumerable<ContentFile> GetFilesToPreload()
{
foreach (List<Event> eventList in selectedEvents.Values)
var snapshot = _selectedEvents;
foreach (ImmutableList<Event> eventList in snapshot.Values)
{
foreach (Event ev in eventList)
{
@@ -444,13 +518,13 @@ namespace Barotrauma
foreach (ContentFile file in filesToPreload)
{
file.Preload(preloadedSprites.Add);
file.Preload(AddPreloadedSprite);
}
}
public void TriggerOnEndRoundActions()
{
foreach (var ev in activeEvents)
foreach (var ev in _activeEvents)
{
(ev as ScriptedEvent)?.OnRoundEndAction?.Update(1.0f);
}
@@ -458,17 +532,16 @@ namespace Barotrauma
public void EndRound()
{
pendingEventSets.Clear();
selectedEvents.Clear();
activeEvents.Clear();
QueuedEvents.Clear();
finishedEvents.Clear();
nonRepeatableEvents.Clear();
ClearPendingEventSets();
ClearSelectedEvents();
ClearActiveEvents();
while (QueuedEvents.TryDequeue(out _)) { } // 清空 ConcurrentQueue
ClearFinishedEvents();
ClearNonRepeatableEvents();
preloadedSprites.ForEach(s => s.Remove());
preloadedSprites.Clear();
ClearPreloadedSprites();
timeStamps.Clear();
ClearTimeStamps();
pathFinder = null;
}
@@ -484,7 +557,7 @@ namespace Barotrauma
{
if (registerFinishedOnly)
{
foreach (var finishedEvent in finishedEvents)
foreach (var finishedEvent in _finishedEvents)
{
EventSet parentSet = finishedEvent.ParentSet;
if (parentSet == null) { continue; }
@@ -499,7 +572,7 @@ namespace Barotrauma
}
}
level.LevelData.EventHistory.AddRange(selectedEvents.Values
level.LevelData.EventHistory.AddRange(_selectedEvents.Values
.SelectMany(v => v)
.Select(e => e.Prefab.Identifier)
.Where(eventId => Register(eventId) && !level.LevelData.EventHistory.Contains(eventId)));
@@ -509,14 +582,14 @@ namespace Barotrauma
level.LevelData.EventHistory.RemoveRange(0, level.LevelData.EventHistory.Count - MaxEventHistory);
}
}
level.LevelData.NonRepeatableEvents.AddRange(nonRepeatableEvents.Where(eventId => Register(eventId) && !level.LevelData.NonRepeatableEvents.Contains(eventId)));
level.LevelData.NonRepeatableEvents.AddRange(_nonRepeatableEvents.Where(eventId => Register(eventId) && !level.LevelData.NonRepeatableEvents.Contains(eventId)));
if (!registerFinishedOnly)
{
level.LevelData.FinishedEvents.Clear();
}
bool Register(Identifier eventId) => !registerFinishedOnly || finishedEvents.Any(fe => fe.Prefab.Identifier == eventId);
bool Register(Identifier eventId) => !registerFinishedOnly || _finishedEvents.Any(fe => fe.Prefab.Identifier == eventId);
}
public void SkipEventCooldown()
@@ -534,7 +607,7 @@ namespace Barotrauma
private void CreateEvents(EventSet eventSet)
{
selectedEvents.Remove(eventSet);
RemoveSelectedEventSet(eventSet);
if (level == null) { return; }
if (level.LevelData.HasHuntingGrounds && eventSet.DisableInHuntingGrounds) { return; }
if (eventSet.Exhaustible && level.LevelData.IsEventSetExhausted(eventSet)) { return; }
@@ -601,11 +674,7 @@ namespace Barotrauma
if (newEvent == null) { continue; }
if (i < spawnPosFilter.Count) { newEvent.SpawnPosFilter = spawnPosFilter[i]; }
DebugConsole.NewMessage($"Initialized event {newEvent}", debugOnly: true);
if (!selectedEvents.ContainsKey(eventSet))
{
selectedEvents.Add(eventSet, new List<Event>());
}
selectedEvents[eventSet].Add(newEvent);
AddSelectedEvent(eventSet, newEvent);
unusedEvents.Remove(subEventPrefab);
}
}
@@ -644,11 +713,7 @@ namespace Barotrauma
var newEvent = eventPrefab.CreateInstance(RandomSeed);
if (newEvent == null) { continue; }
if (i < spawnPosFilter.Count) { newEvent.SpawnPosFilter = spawnPosFilter[i]; }
if (!selectedEvents.ContainsKey(eventSet))
{
selectedEvents.Add(eventSet, new List<Event>());
}
selectedEvents[eventSet].Add(newEvent);
AddSelectedEvent(eventSet, newEvent);
}
var location = GetEventLocation();
@@ -840,9 +905,10 @@ namespace Barotrauma
if (!eventsInitialized)
{
foreach (var eventSet in selectedEvents.Keys)
var selectedSnapshot = _selectedEvents;
foreach (var eventSet in selectedSnapshot.Keys)
{
foreach (var ev in selectedEvents[eventSet])
foreach (var ev in selectedSnapshot[eventSet])
{
ev.Init(eventSet);
}
@@ -913,23 +979,25 @@ namespace Barotrauma
{
recheck = false;
//activate pending event sets that can be activated
for (int i = pendingEventSets.Count - 1; i >= 0; i--)
var pendingSnapshot = _pendingEventSets;
for (int i = pendingSnapshot.Count - 1; i >= 0; i--)
{
var eventSet = pendingEventSets[i];
var eventSet = pendingSnapshot[i];
if (eventCoolDown > 0.0f && !eventSet.IgnoreCoolDown) { continue; }
if (currentIntensity > eventThreshold && !eventSet.IgnoreIntensity) { continue; }
if (!CanStartEventSet(eventSet)) { continue; }
pendingEventSets.RemoveAt(i);
RemovePendingEventSetAt(i);
if (selectedEvents.ContainsKey(eventSet))
var selectedEventsList = GetSelectedEvents(eventSet);
if (selectedEventsList.Count > 0)
{
//start events in this set
foreach (Event ev in selectedEvents[eventSet])
foreach (Event ev in selectedEventsList)
{
activeEvents.Add(ev);
AddActiveEvent(ev);
eventThreshold = settings.DefaultEventThreshold;
if (eventSet.TriggerEventCooldown && selectedEvents[eventSet].Any(e => e.Prefab.TriggerEventCooldown))
if (eventSet.TriggerEventCooldown && selectedEventsList.Any(e => e.Prefab.TriggerEventCooldown))
{
eventCoolDown = settings.EventCooldown;
}
@@ -937,12 +1005,15 @@ namespace Barotrauma
{
ev.Finished += () =>
{
pendingEventSets.Add(eventSet);
CreateEvents(eventSet);
foreach (Event newEvent in selectedEvents[eventSet])
EnqueueDeferredAction(() =>
{
if (!newEvent.Initialized) { newEvent.Init(eventSet); }
}
AddPendingEventSet(eventSet);
CreateEvents(eventSet);
foreach (Event newEvent in GetSelectedEvents(eventSet))
{
if (!newEvent.Initialized) { newEvent.Init(eventSet); }
}
});
};
}
}
@@ -951,37 +1022,40 @@ namespace Barotrauma
//add child event sets to pending
foreach (EventSet childEventSet in eventSet.ChildSets)
{
pendingEventSets.Add(childEventSet);
AddPendingEventSet(childEventSet);
recheck = true;
}
}
} while (recheck);
foreach (Event ev in activeEvents)
var activeSnapshot = _activeEvents;
foreach (Event ev in activeSnapshot)
{
if (!ev.IsFinished)
{
ev.Update(deltaTime);
}
else if (ev.Prefab != null && !finishedEvents.Any(e => e.Prefab == ev.Prefab))
else if (ev.Prefab != null && !IsEventFinished(ev))
{
if (level?.LevelData != null && level.LevelData.Type == LevelData.LevelType.Outpost)
{
if (!level.LevelData.EventHistory.Contains(ev.Prefab.Identifier)) { level.LevelData.EventHistory.Add(ev.Prefab.Identifier); }
}
finishedEvents.Add(ev);
AddFinishedEvent(ev);
}
}
if (QueuedEvents.Count > 0)
if (QueuedEvents.TryDequeue(out var queuedEvent))
{
activeEvents.Add(QueuedEvents.Dequeue());
AddActiveEvent(queuedEvent);
}
ProcessDeferredActions();
}
public void EntitySpawned(Entity entity)
{
foreach (var ev in activeEvents)
foreach (var ev in _activeEvents)
{
if (ev is ScriptedEvent scriptedEvent)
{
@@ -3,6 +3,7 @@ using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using System.Xml.Linq;
namespace Barotrauma
@@ -60,47 +61,48 @@ namespace Barotrauma
return null;
}
#endif
private static readonly Dictionary<Identifier, EventPrefab> AllEventPrefabs = new Dictionary<Identifier, EventPrefab>();
private static volatile ImmutableDictionary<Identifier, EventPrefab> _allEventPrefabs =
ImmutableDictionary<Identifier, EventPrefab>.Empty;
public static IEnumerable<EventPrefab> GetAllEventPrefabs()
{
return AllEventPrefabs.Values;
return _allEventPrefabs.Values;
}
/// <summary>
/// Finds all the event prefabs (both "normal prefabs" that exists by themselves, present in <see cref="EventPrefab.Prefabs"/>, and the ones that exists only inside child event sets),
/// and adds them to <see cref="AllEventPrefabs"/>.
/// and adds them to <see cref="_allEventPrefabs"/>.
/// </summary>
public static void RefreshAllEventPrefabs()
{
AllEventPrefabs.Clear();
var builder = ImmutableDictionary.CreateBuilder<Identifier, EventPrefab>();
foreach (var eventPrefab in EventPrefab.Prefabs)
{
AllEventPrefabs.TryAdd(eventPrefab.Identifier, eventPrefab);
builder.TryAdd(eventPrefab.Identifier, eventPrefab);
}
foreach (var eventSet in Prefabs)
{
AddChildEventPrefabs(eventSet);
AddChildEventPrefabs(eventSet, builder);
}
Interlocked.Exchange(ref _allEventPrefabs, builder.ToImmutable());
}
private static void AddChildEventPrefabs(EventSet set)
private static void AddChildEventPrefabs(EventSet set, ImmutableDictionary<Identifier, EventPrefab>.Builder builder)
{
foreach (var subEventPrefabs in set.EventPrefabs)
{
foreach (var eventPrefab in subEventPrefabs.EventPrefabs)
{
AllEventPrefabs.TryAdd(eventPrefab.Identifier, eventPrefab);
builder.TryAdd(eventPrefab.Identifier, eventPrefab);
}
}
foreach (var childSet in set.ChildSets) { AddChildEventPrefabs(childSet); }
foreach (var childSet in set.ChildSets) { AddChildEventPrefabs(childSet, builder); }
}
public static EventPrefab GetEventPrefab(Identifier identifier)
{
return AllEventPrefabs.GetValueOrDefault(identifier);
return _allEventPrefabs.GetValueOrDefault(identifier);
}
/// <summary>
@@ -41,7 +41,7 @@ namespace Barotrauma
protected override void InitEventSpecific(EventSet parentSet)
{
var matchingItems = Item.ItemList.FindAll(i => i.Condition > 0.0f && targetItemIdentifiers.Contains(i.Prefab.Identifier));
var matchingItems = Item.ItemList.Where(i => i.Condition > 0.0f && targetItemIdentifiers.Contains(i.Prefab.Identifier)).ToList();
int itemAmount = Rand.Range(minItemAmount, maxItemAmount, Rand.RandSync.ServerAndClient);
for (int i = 0; i < itemAmount; i++)
{
@@ -111,7 +111,7 @@ namespace Barotrauma
{
if (!itemTag.IsEmpty)
{
var itemsToDestroy = Item.ItemList.FindAll(it => it.Submarine?.Info.Type != SubmarineType.Player && it.HasTag(itemTag));
var itemsToDestroy = Item.ItemList.Where(it => it.Submarine?.Info.Type != SubmarineType.Player && it.HasTag(itemTag)).ToList();
if (!itemsToDestroy.Any())
{
DebugConsole.ThrowError($"Error in mission \"{Prefab.Identifier}\". Could not find an item with the tag \"{itemTag}\".",
@@ -181,7 +181,7 @@ namespace Barotrauma
return;
}
destructibleItems.Clear();
destructibleItems.AddRange(Item.ItemList.FindAll(it => it.HasTag(destructibleItemTag)));
destructibleItems.AddRange(Item.ItemList.Where(it => it.HasTag(destructibleItemTag)));
if (destructibleItems.None())
{
DebugConsole.ThrowError($"Error in end mission \"{Prefab.Identifier}\". Could not find any destructible items with the tag \"{spawnPointTag}\".",
@@ -283,7 +283,7 @@ namespace Barotrauma
if (!IsClient)
{
PathFinder pathFinder = new PathFinder(WayPoint.WayPointList, false);
PathFinder pathFinder = new PathFinder(WayPoint.WayPointList.ToList(), false);
var path = pathFinder.FindPath(ConvertUnits.ToSimUnits(patrolPos), ConvertUnits.ToSimUnits(preferredSpawnPos));
if (!path.Unreachable)
{
@@ -128,7 +128,7 @@ namespace Barotrauma
{
foreach (var stackedItem in item.GetStackedItems())
{
Item.DeconstructItems.Add(stackedItem);
Item.MarkForDeconstruction(stackedItem);
}
#if CLIENT
HintManager.OnItemMarkedForDeconstruction(order.OrderGiver);
@@ -138,7 +138,7 @@ namespace Barotrauma
{
foreach (var stackedItem in item.GetStackedItems())
{
Item.DeconstructItems.Remove(stackedItem);
Item.UnmarkForDeconstruction(stackedItem);
}
}
}
@@ -1089,7 +1089,7 @@ namespace Barotrauma
//Clear the grids to allow for garbage collection
Powered.Grids.Clear();
Powered.ChangedConnections.Clear();
Powered.ClearChangedConnections();
try
{
@@ -1146,6 +1146,7 @@ namespace Barotrauma
EventManager?.EndRound();
StatusEffect.StopAll();
AfflictionPrefab.ClearAllEffects();
PhysicsBodyQueue.Clear();
IsRunning = false;
#if CLIENT
@@ -5,6 +5,7 @@ using FarseerPhysics.Dynamics;
using FarseerPhysics.Dynamics.Joints;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
#if CLIENT
@@ -24,11 +25,8 @@ namespace Barotrauma.Items.Components
Right
}
private static readonly List<DockingPort> list = new List<DockingPort>();
public static IEnumerable<DockingPort> List
{
get { return list; }
}
private static readonly ConcurrentDictionary<DockingPort, byte> _dockingPortDict = new ConcurrentDictionary<DockingPort, byte>();
public static IEnumerable<DockingPort> List => _dockingPortDict.Keys;
private Sprite overlaySprite;
private float dockingState;
@@ -168,7 +166,7 @@ namespace Barotrauma.Items.Components
IsActive = true;
list.Add(this);
_dockingPortDict.TryAdd(this, 0);
}
public override void FlipX(bool relativeToSub)
@@ -200,7 +198,7 @@ namespace Barotrauma.Items.Components
{
float closestDist = float.MaxValue;
DockingPort closestPort = null;
foreach (DockingPort port in list)
foreach (DockingPort port in List)
{
if (port == this || port.item.Submarine == item.Submarine || port.IsHorizontal != IsHorizontal) { continue; }
float xDist = Math.Abs(port.item.WorldPosition.X - item.WorldPosition.X);
@@ -532,8 +530,8 @@ namespace Barotrauma.Items.Components
wire.TryConnect(recipient, addNode: false);
//Flag connections to be updated
Powered.ChangedConnections.Add(powerConnection);
Powered.ChangedConnections.Add(recipient);
Powered.MarkConnectionChanged(powerConnection);
Powered.MarkConnectionChanged(recipient);
}
private void CreateDoorBody()
@@ -1007,7 +1005,7 @@ namespace Barotrauma.Items.Components
Connection powerConnection = Item.Connections.Find(c => c.IsPower);
if (powerConnection != null)
{
Powered.ChangedConnections.Add(powerConnection);
Powered.MarkConnectionChanged(powerConnection);
}
if (doorBody != null)
@@ -1151,7 +1149,7 @@ namespace Barotrauma.Items.Components
protected override void RemoveComponentSpecific()
{
base.RemoveComponentSpecific();
list.Remove(this);
_dockingPortDict.TryRemove(this, out _);
hulls[0]?.Remove(); hulls[0] = null;
hulls[1]?.Remove(); hulls[1] = null;
gap?.Remove(); gap = null;
@@ -2,6 +2,7 @@
using FarseerPhysics;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using FarseerPhysics.Dynamics;
@@ -14,9 +15,9 @@ namespace Barotrauma.Items.Components
{
partial class Door : Pickable, IDrawableComponent, IServerSerializable
{
private static readonly HashSet<Door> doorList = new HashSet<Door>();
private static readonly ConcurrentDictionary<Door, byte> _doorDict = new ConcurrentDictionary<Door, byte>();
public static IReadOnlyCollection<Door> DoorList { get { return doorList; } }
public static ICollection<Door> DoorList => _doorDict.Keys;
private Gap linkedGap;
private bool isOpen;
@@ -277,7 +278,7 @@ namespace Barotrauma.Items.Components
}
IsActive = true;
doorList.Add(this);
_doorDict.TryAdd(this, 0);
}
public override void OnItemLoaded()
@@ -313,13 +314,21 @@ namespace Barotrauma.Items.Components
public override void Move(Vector2 amount, bool ignoreContacts = false)
{
if (ignoreContacts)
// Defer physics operation if in parallel context (Farseer is not thread-safe)
if (Body != null)
{
Body?.SetTransformIgnoreContacts(Body.SimPosition + ConvertUnits.ToSimUnits(amount), 0.0f);
}
else
{
Body?.SetTransform(Body.SimPosition + ConvertUnits.ToSimUnits(amount), 0.0f);
var capturedBody = Body;
var capturedNewPos = Body.SimPosition + ConvertUnits.ToSimUnits(amount);
if (ignoreContacts)
{
PhysicsBodyQueue.ExecuteOrDefer(() =>
capturedBody.SetTransformIgnoreContacts(capturedNewPos, 0.0f));
}
else
{
PhysicsBodyQueue.ExecuteOrDefer(() =>
capturedBody.SetTransform(capturedNewPos, 0.0f));
}
}
#if CLIENT
UpdateConvexHulls();
@@ -669,7 +678,7 @@ namespace Barotrauma.Items.Components
convexHull2?.Remove();
#endif
doorList.Remove(this);
_doorDict.TryRemove(this, out _);
}
private bool CheckSubmarinesInDoorWay()
@@ -785,13 +794,19 @@ namespace Barotrauma.Items.Components
//immediately teleport it to the correct side
if (Math.Sign(diff) != dir)
{
// Defer physics operation if in parallel context (Farseer is not thread-safe)
var capturedBody = body;
if (IsHorizontal)
{
body.SetTransformIgnoreContacts(new Vector2(body.SimPosition.X, item.SimPosition.Y + dir * doorRectSimSize.Y * 2.0f), body.Rotation);
Vector2 newPos = new Vector2(body.SimPosition.X, item.SimPosition.Y + dir * doorRectSimSize.Y * 2.0f);
float rotation = body.Rotation;
PhysicsBodyQueue.ExecuteOrDefer(() => capturedBody.SetTransformIgnoreContacts(newPos, rotation));
}
else
{
body.SetTransformIgnoreContacts(new Vector2(item.SimPosition.X + dir * doorRectSimSize.X * 1.2f, body.SimPosition.Y), body.Rotation);
Vector2 newPos = new Vector2(item.SimPosition.X + dir * doorRectSimSize.X * 1.2f, body.SimPosition.Y);
float rotation = body.Rotation;
PhysicsBodyQueue.ExecuteOrDefer(() => capturedBody.SetTransformIgnoreContacts(newPos, rotation));
}
}

Some files were not shown because too many files have changed in this diff Show More