160 Commits

Author SHA1 Message Date
NotAlwaysTrue 61f05d25d0 Sync with upstream 2026-06-16 22:09:05 +08:00
Evil Factory 9794fe50b4 Merge branch 'develop' 2026-06-16 09:44:20 -03:00
Evil Factory c1073ff3d2 Merge remote-tracking branch 'upstream/master' into develop 2026-06-16 09:40:27 -03:00
Joonas Rikkonen 06616535f4 Updated bug report template 2026-06-16 15:37:52 +03:00
Regalis11 1cd0178e0a v1.13.3.1 (Summer Update 2026) 2026-06-16 15:35:36 +03:00
Maplewheels a729269763 added gcthread cleanup on failure. 2026-05-30 22:21:15 -04:00
Maplewheels c452493555 New GC algorithm (untested). 2026-05-30 22:19:37 -04:00
Maplewheels d420e1efbb Added experimental fix for inlining issue in assembly init. 2026-05-23 18:27:34 -03:00
Evil Factory eb22fbddff Make the no packages currently executing warning debug-only 2026-05-22 23:48:34 -03:00
MapleWheels 1f5ee454c0 Increased number of GC iterations by one. 2026-05-18 13:29:10 -04:00
Maplewheels d80ac4a38f Added background gc collection task upon unloading assemblies. 2026-05-17 03:17:46 -04:00
MapleWheels cfde6f3579 - Fixed assembly unloading. However, requires 'plugin_forcerungc' to be run multiple times over ~30 seconds at the main menu. 2026-05-15 18:41:39 -04:00
Joonas Rikkonen 81f57ea3e7 Add unstable version option to bug report template 2026-05-13 13:37:43 +03:00
Maplewheels ada9a09410 Added experimental fix for inlining issue in assembly init. 2026-05-10 23:56:14 -04:00
MapleWheels 66717c7fe7 Several changes to improve unloadability of assemblies. 2026-04-29 10:20:16 -04:00
NotAlwaysTrue 59bc21973a OBT/1.2.0(Spring Update)
Sync with Upstream
2026-04-25 13:25:41 +08:00
Evil Factory 3f199a9500 Remove hard coded string LuaCsForBarotrauma everywhere possible 2026-04-23 22:54:06 -03:00
Evil Factory 798954d182 Fix main menu text alignment 2026-04-21 22:58:42 -03:00
Evil Factory 93bdd03f54 Fix missing SettingControl in global Lua table 2026-04-21 12:58:53 -03:00
Evil Factory 8d90ccb4a3 Merge remote-tracking branch 'upstream/master' into develop 2026-04-21 08:18:49 -03:00
Regalis11 7446dc17c1 v1.12.7.0 (Spring Update 2026 Hotfix 1) 2026-04-21 12:21:41 +03:00
Evil Factory 3dc4548077 Fix texture not multiple of 4 2026-04-20 17:03:45 -03:00
MapleWheels 96906ef89e Always purge cashes for plugin content on unloading. 2026-04-20 15:19:05 -04:00
Evil Factory 1e1fb642f0 Fix UseInternalAccessName 2026-04-20 14:33:49 -03:00
Evil Factory 0a3ca2403e Remove missing Value error, it should just use the default value if it's not specified 2026-04-19 17:49:06 -03:00
Evil Factory 7daefe6f16 Some tweaks to the text 2026-04-19 17:11:10 -03:00
MapleWheels 8a08152e65 Added missing diffs for f61f852a25. 2026-04-19 15:08:21 -04:00
MapleWheels f61f852a25 Added networking tests for configs. 2026-04-19 15:07:25 -04:00
Evil Factory 3c0621c5a0 Missing diffs from bb21a09244 2026-04-19 15:36:33 -03:00
Maplewheels f612112445 Fixed OnUpdate() not passing in deltaTime instead of totalTime. 2026-04-18 22:10:28 -04:00
Evil Factory ab77e9d40c Fix client.packages hook sending normal packages 2026-04-18 17:38:27 -03:00
Evil Factory bb21a09244 New icon for settings and make the top left text more user friendly 2026-04-18 16:36:36 -03:00
Evil Factory f5f7eb7af9 Allow reloadlua to force the state to running 2026-04-18 15:37:49 -03:00
Evil Factory 12e05cb07e 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 2026-04-18 00:00:00 -03:00
MapleWheels 8f33e0af43 - 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.
2026-04-17 09:00:13 -04:00
Evil Factory 8896377f9c Add alias for human.CPRSuccess and human.CPRFailed 2026-04-15 07:54:51 -03:00
MapleWheels 17003e47cd - Fixed networking sync vars failing to sync initially.
- Fixed lua failing to differentiate overloads ISettingBase.
2026-04-14 12:17:12 -04:00
MapleWheels 38f0055b74 Added "RunUnrestricted" mode for lua scripts that need to run outside of sandbox. 2026-04-13 15:36:11 -04:00
MapleWheels a2c67f7af5 Changed csharp script compilation algorithm to be best effort. 2026-04-13 14:19:31 -04:00
MapleWheels 344027d757 Added LoggerService to Lua env. Made ILoggerService compliant with LuaCsLogger API. 2026-04-13 13:08:41 -04:00
MapleWheels addd3f7a45 Added legacy overload of AddCommand for mod compat. 2026-04-13 12:02:16 -04:00
Maplewheels a40e54f4e4 Added fallback compilation for UseInternalsAwareAssembly if the publicized script compilation fails. 2026-04-13 02:13:29 -04:00
Evil Factory 666b96f068 Keep the LuaCsConfig class laying around for compatibility, not sure anywhere in our code base (and shouldn't be) 2026-04-12 11:00:45 -03:00
Maplewheels c6ccc68b2e Fixed "Folder" attribute files not being found. 2026-04-11 23:20:11 -04:00
Evil Factory c5976ccae1 Add safety check so this stops crashing when we look at it the wrong way 2026-04-11 23:32:40 -03:00
Maplewheels 1b8bcf4052 Added configinfo null check to SettingBase.cs 2026-04-11 21:48:33 -04:00
Evil Factory eeef7aff68 Register Voronoi2 stuff 2026-04-11 22:25:04 -03:00
Maplewheels 3605571f71 Added fallback friendly name resolution for ModConfig assembly contents. 2026-04-11 21:00:42 -04:00
Evil Factory f3535e242c Load console commands after executing and possible fix for the not console command permitted 2026-04-11 21:49:12 -03:00
Evil Factory 1703b5a103 Ignore our workshop packages from the game's dependency thing since it doesn't really make sense 2026-04-11 00:08:49 -03:00
Evil Factory 775ae0ef91 Fix server host being prompted twice with the C# popup 2026-04-10 23:49:16 -03:00
Evil Factory ac6147e4a0 Fix Cs prompt showing up more than one between rounds 2026-04-10 22:36:29 -03:00
Evil Factory 3cc7467d99 Fix hide username in logs config not doing anything 2026-04-10 22:10:44 -03:00
Evil Factory f97c00195c Oops, fix NRE crash 2026-04-10 21:56:39 -03:00
Evil Factory df8bfb1807 Fix LuaCs initializing too late for singleplayer campaigns and rework the C# prompt to only show when enabling mods/joining server 2026-04-10 21:40:32 -03:00
MapleWheels ccd0b9f7cc Added caching toggle to PluginManagementService 2026-04-10 18:13:25 -04:00
Evil Factory 5505f20925 PackageId -> PackageName 2026-04-10 16:50:15 -03:00
Evil Factory 1adf76168d Fix garbage network data being read by the game when reading LuaCs network messages 2026-04-10 13:37:01 -03:00
MapleWheels 9250e27953 - Fixed assembly logging style.
- Fixed double logging during execution.
2026-04-10 10:48:55 -04:00
MapleWheels c3a9551683 - Changed event aliases to be case insensitive. 2026-04-10 10:14:53 -03:00
Evil Factory fd324ac67e Fix Hook.Remove not being implemented properly 2026-04-10 09:58:43 -03:00
Evil Factory f6ea4df489 Load languages files even if the package is disabled 2026-04-10 00:36:36 -03:00
Evil Factory a86b8142eb Actually use the PackageId const everywhere we need to refer to our content package 2026-04-10 00:11:33 -03:00
Evil Factory e3b2b34940 Fix Game.AssignOnExecute having command arguments be passed as varargs instead of a table 2026-04-09 23:54:21 -03:00
Evil Factory b68ffa2725 Add LuaCsSetup.Lua back for compatibility 2026-04-09 13:56:56 -03:00
Evil Factory a85d489907 Fix modifyChatMessage hook 2026-04-09 12:39:39 -03:00
Joonas Rikkonen 2cfa1e6ffd Update bug-reports.yml 2026-04-09 15:17:26 +03: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 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
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
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
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
144 changed files with 3424 additions and 1191 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.13.3.1 (Summer Update 2026)
- Other
validations:
required: true
+18 -4
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: |
+1
View File
@@ -60,3 +60,4 @@ Deploy/DeployAll/PrivateKey.*
#Rider
*.DotSettings.user
.vscode/settings.json
@@ -258,7 +258,9 @@ namespace Barotrauma
//RelativeSpacing = 0.05f
};
InventorySlotContainer = new GUICustomComponent(new RectTransform(new Vector2(0.1f, 1.0f), characterIndicatorArea.RectTransform, Anchor.TopLeft, Pivot.TopRight),
GUIFrame left = new(new RectTransform(new Vector2(0.25f, 1f), characterIndicatorArea.RectTransform), style: null);
InventorySlotContainer = new GUICustomComponent(new RectTransform(Vector2.One, left.RectTransform),
(spriteBatch, component) =>
{
for (int i = 0; i < character.Inventory.Capacity; i++)
@@ -266,6 +268,10 @@ namespace Barotrauma
if (character.Inventory.SlotTypes[i] != InvSlotType.HealthInterface) { continue; }
if (character.Inventory.HideSlot(i)) { continue; }
int width = Character.Inventory.visualSlots[i].Rect.Width;
left.RectTransform.MinSize = new Point(width, left.RectTransform.MinSize.Y);
if (afflictionIconList != null) { afflictionIconList.RectTransform.MinSize = new Point(width, afflictionIconList.RectTransform.MinSize.Y); }
//don't draw the item if it's being dragged out of the slot
bool drawItem = !Inventory.DraggingItems.Any() || !Character.Inventory.GetItemsAt(i).All(it => Inventory.DraggingItems.Contains(it)) || character.Inventory.visualSlots[i].MouseOn();
@@ -292,8 +298,7 @@ namespace Barotrauma
}
});
cprButton = new GUIButton(new RectTransform(new Vector2(0.17f, 0.17f), characterIndicatorArea.RectTransform, Anchor.BottomLeft, scaleBasis: ScaleBasis.Smallest), text: "", style: "CPRButton")
cprButton = new GUIButton(new RectTransform(new Vector2(0.75f), left.RectTransform, Anchor.BottomLeft, scaleBasis: ScaleBasis.Smallest), text: "", style: "CPRButton")
{
UserData = UIHighlightAction.ElementId.CPRButton,
OnClicked = (button, userData) =>
@@ -316,12 +321,11 @@ namespace Barotrauma
return true;
},
ToolTip = TextManager.Get("doctor.cprobjective"),
IgnoreLayoutGroups = true,
ToolTip = TextManager.Get("tutorial.roles.medic.objective.cpr"),
Visible = false
};
var limbSelection = new GUICustomComponent(new RectTransform(new Vector2(0.4f, 1.0f), characterIndicatorArea.RectTransform),
var limbSelection = new GUICustomComponent(new RectTransform(new Vector2(0.5f, 1.0f), characterIndicatorArea.RectTransform),
(spriteBatch, component) =>
{
DrawHealthWindow(spriteBatch, component.RectTransform.Rect, true);
@@ -368,8 +372,6 @@ namespace Barotrauma
CanBeFocused = false
};
characterIndicatorArea.Recalculate();
healthBarHolder = new GUIFrame(new RectTransform(Point.Zero, GUI.Canvas), style: null)
{
HoverCursor = CursorState.Hand
@@ -866,7 +866,20 @@ namespace Barotrauma
GameSettings.SaveCurrentConfig();
});
}, isCheat: false));
commands.Add(new Command("togglespoofeventmanagerid", "togglespoofeventmanagerid: Forces the client to report the last received event ID as always being 1, making the server believe the client is always behind.", (string[] args) =>
{
if (GameMain.Client != null)
{
GameMain.Client.SpoofEntityManagerReceivedId = !GameMain.Client.SpoofEntityManagerReceivedId;
DebugConsole.NewMessage(GameMain.Client.SpoofEntityManagerReceivedId ? "Spoofing enabled ": "Spoofing disabled", Color.Green);
}
else
{
DebugConsole.NewMessage("Not connected to server", Color.Red);
}
}));
commands.Add(new Command("togglegrid", "Toggle visual snap grid in sub editor.", (string[] args) =>
{
SubEditorScreen.ShouldDrawGrid = !SubEditorScreen.ShouldDrawGrid;
@@ -3544,6 +3557,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 +3595,14 @@ namespace Barotrauma
}
}));*/
AssignOnClientExecute(
"ShowServerPerf",
(string[] args) =>
{
GameMain.Client?.SendConsoleCommand("ShowServerPerf");
}
);
AssignOnClientExecute(
"giveperm",
(string[] args) =>
@@ -4441,17 +4467,24 @@ namespace Barotrauma
public static void StartLocalMPSession(int numClients = 2)
{
string extraArguments = "-multiclienttestmode";
if (NetConfig.UseLenientHandshake)
{
extraArguments += " -lenienthandshake";
}
try
{
if (Process.GetProcessesByName("DedicatedServer").Length == 0)
{
#if WINDOWS
Process.Start("DedicatedServer.exe", arguments: "-multiclienttestmode");
Process.Start("DedicatedServer.exe", arguments: extraArguments);
#else
Process.Start("./DedicatedServer", arguments: "-multiclienttestmode");
Process.Start("./DedicatedServer", arguments: extraArguments);
#endif
System.Threading.Thread.Sleep(1000);
}
#if DEBUG
GameClient.MultiClientTestMode = true;
#endif
@@ -4465,10 +4498,13 @@ namespace Barotrauma
for (int i = 2; i <= numClients; i++)
{
System.Threading.Thread.Sleep(1000);
string clientArguments = $"-connect server localhost -username client{i} -skipintro";
#if WINDOWS
Process.Start("Barotrauma.exe", arguments: "-connect server localhost -username client" + i + " -multiclienttestmode");
Process.Start("Barotrauma.exe", arguments: $"{clientArguments} {extraArguments}");
#else
Process.Start("./Barotrauma", arguments: "-connect server localhost -username client" + i + " -multiclienttestmode");
Process.Start("./Barotrauma", arguments: $"{clientArguments} {extraArguments}");
#endif
}
}
@@ -14,6 +14,7 @@ using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
namespace Barotrauma
{
@@ -649,12 +650,12 @@ namespace Barotrauma
DrawMessages(spriteBatch, cam);
if (MouseOn != null)
{
{
MouseOn.OnDrawToolTip?.Invoke(MouseOn);
if (!MouseOn.ToolTip.IsNullOrWhiteSpace())
{
MouseOn.DrawToolTip(spriteBatch);
}
MouseOn.OnDrawToolTip?.Invoke(MouseOn);
}
if (SubEditorScreen.IsSubEditor())
@@ -2323,10 +2324,10 @@ namespace Barotrauma
/// <summary>
/// Creates a 7-segment display.
/// </summary>
/// <param name="leftLabel">Returns <see langword="null"/> if <paramref name="leftLabelText"/> is <see langword="null"/> or empty.</param>
/// <param name="leftLabel">Returns <see langword="null"/> if <paramref name="leftLabelText"/> is <see langword="null"/>.</param>
/// <param name="rightLabelText">Defaults to <c>TextManager.Get("kilowatt")</c>.</param>
/// <param name="leftLabelFont">Defaults to <see cref="GUIStyle.LargeFont"/>.</param>
public static GUITextBlock CreateDigitalDisplay(RectTransform rect, out GUITextBlock? leftLabel, out GUITextBlock rightLabel, LocalizedString? leftLabelText = null, LocalizedString? rightLabelText = null, LocalizedString? tooltip = null, GUIFont? leftLabelFont = null)
public static GUITextBlock CreateDigitalDisplay(RectTransform rect, [NotNullIfNotNull(nameof(leftLabelText))] out GUITextBlock? leftLabel, out GUITextBlock rightLabel, LocalizedString? leftLabelText = null, LocalizedString? rightLabelText = null, LocalizedString? tooltip = null, GUIFont? leftLabelFont = null)
{
GUILayoutGroup textArea = new(rect, isHorizontal: true, childAnchor: Anchor.CenterLeft)
{
@@ -2337,7 +2338,7 @@ namespace Barotrauma
};
leftLabel = null;
if (!leftLabelText.IsNullOrEmpty())
if (leftLabelText != null)
{
leftLabel = new GUITextBlock(new RectTransform(new Vector2(0.4f, 1f), textArea.RectTransform), leftLabelText, textColor: GUIStyle.TextColorBright, font: leftLabelFont ?? GUIStyle.LargeFont, textAlignment: Alignment.CenterRight);
}
@@ -384,14 +384,14 @@ namespace Barotrauma
CaretIndex = forcedCaretIndex == - 1 ? textBlock.GetCaretIndexFromScreenPos(PlayerInput.MousePosition) : forcedCaretIndex;
CalculateCaretPos();
ClearSelection();
bool wasSelected = selected;
selected = true;
GUI.KeyboardDispatcher.Subscriber = this;
OnSelected?.Invoke(this, Keys.None);
if (!wasSelected && PlaySoundOnSelect && !ignoreSelectSound)
if (!selected && PlaySoundOnSelect && !ignoreSelectSound)
{
SoundPlayer.PlayUISound(GUISoundType.Select);
}
selected = true;
//set this after we've set selected to true -> otherwise the textbox taking keyboard focus will trigger Select again
GUI.KeyboardDispatcher.Subscriber = this;
}
public void Deselect()
@@ -113,7 +113,10 @@ namespace Barotrauma
spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, samplerState: GUI.SamplerState);
GUI.DrawBackgroundSprite(spriteBatch, currentBackgroundTexture, Color.White, drawArea);
if (currentBackgroundTexture.Texture != null)
{
GUI.DrawBackgroundSprite(spriteBatch, currentBackgroundTexture, Color.White, drawArea);
}
overlay.Draw(spriteBatch, Vector2.Zero, scale: overlayScale);
double noiseT = Timing.TotalTime * 0.02f;
@@ -278,6 +278,12 @@ namespace Barotrauma
GameClient.MultiClientTestMode = true;
}
#endif
if (ConsoleArguments.Contains("-lenienthandshake"))
{
NetConfig.UseLenientHandshake = true;
}
GUI.KeyboardDispatcher = new EventInput.KeyboardDispatcher(Window);
PerformanceCounter = new PerformanceCounter();
@@ -127,12 +127,6 @@ namespace Barotrauma
(int)SlotPositions[i].X,
(int)SlotPositions[i].Y,
(int)(slotSprite.size.X * multiplier), (int)(slotSprite.size.Y * multiplier));
if (SlotTypes[i] == InvSlotType.HealthInterface &&
character.CharacterHealth?.InventorySlotContainer != null)
{
slotRect.Width = slotRect.Height = (int)(character.CharacterHealth.InventorySlotContainer.Rect.Width * 1.2f);
}
ItemContainer itemContainer = slots[i].FirstOrDefault()?.GetComponent<ItemContainer>();
if (itemContainer != null)
@@ -622,6 +616,7 @@ namespace Barotrauma
for (int i = 0; i < capacity; i++)
{
if (HideSlot(i)) { continue; }
var item = slots[i].FirstOrDefault();
if (item != null)
{
@@ -26,7 +26,7 @@ namespace Barotrauma.Items.Components
}
else
{
DisplayMsg = TextManager.ParseInputTypes(TextManager.Get(Msg));
DisplayMsg = TextManager.ParseInputTypes(TextManager.Get(Msg)).Fallback(Msg);
}
CharacterHUD.RecreateHudTextsIfControlling(Character.Controlled);
@@ -12,9 +12,12 @@ namespace Barotrauma.Items.Components
private partial class PowerGroup
{
private GUIFrame? frame;
private GUIFrame? groupContent;
private GUILayoutGroup? nameGroup;
private GUITextBox? nameBox;
private GUITextBlock? loadDisplayNameLabel;
private GUIScrollBar? ratioSlider;
private readonly List<GUITextBlock> powerUnitLabels = new List<GUITextBlock>();
private readonly List<GUITextBlock> powerUnitLabels = [];
private GUIFrame? divider;
public bool IsVisible { get; private set; } = true;
@@ -22,9 +25,9 @@ namespace Barotrauma.Items.Components
public void CreateGUI()
{
frame = new GUIFrame(new RectTransform(new Vector2(1f, 0.25f), distributor.groupList!.Content.RectTransform, minSize: (0, 130)), style: null);
GUIFrame groupContent = new(new RectTransform(frame.Rect.Size - new Point(10), frame.RectTransform, Anchor.Center), style: null);
groupContent = new GUIFrame(new RectTransform(frame.Rect.Size - new Point(10), frame.RectTransform, Anchor.Center), style: null);
GUILayoutGroup nameGroup = new(new RectTransform(new Vector2(0.65f, 0.33f), groupContent.RectTransform, Anchor.TopLeft), isHorizontal: true, childAnchor: Anchor.CenterLeft)
nameGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.65f, 0.33f), groupContent.RectTransform, Anchor.TopLeft), isHorizontal: true, childAnchor: Anchor.CenterLeft)
{
Stretch = true
};
@@ -37,7 +40,7 @@ namespace Barotrauma.Items.Components
return true;
}
};
nameBox = new GUITextBox(new RectTransform(Vector2.One, nameGroup.RectTransform), Name, font: GUIStyle.SubHeadingFont, style: "GUITextBoxNoStyle")
nameBox = new GUITextBox(new RectTransform(Vector2.One, nameGroup.RectTransform), Screen.Selected == GameMain.SubEditorScreen ? Name : DisplayName.Value, font: GUIStyle.SubHeadingFont, style: "GUITextBoxNoStyle")
{
MaxTextLength = MaxNameLength,
OverflowClip = true,
@@ -48,17 +51,40 @@ namespace Barotrauma.Items.Components
return true;
}
};
nameBox.OnSelected += (tb, _) =>
{
if (tb.Selected) { return; }
tb.Text = Name;
};
nameBox.OnDeselected += (tb, _) =>
{
Name = tb.Text;
tb.CaretIndex = 0;
if (GameMain.Client == null) { return; }
distributor.item.CreateClientEvent(distributor, new EventData(this, EventType.NameChange));
};
nameBox.GetChild<GUIFrame>().GetChild<GUICustomComponent>().OnDrawToolTip = comp =>
{
if (Screen.Selected != GameMain.SubEditorScreen && !nameBox.Selected)
{
comp.ToolTip = null;
return;
}
LocalizedString localizedText = TextManager.Get(nameBox.Text);
comp.ToolTip = localizedText.IsNullOrEmpty()
? TextManager.GetWithVariable("StringPropertyCannotTranslate", "[tag]", nameBox.Text)
: TextManager.GetWithVariable("StringPropertyTranslate", "[translation]", localizedText);
};
GUITextBlock loadDisplay = GUI.CreateDigitalDisplay(new RectTransform(new Vector2(0.35f, 0.33f), groupContent.RectTransform, Anchor.TopRight) { AbsoluteOffset = (5, 0) },
out GUITextBlock? _, out GUITextBlock loadDisplayUnitLabel, TextManager.Get("PowerTransferLoadLabel"), tooltip: TextManager.Get("PowerTransferTipLoad"), leftLabelFont: GUIStyle.Font);
out loadDisplayNameLabel, out GUITextBlock loadDisplayUnitLabel, TextManager.Get("PowerTransferLoadLabel"), tooltip: TextManager.Get("PowerTransferTipLoad"), leftLabelFont: GUIStyle.Font);
loadDisplay.TextGetter = () => MathUtils.RoundToInt(Load).ToString();
float textAndPaddingWidth = loadDisplayNameLabel!.Font.MeasureString(loadDisplayNameLabel!.Text).X + loadDisplayNameLabel.Padding.X + loadDisplayNameLabel.Padding.Z;
float availableWidth = groupContent!.Rect.Width - loadDisplayNameLabel.Parent.Rect.Width + loadDisplayNameLabel.Rect.Width - textAndPaddingWidth;
nameGroup!.RectTransform.Resize(new Point((int)availableWidth, nameGroup.Rect.Height));
ratioSlider = new GUIScrollBar(new RectTransform(new Vector2(1f, 0.33f), groupContent.RectTransform, Anchor.Center), barSize: 0.15f, style: "DeviceSlider")
{
Step = SupplyRatioStep,
@@ -78,16 +104,17 @@ namespace Barotrauma.Items.Components
ratioSlider.Bar.RectTransform.MaxSize = new Point(ratioSlider.Bar.Rect.Height);
GUITextBlock ratioDisplay = GUI.CreateDigitalDisplay(new RectTransform(new Vector2(0.2f, 0.33f), groupContent.RectTransform, Anchor.BottomLeft),
out GUITextBlock? _, out GUITextBlock _,
out GUITextBlock? _, out GUITextBlock ratioDisplayUnitLabel,
rightLabelText: "%");
ratioDisplay.TextGetter = () => DisplayRatio.ToString();
GUITextBlock outputDisplay = GUI.CreateDigitalDisplay(new RectTransform(new Vector2(0.35f, 0.33f), groupContent.RectTransform, Anchor.BottomRight) { AbsoluteOffset = (5, 0) },
out GUITextBlock? _, out GUITextBlock outputDisplayUnitLabel,
out GUITextBlock? outputDisplayNameLabel, out GUITextBlock outputDisplayUnitLabel,
TextManager.Get("powerdistributor.supplylabel"), tooltip: TextManager.Get("PowerTransferTipPower"), leftLabelFont: GUIStyle.Font);
outputDisplay.TextGetter = () => distributor.IsShortCircuited(PowerOut) ? "err" : MathUtils.RoundToInt(distributor.CalculatePowerOut(this)).ToString();
powerUnitLabels.Add(loadDisplayUnitLabel);
powerUnitLabels.Add(ratioDisplayUnitLabel);
powerUnitLabels.Add(outputDisplayUnitLabel);
GUITextBlock.AutoScaleAndNormalize(powerUnitLabels);
@@ -111,7 +138,14 @@ namespace Barotrauma.Items.Components
IsVisible = PowerOut.Wires.Count >= 1;
frame!.Visible = IsVisible;
divider!.Visible = IsVisible && distributor.powerGroups.Last(group => group.frame!.Visible) != this;
if (distributor.prevLanguage != GameSettings.CurrentConfig.Language) { GUITextBlock.AutoScaleAndNormalize(powerUnitLabels); }
if (distributor.prevLanguage != GameSettings.CurrentConfig.Language)
{
GUITextBlock.AutoScaleAndNormalize(powerUnitLabels);
float textAndPaddingWidth = loadDisplayNameLabel!.Font.MeasureString(loadDisplayNameLabel!.Text).X + loadDisplayNameLabel.Padding.X + loadDisplayNameLabel.Padding.Z;
float availableWidth = groupContent!.Rect.Width - loadDisplayNameLabel.Parent.Rect.Width + loadDisplayNameLabel.Rect.Width - textAndPaddingWidth;
nameGroup!.RectTransform.Resize(new Point((int)availableWidth, nameGroup.Rect.Height));
}
}
}
@@ -716,13 +716,19 @@ namespace Barotrauma.Items.Components
GetAvailablePower(out float batteryCharge, out float batteryCapacity);
List<Item> availableAmmo = new List<Item>();
List<Item> availableAmmo = [];
AddAmmoFromContainer(item.GetComponent<ItemContainer>());
foreach (MapEntity e in item.linkedTo)
{
if (!(e is Item linkedItem)) { continue; }
var itemContainer = linkedItem.GetComponent<ItemContainer>();
if (itemContainer == null) { continue; }
if (e is not Item linkedItem) { continue; }
AddAmmoFromContainer(linkedItem.GetComponent<ItemContainer>());
}
void AddAmmoFromContainer(ItemContainer itemContainer)
{
if (itemContainer == null) { return; }
availableAmmo.AddRange(itemContainer.Inventory.AllItems);
//add empty slots too
for (int i = 0; i < itemContainer.Inventory.Capacity - itemContainer.Inventory.AllItems.Count(); i++)
{
availableAmmo.Add(null);
@@ -339,6 +339,19 @@ namespace Barotrauma
toolTip += $"‖color:{conditionColorStr}‖ ({(int)item.ConditionPercentage} %)‖color:end‖";
}
if (!description.IsNullOrEmpty()) { toolTip += '\n' + description; }
if (item.Prefab.UnlockedRecipeInToolTip.Length > 0 && GameMain.GameSession is { } GameSession)
{
if (item.Prefab.UnlockedRecipeInToolTip.All(id => GameSession.HasUnlockedRecipe(Character.Controlled, id)))
{
toolTip += $"\n‖color:{XMLExtensions.ToStringHex(GUIStyle.Green)}‖{TextManager.Get("unlockedrecipe.true")}‖color:end‖";
}
else
{
toolTip += $"\n‖color:{XMLExtensions.ToStringHex(GUIStyle.Yellow)}‖{TextManager.Get("unlockedrecipe.false")}‖color:end‖";
}
}
if (item.Prefab.ContentPackage != GameMain.VanillaContent && item.Prefab.ContentPackage != null)
{
colorStr = XMLExtensions.ToStringHex(Color.MediumPurple);
@@ -356,19 +369,7 @@ namespace Barotrauma
}
#if DEBUG
toolTip += $" ({item.Prefab.Identifier})";
#endif
if (!item.Prefab.UnlockedRecipeInToolTip.IsEmpty && GameMain.GameSession is { } GameSession)
{
if (GameSession.HasUnlockedRecipe(Character.Controlled, item.Prefab.UnlockedRecipeInToolTip))
{
toolTip += TextManager.Get("unlockedrecipe.true");
}
else
{
toolTip += $"\n‖color:{XMLExtensions.ToStringHex(GUIStyle.Yellow)}‖{TextManager.Get("unlockedrecipe.false")}‖color:end‖";
}
}
#endif
if (PlayerInput.KeyDown(InputType.ContextualCommand))
{
toolTip += $"\n‖color:gui.blue‖{TextManager.ParseInputTypes(TextManager.Get("itemmsgcontextualorders"))}‖color:end‖";
@@ -1300,8 +1301,7 @@ namespace Barotrauma
SubEditorScreen.StoreCommand(new InventoryPlaceCommand(DraggingItems.First().ParentInventory, new List<Item>(DraggingItems), true));
}
}
SoundPlayer.PlayUISound(GUISoundType.DropItem);
bool removed = false;
if (Screen.Selected is SubEditorScreen editor)
{
@@ -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 = $"{LuaCsSetup.PackageName}.SettingsMenu.ResetVisibleSettings";
private const string SettingsResetPromptTitle = $"{LuaCsSetup.PackageName}.SettingsMenu.ResetPrompt.Title";
private const string SettingsResetPromptContents = $"{LuaCsSetup.PackageName}.SettingsMenu.ResetPrompt.Message";
private const string SettingsResetPromptYesText = $"{LuaCsSetup.PackageName}.SettingsMenu.ResetPrompt.Yes";
private const string SettingsResetPromptNoText = $"{LuaCsSetup.PackageName}.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($"{LuaCsSetup.PackageName}.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",
$"{LuaCsSetup.PackageName}.SettingsMenu.ModGameplayButton");
/*_controlsContentFrame = CreateNewContentTab(tabControlsIndex, __instance,
"SettingsMenuTab.Controls", "LuaCsForBarotrauma.SettingsMenu.ModControlsButton");
"SettingsMenuTab.Controls", $"{LuaCsSetup.PackageName}.SettingsMenu.ModControlsButton");
*/
_gameplayMenuInstance = new ModsGameplaySettingsMenu(_gameplayContentFrame, _packageManagementService, _configService, __instance);
_gameplayMenuInstance = new ModsGameplaySettingsMenu(_gameplayContentFrame, _packageManagementService, _configService, _loggerService, __instance);
//_controlsMenuInstance = new ModsControlsSettingsMenu(_controlsContentFrame, _packageManagementService, _configService, __instance);
}
@@ -8,7 +8,7 @@ using System.Xml.Linq;
namespace Barotrauma
{
class BackgroundCreature : ISteerable
class BackgroundCreature : ISteerable, ILevelRenderableObject
{
const float MaxDepth = 10000.0f;
@@ -76,6 +76,8 @@ namespace Barotrauma
set;
}
public Vector3 Position => new Vector3(position.X, position.Y, Depth);
public BackgroundCreature(BackgroundCreaturePrefab prefab, Vector2 position)
{
this.Prefab = prefab;
@@ -3,7 +3,6 @@ using Microsoft.Xna.Framework.Graphics;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma
{
@@ -15,52 +14,11 @@ namespace Barotrauma
private float checkVisibleTimer;
private readonly List<BackgroundCreature> creatures = new List<BackgroundCreature>();
private readonly List<BackgroundCreature> creatures = [];
private readonly List<BackgroundCreature> visibleCreatures = new List<BackgroundCreature>();
private readonly List<BackgroundCreature> visibleCreatures = [];
public BackgroundCreatureManager()
{
/*foreach(var file in files)
{
LoadConfig(file.Path);
}*/
}
/*public BackgroundCreatureManager(string path)
{
DebugConsole.AddWarning($"Couldn't find any BackgroundCreaturePrefabs files, falling back to {path}");
LoadConfig(ContentPath.FromRaw(null, path));
}
private void LoadConfig(ContentPath configPath)
{
try
{
XDocument doc = XMLExtensions.TryLoadXml(configPath);
if (doc == null) { return; }
var mainElement = doc.Root.FromPackage(configPath.ContentPackage);
if (mainElement.IsOverride())
{
mainElement = mainElement.FirstElement();
Prefabs.Clear();
DebugConsole.NewMessage($"Overriding all background creatures with '{configPath}'", Color.MediumPurple);
}
else if (Prefabs.Any())
{
DebugConsole.NewMessage($"Loading additional background creatures from file '{configPath}'");
}
foreach (var element in mainElement.Elements())
{
Prefabs.Add(new BackgroundCreaturePrefab(element));
};
}
catch (Exception e)
{
DebugConsole.ThrowError(String.Format("Failed to load BackgroundCreatures from {0}", configPath), e);
}
}*/
public IEnumerable<BackgroundCreature> VisibleCreatures => visibleCreatures;
public void SpawnCreatures(Level level, int count, Vector2? position = null)
{
@@ -161,14 +119,6 @@ namespace Barotrauma
}
}
public void Draw(SpriteBatch spriteBatch, Camera cam)
{
foreach (BackgroundCreature creature in visibleCreatures)
{
creature.Draw(spriteBatch, cam);
}
}
public void DrawLights(SpriteBatch spriteBatch, Camera cam)
{
foreach (BackgroundCreature creature in visibleCreatures)
@@ -140,7 +140,7 @@ namespace Barotrauma
public void DrawFront(SpriteBatch spriteBatch, Camera cam)
{
renderer?.DrawForeground(spriteBatch, cam, LevelObjectManager);
renderer?.DrawForeground(spriteBatch, cam, backgroundCreatureManager, LevelObjectManager);
}
public void ClientEventRead(IReadMessage msg, float sendingTime)
{
@@ -12,7 +12,7 @@ using System.Xml.Linq;
namespace Barotrauma
{
partial class LevelObject
partial class LevelObject : ILevelRenderableObject
{
public float SwingTimer;
public float ScaleOscillateTimer;
@@ -8,13 +8,20 @@ using System.Linq;
namespace Barotrauma
{
public interface ILevelRenderableObject
{
public Vector3 Position { get; }
}
partial class LevelObjectManager
{
// Pre-initialized to the max size, so that we don't have to resize the lists at runtime. TODO: Could the capacity (of some collections?) be lower?
private readonly List<LevelObject> visibleObjectsBack = new List<LevelObject>(MaxVisibleObjects);
private readonly List<LevelObject> visibleObjectsMid = new List<LevelObject>(MaxVisibleObjects);
private readonly List<LevelObject> visibleObjectsFront = new List<LevelObject>(MaxVisibleObjects);
private readonly HashSet<LevelObject> allVisibleObjects = new HashSet<LevelObject>(MaxVisibleObjects);
private readonly List<ILevelRenderableObject> visibleObjectsBack = new List<ILevelRenderableObject>(MaxVisibleObjects);
private readonly List<ILevelRenderableObject> visibleObjectsMid = new List<ILevelRenderableObject>(MaxVisibleObjects);
private readonly List<ILevelRenderableObject> visibleObjectsFront = new List<ILevelRenderableObject>(MaxVisibleObjects);
private readonly HashSet<ILevelRenderableObject> allVisibleObjects = new HashSet<ILevelRenderableObject>(MaxVisibleObjects);
private double NextRefreshTime;
@@ -35,35 +42,44 @@ namespace Barotrauma
partial void UpdateProjSpecific(float deltaTime, Camera cam)
{
foreach (LevelObject obj in visibleObjectsBack)
foreach (ILevelRenderableObject obj in visibleObjectsBack)
{
obj.Update(deltaTime, cam);
if (obj is LevelObject levelObj)
{
levelObj.Update(deltaTime, cam);
}
}
foreach (LevelObject obj in visibleObjectsMid)
foreach (ILevelRenderableObject obj in visibleObjectsMid)
{
obj.Update(deltaTime, cam);
if (obj is LevelObject levelObj)
{
levelObj.Update(deltaTime, cam);
}
}
foreach (LevelObject obj in visibleObjectsFront)
foreach (ILevelRenderableObject obj in visibleObjectsFront)
{
obj.Update(deltaTime, cam);
if (obj is LevelObject levelObj)
{
levelObj.Update(deltaTime, cam);
}
}
}
/// <summary>
/// Returns all visible objects, but not in order, because internally uses a HashSet.
/// </summary>
public IEnumerable<LevelObject> GetAllVisibleObjects()
public IEnumerable<ILevelRenderableObject> GetAllVisibleObjects()
{
allVisibleObjects.Clear();
foreach (LevelObject obj in visibleObjectsBack)
foreach (ILevelRenderableObject obj in visibleObjectsBack)
{
allVisibleObjects.Add(obj);
}
foreach (LevelObject obj in visibleObjectsMid)
foreach (ILevelRenderableObject obj in visibleObjectsMid)
{
allVisibleObjects.Add(obj);
}
foreach (LevelObject obj in visibleObjectsFront)
foreach (ILevelRenderableObject obj in visibleObjectsFront)
{
allVisibleObjects.Add(obj);
}
@@ -73,7 +89,7 @@ namespace Barotrauma
/// <summary>
/// Checks which level objects are in camera view and adds them to the visibleObjects lists
/// </summary>
private void RefreshVisibleObjects(Rectangle currentIndices, float zoom)
private void RefreshVisibleObjects(Rectangle currentIndices, BackgroundCreatureManager backgroundCreatureManager, float zoom)
{
visibleObjectsBack.Clear();
visibleObjectsMid.Clear();
@@ -152,6 +168,27 @@ namespace Barotrauma
}
}
foreach (var backgroundCreature in backgroundCreatureManager.VisibleCreatures)
{
int drawOrderIndex = 0;
for (int i = 0; i < visibleObjectsBack.Count; i++)
{
if (visibleObjectsBack[i].Position.Z > backgroundCreature.Position.Z)
{
break;
}
else
{
drawOrderIndex = i + 1;
if (drawOrderIndex >= MaxVisibleObjects) { break; }
}
}
if (drawOrderIndex >= 0 && drawOrderIndex < MaxVisibleObjects)
{
visibleObjectsBack.Insert(drawOrderIndex, backgroundCreature);
}
}
//object grid is sorted in an ascending order
//(so we prefer the objects in the foreground instead of ones in the background if some need to be culled)
//rendering needs to be done in a descending order though to get the background objects to be drawn first -> reverse the lists
@@ -165,28 +202,28 @@ namespace Barotrauma
/// <summary>
/// Draw the objects behind the level walls
/// </summary>
public void DrawObjectsBack(SpriteBatch spriteBatch, Camera cam)
public void DrawObjectsBack(SpriteBatch spriteBatch, BackgroundCreatureManager backgroundCreatureManager, Camera cam)
{
DrawObjects(spriteBatch, cam, visibleObjectsBack);
DrawObjects(spriteBatch, cam, backgroundCreatureManager, visibleObjectsBack);
}
/// <summary>
/// Draw the objects in front of the level walls, but behind characters
/// </summary>
public void DrawObjectsMid(SpriteBatch spriteBatch, Camera cam)
public void DrawObjectsMid(SpriteBatch spriteBatch, BackgroundCreatureManager backgroundCreatureManager, Camera cam)
{
DrawObjects(spriteBatch, cam, visibleObjectsMid);
DrawObjects(spriteBatch, cam, backgroundCreatureManager, visibleObjectsMid);
}
/// <summary>
/// Draw the objects in front of the level walls and characters
/// </summary>
public void DrawObjectsFront(SpriteBatch spriteBatch, Camera cam)
public void DrawObjectsFront(SpriteBatch spriteBatch, BackgroundCreatureManager backgroundCreatureManager, Camera cam)
{
DrawObjects(spriteBatch, cam, visibleObjectsFront);
DrawObjects(spriteBatch, cam, backgroundCreatureManager, visibleObjectsFront);
}
private void DrawObjects(SpriteBatch spriteBatch, Camera cam, List<LevelObject> objectList)
private void DrawObjects(SpriteBatch spriteBatch, Camera cam, BackgroundCreatureManager backgroundCreatureManager, List<ILevelRenderableObject> objectList)
{
Rectangle indices = Rectangle.Empty;
indices.X = (int)Math.Floor(cam.WorldView.X / (float)GridSize);
@@ -207,7 +244,7 @@ namespace Barotrauma
float z = 0.0f;
if (ForceRefreshVisibleObjects || (currentGridIndices != indices && Timing.TotalTime > NextRefreshTime))
{
RefreshVisibleObjects(indices, cam.Zoom);
RefreshVisibleObjects(indices, backgroundCreatureManager, cam.Zoom);
ForceRefreshVisibleObjects = false;
if (cam.Zoom < 0.1f)
{
@@ -216,61 +253,93 @@ namespace Barotrauma
}
}
foreach (LevelObject obj in objectList)
bool prevObjectHasDeformableSprite = false;
foreach (ILevelRenderableObject obj2 in objectList)
{
Vector2 camDiff = new Vector2(obj.Position.X, obj.Position.Y) - cam.WorldViewCenter;
Vector2 camDiff = new Vector2(obj2.Position.X, obj2.Position.Y) - cam.WorldViewCenter;
camDiff.Y = -camDiff.Y;
Sprite activeSprite = obj.Sprite;
activeSprite?.Draw(
spriteBatch,
new Vector2(obj.Position.X, -obj.Position.Y) - camDiff * obj.Position.Z * ParallaxStrength,
Color.Lerp(obj.Prefab.SpriteColor, obj.Prefab.SpriteColor.Multiply(Level.Loaded.BackgroundTextureColor), obj.Position.Z / obj.Prefab.FadeOutDepth),
activeSprite.Origin,
obj.CurrentRotation,
obj.CurrentScale,
SpriteEffects.None,
z);
if (obj.ActivePrefab.DeformableSprite != null)
bool hasDeformableSprite = false;
if (obj2 is LevelObject levelObject)
{
if (obj.CurrentSpriteDeformation != null)
hasDeformableSprite = levelObject.ActivePrefab.DeformableSprite != null;
if (hasDeformableSprite != prevObjectHasDeformableSprite)
{
obj.ActivePrefab.DeformableSprite.Deform(obj.CurrentSpriteDeformation);
spriteBatch.End();
spriteBatch.Begin(SpriteSortMode.Deferred,
BlendState.NonPremultiplied,
SamplerState.LinearWrap, DepthStencilState.DepthRead,
transformMatrix: cam.Transform);
}
else
Sprite activeSprite = levelObject.Sprite;
activeSprite?.Draw(
spriteBatch,
new Vector2(levelObject.Position.X, -levelObject.Position.Y) - camDiff * levelObject.Position.Z * ParallaxStrength,
Color.Lerp(levelObject.Prefab.SpriteColor, levelObject.Prefab.SpriteColor.Multiply(Level.Loaded.BackgroundTextureColor), levelObject.Position.Z / levelObject.Prefab.FadeOutDepth),
activeSprite.Origin,
levelObject.CurrentRotation,
levelObject.CurrentScale,
SpriteEffects.None,
z);
if (hasDeformableSprite)
{
obj.ActivePrefab.DeformableSprite.Reset();
}
obj.ActivePrefab.DeformableSprite?.Draw(cam,
new Vector3(new Vector2(obj.Position.X, obj.Position.Y) - camDiff * obj.Position.Z * ParallaxStrength, z * 10.0f),
obj.ActivePrefab.DeformableSprite.Origin,
obj.CurrentRotation,
obj.CurrentScale,
Color.Lerp(obj.Prefab.SpriteColor, obj.Prefab.SpriteColor.Multiply(Level.Loaded.BackgroundTextureColor), obj.Position.Z / 5000.0f));
}
if (GameMain.DebugDraw)
{
GUI.DrawRectangle(spriteBatch, new Vector2(obj.Position.X, -obj.Position.Y), new Vector2(10.0f, 10.0f), GUIStyle.Red, true);
if (obj.Triggers == null) { continue; }
foreach (LevelTrigger trigger in obj.Triggers)
{
if (trigger.PhysicsBody == null) continue;
GUI.DrawLine(spriteBatch, new Vector2(obj.Position.X, -obj.Position.Y), new Vector2(trigger.WorldPosition.X, -trigger.WorldPosition.Y), Color.Cyan, 0, 3);
Vector2 flowForce = trigger.GetWaterFlowVelocity();
if (flowForce.LengthSquared() > 1)
if (levelObject.CurrentSpriteDeformation != null)
{
flowForce.Y = -flowForce.Y;
GUI.DrawLine(spriteBatch, new Vector2(trigger.WorldPosition.X, -trigger.WorldPosition.Y), new Vector2(trigger.WorldPosition.X, -trigger.WorldPosition.Y) + flowForce * 10, GUIStyle.Orange, 0, 5);
levelObject.ActivePrefab.DeformableSprite.Deform(levelObject.CurrentSpriteDeformation);
}
trigger.PhysicsBody.UpdateDrawPosition();
trigger.PhysicsBody.DebugDraw(spriteBatch, trigger.IsTriggered ? Color.Cyan : Color.DarkCyan);
else
{
levelObject.ActivePrefab.DeformableSprite.Reset();
}
levelObject.ActivePrefab.DeformableSprite?.Draw(cam,
new Vector3(new Vector2(levelObject.Position.X, levelObject.Position.Y) - camDiff * levelObject.Position.Z * ParallaxStrength, z * 10.0f),
levelObject.ActivePrefab.DeformableSprite.Origin,
levelObject.CurrentRotation,
levelObject.CurrentScale,
Color.Lerp(levelObject.Prefab.SpriteColor, levelObject.Prefab.SpriteColor.Multiply(Level.Loaded.BackgroundTextureColor), levelObject.Position.Z / 5000.0f));
}
prevObjectHasDeformableSprite = hasDeformableSprite;
if (GameMain.DebugDraw)
{
GUI.DrawRectangle(spriteBatch, new Vector2(levelObject.Position.X, -levelObject.Position.Y), new Vector2(10.0f, 10.0f), GUIStyle.Red, true);
if (levelObject.Triggers == null) { continue; }
foreach (LevelTrigger trigger in levelObject.Triggers)
{
if (trigger.PhysicsBody == null) continue;
GUI.DrawLine(spriteBatch, new Vector2(levelObject.Position.X, -levelObject.Position.Y), new Vector2(trigger.WorldPosition.X, -trigger.WorldPosition.Y), Color.Cyan, 0, 3);
Vector2 flowForce = trigger.GetWaterFlowVelocity();
if (flowForce.LengthSquared() > 1)
{
flowForce.Y = -flowForce.Y;
GUI.DrawLine(spriteBatch, new Vector2(trigger.WorldPosition.X, -trigger.WorldPosition.Y), new Vector2(trigger.WorldPosition.X, -trigger.WorldPosition.Y) + flowForce * 10, GUIStyle.Orange, 0, 5);
}
trigger.PhysicsBody.UpdateDrawPosition();
trigger.PhysicsBody.DebugDraw(spriteBatch, trigger.IsTriggered ? Color.Cyan : Color.DarkCyan);
}
}
}
else if (obj2 is BackgroundCreature backgroundCreature && cam.Zoom > 0.05f)
{
hasDeformableSprite = backgroundCreature.Prefab.DeformableSprite != null;
if (hasDeformableSprite != prevObjectHasDeformableSprite)
{
spriteBatch.End();
spriteBatch.Begin(SpriteSortMode.Deferred,
BlendState.NonPremultiplied,
SamplerState.LinearWrap, DepthStencilState.DepthRead,
transformMatrix: cam.Transform);
}
backgroundCreature.Draw(spriteBatch, cam);
}
prevObjectHasDeformableSprite = hasDeformableSprite;
z += 0.0001f;
}
@@ -214,8 +214,9 @@ namespace Barotrauma
//calculate the sum of the forces of nearby level triggers
//and use it to move the water texture and water distortion effect
Vector2 currentWaterParticleVel = level.GenerationParams.WaterParticleVelocity;
foreach (LevelObject levelObject in level.LevelObjectManager.GetAllVisibleObjects())
foreach (ILevelRenderableObject obj in level.LevelObjectManager.GetAllVisibleObjects())
{
if (obj is not LevelObject levelObject) { continue; }
if (levelObject.Triggers == null) { continue; }
//use the largest water flow velocity of all the triggers
Vector2 objectMaxFlow = Vector2.Zero;
@@ -274,11 +275,7 @@ namespace Barotrauma
SamplerState.LinearWrap, DepthStencilState.DepthRead, null, null,
cam.Transform);
backgroundSpriteManager?.DrawObjectsBack(spriteBatch, cam);
if (cam.Zoom > 0.05f)
{
backgroundCreatureManager?.Draw(spriteBatch, cam);
}
backgroundSpriteManager?.DrawObjectsBack(spriteBatch, backgroundCreatureManager, cam);
level.GenerationParams.DrawWaterParticles(spriteBatch, cam, waterParticleOffset);
@@ -292,17 +289,18 @@ namespace Barotrauma
BlendState.NonPremultiplied,
SamplerState.LinearClamp, DepthStencilState.DepthRead, null, null,
cam.Transform);
backgroundSpriteManager?.DrawObjectsMid(spriteBatch, cam);
backgroundSpriteManager?.DrawObjectsMid(spriteBatch, backgroundCreatureManager, cam);
spriteBatch.End();
}
public void DrawForeground(SpriteBatch spriteBatch, Camera cam, LevelObjectManager backgroundSpriteManager = null)
public void DrawForeground(SpriteBatch spriteBatch, Camera cam,
BackgroundCreatureManager backgroundCreatureManager, LevelObjectManager backgroundSpriteManager = null)
{
spriteBatch.Begin(SpriteSortMode.Deferred,
BlendState.NonPremultiplied,
SamplerState.LinearClamp, DepthStencilState.DepthRead, null, null,
cam.Transform);
backgroundSpriteManager?.DrawObjectsFront(spriteBatch, cam);
backgroundSpriteManager?.DrawObjectsFront(spriteBatch, backgroundCreatureManager, cam);
spriteBatch.End();
}
@@ -2547,7 +2547,7 @@ namespace Barotrauma.Networking
segmentTable.StartNewSegment(ClientNetSegment.SyncIds);
//outmsg.Write(GameMain.NetLobbyScreen.LastUpdateID);
outmsg.WriteUInt16(ChatMessage.LastID);
outmsg.WriteUInt16(EntityEventManager.LastReceivedID);
outmsg.WriteUInt16(SpoofEntityManagerReceivedId ? (ushort)1 : EntityEventManager.LastReceivedID);
outmsg.WriteUInt16(LastClientListUpdateID);
if (!(GameMain.GameSession?.GameMode is MultiPlayerCampaign campaign) || campaign.LastSaveID == 0)
@@ -3370,6 +3370,12 @@ namespace Barotrauma.Networking
{
get { return votingInterface; }
}
/// <summary>
/// Forces the client to report the last received event ID as always being 1, making the server believe the client is always behind.
/// </summary>
public bool SpoofEntityManagerReceivedId { get; set; }
private VotingInterface votingInterface;
public bool TypingChatMessage(GUITextBox textBox, string text)
@@ -89,6 +89,7 @@ namespace Barotrauma.Networking
{
byte queueId = msg.ReadByte();
float distanceFactor = msg.ReadRangedSingle(0.0f, 1.0f, 8);
bool isRadio = msg.ReadBoolean();
VoipQueue queue = queues.Find(q => q.QueueID == queueId);
if (queue == null)
@@ -117,19 +118,21 @@ namespace Barotrauma.Networking
float rangeMultiplier = spectating ? 2.0f : 1.0f;
WifiComponent senderRadio = null;
var messageType =
!client.VoipQueue.ForceLocal &&
ChatMessage.CanUseRadio(client.Character, out senderRadio) &&
(spectating || (ChatMessage.CanUseRadio(Character.Controlled, out var recipientRadio) && senderRadio.CanReceive(recipientRadio)))
? ChatMessageType.Radio : ChatMessageType.Default;
var messageType = isRadio ? ChatMessageType.Radio : ChatMessageType.Default;
client.Character.ShowTextlessSpeechBubble(1.25f, ChatMessage.MessageColor[(int)messageType]);
client.VoipSound.UseRadioFilter = messageType == ChatMessageType.Radio && !GameSettings.CurrentConfig.Audio.DisableVoiceChatFilters;
client.RadioNoise = 0.0f;
if (messageType == ChatMessageType.Radio)
{
//If the client cannot establish a radio, use a headsets default range as a fallback to calculate the radio noise.
//This cannot happen in an un-modded setting as CanUseRadio is part of the server side check for isRadio to be true.
ChatMessage.CanUseRadio(client.Character, out senderRadio);
float senderRadioRange = (senderRadio == null) ? 35000.0f : senderRadio.Range;
client.VoipSound.UsingRadio = true;
client.VoipSound.SetRange(senderRadio.Range * RangeNear * speechImpedimentMultiplier * rangeMultiplier, senderRadio.Range * speechImpedimentMultiplier * rangeMultiplier);
client.VoipSound.SetRange(senderRadioRange * RangeNear * speechImpedimentMultiplier * rangeMultiplier, senderRadioRange * speechImpedimentMultiplier * rangeMultiplier);
if (distanceFactor > RangeNear && !spectating)
{
//noise starts increasing exponentially after 40% range
@@ -533,6 +533,18 @@ namespace Barotrauma
return true;
}
};
new GUITickBox(new RectTransform(new Point(300, 30), Frame.RectTransform, Anchor.TopRight) { AbsoluteOffset = new Point(40, 280) },
"Lenient server startup timeouts")
{
Selected = NetConfig.UseLenientHandshake,
ToolTip = "Start with more lenient Lidgren handshake timeouts. The server is more likely to start even when running multiple instances on the same machine under heavy load.",
OnSelected = (tickBox) =>
{
NetConfig.UseLenientHandshake = tickBox.Selected;
return true;
}
};
#endif
var minButtonSize = new Point(120, 20);
@@ -1015,25 +1027,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)
@@ -1131,6 +1138,11 @@ namespace Barotrauma
}
#endif
if (NetConfig.UseLenientHandshake)
{
arguments.Add("-lenienthandshake");
}
var processInfo = new ProcessStartInfo
{
FileName = fileName,
@@ -1,19 +1,22 @@
using Barotrauma.Extensions;
using Barotrauma.IO;
using Barotrauma.Items.Components;
using Barotrauma.Sounds;
using Barotrauma.Steam;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Steamworks;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Globalization;
using System.IO.Compression;
using System.Linq;
using System.Threading;
using System.Xml;
using System.Xml.Linq;
using Barotrauma.LuaCs.Events;
using Barotrauma.Sounds;
namespace Barotrauma
{
@@ -2044,6 +2047,8 @@ namespace Barotrauma
private bool SaveSubToFile(string name, ContentPackage packageToSaveTo)
{
bool remoteStorageWasEnabled = Submarine.MainSub.Info.SaveToRemoteStorage;
Type subFileType = DetermineSubFileType(MainSub?.Info.Type ?? SubmarineType.Player);
static string getExistingFilePath(ContentPackage package, string fileName)
@@ -2272,6 +2277,16 @@ namespace Barotrauma
linkedSubBox.AddItem(sub.Name, sub);
}
subNameLabel.Text = ToolBox.LimitString(MainSub.Info.Name, subNameLabel.Font, subNameLabel.Rect.Width);
if (remoteStorageWasEnabled)
{
Submarine.MainSub.Info.SaveToRemoteStorage = true;
RemoteStorageHelper.TryWrite(
localPath: MainSub.Info.FilePath,
saveAs: MainSub.Info.FilePath.CleanUpPathCrossPlatform(correctFilenameCase: false),
allowOverwrite: true);
}
}
}
}
@@ -3223,6 +3238,42 @@ namespace Barotrauma
previewImageButtonHolder.RectTransform.MinSize = new Point(0, previewImageButtonHolder.RectTransform.Children.Max(c => c.MinSize.Y));
if (SteamManager.IsInitialized)
{
GUILayoutGroup remoteStorageArea = new(
new RectTransform(new Vector2(1f, 0.05f), rightColumn.RectTransform,
minSize: new Point(0, minHeight)),
isHorizontal: true,
childAnchor: Anchor.CenterLeft)
{
Stretch = true,
AbsoluteSpacing = 5
};
new GUITextBlock(new RectTransform(Vector2.One, remoteStorageArea.RectTransform),
TextManager.Get("RemoteStorageToggle.Title"), textAlignment: Alignment.CenterLeft, wrap: true);
new GUITickBox(new RectTransform(Vector2.One, remoteStorageArea.RectTransform), label: "")
{
OnAddedToGUIUpdateList = component =>
{
// Values may change outside of game.
component.Enabled = SteamRemoteStorage.IsCloudEnabledForAccount;
component.ToolTip = !SteamRemoteStorage.IsCloudEnabledForAccount ? TextManager.Get("RemoteStorageToggle.Disabled") : "";
((GUITickBox)component).SetSelected(SteamRemoteStorage.IsCloudEnabled && MainSub.Info.SaveToRemoteStorage, callOnSelected: false);
},
OnSelected = tickBox =>
{
if (tickBox.Selected && !SteamRemoteStorage.IsCloudEnabledForApp)
{
RemoteStorageHelper.AskToEnable(onAccepted: () => MainSub.Info.SaveToRemoteStorage = true);
return false;
}
return MainSub.Info.SaveToRemoteStorage = tickBox.Selected;
}
};
}
var contentPackageTabber = new GUILayoutGroup(new RectTransform((1.0f, 0.075f), rightColumn.RectTransform), isHorizontal: true);
GUIButton createTabberBtn(string labelTag)
@@ -3746,7 +3797,7 @@ namespace Barotrauma
}
var package = GetLocalPackageThatOwnsSub(subInfo);
if (package != null)
if (package != null || subInfo.IsFromRemoteStorage)
{
deleteBtn.Enabled = true;
}
@@ -3771,13 +3822,29 @@ namespace Barotrauma
searchBox.OnDeselected += (sender, userdata) => { searchTitle.Visible = sender.Text.IsNullOrEmpty(); };
searchBox.OnTextChanged += (textBox, text) => { FilterSubs(subList, text); return true; };
var sortedSubs = GetLoadableSubs()
.OrderBy(s => s.Type)
.ThenBy(s => s.Name)
.ToList();
List<SubmarineInfo> allSubs = [.. SubmarineInfo.SavedSubmarines];
foreach (SteamRemoteStorage.RemoteFile remoteFile in SteamRemoteStorage.Files.Where(file => file.Filename.EndsWith(".sub")))
{
if (!remoteFile.TryRead(out byte[] bytes)) { continue; }
using System.IO.MemoryStream stream = new(bytes);
using GZipStream zipStream = new(stream, CompressionMode.Decompress);
if (XMLExtensions.TryLoadXml(zipStream) is not XDocument doc)
{
DebugConsole.ThrowError($"{RemoteStorageHelper.DebugPrefix} Failed to load submarine \"{remoteFile.Filename}\" from remote storage: file is not a valid XML document.");
continue;
}
SubmarineInfo subInfo = new(remoteFile.Filename, element: doc.Root, tryLoad: false) { IsFromRemoteStorage = true };
allSubs.Add(subInfo);
}
IOrderedEnumerable<SubmarineInfo> sortedSubs = allSubs
.OrderBy(kvp => kvp.Type)
.ThenBy(kvp => kvp.Name)
.ThenBy(kvp => kvp.IsFromRemoteStorage);
SubmarineInfo prevSub = null;
foreach (SubmarineInfo sub in sortedSubs)
{
if (prevSub == null || prevSub.Type != sub.Type)
@@ -3795,35 +3862,44 @@ namespace Barotrauma
prevSub = sub;
}
string pathWithoutUserName = Path.GetFullPath(sub.FilePath);
string saveFolder = Path.GetFullPath(SaveUtil.DefaultSaveFolder);
if (pathWithoutUserName.StartsWith(saveFolder))
string displayPath = sub.FilePath;
if (sub.IsFromRemoteStorage)
{
pathWithoutUserName = "..." + pathWithoutUserName[saveFolder.Length..];
displayPath += $" {TextManager.Get("RemoteStorage")}";
}
else
{
pathWithoutUserName = sub.FilePath;
string saveFolder = Path.GetFullPath(SaveUtil.DefaultSaveFolder);
string fullPath = Path.GetFullPath(displayPath);
if (fullPath.StartsWith(saveFolder))
{
displayPath = $"...{fullPath[saveFolder.Length..]}";
}
}
GUITextBlock textBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), subList.Content.RectTransform) { MinSize = new Point(0, 30) },
ToolBox.LimitString(sub.Name, GUIStyle.Font, subList.Rect.Width - 80))
LocalizedString limitedName = ToolBox.LimitString(sub.Name, GUIStyle.Font, subList.Rect.Width - 80);
GUITextBlock textBlock = new(new RectTransform(Vector2.UnitX, subList.Content.RectTransform) { MinSize = new Point(0, 30) }, limitedName)
{
UserData = sub,
ToolTip = pathWithoutUserName
ToolTip = displayPath
};
if (!(ContentPackageManager.VanillaCorePackage?.Files.Any(f => f.Path == sub.FilePath) ?? false))
if (sub.IsFromRemoteStorage)
{
// remote storage
textBlock.OverrideTextColor(RemoteStorageHelper.SteamColor);
}
else if (ContentPackageManager.VanillaCorePackage == null || ContentPackageManager.VanillaCorePackage.Files.None(f => f.Path == sub.FilePath))
{
if (GetLocalPackageThatOwnsSub(sub) == null &&
ContentPackageManager.AllPackages.FirstOrDefault(p => p.Files.Any(f => f.Path == sub.FilePath)) is ContentPackage subPackage)
{
//workshop mod
// workshop mod
textBlock.OverrideTextColor(Color.MediumPurple);
}
else
{
//local mod
// local mod
textBlock.OverrideTextColor(GUIStyle.TextColorBright);
}
}
@@ -4018,10 +4094,9 @@ namespace Barotrauma
return false;
}
if (!(subList.SelectedComponent?.UserData is SubmarineInfo selectedSubInfo)) { return false; }
if (subList.SelectedComponent?.UserData is not SubmarineInfo selectedSubInfo) { return false; }
var ownerPackage = GetLocalPackageThatOwnsSub(selectedSubInfo);
if (ownerPackage is null)
if (!selectedSubInfo.IsFromRemoteStorage && GetLocalPackageThatOwnsSub(selectedSubInfo) is null)
{
if (IsVanillaSub(selectedSubInfo))
{
@@ -4183,21 +4258,23 @@ namespace Barotrauma
{
if (sub == null) { return; }
//If the sub is included in a content package that only defines that one sub,
//check that it's a local content package and only allow deletion if it is.
//(deleting from the Submarines folder is also currently allowed, but this is temporary)
var subPackage = GetLocalPackageThatOwnsSub(sub);
if (!ContentPackageManager.LocalPackages.Regular.Contains(subPackage)) { return; }
var msgBox = new GUIMessageBox(
TextManager.Get("DeleteDialogLabel"),
TextManager.GetWithVariable("DeleteDialogQuestion", "[file]", sub.Name),
new LocalizedString[] { TextManager.Get("Yes"), TextManager.Get("Cancel") });
// If the sub is included in a content package that only defines that one sub,
// check that it's a local content package and only allow deletion if it is.
// (deleting from the Submarines folder is also currently allowed, but this is temporary)
ContentPackage subPackage = GetLocalPackageThatOwnsSub(sub);
if (!ContentPackageManager.LocalPackages.Regular.Contains(subPackage)) { subPackage = null; }
if (!sub.IsFromRemoteStorage && subPackage == null) { return; }
GUIMessageBox msgBox = new(TextManager.Get("DeleteDialogLabel"), TextManager.GetWithVariable("DeleteDialogQuestion", "[file]", sub.Name), [TextManager.Get("Yes"), TextManager.Get("Cancel")]);
msgBox.Buttons[0].OnClicked += (btn, userData) =>
{
try
if (sub.IsFromRemoteStorage)
{
if (subPackage != null)
RemoteStorageHelper.TryDelete(sub.FilePath);
}
else if (subPackage != null)
{
try
{
File.Delete(sub.FilePath, catchUnauthorizedAccessExceptions: false);
ModProject modProject = new ModProject(subPackage);
@@ -4208,17 +4285,17 @@ namespace Barotrauma
{
MainSub.Info.FilePath = null;
}
}
sub.Dispose();
CreateLoadScreen();
}
catch (Exception e)
{
DebugConsole.ThrowErrorLocalized(TextManager.GetWithVariable("DeleteFileError", "[file]", sub.FilePath), e);
}
}
catch (Exception e)
{
DebugConsole.ThrowErrorLocalized(TextManager.GetWithVariable("DeleteFileError", "[file]", sub.FilePath), e);
}
return true;
sub.Dispose();
CreateLoadScreen();
return msgBox.Close(btn, userData);
};
msgBox.Buttons[0].OnClicked += msgBox.Close;
msgBox.Buttons[1].OnClicked += msgBox.Close;
}
@@ -445,7 +445,7 @@ namespace Barotrauma
{
DebugConsole.NewMessage("Missing Localization for property: " + propertyTag);
MissingLocalizations.Add($"sp.{propertyTag}.name|{displayName}");
MissingLocalizations.Add($"sp.{propertyTag}.description|{property.GetAttribute<Serialize>().Description}");
MissingLocalizations.Add($"sp.{propertyTag}.description|{property.GetAttribute<Serialize>()?.Description}");
}
}
#endif
@@ -467,7 +467,7 @@ namespace Barotrauma
}
if (toolTip.IsNullOrEmpty())
{
toolTip = property.GetAttribute<Serialize>().Description;
toolTip = property.GetAttribute<Serialize>()?.Description;
}
GUIComponent propertyField = null;
@@ -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);
@@ -0,0 +1,34 @@
#nullable enable
using Steamworks;
using System;
namespace Barotrauma.Steam;
internal static partial class RemoteStorageHelper
{
/// <summary>
/// Asks the user if they wish to enable remote storage. Accepting enables it automatically.
/// </summary>
/// <param name="onAccepted">Invoked when the user accepts enabling remote storage.</param>
/// <param name="onRejected">Invoked when the user rejects enabling remote storage.</param>
/// <remarks>Closes automatically if remote storage was enabled outside of the game, or if remote storage can not be enabled.</remarks>
public static void AskToEnable(Action? onAccepted = null, Action? onRejected = null)
{
GUIMessageBox confirmBox = new GUIMessageBox(
TextManager.Get("RemoteStorageEnablePopup.Header"),
TextManager.Get("RemoteStorageEnablePopup.Text"),
[TextManager.Get("Yes"), TextManager.Get("No")],
autoCloseCondition: () => !SteamRemoteStorage.IsCloudEnabledForAccount || SteamRemoteStorage.IsCloudEnabledForApp);
confirmBox.Buttons[0].OnClicked += (btn, data) =>
{
SteamRemoteStorage.IsCloudEnabledForApp = true;
onAccepted?.Invoke();
return confirmBox.Close(btn, data);
};
confirmBox.Buttons[1].OnClicked += (btn, data) =>
{
onRejected?.Invoke();
return confirmBox.Close(btn, data);
};
}
}
@@ -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.13.3.1</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.13.3.1</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.13.3.1</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.13.3.1</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.13.3.1</Version>
<Copyright>Copyright © FakeFish 2018-2023</Copyright>
<Platforms>AnyCPU;x64</Platforms>
<AssemblyName>DedicatedServer</AssemblyName>
@@ -1799,22 +1799,35 @@ namespace Barotrauma
(Client client, Vector2 cursorWorldPos, string[] args) =>
{
if (Submarine.MainSub == null || Level.Loaded == null) { return; }
Submarine submarineToTeleport = Submarine.MainSub;
if (args.Length > 1)
{
foreach (Submarine sub in Submarine.Loaded.Where(s => s.PhysicsBody.BodyType == FarseerPhysics.BodyType.Dynamic))
{
if ((sub.Info.Name + "_" + sub.TeamID) == args[1])
{
submarineToTeleport = sub;
break;
}
}
}
if (args.Length == 0 || args[0].Equals("cursor", StringComparison.OrdinalIgnoreCase))
{
Submarine.MainSub.SetPosition(cursorWorldPos);
submarineToTeleport.SetPosition(cursorWorldPos);
}
else if (args[0].Equals("start", StringComparison.OrdinalIgnoreCase))
{
Submarine.MainSub.SetPosition(Level.Loaded.StartPosition - Vector2.UnitY * Submarine.MainSub.Borders.Height);
submarineToTeleport.SetPosition(Level.Loaded.StartPosition - Vector2.UnitY * submarineToTeleport.Borders.Height);
}
else if (args[0].Equals("end", StringComparison.OrdinalIgnoreCase))
{
Submarine.MainSub.SetPosition(Level.Loaded.EndPosition - Vector2.UnitY * Submarine.MainSub.Borders.Height);
submarineToTeleport.SetPosition(Level.Loaded.EndPosition - Vector2.UnitY * submarineToTeleport.Borders.Height);
}
else if (args[0].Equals("endoutpost", StringComparison.OrdinalIgnoreCase))
{
Submarine.MainSub.SetPosition(Level.Loaded.EndExitPosition - Vector2.UnitY * Submarine.MainSub.Borders.Height);
var submarineDockingPort = DockingPort.List.FirstOrDefault(d => d.Item.Submarine == Submarine.MainSub);
submarineToTeleport.SetPosition(Level.Loaded.EndExitPosition - Vector2.UnitY * submarineToTeleport.Borders.Height);
var submarineDockingPort = DockingPort.List.FirstOrDefault(d => d.Item.Submarine == submarineToTeleport);
if (Level.Loaded?.EndOutpost == null)
{
NewMessage("Can't teleport the sub to the end outpost (no outpost at the end of the level).", Color.Red);
@@ -2735,6 +2748,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) =>
{
@@ -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();
@@ -1179,6 +1179,7 @@ namespace Barotrauma
NetWalletTransfer transfer = INetSerializableStruct.Read<NetWalletTransfer>(msg);
if (GameMain.Server is null) { return; }
if (transfer.Amount <= 0) { return; }
if (transfer.Sender.TryUnwrap(out var id))
{
@@ -193,13 +193,6 @@ namespace Barotrauma
(item.PreviousParentInventory == null ||
!sender.Character.CanAccessInventory(item.PreviousParentInventory));
// Prevent modified clients from being able to steal items from characters by item swapping with an existing item
// due to drag and drop being enabled
if (!sender.Character.CanAccessInventory(this, CharacterInventory.AccessLevel.AllowBotsAndPets) && GetItemAt(slotIndex) != null)
{
itemAccessDenied = true;
}
//more restricted "adding" of handcuffs: we can't allow putting handcuffs on a player just because dragging and dropping is allowed
if (item.HasTag(Tags.HandLockerItem) && !itemAccessDenied)
{
@@ -219,7 +212,7 @@ namespace Barotrauma
continue;
}
}
TryPutItem(item, slotIndex, true, true, sender.Character, false);
TryPutItem(item, slotIndex, allowSwapping: false, allowCombine: false, user: sender.Character, createNetworkEvent: false);
for (int j = 0; j < capacity; j++)
{
if (slots[j].Contains(item) && !receivedItemIdsFromClient[j].Contains(item.ID))
@@ -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)
@@ -8,6 +8,17 @@ namespace Barotrauma.Networking
{
partial class ChatMessage
{
private static string SanitizeText(Client client, string text)
{
if (!client.HasPermission(ClientPermissions.SpamImmunity))
{
// Prevent clients without spam immunity from being able to use RichString features
text = text.Replace('‖', ' ');
}
return text;
}
public static void ServerRead(IReadMessage msg, Client c)
{
c.KickAFKTimer = 0.0f;
@@ -69,8 +80,7 @@ namespace Barotrauma.Networking
txt = msg.ReadString() ?? "";
}
// Sanitize incoming text message from client so they can't use RichString features
txt = txt.Replace('‖', ' ');
txt = SanitizeText(c, txt);
if (!NetIdUtils.IdMoreRecent(ID, c.LastSentChatMsgID)) { return; }
@@ -904,7 +904,10 @@ namespace Barotrauma.Networking
string subHash = inc.ReadString();
CampaignSettings settings = INetSerializableStruct.Read<CampaignSettings>(inc);
var matchingSub = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name == subName && s.MD5Hash.StringRepresentation == subHash);
var matchingSub =
ServerSettings.AllowSubVoting ?
Voting.HighestVoted<SubmarineInfo>(VoteType.Sub, connectedClients) :
SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name == subName && s.MD5Hash.StringRepresentation == subHash);
if (GameStarted)
{
@@ -1150,7 +1153,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 +1219,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:");
@@ -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));
(!c.NeedsMidRoundSync || 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;
@@ -54,13 +54,14 @@ namespace Barotrauma.Networking
{
if (recipient == sender) { continue; }
if (!CanReceive(sender, recipient, out float distanceFactor)) { continue; }
if (!CanReceive(sender, recipient, out float distanceFactor, out bool isRadio)) { continue; }
IWriteMessage msg = new WriteOnlyMessage();
msg.WriteByte((byte)ServerPacketHeader.VOICE);
msg.WriteByte((byte)queue.QueueID);
msg.WriteRangedSingle(distanceFactor, 0.0f, 1.0f, 8);
msg.WriteBoolean(isRadio);
queue.Write(msg);
netServer.Send(msg, recipient.Connection, DeliveryMethod.Unreliable);
@@ -68,15 +69,17 @@ namespace Barotrauma.Networking
}
}
private static bool CanReceive(Client sender, Client recipient, out float distanceFactor)
private static bool CanReceive(Client sender, Client recipient, out float distanceFactor, out bool isRadio)
{
if (Screen.Selected != GameMain.GameScreen)
{
distanceFactor = 0.0f;
return true;
isRadio = false;
return true;
}
distanceFactor = 0.0f;
isRadio = false;
//no-one can hear muted players
if (sender.Muted) { return false; }
@@ -109,12 +112,14 @@ namespace Barotrauma.Networking
if (recipientSpectating)
{
isRadio = true;
if (recipient.SpectatePos == null) { return true; }
distanceFactor = MathHelper.Clamp(Vector2.Distance(sender.Character.WorldPosition, recipient.SpectatePos.Value) / senderRadio.Range, 0.0f, 1.0f);
return distanceFactor < 1.0f;
}
else if (recipientRadio != null && recipientRadio.CanReceive(senderRadio))
{
isRadio = true;
distanceFactor = MathHelper.Clamp(Vector2.Distance(sender.Character.WorldPosition, recipient.Character.WorldPosition) / senderRadio.Range, 0.0f, 1.0f);
return true;
}
@@ -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)
{
@@ -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.13.3.1</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" />
@@ -23,6 +23,70 @@
</Holdable>
</Item>
<Item name="fliptestweapon" identifier="fliptestweapon" category="Weapon" cargocontaineridentifier="metalcrate" tags="mediumitem,weapon,gun,gunsmith,provocativetohumanai,mountableweapon" Scale="0.5" impactsoundtag="impact_metal_light">
<PreferredContainer primary="secarmcab" amount="1" spawnprobability="0.2" notcampaign="true" notpvp="true" />
<PreferredContainer secondary="wrecksecarmcab,abandonedsecarmcab,piratesecarmcab" spawnprobability="0.1" />
<PreferredContainer secondary="armcab" />
<Price baseprice="1000" sold="false" minleveldifficulty="30">
<Price storeidentifier="merchantoutpost" multiplier="1.5" />
<Price storeidentifier="merchantcity" multiplier="1.25" />
<Price storeidentifier="merchantresearch" multiplier="1.25" />
<Price storeidentifier="merchantmilitary" sold="true" multiplier="0.9" minavailable="1" />
<Price storeidentifier="merchantmine" multiplier="1.25" />
<Price storeidentifier="merchantarmory" sold="true" multiplier="0.9" minavailable="1" />
</Price>
<Deconstruct time="10">
<Item identifier="steel" />
<Item identifier="titaniumaluminiumalloy" />
</Deconstruct>
<InventoryIcon texture="Content/Items/InventoryIconAtlas.png" sourcerect="896,831,64,64" origin="0.5,0.5" />
<Sprite texture="Content/Items/Weapons/weapons_new.png" sourcerect="0,244,186,65" depth="0.55" origin="0.5,0.5" />
<Body width="170" height="40" density="25" />
<Holdable slots="RightHand+LeftHand" controlpose="true" holdpos="40,-20" aimpos="45,-10" handle1="-33,-15" handle2="26,5" holdangle="-25">
<StatusEffect type="OnActive" target="This" offset="80,20" OffsetCopiesEntityTransform="true">
<ParticleEmitter particle="electricshock" particlespersecond="60" copyentityangle="true" velocitymin="0" velocitymax="100" distancemin="0" distancemax="10" scalemin="0.03" scalemax="0.04" />
</StatusEffect>
</Holdable>
<Wearable slots="Bag" msg="ItemMsgEquipSelect" canbeselected="false" canbepicked="true" pickkey="Select">
<sprite name="Grenade Launcher Worn" texture="Content/Items/Weapons/weapons_new.png" canbehiddenbyotherwearables="false" sourcerect="0,244,186,65" rotation="90" depth="0.6" limb="Torso" depthlimb="LeftArm" scale="0.5" origin="0.5,0.8" />
</Wearable>
<RangedWeapon barrelpos="80,11" reload="0.8" spread="1" unskilledspread="10" combatPriority="75" drawhudwhenequipped="true" crosshairscale="0.2">
<Crosshair texture="Content/Items/Weapons/Crosshairs.png" sourcerect="0,256,256,256" />
<CrosshairPointer texture="Content/Items/Weapons/Crosshairs.png" sourcerect="256,256,256,256" />
<ParticleEmitter particle="muzzleflashchaingun" particleamount="1" velocitymin="0" velocitymax="0" scalemin="0.5" scalemax="0.6" />
<ParticleEmitter particle="explosionsmoke" particleamount="1" velocitymin="0" velocitymax="0" scalemin="0.5" scalemax="0.6" />
<Sound file="Content/Items/Weapons/GrenadeLauncherShot1.ogg" type="OnUse" range="1000" />
<Sound file="Content/Items/Weapons/GrenadeLauncherShot2.ogg" type="OnUse" range="1000" />
<Sound file="Content/Items/Weapons/GrenadeLauncherShot3.ogg" type="OnUse" range="1000" />
<StatusEffect type="OnUse" target="This">
<Explosion range="150.0" force="2" shockwave="false" smoke="false" flames="false" flash="true" sparks="false" underwaterbubble="false" applyfireeffects="false" camerashake="6.0" />
</StatusEffect>
<RequiredItems items="grenade" type="Contained" msg="ItemMsgAmmoRequired" />
<RequiredSkill identifier="weapons" level="60" />
</RangedWeapon>
<!--Holds six grenades at a time-->
<ItemContainer capacity="6" maxstacksize="1" hideitems="false" ShowTotalStackCapacityInContainedStateIndicator="true" containedstateindicatorstyle="bullet" containedspritedepth="0.56">
<Containable items="grenade" hide="true" />
<SlotIcon slotindex="0" texture="Content/UI/StatusMonitorUI.png" sourcerect="448,448,64,64" origin="0.5,0.5" />
<SlotIcon slotindex="1" texture="Content/UI/StatusMonitorUI.png" sourcerect="448,448,64,64" origin="0.5,0.5" />
<SlotIcon slotindex="2" texture="Content/UI/StatusMonitorUI.png" sourcerect="448,448,64,64" origin="0.5,0.5" />
<SlotIcon slotindex="3" texture="Content/UI/StatusMonitorUI.png" sourcerect="448,448,64,64" origin="0.5,0.5" />
<SlotIcon slotindex="4" texture="Content/UI/StatusMonitorUI.png" sourcerect="448,448,64,64" origin="0.5,0.5" />
<SlotIcon slotindex="5" texture="Content/UI/StatusMonitorUI.png" sourcerect="448,448,64,64" origin="0.5,0.5" />
<SlotIcon slotindex="6" texture="Content/UI/StatusMonitorUI.png" sourcerect="320,448,64,64" origin="0.5,0.5" />
<SubContainer capacity="1" maxstacksize="1">
<Containable items="flashlight" hide="false" itempos="22,-1" setactive="true" />
</SubContainer>
</ItemContainer>
<aitarget sightrange="500" soundrange="500" fadeouttime="3" />
<Quality>
<QualityStat stattype="ExplosionRadius" value="0.1" />
<QualityStat stattype="ExplosionDamage" value="0.1" />
</Quality>
<Upgrade gameversion="0.10.0.0" scale="0.5" />
<SkillRequirementHint identifier="weapons" level="60" />
</Item>
<Item name="fliptestlight" identifier="fliptestlighttower" width="176" height="352" texturescale="1.0,1.0" scale="0.5" category="Decorative" subcategory="mining" noninteractable="true">
<sprite texture="Content/Map/Outposts/Art/TunnelWalls.png" sourcerect="849,1697,176,352" depth="0.97" premultiplyalpha="false" origin="0.5,0.5" />
<LightComponent range="160.0" lightcolor="255,234,181,200" IsOn="true" castshadows="false" LightOffset="200,147" allowingameediting="false">
@@ -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>
@@ -524,8 +524,7 @@ namespace Barotrauma
{
UnlockAchievement(causeOfDeath.Killer, "killpoison".ToIdentifier());
}
else if (item.Prefab.Identifier == "nuclearshell" ||
item.Prefab.Identifier == "nucleardepthcharge")
else if (item.Prefab.Tags.Contains("nuclearexplosive"))
{
UnlockAchievement(causeOfDeath.Killer, "killnuke".ToIdentifier());
}
@@ -197,6 +197,18 @@ namespace Barotrauma
private readonly List<Body> myBodies;
#if SERVER
/// <summary>
/// How often the server can send messages about a limb targeting some attack target.
/// Mainly relevant for attacks with no cooldown, e.g. fractal guardian's steam cannons which run continuously over time (we can't send events every frame)
/// </summary>
private const double MinSetAttackTargetEventInterval = 0.5;
private IDamageable lastDamageTarget;
private Limb lastTargetLimb;
private Limb lastAttackLimb;
private double lastSetAttackTargetEventTime;
#endif
public LatchOntoAI LatchOntoAI { get; private set; }
public SwarmBehavior SwarmBehavior { get; private set; }
public PetBehavior PetBehavior { get; private set; }
@@ -242,10 +254,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)
@@ -2676,11 +2691,19 @@ namespace Barotrauma
if (!ActiveAttack.IsRunning)
{
#if SERVER
GameMain.NetworkMember.CreateEntityEvent(Character, new Character.SetAttackTargetEventData(
AttackLimb,
damageTarget,
targetLimb,
SimPosition));
if (Timing.TotalTime > lastSetAttackTargetEventTime + MinSetAttackTargetEventInterval ||
damageTarget != lastDamageTarget || AttackLimb != lastAttackLimb || targetLimb != lastTargetLimb)
{
GameMain.NetworkMember.CreateEntityEvent(Character, new Character.SetAttackTargetEventData(
AttackLimb,
damageTarget,
targetLimb,
SimPosition));
lastSetAttackTargetEventTime = Timing.TotalTime;
lastDamageTarget = damageTarget;
lastAttackLimb = AttackLimb;
lastTargetLimb = targetLimb;
}
#else
Character.PlaySound(CharacterSound.SoundType.Attack, maxInterval: 3);
#endif
@@ -3075,11 +3098,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;
@@ -46,7 +46,7 @@ namespace Barotrauma
private float respondToAttackTimer;
private const float RespondToAttackInterval = 1.0f;
private bool wasConscious;
private bool wasDead;
private bool freezeAI;
@@ -201,7 +201,7 @@ namespace Barotrauma
}
if (isIncapacitated) { return; }
wasConscious = true;
wasDead = false;
respondToAttackTimer -= deltaTime;
if (respondToAttackTimer <= 0.0f)
@@ -1256,14 +1256,15 @@ namespace Barotrauma
public override void OnAttacked(Character attacker, AttackResult attackResult)
{
// The attack incapacitated/killed the character: respond immediately to trigger nearby characters because the update loop no longer runs
if (wasConscious && (Character.IsIncapacitated || Character.Stun > 0.0f))
// If the character is incapacitated or dead, respond to the attack anyway to let other nearby characters react to it
// (But if the character is already dead, and was dead before this attack, don't react)
if (Character.IsDead && wasDead) { return; }
if (Character.IsIncapacitated || Character.Stun > 0.0f)
{
RespondToAttack(attacker, attackResult);
wasConscious = false;
wasDead = Character.IsDead;
return;
}
if (Character.IsDead) { return; }
if (attacker == null || Character.IsPlayer)
{
// The player characters need to "respond" to the attack always, because the update loop doesn't run for them.
@@ -1467,10 +1468,10 @@ namespace Barotrauma
otherHumanAI.VisibleHulls.Contains(attacker.CurrentHull) ||
otherCharacter.CanSeeTarget(attacker, seeThroughWindows: true);
if (!isWitnessing)
{
if (Character.IsDead || Character.IsUnconscious || otherCharacter.TeamID != Character.TeamID)
{
if (Character.IsKnockedDown || otherCharacter.TeamID != Character.TeamID)
{
// Dead or in different team -> cannot report.
// Knocked down or in different team -> cannot report.
continue;
}
if (otherHumanAI.objectiveManager.HasOrders())
@@ -1494,6 +1495,14 @@ namespace Barotrauma
continue;
}
}
else if (!otherCharacter.IsSecurity)
{
//witnessed the attack as non-security, trigger security
foreach (Character security in Character.CharacterList.Where(c => c.TeamID == otherCharacter.TeamID))
{
TriggerSecurity(security.AIController as HumanAIController, attacker, DetermineCombatMode(security, cumulativeDamage, isWitnessing));
}
}
float delay = isWitnessing ? GetReactionTime() : Rand.Range(2.0f, 3.0f, Rand.RandSync.Unsynced);
otherHumanAI.AddCombatObjective(combatMode, attacker, delay);
}
@@ -1926,12 +1935,12 @@ namespace Barotrauma
character.IsCriminal = true;
character.IsActingOffensively = true;
}
if (!TriggerSecurity(otherHumanAI, combatMode))
if (!TriggerSecurity(otherHumanAI, character, combatMode))
{
// Else call the others
foreach (Character security in Character.CharacterList.Where(c => c.TeamID == otherCharacter.TeamID).OrderBy(c => Vector2.DistanceSquared(character.WorldPosition, c.WorldPosition)))
{
if (!TriggerSecurity(security.AIController as HumanAIController, combatMode))
if (!TriggerSecurity(security.AIController as HumanAIController, character, combatMode))
{
// Only alert one guard at a time
return;
@@ -1941,25 +1950,25 @@ namespace Barotrauma
}
}
bool TriggerSecurity(HumanAIController humanAI, AIObjectiveCombat.CombatMode combatMode)
{
if (humanAI == null) { return false; }
if (!humanAI.Character.IsSecurity) { return false; }
if (humanAI.ObjectiveManager.IsCurrentObjective<AIObjectiveCombat>()) { return false; }
humanAI.AddCombatObjective(combatMode, character, delay: GetReactionTime(),
onCompleted: () =>
{
//if the target is arrested successfully, reset the damage accumulator
foreach (Character anyCharacter in Character.CharacterList)
}
private static bool TriggerSecurity(HumanAIController humanAI, Character targetCharacter, AIObjectiveCombat.CombatMode combatMode)
{
if (humanAI == null) { return false; }
if (!humanAI.Character.IsSecurity) { return false; }
if (humanAI.ObjectiveManager.IsCurrentObjective<AIObjectiveCombat>()) { return false; }
humanAI.AddCombatObjective(combatMode, targetCharacter, delay: GetReactionTime(),
onCompleted: () =>
{
//if the target is arrested successfully, reset the damage accumulator
foreach (Character anyCharacter in Character.CharacterList)
{
if (anyCharacter.AIController is HumanAIController anyAI)
{
if (anyCharacter.AIController is HumanAIController anyAI)
{
anyAI.structureDamageAccumulator?.Remove(character);
}
anyAI.structureDamageAccumulator?.Remove(targetCharacter);
}
});
return true;
}
}
});
return true;
}
public static void ItemTaken(Item item, Character thief)
@@ -133,7 +133,7 @@ namespace Barotrauma
bool operateExtinguisher = !moveCloser || (dist < extinguisher.Range * 1.2f && character.CanSeeTarget(targetHull));
if (operateExtinguisher)
{
character.CursorPosition = fs.Position;
character.CursorPosition = FarseerPhysics.ConvertUnits.ToDisplayUnits(Submarine.GetRelativeSimPositionFromWorldPosition(fs.WorldPosition, character.Submarine, fs.Submarine));
Vector2 fromCharacterToFireSource = fs.WorldPosition - character.WorldPosition;
character.CursorPosition += VectorExtensions.Forward(extinguisherItem.body.TransformedRotation + (float)Math.Sin(sinTime) / 2, fromCharacterToFireSource.Length() / 2);
if (extinguisherItem.RequireAimToUse)
@@ -173,6 +173,16 @@ namespace Barotrauma
if (character.CanInteractWith(Item, out _, checkLinked: false))
{
waitTimer += deltaTime;
//if we're climbing upwards to the item, ensure the character stays within arm's length of it
//without this, the character can get stuck in a loop where the GoTo objective takes them close enough to the item,
//then the character shifts a bit downwards on the ladder and goes outside interaction range, and the GoTo objective kicks in again
if (character.IsClimbing &&
Item.WorldPosition.Y > character.WorldPosition.Y + FarseerPhysics.ConvertUnits.ToDisplayUnits(character.AnimController.ArmLength))
{
character.AIController.SteeringManager.SteeringManual(deltaTime, Vector2.UnitY);
}
if (waitTimer < WaitTimeBeforeRepair) { return; }
HumanAIController.FaceTarget(Item);
@@ -1193,6 +1193,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 +1218,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;
@@ -2235,8 +2240,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)
{
@@ -758,6 +758,11 @@ namespace Barotrauma
public float CoolDownTimer { get; set; }
public float CurrentRandomCoolDown { get; private set; }
public float SecondaryCoolDownTimer { get; set; }
/// <summary>
/// The attack is considered to be running from the moment it starts until the <see cref="AttackTimer"/> reaches the <see cref="Duration"/> of the attack, or until the attack lands successfully.
/// E.g. from the moment the monster decides to lunge itself towards the target until it hits a target or until it completes that lunge.
/// </summary>
public bool IsRunning { get; private set; }
public float AfterAttackTimer { get; set; }
@@ -997,6 +997,9 @@ namespace Barotrauma
public bool IsForceRagdolled;
public bool FollowCursor = true;
/// <summary>
/// Is the character currently dead, unconscious or paralyzed?
/// </summary>
public bool IsIncapacitated
{
get
@@ -1006,6 +1009,9 @@ namespace Barotrauma
}
}
/// <summary>
/// Is the character dead or below 0 vitality and not able to stay conscious?
/// </summary>
public bool IsUnconscious
{
get { return CharacterHealth.IsUnconscious; }
@@ -1673,7 +1679,8 @@ namespace Barotrauma
AnimController.FindHull(setInWater: true);
if (AnimController.CurrentHull != null) { Submarine = AnimController.CurrentHull.Submarine; }
IsContainable = prefab.ConfigElement.GetAttributeBool(nameof(IsContainable), def: Mass <= 30.0f);
//mass < 35 = husk chimera is the largest vanilla monster that can be contained by default
IsContainable = prefab.ConfigElement.GetAttributeBool(nameof(IsContainable), def: Mass < 35.0f);
CharacterList.Add(this);
@@ -2262,7 +2269,10 @@ namespace Barotrauma
{
Vector2 targetMovement = GetTargetMovement();
AnimController.TargetMovement = targetMovement;
AnimController.IgnorePlatforms = AnimController.TargetMovement.Y < -0.1f;
if (SelectedItem?.GetComponent<Controller>() is not { ControlCharacterPose: true })
{
AnimController.IgnorePlatforms = AnimController.TargetMovement.Y < -0.1f;
}
}
if (AnimController is HumanoidAnimController humanAnimController)
@@ -3432,21 +3442,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
@@ -3520,9 +3518,11 @@ namespace Barotrauma
UpdateAttackers(deltaTime);
foreach (var characterTalent in characterTalents)
//use a for loop instead of foreach because talents can unlock other talents via StatusEffectAction (see #17328)
//this way we'll just add them to the end of the list without causing a collection was modified exception
for (int i = 0; i < characterTalents.Count; i++)
{
characterTalent.UpdateTalent(deltaTime);
characterTalents[i].UpdateTalent(deltaTime);
}
if (IsDead) { return; }
@@ -5813,6 +5813,12 @@ namespace Barotrauma
return info.UnlockedTalents.Contains(identifier);
}
public bool IsTalentLocked(Identifier talentIdentifier)
{
if (info == null) { return true; }
return Info.GetSavedStatValue(StatTypes.LockedTalents, talentIdentifier) >= 1;
}
public bool HasUnlockedAllTalents()
{
if (TalentTree.JobTalentTrees.TryGet(Info.Job.Prefab.Identifier, out TalentTree talentTree))
@@ -505,17 +505,19 @@ namespace Barotrauma
/// </summary>
public float GetResistance(AfflictionPrefab afflictionPrefab, LimbType limbType)
{
// This is a % resistance (0 to 1.0)
float resistance = 0.0f;
foreach (KeyValuePair<Affliction, LimbHealth> kvp in afflictions)
{
var affliction = kvp.Key;
resistance += affliction.GetResistance(afflictionPrefab.Identifier, limbType);
lock (afflictions) {
// This is a % resistance (0 to 1.0)
float resistance = 0.0f;
foreach (KeyValuePair<Affliction, LimbHealth> kvp in afflictions)
{
var affliction = kvp.Key;
resistance += affliction.GetResistance(afflictionPrefab.Identifier, limbType);
}
// This is a multiplier, ie. 0.0 = 100% resistance and 1.0 = 0% resistance
float abilityResistanceMultiplier = Character.GetAbilityResistance(afflictionPrefab);
// The returned value is calculated to be a % resistance again
return 1 - ((1 - resistance) * abilityResistanceMultiplier);
}
// This is a multiplier, ie. 0.0 = 100% resistance and 1.0 = 0% resistance
float abilityResistanceMultiplier = Character.GetAbilityResistance(afflictionPrefab);
// The returned value is calculated to be a % resistance again
return 1 - ((1 - resistance) * abilityResistanceMultiplier);
}
public float GetStatValue(StatTypes statType)
@@ -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
@@ -1022,6 +1022,15 @@ namespace Barotrauma
partial void UpdateProjSpecific(float deltaTime);
#if SERVER
/// <summary>
/// How often the server can send messages about attacks being executed. Note that the timer is per-limb: if one limb executes an attack immediately after another, an network event can still be created.
/// Mainly relevant for attacks with no cooldown, e.g. fractal guardian's steam cannons which run continuously over time (we can't send events every frame)
/// </summary>
private const double MinExecuteAttackEventInterval = 0.5f;
private double lastExecuteAttackEventTime;
#endif
private readonly List<Body> contactBodies = new List<Body>();
/// <summary>
/// Returns true if the attack successfully hit something. If the distance is not given, it will be calculated.
@@ -1142,9 +1151,13 @@ namespace Barotrauma
ExecuteAttack(damageTarget, targetLimb, out attackResult);
}
#if SERVER
GameMain.NetworkMember.CreateEntityEvent(character, new Character.ExecuteAttackEventData(
attackLimb: this, targetEntity: damageTarget, targetLimb: targetLimb,
targetSimPos: attackSimPos));
if (Timing.TotalTime > lastExecuteAttackEventTime + MinExecuteAttackEventInterval)
{
GameMain.NetworkMember.CreateEntityEvent(character, new Character.ExecuteAttackEventData(
attackLimb: this, targetEntity: damageTarget, targetLimb: targetLimb,
targetSimPos: attackSimPos));
lastExecuteAttackEventTime = Timing.TotalTime;
}
#endif
}
@@ -43,14 +43,14 @@ namespace Barotrauma.Abilities
{
foreach (Identifier identifier in option.TalentIdentifiers)
{
if (IsShowCaseTalent(identifier, option) || TalentTree.IsTalentLocked(identifier, characters)) { continue; }
if (IsShowCaseTalent(identifier, option) || Character.IsTalentLocked(identifier)) { continue; }
identifiers.Add(identifier);
}
foreach (var (_, value) in option.ShowCaseTalents)
{
var ids = value.Where(i => !TalentTree.IsTalentLocked(i, characters)).ToImmutableHashSet();
var ids = value.Where(i => !Character.IsTalentLocked(i)).ToImmutableHashSet();
if (ids.Count is 0) { continue; }
identifiers.Add(value.GetRandomUnsynced());
@@ -131,7 +131,7 @@ namespace Barotrauma
if (character.Info.GetTotalTalentPoints() - selectedTalents.Count <= 0) { return false; }
if (!JobTalentTrees.TryGet(character.Info.Job.Prefab.Identifier, out TalentTree talentTree)) { return false; }
if (IsTalentLocked(talentIdentifier, Character.GetFriendlyCrew(character))) { return false; }
if (character.IsTalentLocked(talentIdentifier)) { return false; }
if (character.Info.GetUnlockedTalentsInTree().Contains(talentIdentifier))
{
@@ -163,16 +163,6 @@ namespace Barotrauma
return false;
}
public static bool IsTalentLocked(Identifier talentIdentifier, IEnumerable<Character> characterList)
{
foreach (Character c in characterList)
{
if (c.Info.GetSavedStatValue(StatTypes.LockedTalents, talentIdentifier) >= 1) { return true; }
}
return false;
}
public static List<Identifier> CheckTalentSelection(Character controlledCharacter, IEnumerable<Identifier> selectedTalents)
{
List<Identifier> viableTalents = new List<Identifier>();
@@ -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);
}
});
@@ -252,7 +252,7 @@ namespace Barotrauma
GameMain.NetworkMember.ShowNetStats = !GameMain.NetworkMember.ShowNetStats;
}));
commands.Add(new Command("spawn|spawncharacter", "spawn [creaturename/jobname] [near/inside/outside/cursor] [team] [add to crew (true/false)]: Spawn a creature at a random spawnpoint (use the second parameter to only select spawnpoints near/inside/outside the submarine). You can also enter the name of a job (e.g. \"Mechanic\") to spawn a character with a specific job and the appropriate equipment.", null,
commands.Add(new Command("spawn|spawncharacter", "spawn [creaturename/jobname] [near/inside/outside/cursor] [team] [add to crew (true/false)] [name]: Spawn a creature at a random spawnpoint (use the second parameter to only select spawnpoints near/inside/outside the submarine). You can also enter the name of a job (e.g. \"Mechanic\") to spawn a character with a specific job and the appropriate equipment.", null,
() =>
{
string[] creatureAndJobNames =
@@ -271,7 +271,7 @@ namespace Barotrauma
};
}, isCheat: true));
commands.Add(new Command("give|giveitem", "give|giveitem [itemname/itemidentifier] [amount] [condition]: Spawn an item in the inventory of the controlled character",
commands.Add(new Command("give|giveitem", "give|giveitem [itemname/itemidentifier] [amount] [condition] [quality]: Spawn an item in the inventory of the controlled character",
(string[] args) =>
{
if (Character.Controlled == null)
@@ -292,9 +292,12 @@ namespace Barotrauma
},
getValidArgs: () =>
{
return new string[][]
return new string[][]
{
GetItemNameOrIdParams().ToArray()
GetItemNameOrIdParams().ToArray(),
new string[] { "1" },
new string[] { "100" },
ItemQualityNames.ToArray()
};
}, isCheat: true));
@@ -311,7 +314,7 @@ namespace Barotrauma
};
}, isCheat: true));
commands.Add(new Command("spawnitem", "spawnitem [itemname/itemidentifier] [cursor/inventory/cargo/random/[name]] [amount] [condition]: Spawn an item at the position of the cursor, in the inventory of the controlled character, in the inventory of the client with the given name, or at a random spawnpoint if the location parameter is omitted or \"random\".",
commands.Add(new Command("spawnitem", "spawnitem [itemname/itemidentifier] [cursor/inventory/cargo/random/[name]] [amount] [condition] [quality]: Spawn an item at the position of the cursor, in the inventory of the controlled character, in the inventory of the client with the given name, or at a random spawnpoint if the location parameter is omitted or \"random\".",
(string[] args) =>
{
TrySpawnItem(args);
@@ -321,7 +324,10 @@ namespace Barotrauma
return new string[][]
{
GetItemNameOrIdParams().ToArray(),
GetSpawnPosParams().ToArray()
GetSpawnPosParams().ToArray(),
new string[] { "1" },
new string[] { "100" },
ItemQualityNames.ToArray()
};
}, isCheat: true));
@@ -1324,6 +1330,7 @@ namespace Barotrauma
}
else
{
if (GameMain.GameSession?.Map is Map map) { NewMessage("Map seed: " + map.Seed); }
NewMessage("Level seed: " + Level.Loaded.Seed);
NewMessage("Level generation params: " + Level.Loaded.GenerationParams.Identifier);
NewMessage("Adjacent locations: " + (Level.Loaded.StartLocation?.Type.Identifier ?? "none".ToIdentifier()) + ", " + (Level.Loaded.StartLocation?.Type.Identifier ?? "none".ToIdentifier()));
@@ -1333,17 +1340,29 @@ namespace Barotrauma
}
},null));
commands.Add(new Command("teleportsub", "teleportsub [start/end/endoutpost/cursor]: Teleport the submarine to the position of the cursor, or the start or end of the level. The 'endoutpost' argument also automatically docks the sub with the outpost at the end of the level. WARNING: does not take outposts into account, so often leads to physics glitches. Only use for debugging.",
commands.Add(new Command("teleportsub", "teleportsub [start/end/endoutpost/cursor] [submarine_team]: Teleport the submarine to the position of the cursor, or the start or end of the level. The 'endoutpost' argument also automatically docks the sub with the outpost at the end of the level. WARNING: does not take outposts into account, so often leads to physics glitches. Only use for debugging.",
onExecute:(string[] args) =>
{
if (Submarine.MainSub == null) { return; }
Submarine submarineToTeleport = Submarine.MainSub;
if (args.Length > 1)
{
foreach (Submarine sub in Submarine.Loaded.Where(s => s.PhysicsBody.BodyType == FarseerPhysics.BodyType.Dynamic))
{
if ((sub.Info.Name + "_" + sub.TeamID) == args[1])
{
submarineToTeleport = sub;
break;
}
}
}
if (args.Length == 0 || args[0].Equals("cursor", StringComparison.OrdinalIgnoreCase))
{
#if SERVER
ThrowError("Cannot teleport the sub to the position of the cursor. Use \"start\" or \"end\", or execute the command as a client.");
#else
Submarine.MainSub.SetPosition(Screen.Selected.Cam.ScreenToWorld(PlayerInput.MousePosition));
submarineToTeleport.SetPosition(Screen.Selected.Cam.ScreenToWorld(PlayerInput.MousePosition));
#endif
}
else if (args[0].Equals("start", StringComparison.OrdinalIgnoreCase))
@@ -1356,9 +1375,9 @@ namespace Barotrauma
Vector2 pos = Level.Loaded.StartPosition;
if (Level.Loaded.StartOutpost != null)
{
pos -= Vector2.UnitY * (Submarine.MainSub.Borders.Height + Level.Loaded.StartOutpost.Borders.Height) / 2;
pos -= Vector2.UnitY * (submarineToTeleport.Borders.Height + Level.Loaded.StartOutpost.Borders.Height) / 2;
}
Submarine.MainSub.SetPosition(pos);
submarineToTeleport.SetPosition(pos);
}
else if (args[0].Equals("end", StringComparison.OrdinalIgnoreCase))
{
@@ -1370,9 +1389,9 @@ namespace Barotrauma
Vector2 pos = Level.Loaded.EndPosition;
if (Level.Loaded.EndOutpost != null)
{
pos -= Vector2.UnitY * (Submarine.MainSub.Borders.Height + Level.Loaded.EndOutpost.Borders.Height) / 2;
pos -= Vector2.UnitY * (submarineToTeleport.Borders.Height + Level.Loaded.EndOutpost.Borders.Height) / 2;
}
Submarine.MainSub.SetPosition(pos);
submarineToTeleport.SetPosition(pos);
}
else if (args[0].Equals("endoutpost", StringComparison.OrdinalIgnoreCase))
{
@@ -1381,8 +1400,8 @@ namespace Barotrauma
NewMessage("Can't teleport the sub to the end outpost (no outpost at the end of the level).", Color.Red);
return;
}
Submarine.MainSub.SetPosition(Level.Loaded.EndExitPosition - Vector2.UnitY * Submarine.MainSub.Borders.Height);
var submarineDockingPort = DockingPort.List.FirstOrDefault(d => d.Item.Submarine == Submarine.MainSub);
submarineToTeleport.SetPosition(Level.Loaded.EndExitPosition - Vector2.UnitY * submarineToTeleport.Borders.Height);
var submarineDockingPort = DockingPort.List.FirstOrDefault(d => d.Item.Submarine == submarineToTeleport);
var outpostDockingPort = DockingPort.List.FirstOrDefault(d => d.Item.Submarine == Level.Loaded.EndOutpost);
if (submarineDockingPort != null && outpostDockingPort != null)
{
@@ -1394,7 +1413,8 @@ namespace Barotrauma
{
return new string[][]
{
new string[] { "start", "end", "endoutpost", "cursor" }
new string[] { "start", "end", "endoutpost", "cursor" },
ListAvailableSubmarines()
};
}, isCheat: true));
@@ -2569,7 +2589,17 @@ namespace Barotrauma
return locationNames.ToArray();
}
private static string[] ListAvailableSubmarines()
{
List<string> submarineNames = new();
foreach (var submarine in Submarine.Loaded.Where(s => s.PhysicsBody.BodyType == FarseerPhysics.BodyType.Dynamic))
{
submarineNames.Add(submarine.Info.Name + "_" + submarine.TeamID);
}
return submarineNames.ToArray();
}
private static bool TryFindTeleportPosition(string locationName, out Vector2 teleportPosition)
{
if (Submarine.MainSub is Submarine mainSub && string.Equals(locationName, "mainsub", StringComparison.InvariantCultureIgnoreCase))
@@ -2952,7 +2982,7 @@ namespace Barotrauma
isHuman = job != null || characterLowerCase == CharacterPrefab.HumanSpeciesName;
}
ParseOptionalArgs(out Vector2 spawnPosition, out WayPoint spawnPoint, out CharacterTeamType? teamType, out bool addToCrew);
ParseOptionalArgs(out Vector2 spawnPosition, out WayPoint spawnPoint, out CharacterTeamType? teamType, out bool addToCrew, out string renameCharacter);
if (usePreConfiguredNPC)
{
@@ -2983,6 +3013,14 @@ namespace Barotrauma
CharacterInfo characterInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobOrJobPrefab: job, variant: variant);
Entity.Spawner.AddCharacterToSpawnQueue(CharacterPrefab.HumanSpeciesName, spawnPosition, characterInfo, onSpawn: newCharacter =>
{
if (renameCharacter != null)
{
if (renameCharacter.Length > 31)
{
renameCharacter = renameCharacter.Substring(0, 32);
}
newCharacter.Info.Name = renameCharacter;
}
SetTeamAndCrew(newCharacter);
newCharacter.GiveJobItems(isPvPMode: GameMain.GameSession?.GameMode is PvPMode, spawnPoint);
newCharacter.GiveIdCardTags(spawnPoint);
@@ -3010,7 +3048,7 @@ namespace Barotrauma
}
}
void ParseOptionalArgs(out Vector2 spawnPosition, out WayPoint spawnPoint, out CharacterTeamType? teamType, out bool addToCrew)
void ParseOptionalArgs(out Vector2 spawnPosition, out WayPoint spawnPoint, out CharacterTeamType? teamType, out bool addToCrew, out string renameCharacter)
{
spawnPosition = Vector2.Zero;
spawnPoint = null;
@@ -3096,6 +3134,12 @@ namespace Barotrauma
ThrowError($"Could not parse the \"add to crew\" argument ({args[argIndex]}). Defaulting to {addToCrew}.");
}
}
argIndex++;
renameCharacter = null;
if (args.Length > argIndex)
{
renameCharacter = args[argIndex];
}
}
}
@@ -3103,6 +3147,7 @@ namespace Barotrauma
{
yield return "cursor";
yield return "inventory";
yield return "cargo";
#if SERVER
if (GameMain.Server != null)
@@ -3143,6 +3188,8 @@ namespace Barotrauma
}
}
private static ImmutableArray<string> ItemQualityNames = ["normal", "good", "excellent", "masterwork"];
private static void TrySpawnItem(string[] args)
{
try
@@ -3203,7 +3250,8 @@ namespace Barotrauma
int amount = 1;
int conditionPrc = 100;
int itemQuality = 0;
if (TryGetSpawnPosParam(out string spawnLocation, out int spawnLocationIndex))
{
switch (spawnLocation)
@@ -3223,7 +3271,7 @@ namespace Barotrauma
break;
default:
var matchingCharacter = FindMatchingCharacter(args.Skip(1).Take(1).ToArray());
if (matchingCharacter != null){ spawnInventory = matchingCharacter.Inventory; }
if (matchingCharacter != null) { spawnInventory = matchingCharacter.Inventory; }
break;
}
@@ -3232,10 +3280,21 @@ namespace Barotrauma
if (!int.TryParse(args[spawnLocationIndex + 1], NumberStyles.Any, CultureInfo.InvariantCulture, out amount)) { amount = 1; }
amount = Math.Min(amount, 100);
}
if (args.Length > spawnLocationIndex + 2)
{
if (!int.TryParse(args[^1], NumberStyles.Any, CultureInfo.InvariantCulture, out conditionPrc)) { conditionPrc = 100; }
if (!int.TryParse(args[spawnLocationIndex + 2], NumberStyles.Any, CultureInfo.InvariantCulture, out conditionPrc)) { conditionPrc = 100; }
}
if (args.Length > spawnLocationIndex + 3)
{
for (int i = 0; i <= Quality.MaxQuality; i++)
{
if (args[spawnLocationIndex + 3].ToLowerInvariant() == ItemQualityNames[i])
{
itemQuality = i;
}
}
}
}
@@ -3257,7 +3316,7 @@ namespace Barotrauma
}
else
{
Entity.Spawner?.AddItemToSpawnQueue(itemPrefab, spawnPos.Value, condition: itemCondition);
Entity.Spawner?.AddItemToSpawnQueue(itemPrefab, spawnPos.Value, condition: itemCondition, quality: itemQuality);
}
}
else if (spawnInventory != null)
@@ -3284,6 +3343,7 @@ namespace Barotrauma
}
item.Condition = item.Health * Math.Clamp(conditionPrc / 100f, 0f, 1f);
item.Quality = itemQuality;
}
}
}
@@ -31,12 +31,17 @@ namespace Barotrauma
Actions = new List<EventAction>();
foreach (var e in element.Elements())
{
if (e.Name.ToString().Equals("statuseffect", StringComparison.OrdinalIgnoreCase))
if (e.NameAsIdentifier().Equals("statuseffect"))
{
DebugConsole.ThrowError($"Error in event prefab \"{scriptedEvent.Prefab.Identifier}\". Status effect configured as a sub action. Please configure status effects as child elements of a StatusEffectAction.",
contentPackage: element.ContentPackage);
continue;
}
else if (e.NameAsIdentifier().Equals(nameof(OnRoundEndAction)))
{
DebugConsole.ThrowError($"Error in event prefab \"{scriptedEvent.Prefab.Identifier}\". {nameof(OnRoundEndAction)} configured as a sub action. Please configure it as an action at the end of the event.",
contentPackage: element.ContentPackage);
}
var action = Instantiate(scriptedEvent, e);
if (action != null) { Actions.Add(action); }
}
@@ -123,7 +123,7 @@ namespace Barotrauma
AddTargetPredicate(
Tags.Traitor,
ScriptedEvent.TargetPredicate.EntityType.Character,
e => e is Character c && (c.IsPlayer || c.IsBot) && c.IsTraitor && !c.IsIncapacitated);
e => e is Character c && (c.IsPlayer || c.IsBot) && c.IsTraitor && !c.IsIncapacitated && CharacterTeamMatches(c));
}
private void TagNonTraitors()
@@ -131,7 +131,7 @@ namespace Barotrauma
AddTargetPredicate(
Tags.NonTraitor,
ScriptedEvent.TargetPredicate.EntityType.Character,
e => e is Character c && (c.IsPlayer || c.IsBot) && !c.IsTraitor && c.IsOnPlayerTeam && !c.IsIncapacitated);
e => e is Character c && (c.IsPlayer || c.IsBot) && !c.IsTraitor && c.IsOnPlayerTeam && !c.IsIncapacitated && CharacterTeamMatches(c));
}
private void TagNonTraitorPlayers()
@@ -139,7 +139,7 @@ namespace Barotrauma
AddTargetPredicate(
Tags.NonTraitorPlayer,
ScriptedEvent.TargetPredicate.EntityType.Character,
e => e is Character c && c.IsPlayer && !c.IsTraitor && c.IsOnPlayerTeam && !c.IsIncapacitated);
e => e is Character c && c.IsPlayer && !c.IsTraitor && c.IsOnPlayerTeam && !c.IsIncapacitated && CharacterTeamMatches(c));
}
private void TagBots(bool playerCrewOnly)
@@ -151,7 +151,8 @@ namespace Barotrauma
e is Character c &&
c.IsBot &&
(!c.IsIncapacitated || !IgnoreIncapacitatedCharacters) &&
(!playerCrewOnly || c.TeamID == CharacterTeamType.Team1));
(!playerCrewOnly || c.TeamID == CharacterTeamType.Team1) &&
CharacterTeamMatches(c));
}
private void TagCrew()
@@ -171,7 +172,7 @@ namespace Barotrauma
private void TagHumansByTag(Identifier tag)
{
AddTarget(Tag, Character.CharacterList.Where(c => c.HumanPrefab != null && c.HumanPrefab.GetTags().Contains(tag)));
AddTarget(Tag, Character.CharacterList.Where(c => c.HumanPrefab != null && c.HumanPrefab.GetTags().Contains(tag) && CharacterTeamMatches(c)));
}
private void TagHumansByJobIdentifier(Identifier jobIdentifier)
@@ -168,6 +168,9 @@ namespace Barotrauma.Items.Components
set { attachedByDefault = value; }
}
#if DEBUG
[Editable]
#endif
[Serialize("0.0,0.0", IsPropertySaveable.No, description: "The position the character holds the item at (in pixels, as an offset from the character's shoulder)."+
" For example, a value of 10,-100 would make the character hold the item 100 pixels below the shoulder and 10 pixels forwards.")]
public Vector2 HoldPos
@@ -177,8 +180,10 @@ namespace Barotrauma.Items.Components
}
//the distance from the holding characters elbow to center of the physics body of the item
protected Vector2 holdPos;
#if DEBUG
[Editable]
#endif
[Serialize("0.0,0.0", IsPropertySaveable.No, description: "The position the character holds the item at when aiming (in pixels, as an offset from the character's shoulder)."+
" Works similarly as HoldPos, except that the position is rotated according to the direction the player is aiming at. For example, a value of 10,-100 would make the character hold the item 100 pixels below the shoulder and 10 pixels forwards when aiming directly to the right.")]
public Vector2 AimPos
@@ -279,6 +284,9 @@ namespace Barotrauma.Items.Components
/// <summary>
/// For setting the handle positions using status effects
/// </summary>
#if DEBUG
[Editable]
#endif
public Vector2 Handle1
{
get { return ConvertUnits.ToDisplayUnits(handlePos[0]); }
@@ -299,6 +307,9 @@ namespace Barotrauma.Items.Components
/// <summary>
/// For setting the handle positions using status effects
/// </summary>
#if DEBUG
[Editable]
#endif
public Vector2 Handle2
{
get { return ConvertUnits.ToDisplayUnits(handlePos[1]); }
@@ -119,7 +119,7 @@ namespace Barotrauma.Items.Components
return OnPicked(picker, pickDroppedStack: true);
}
public virtual bool OnPicked(Character picker, bool pickDroppedStack)
public bool OnPicked(Character picker, bool pickDroppedStack, bool playSound = true)
{
//if the item has multiple Pickable components (e.g. Holdable and Wearable, check that we don't equip it in hands when the item is worn or vice versa)
if (item.GetComponents<Pickable>().Any())
@@ -156,7 +156,7 @@ namespace Barotrauma.Items.Components
ApplyStatusEffects(ActionType.OnPicked, 1.0f, picker);
#if CLIENT
if (!GameMain.Instance.LoadingScreenOpen && picker == Character.Controlled) { SoundPlayer.PlayUISound(GUISoundType.PickItem); }
if (!GameMain.Instance.LoadingScreenOpen && playSound && picker == Character.Controlled) { SoundPlayer.PlayUISound(GUISoundType.PickItem); }
PlaySound(ActionType.OnPicked, picker);
#endif
if (pickDroppedStack)
@@ -164,7 +164,7 @@ namespace Barotrauma.Items.Components
foreach (var droppedItem in droppedStack)
{
if (droppedItem == item) { continue; }
droppedItem.GetComponent<Pickable>().OnPicked(picker, pickDroppedStack: false);
droppedItem.GetComponent<Pickable>().OnPicked(picker, pickDroppedStack: false, playSound: false);
}
}
return true;
@@ -99,7 +99,7 @@ namespace Barotrauma.Items.Components
}
}
[Serialize("0.0,0.0", IsPropertySaveable.No, description: "The position where the contained items get drawn at. In the case of items with a physics body, the offset is from the center of the body, on items without one from the top-left corner of the sprite. In pixels.")]
[Serialize("0.0,0.0", IsPropertySaveable.No, description: "The position where the contained items get drawn at (offset from the upper left corner of the sprite in pixels).")]
public Vector2 ItemPos { get; set; }
[Serialize("0.0,0.0", IsPropertySaveable.No, description: "The interval at which the contained items are spaced apart from each other (in pixels).")]
@@ -1093,25 +1093,21 @@ namespace Barotrauma.Items.Components
{
if (item.body == null)
{
//if the item is a holdable item currently attached to a wall (i.e. normally has a physics body, but the body is now disabled),
//we must position the contained items using the center as the origin since the item positions have been configured with the assumption the item has a body
bool isAttachedHoldable = item.GetComponent<Holdable>() is { Attached: true };
bool useCenterAsOrigin = isAttachedHoldable;
if (flippedX)
{
transformedItemPos.X = -transformedItemPos.X;
if (!useCenterAsOrigin) { transformedItemPos.X += item.Rect.Width; }
transformedItemPos.X += item.Rect.Width;
transformedItemInterval.X = -transformedItemInterval.X;
transformedItemIntervalHorizontal.X = -transformedItemIntervalHorizontal.X;
}
if (flippedY)
{
transformedItemPos.Y = -transformedItemPos.Y;
if (!useCenterAsOrigin) { transformedItemPos.Y -= item.Rect.Height; }
transformedItemPos.Y -= item.Rect.Height;
transformedItemInterval.Y = -transformedItemInterval.Y;
transformedItemIntervalVertical.Y = -transformedItemIntervalVertical.Y;
}
transformedItemPos += useCenterAsOrigin ? item.Position : new Vector2(item.Rect.X, item.Rect.Y);
transformedItemPos += new Vector2(item.Rect.X, item.Rect.Y);
if (drawPosition)
{
if (item.Submarine != null) { transformedItemPos += item.Submarine.DrawPosition; }
@@ -1129,6 +1125,16 @@ namespace Barotrauma.Items.Components
}
else
{
if (item.GetComponent<Holdable>() is { Attachable: true })
{
//if the item is attachable to walls, we need a bit of special logic because the item can either
//have or not have a body depending on whether it's attached.
//since it seems previously the contained item positions have always been configured as if the item had no body (using the top-left corner as the origin),
//let's modify the position here to position the items correctly even when the body is active (moving the origin from the center of the body to the top-left corner)
transformedItemPos -= item.Rect.Size.FlipY().ToVector2() / 2;
}
Matrix transform = Matrix.CreateRotationZ(drawPosition ? item.body.DrawRotation : item.body.Rotation);
if (bodyFlipped)
{
@@ -42,6 +42,9 @@ namespace Barotrauma.Items.Components
set;
}
[Serialize(true, IsPropertySaveable.No, description: $"Should this item be removed if the linked character is null?")]
public bool RemoveItemIfCharacterNull { get; set; }
public Character? Character { get; private set; }
public bool DoesBleed => Character?.DoesBleed == true;
@@ -50,6 +53,8 @@ namespace Barotrauma.Items.Components
public LinkedControllerCharacterComponent(Item item, ContentXElement element) : base(item, element)
{
IsActive = true;
#if CLIENT
spriteOverrides = element.Elements()
.Where(static e => e.Name.LocalName.ToLowerInvariant() == "spriteoverride")
@@ -58,6 +63,16 @@ namespace Barotrauma.Items.Components
#endif
}
public override void Update(float deltaTime, Camera cam)
{
base.Update(deltaTime, cam);
if (RemoveItemIfCharacterNull && GameMain.NetworkMember is not { IsClient: true } && (Character == null || Character.Removed))
{
Entity.Spawner?.AddEntityToRemoveQueue(Item);
}
}
public void UpdateLinkedCharacter(Character? character)
{
Character = character;
@@ -525,7 +525,7 @@ namespace Barotrauma.Items.Components
return true;
}
if (containerToSpawnOnSelectedItem.Inventory.AllItems.Any(x => x.Prefab == spawnItemOnSelectedPrefab))
if (containerToSpawnOnSelectedItem.Inventory.AllItems.Any(item => item == spawnedItemOnSelected))
{
return true;
}
@@ -862,10 +862,6 @@ namespace Barotrauma.Items.Components
}
else if (CanBeSelectedByCharacters)
{
#if SERVER
GameServer.Log($"{GameServer.CharacterLogName(activator)} entered {item.Name}", ServerLog.MessageType.ItemInteraction);
#endif
activator.DeselectCharacter();
User = activator;
@@ -345,6 +345,8 @@ namespace Barotrauma.Items.Components
if (targetItem.AllowDeconstruct && allowRemove)
{
ApplyDeconstructionStatusEffects(targetItem, ActionType.OnDeconstructed, 1f);
//drop all items that are inside the deconstructed item
foreach (ItemContainer ic in targetItem.GetComponents<ItemContainer>())
{
@@ -480,6 +482,7 @@ namespace Barotrauma.Items.Components
// Move items again since the status effect could have spawned additional items in the character inventory
MoveItemsFromCharacterToOutput();
character.Kill(CauseOfDeathType.Unknown, causeOfDeathAffliction: null);
Entity.Spawner?.AddEntityToRemoveQueue(character);
}, 0.1f);
}
@@ -732,6 +732,7 @@ namespace Barotrauma.Items.Components
}
private readonly HashSet<Item> usedIngredients = new HashSet<Item>();
private readonly Dictionary<ItemPrefab, int> ingredientFlexibilityCache = new Dictionary<ItemPrefab, int>();
public bool MissingRequiredRecipe(FabricationRecipe fabricableItem, Character character)
{
@@ -786,10 +787,24 @@ namespace Barotrauma.Items.Components
//maintain a list of used ingredients so we don't end up considering the same item a suitable for multiple required ingredients
usedIngredients.Clear();
return fabricableItem.RequiredItems.All(requiredItem =>
// Items are considered more flexible if they can be used in many different requirements
ingredientFlexibilityCache.Clear();
foreach (var prefab in fabricableItem.RequiredItems.SelectMany(static r => r.ItemPrefabs))
{
ingredientFlexibilityCache[prefab] = fabricableItem.RequiredItems.Count(r => r.ItemPrefabs.Contains(prefab));
}
return fabricableItem.RequiredItems
// Match the most restrictive requirements to least restrictive first, while we still have items that we can use
.OrderBy(static r => r.ItemPrefabs.Count())
.ThenByDescending(static requiredItem => requiredItem.Amount)
.All(requiredItem =>
{
int availableItemsAmount = 0;
foreach (ItemPrefab requiredPrefab in requiredItem.ItemPrefabs)
foreach (ItemPrefab requiredPrefab in requiredItem.ItemPrefabs
// Fill in the least flexible and more abundant items first, so we don't end up using unique items first
.OrderBy(GetItemFlexibility)
.ThenByDescending(GetAvailableItemsCount))
{
if (!availableIngredients.TryGetValue(requiredPrefab.Identifier, out var availableItems)) { continue; }
@@ -811,6 +826,16 @@ namespace Barotrauma.Items.Components
return false;
});
int GetAvailableItemsCount(ItemPrefab itemPrefab)
{
return availableIngredients.TryGetValue(itemPrefab.Identifier, out var list) ? list.Count : 0;
}
int GetItemFlexibility(ItemPrefab itemPrefab)
{
return ingredientFlexibilityCache[itemPrefab];
}
}
private float GetRequiredTime(FabricationRecipe fabricableItem, Character user)
@@ -36,7 +36,7 @@ namespace Barotrauma.Items.Components
}
}
public LocalizedString? DisplayName { get; private set; }
public LocalizedString DisplayName { get; private set; }
private float supplyRatio = 1f;
public float SupplyRatio
@@ -80,6 +80,7 @@ namespace Barotrauma.Items.Components
SupplyRatio = element.GetAttributeFloat("ratio", SupplyRatio);
}
DisplayName = TextManager.Get(name).Fallback(name);
#if CLIENT
CreateGUI();
if (Screen.Selected is not { IsEditor: true })
@@ -252,7 +252,7 @@ namespace Barotrauma.Items.Components
{
PhysicsBody = new PhysicsBody(currentWidth, currentHeight, radius: 0.0f, density: 1.5f, BodyType.Static, Physics.CollisionWall, LevelTrigger.GetCollisionCategories(triggeredBy))
{
UserData = item
UserData = this
};
}
else
@@ -260,7 +260,7 @@ namespace Barotrauma.Items.Components
currentRadius = Math.Max(ConvertUnits.ToSimUnits(Radius * item.Scale), 0.01f);
PhysicsBody = new PhysicsBody(width: 0.0f, height: 0.0f, radius: currentRadius, density: 1.5f, BodyType.Static, Physics.CollisionWall, LevelTrigger.GetCollisionCategories(triggeredBy))
{
UserData = item
UserData = this
};
}
@@ -734,8 +734,8 @@ namespace Barotrauma
[Serialize(false, IsPropertySaveable.No, description: "Hides the condition displayed in the item's tooltip.")]
public bool HideConditionInTooltip { get; set; }
[Serialize("", IsPropertySaveable.No, description: "If set, displays if the given fabrication recipe has been unlocked or not in the tooltip. The actual unlocking of the recipe should be handled in a status effect.")]
public Identifier UnlockedRecipeInToolTip { get; set; }
[Serialize("", IsPropertySaveable.No, description: "If set, the item's tooltip displays if the given fabrication recipe has been unlocked or not. The actual unlocking of the recipe should be handled in a status effect.")]
public Identifier[] UnlockedRecipeInToolTip { get; set; }
//if true and the item has trigger areas defined, characters need to be within the trigger to interact with the item
//if false, trigger areas define areas that can be used to highlight the item
@@ -11,6 +11,7 @@ public interface ILuaCsHook : ILuaPatcher, ILuaCsShim
void Add(string eventName, string identifier, LuaCsFunc callback, object owner = null);
[Obsolete("ACsMod is deprecated. Use ILuaEventService.Add() instead.")]
void Add(string eventName, LuaCsFunc callback, object owner = null);
void Remove(string eventName, string identifier);
// Does anyone use this? TODO: Analyze old Lua mods for usage scenarios.
//bool Exists(string eventName, string identifier);
object Call(string eventName, params object[] args);
@@ -0,0 +1,305 @@
using Barotrauma;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.Loader;
using System.Xml.Linq;
namespace Barotrauma;
class LuaCsConfig
{
private enum ValueType
{
None,
Text,
Integer,
Decimal,
Boolean,
Collection,
Object,
Enum
}
private static Type[] LoadDocTypes(XElement typesElem)
{
var result = new List<Type>();
var loadedTypes = AssemblyLoadContext.All
.Where(alc => alc != AssemblyLoadContext.Default)
.SelectMany(alc => alc.Assemblies)
.SelectMany(asm => asm.GetTypes())
.ToImmutableArray();
foreach (var elem in typesElem.Elements())
{
var typesFound = loadedTypes.Where(t => t.FullName?.EndsWith(elem.Value) ?? false).ToImmutableList();
if (!typesFound.Any())
{
ModUtils.Logging.PrintError(
$"{nameof(LuaCsConfig)}::{nameof(LoadDocTypes)}() | Unable to find a matching type for {elem.Value}");
continue;
}
result.AddRange(typesFound);
}
return result.ToArray();
}
private static IEnumerable<XElement> SaveDocTypes(IEnumerable<Type> types)
{
return types.Select(t => new XElement("Type", t.ToString()));
}
private static Type GetTypeAttr(Type[] types, XElement elem)
{
var idx = elem.GetAttributeInt("Type", -1);
if (idx < 0 || idx >= types.Length) throw new Exception($"Type index '{idx}' is outside of saved types bounds");
return types[idx];
}
private static ValueType GetValueType(XElement elem)
{
Enum.TryParse(typeof(ValueType), elem.Attribute("Value")?.Value, out object result);
if (result != null) return (ValueType)result;
else return ValueType.None;
}
private static object ParseValue(Type[] types, XElement elem)
{
var type = GetValueType(elem);
if (elem.IsEmpty) return null;
if (type == ValueType.Enum)
{
var tType = GetTypeAttr(types, elem);
if (tType == null || !tType.IsSubclassOf(typeof(Enum))) return null;
if (Enum.TryParse(tType, elem.Value, out object result)) return result;
else return null;
}
if (type == ValueType.Collection)
{
var tType = GetTypeAttr(types, elem);
var tInt = tType.GetInterfaces().FirstOrDefault(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IEnumerable<>));
var gArg = tInt.GetGenericArguments()[0];
if (tType == null || !tType.GetInterfaces().Any(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IEnumerable<>))) return null;
object result = null;
if (result == null)
{
var ctor = tType.GetConstructors(BindingFlags.Public | BindingFlags.Instance).FirstOrDefault(c =>
{
var param = c.GetParameters();
return param.Count() == 1 && param.Any(p => p.ParameterType.IsGenericType && p.ParameterType.GetGenericTypeDefinition() == typeof(IEnumerable<>));
});
if (ctor != null)
{
var elements = elem.Elements().Select(x => ParseValue(types, x));
var castElems = typeof(Enumerable).GetMethod("Cast").MakeGenericMethod(gArg).Invoke(elements, new object[] { elements });
result = ctor.Invoke(new object[] { castElems });
}
}
if (result == null)
{
var ctor = tType.GetConstructors(BindingFlags.Public | BindingFlags.Instance).FirstOrDefault(c => c.GetParameters().Count() == 0);
var addMethod = tType.GetMethods(BindingFlags.Instance | BindingFlags.Public).FirstOrDefault(m =>
{
if (m.Name != "Add") return false;
var param = m.GetParameters();
return param.Count() == 1 && param[0].ParameterType == gArg;
});
if (ctor != null && addMethod != null)
{
var elements = elem.Elements().Select(x => ParseValue(types, x));
result = ctor.Invoke(null);
foreach (var el in elements) addMethod.Invoke(result, new object[] { el });
}
}
if (result == null)
{
var ctor = tType.GetConstructors(BindingFlags.Public | BindingFlags.Instance).FirstOrDefault();
var setMethod = tType.GetMethods(BindingFlags.Instance | BindingFlags.Public).FirstOrDefault(m =>
{
if (m.Name != "Set") return false;
var param = m.GetParameters();
return param.Count() == 2 && param[0].ParameterType == typeof(int) && param[1].ParameterType == gArg;
});
if (ctor != null || setMethod != null)
{
var elements = elem.Elements().Select(x => ParseValue(types, x));
result = ctor.Invoke(new object[] { elements.Count() });
int i = 0;
foreach (var el in elements)
{
setMethod.Invoke(result, new object[] { i, el });
i++;
}
}
}
return result;
}
else if (type == ValueType.Text) return elem.Value;
else if (type == ValueType.Integer)
{
int.TryParse(elem.Value, out var num);
return num;
}
else if (type == ValueType.Decimal)
{
float.TryParse(elem.Value, out var num);
return num;
}
else if (type == ValueType.Boolean)
{
bool.TryParse(elem.Value, out var boolean);
return boolean;
}
else if (type == ValueType.Object)
{
var tType = GetTypeAttr(types, elem);
if (tType == null) return null;
IEnumerable<FieldInfo> fields = tType.GetFields(BindingFlags.Instance | BindingFlags.Public)
.Concat(tType.GetFields(BindingFlags.Instance | BindingFlags.NonPublic));
IEnumerable<PropertyInfo> properties = tType.GetProperties(BindingFlags.Instance | BindingFlags.Public).Where(p => p.GetSetMethod() != null)
.Concat(tType.GetProperties(BindingFlags.Instance | BindingFlags.NonPublic).Where(p => p.GetSetMethod() != null));
object result = null;
var ctor = tType.GetConstructors(BindingFlags.Public | BindingFlags.Instance).FirstOrDefault(c => c.GetParameters().Count() == 0);
if (ctor == null)
{
if (!tType.IsValueType) return null;
result = Activator.CreateInstance(tType);
}
else result = ctor.Invoke(null);
foreach (var el in elem.Elements())
{
var value = ParseValue(types, el);
var field = fields.FirstOrDefault(f => f.Name == el.Name.LocalName);
if (field != null) field.SetValue(result, value);
var property = properties.FirstOrDefault(p => p.Name == el.Name.LocalName);
if (property != null) property.SetValue(result, value);
}
return result;
}
else return elem.Value;
}
private static void AddTypeAttr(List<Type> types, Type type, XElement elem)
{
if (!types.Contains(type)) types.Add(type);
elem.SetAttributeValue("Type", types.IndexOf(type));
}
private static XElement ParseObject(List<Type> types, string name, object value)
{
XElement result = new XElement(name);
if (value != null)
{
var tType = value.GetType();
if (tType.IsEnum)
{
result.SetAttributeValue("Value", ValueType.Enum);
AddTypeAttr(types, tType, result);
result.Value = Enum.GetName(tType, value) ?? "";
}
else if (value is string str)
{
result.SetAttributeValue("Value", ValueType.Text);
result.Value = str;
}
else if (value is int integer)
{
result.SetAttributeValue("Value", ValueType.Integer);
result.Value = integer.ToString();
}
else if (value is float || value is double)
{
result.SetAttributeValue("Value", ValueType.Decimal);
result.Value = value.ToString();
}
else if (value is bool boolean)
{
result.SetAttributeValue("Value", ValueType.Boolean);
result.Value = boolean.ToString();
}
else if (tType.GetInterfaces().Any(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IEnumerable<>)))
{
result.SetAttributeValue("Value", ValueType.Collection);
AddTypeAttr(types, tType, result);
var enumerator = (IEnumerator)tType.GetMethod("GetEnumerator").Invoke(value, null);
while (enumerator.MoveNext())
{
var elVal = ParseObject(types, "Item", enumerator.Current);
result.Add(elVal);
}
}
else if (tType.IsClass || tType.IsValueType)
{
result.SetAttributeValue("Value", ValueType.Object);
AddTypeAttr(types, tType, result);
IEnumerable<FieldInfo> fields = tType.GetFields(BindingFlags.Instance | BindingFlags.Public)
.Concat(tType.GetFields(BindingFlags.Instance | BindingFlags.NonPublic));
IEnumerable<PropertyInfo> properties = tType.GetProperties(BindingFlags.Instance | BindingFlags.Public).Where(p => p.GetSetMethod() != null)
.Concat(tType.GetProperties(BindingFlags.Instance | BindingFlags.NonPublic).Where(p => p.GetSetMethod() != null));
foreach (var field in fields) result.Add(ParseObject(types, field.Name, field.GetValue(value)));
foreach (var property in properties) result.Add(ParseObject(types, property.Name, property.GetValue(value)));
}
else
{
result.SetAttributeValue("Value", ValueType.None);
result.Value = value.ToString();
}
}
return result;
}
public static T Load<T>(FileStream file)
{
var doc = XDocument.Load(file);
var rootElems = doc.Root.Elements().ToArray();
var types = rootElems[0];
var elem = rootElems[1];
var dict = ParseValue(LoadDocTypes(types), elem);
if (dict.GetType() == typeof(T)) return (T)dict;
else throw new Exception($"Loaded configuration is not of the type '{typeof(T).Name}'");
}
public static void Save(FileStream file, object obj)
{
var types = new List<Type>();
var elem = ParseObject(types, "Root", obj);
var root = new XElement("Configuration", new XElement("Types", SaveDocTypes(types)), elem);
var doc = new XDocument(root);
doc.Save(file);
}
public static T Load<T>(string path)
{
using (var file = LuaCsFile.OpenRead(path)) return Load<T>(file);
}
public static void Save(string path, object obj)
{
using (var file = LuaCsFile.OpenWrite(path)) Save(file, obj);
}
}
@@ -56,6 +56,7 @@ public record ConfigResourceInfo : BaseResourceInfo, IConfigResourceInfo {}
public record LuaScriptsResourceInfo : BaseResourceInfo, ILuaScriptResourceInfo
{
public bool IsAutorun { get; init; }
public bool RunUnrestricted { get; init; }
}
#endregion
@@ -21,6 +21,12 @@ public interface ILuaScriptResourceInfo : IBaseResourceInfo
/// </summary>
[XmlAttribute("IsAutorun")]
public bool IsAutorun { get; }
/// <summary>
/// Indicates that this lua resources needs to run outside sandbox/requires unrestricted access.
/// </summary>
[XmlAttribute("RunUnrestricted")]
public bool RunUnrestricted { get; }
}
public interface IAssemblyResourceInfo : IBaseResourceInfo
@@ -5,6 +5,7 @@ using System.Xml.Linq;
using Barotrauma.LuaCs.Data;
using Barotrauma.LuaCs;
using Barotrauma.Networking;
using OneOf;
namespace Barotrauma.LuaCs.Data;
@@ -34,8 +35,8 @@ public partial interface ISettingBase : IDataInfo, IEquatable<ISettingBase>, IDi
Type GetValueType();
string GetStringValue();
string GetDefaultStringValue();
bool TrySetValue(OneOf.OneOf<string, XElement> value);
event Action<ISettingBase> OnValueChanged;
bool TrySetSerializedValue(OneOf<string, XElement> value);
event Action<ISettingBase> OnValueChanged;
OneOf.OneOf<string, XElement> GetSerializableValue();
}
@@ -1,6 +1,8 @@
using System;
using System.Collections.Concurrent;
using System.Xml.Linq;
using Barotrauma.LuaCs.Data;
using Microsoft.Toolkit.Diagnostics;
using Microsoft.Xna.Framework;
using OneOf;
@@ -10,6 +12,7 @@ public abstract class SettingBase : ISettingBase
{
protected SettingBase(IConfigInfo configInfo)
{
Guard.IsNotNull(configInfo, nameof(configInfo));
ConfigInfo = configInfo;
}
@@ -57,8 +60,8 @@ public abstract class SettingBase : ISettingBase
public abstract Type GetValueType();
public abstract string GetStringValue();
public abstract string GetDefaultStringValue();
public abstract bool TrySetSerializedValue(OneOf<string, XElement> value);
public abstract bool TrySetValue(OneOf<string, XElement> value);
public abstract event Action<ISettingBase> OnValueChanged;
public abstract OneOf<string, XElement> GetSerializableValue();
#if CLIENT

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