diff --git a/.github/DISCUSSION_TEMPLATE/bug-reports.yml b/.github/DISCUSSION_TEMPLATE/bug-reports.yml index cdefa3753..71271780c 100644 --- a/.github/DISCUSSION_TEMPLATE/bug-reports.yml +++ b/.github/DISCUSSION_TEMPLATE/bug-reports.yml @@ -73,7 +73,7 @@ body: label: Version description: Which version of the game did the bug happen in? You can see the current version number in the bottom left corner of your screen in the main menu. options: - - v1.11.5.0 (Winter Update 2025 Hotfix 1) + - v1.12.6.2 (Spring Update 2026) - Other validations: required: true diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index d4283cdd3..205c5062f 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -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: @@ -26,13 +40,10 @@ env: windows:server:Windows/Server linux:server:Linux/Server mac:server:Mac/Server + windows:client:Windows/Client + linux:client:Linux/Client + mac:client:Mac/Client/Barotrauma.app/Contents/MacOS ARCHIVE_BASE_NAME: luacsforbarotraumaEP - - # windows:client:Windows/Client - # linux:client:Linux/Client - # mac:client:Mac/Client/Barotrauma.app/Contents/MacOS - # we do not currently provide a CL - # XXX: these file names are subject to shell expansion. # Be careful when using special characters. ARCHIVE_FILES_SERVER: | @@ -67,15 +78,19 @@ env: Mono.Cecil.Mdb.dll Mono.Cecil.Pdb.dll Mono.Cecil.Rocks.dll - Microsoft.CodeAnalysis.CSharp.Scripting.dll + LightInject.dll + OneOf.dll + FluentResults.dll + Basic.Reference.Assemblies.Net80.dll + Microsoft.Extensions.Logging.Abstractions.dll + Microsoft.Toolkit.Diagnostics.dll Microsoft.CodeAnalysis.CSharp.dll Microsoft.CodeAnalysis.dll - Microsoft.CodeAnalysis.Scripting.dll System.Collections.Immutable.dll System.Reflection.Metadata.dll System.Runtime.CompilerServices.Unsafe.dll mscordaccore_amd64_amd64_* - Lua + LocalMods/LuaCsForBarotrauma jobs: build: diff --git a/.gitignore b/.gitignore index 56e643245..73cc11f3b 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,7 @@ bld/ [Rr]eleaseMac/ [Dd]ebugLinux/ [Rr]eleaseLinux/ +LocalMods/ *.o */Barotrauma*/doc/ diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/EnemyAIController.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/EnemyAIController.cs index 49c4b4309..1e7554604 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/EnemyAIController.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/EnemyAIController.cs @@ -29,7 +29,7 @@ namespace Barotrauma } } } - else if (SelectedAiTarget?.Entity != null) + else if (SelectedAiTarget?.Entity != null && AttackLimb != null) { Vector2 targetPos = SelectedAiTarget.Entity.DrawPosition; if (State == AIState.Attack) @@ -37,15 +37,16 @@ namespace Barotrauma targetPos = attackWorldPos; } targetPos.Y = -targetPos.Y; - - GUI.DrawLine(spriteBatch, pos, targetPos, GUIStyle.Red * 0.5f, 0, 4); + Vector2 attackLimbPos = AttackLimb.DrawPosition; + attackLimbPos.Y = -attackLimbPos.Y; + GUI.DrawLine(spriteBatch, attackLimbPos, targetPos, GUIStyle.Red * 0.75f, 0, 4); if (wallTarget != null && !IsCoolDownRunning) { Vector2 wallTargetPos = wallTarget.Position; if (wallTarget.Structure.Submarine != null) { wallTargetPos += wallTarget.Structure.Submarine.DrawPosition; } wallTargetPos.Y = -wallTargetPos.Y; GUI.DrawRectangle(spriteBatch, wallTargetPos - new Vector2(10.0f, 10.0f), new Vector2(20.0f, 20.0f), Color.Orange, false); - GUI.DrawLine(spriteBatch, pos, wallTargetPos, Color.Orange * 0.5f, 0, 5); + GUI.DrawLine(spriteBatch, attackLimbPos, wallTargetPos, Color.Orange * 0.75f, 0, 5); } GUI.DrawString(spriteBatch, pos - Vector2.UnitY * 60.0f, $"{SelectedAiTarget.Entity}", GUIStyle.Red, Color.Black); GUI.DrawString(spriteBatch, pos - Vector2.UnitY * 40.0f, $"{targetValue.FormatZeroDecimal()} (M: {CurrentTargetMemory?.Priority.FormatZeroDecimal()}, P: {CurrentTargetingParams?.Priority.FormatZeroDecimal()})", GUIStyle.Red, Color.Black); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/HumanAIController.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/HumanAIController.cs index 4e628af8b..9e3ac4669 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/HumanAIController.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/HumanAIController.cs @@ -23,6 +23,8 @@ namespace Barotrauma //GUI.DrawString(spriteBatch, pos + textOffset, $"AI TARGET: {SelectedAiTarget.Entity.ToString()}", Color.White, Color.Black); } + Vector2 spacing = new Vector2(0, GUIStyle.Font.MeasureChar('T').Y); + Vector2 stringDrawPos = pos + textOffset; GUI.DrawString(spriteBatch, stringDrawPos, Character.Name, Color.White, Color.Black); @@ -33,14 +35,14 @@ namespace Barotrauma currentOrders.Sort((x, y) => y.ManualPriority.CompareTo(x.ManualPriority)); for (int i = 0; i < currentOrders.Count; i++) { - stringDrawPos += new Vector2(0, 20); + stringDrawPos += spacing; var order = currentOrders[i]; GUI.DrawString(spriteBatch, stringDrawPos, $"ORDER {i + 1}: {order.Objective.DebugTag} ({order.Objective.Priority.FormatZeroDecimal()})", Color.White, Color.Black); } } else if (ObjectiveManager.WaitTimer > 0) { - stringDrawPos += new Vector2(0, 20); + stringDrawPos += spacing; GUI.DrawString(spriteBatch, stringDrawPos - textOffset, $"Waiting... {ObjectiveManager.WaitTimer.FormatZeroDecimal()}", Color.White, Color.Black); } var currentObjective = ObjectiveManager.CurrentObjective; @@ -49,19 +51,19 @@ namespace Barotrauma int offset = currentOrder != null ? 20 + ((ObjectiveManager.CurrentOrders.Count - 1) * 20) : 0; if (currentOrder == null || currentOrder.Priority <= 0) { - stringDrawPos += new Vector2(0, 20); + stringDrawPos += spacing; GUI.DrawString(spriteBatch, stringDrawPos, $"MAIN OBJECTIVE: {currentObjective.DebugTag} ({currentObjective.Priority.FormatZeroDecimal()})", Color.White, Color.Black); } var subObjective = currentObjective.CurrentSubObjective; if (subObjective != null) { - stringDrawPos += new Vector2(0, 20); + stringDrawPos += spacing; GUI.DrawString(spriteBatch, stringDrawPos, $"SUBOBJECTIVE: {subObjective.DebugTag} ({subObjective.Priority.FormatZeroDecimal()})", Color.White, Color.Black); } var activeObjective = ObjectiveManager.GetActiveObjective(); if (activeObjective != null) { - stringDrawPos += new Vector2(0, 20); + stringDrawPos += spacing; GUI.DrawString(spriteBatch, stringDrawPos, $"ACTIVE OBJECTIVE: {activeObjective.DebugTag} ({activeObjective.Priority.FormatZeroDecimal()})", Color.White, Color.Black); } if (currentObjective is AIObjectiveCombat @@ -85,12 +87,12 @@ namespace Barotrauma } } - Vector2 objectiveStringDrawPos = stringDrawPos + new Vector2(120, 40); + Vector2 objectiveStringDrawPos = stringDrawPos + new Vector2(120, spacing.Y * 2); for (int i = 0; i < ObjectiveManager.Objectives.Count; i++) { var objective = ObjectiveManager.Objectives[i]; GUI.DrawString(spriteBatch, objectiveStringDrawPos, $"{objective.DebugTag} ({objective.Priority.FormatZeroDecimal()})", Color.White, Color.Black * 0.5f); - objectiveStringDrawPos += new Vector2(0, 18); + objectiveStringDrawPos += spacing * 0.8f; } if (steeringManager is IndoorsSteeringManager pathSteering) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Animation/Ragdoll.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Animation/Ragdoll.cs index 6e2ac141b..ce45568d2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Animation/Ragdoll.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Animation/Ragdoll.cs @@ -547,7 +547,7 @@ namespace Barotrauma } } - public void Draw(SpriteBatch spriteBatch, Camera cam) + public void Draw(SpriteBatch spriteBatch, Camera cam, bool onlyDrawSeveredLimbs) { if (simplePhysicsEnabled) { return; } @@ -573,8 +573,12 @@ namespace Barotrauma { foreach (Limb limb in limbs) { limb.ActiveSprite.Depth += depthOffset; } } - for (int i = 0; i < limbs.Length; i++) + for (int i = 0; i < inversedLimbDrawOrder.Length; i++) { + if (onlyDrawSeveredLimbs && !inversedLimbDrawOrder[i].IsSevered) + { + continue; + } inversedLimbDrawOrder[i].Draw(spriteBatch, cam, color); } if (!MathUtils.NearlyEqual(depthOffset, 0.0f)) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs index 0180efb8c..c38878268 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs @@ -938,8 +938,8 @@ namespace Barotrauma public void Draw(SpriteBatch spriteBatch, Camera cam) { - if (!Enabled || InvisibleTimer > 0.0f) { return; } - AnimController.Draw(spriteBatch, cam); + if (!Enabled) { return; } + AnimController.Draw(spriteBatch, cam, onlyDrawSeveredLimbs: InvisibleTimer > 0.0f); } public void DrawHUD(SpriteBatch spriteBatch, Camera cam, bool drawHealth = true) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/InteractionLabelManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/InteractionLabelManager.cs index 897637950..cd9ba6820 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/InteractionLabelManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/InteractionLabelManager.cs @@ -4,6 +4,7 @@ using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System.Collections.Generic; using Barotrauma.Items.Components; +using System.Linq; namespace Barotrauma; @@ -106,12 +107,17 @@ public static class InteractionLabelManager } RectangleF textRect = GetLabelRect(interactableInRange, cam); - - if (labels.None(l => l.Item == interactableInRange)) + var existingLabel = labels.FirstOrDefault(l => l.Item == interactableInRange); + if (existingLabel == null) { var labelData = new LabelData(interactableInRange, textRect, RichString.Rich(interactableInRange.Prefab.Name), cam); labels.Add(labelData); } + //size of the label doesn't match - can happen when we're using a CJK font which we asynchronously render new symbols for + else if (existingLabel.TextRect.Size != textRect.Size) + { + existingLabel.TextRect = textRect; + } } PreventInteractionLabelOverlap(centerPos: character.Position); @@ -127,7 +133,11 @@ public static class InteractionLabelManager private static RectangleF GetLabelRect(Item item, Camera cam) { // create rectangle for overlap prevention - Vector2 itemTextSizeScreen = GUIStyle.SubHeadingFont.MeasureString(RichString.Rich(item.Prefab.Name).SanitizedValue) * LabelScale; + + string nameText = RichString.Rich(item.Prefab.Name).SanitizedValue; + + var font = GUIStyle.SubHeadingFont.GetFontForStr(nameText)!; + Vector2 itemTextSizeScreen = font.MeasureString(nameText) * LabelScale; Vector2 interactablePosScreen = cam.WorldToScreen(item.Position); RectangleF textRect = new RectangleF(interactablePosScreen.X, interactablePosScreen.Y, itemTextSizeScreen.X, itemTextSizeScreen.Y); // center the rectangle on the item diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs index 47e90a6cb..0d9c45be1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs @@ -340,10 +340,6 @@ namespace Barotrauma break; case "randomcolor": randomColor = subElement.GetAttributeColorArray("colors", null)?.GetRandomUnsynced(); - if (randomColor.HasValue) - { - Params.GetSprite().Color = randomColor.Value; - } break; case "lightsource": LightSource = new LightSource(subElement, GetConditionalTarget()) @@ -631,6 +627,8 @@ namespace Barotrauma SoundPlayer.PlayDamageSound(damageSoundType, Math.Max(damage, bleedingDamage), WorldPosition); } + if (character.InvisibleTimer > 0.0f) { return; } + // spawn damage particles float damageParticleAmount = damage < 1 ? 0 : Math.Min(damage / 5, 1.0f) * damageMultiplier; if (damageParticleAmount > 0.001f) @@ -734,7 +732,8 @@ namespace Barotrauma if (spriteParams == null || Alpha <= 0) { return; } float burn = spriteParams.IgnoreTint ? 0 : burnOverLayStrength; float brightness = Math.Max(1.0f - burn, 0.2f); - Color tintedColor = spriteParams.Color; + Color baseColor = randomColor ?? spriteParams.Color; + Color tintedColor = baseColor; if (!spriteParams.IgnoreTint) { tintedColor = tintedColor.Multiply(ragdoll.RagdollParams.Color); @@ -752,7 +751,7 @@ namespace Barotrauma } } Color color = new Color(tintedColor.Multiply(brightness), tintedColor.A); - Color colorWithoutTint = new Color(spriteParams.Color.Multiply(brightness), spriteParams.Color.A); + Color colorWithoutTint = new Color(baseColor.Multiply(brightness), baseColor.A); Color blankColor = new Color(brightness, brightness, brightness, 1); if (deadTimer > 0) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxInputOutputNode.cs b/Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxInputOutputNode.cs index d9658086f..7e0a3b547 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxInputOutputNode.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxInputOutputNode.cs @@ -31,7 +31,7 @@ namespace Barotrauma GUILayoutGroup connLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.12f), labelList.Content.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft); new GUITextBlock(new RectTransform(new Vector2(0.4f, 1f), connLayout.RectTransform), text: conn.Connection.DisplayName, font: GUIStyle.SubHeadingFont); - GUITextBox box = GUI.CreateTextBoxWithPlaceholder(new RectTransform(new Vector2(0.6f, 1f), connLayout.RectTransform), text: found ? labelOverride : string.Empty, conn.Connection.DisplayName.Value); + GUITextBox box = GUI.CreateTextBoxWithPlaceholder(new RectTransform(new Vector2(0.6f, 1f), connLayout.RectTransform), text: found ? labelOverride : string.Empty, conn.Connection.DefaultDisplayName.Value); box.MaxTextLength = MaxConnectionLabelLength; textBoxes.Add(conn.Name, box); diff --git a/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs b/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs index f2831f0ee..3ef95fc7b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs @@ -16,6 +16,7 @@ using System.Linq; using System.Text; using System.Threading.Tasks; using System.Xml.Linq; +using Barotrauma.LuaCs.Events; using static Barotrauma.FabricationRecipe; namespace Barotrauma @@ -222,8 +223,6 @@ namespace Barotrauma private static bool IsCommandPermitted(Identifier command, GameClient client) { - if (GameMain.LuaCs.Game.IsCustomCommandPermitted(command)) { return true; } - switch (command.Value.ToLowerInvariant()) { case "kick": @@ -659,14 +658,6 @@ namespace Barotrauma return; } - bool luaCsEnabled = true; - if (args.Length > 3) - { - bool.TryParse(args[3], out luaCsEnabled); - } - - if (luaCsEnabled) { GameMain.LuaCs.Initialize(); } - GameMain.MainMenuScreen.QuickStart(fixedSeed: false, subName, difficulty, levelGenerationParams); }, getValidArgs: () => new[] { SubmarineInfo.SavedSubmarines.Select(s => s.Name).Distinct().OrderBy(s => s).ToArray() })); @@ -3553,6 +3544,11 @@ namespace Barotrauma ContentPackageManager.RegularPackages.Select(p => p.Name).ToArray() })); + commands.Add(new Command("ShowServerPerf", "Immediately log server performance info", (string[] args) => + { + // TODO: Not yet :) + })); + #if WINDOWS commands.Add(new Command("startdedicatedserver", "", (string[] args) => { @@ -3586,6 +3582,14 @@ namespace Barotrauma } }));*/ + AssignOnClientExecute( + "ShowServerPerf", + (string[] args) => + { + GameMain.Client?.SendConsoleCommand("ShowServerPerf"); + } + ); + AssignOnClientExecute( "giveperm", (string[] args) => @@ -4223,51 +4227,8 @@ namespace Barotrauma NewMessage("Minimum main path width: " + (Level.Loaded.LevelData?.MinMainPathWidth?.ToString() ?? "unknown")); } }); - - commands.Add(new Command("cl_lua", $"cl_lua: Runs a string on the client.", (string[] args) => - { - if (GameMain.Client != null && !GameMain.Client.HasPermission(ClientPermissions.ConsoleCommands)) - { - ThrowError("Command not permitted."); - return; - } - - if (GameMain.LuaCs.Lua == null) - { - ThrowError("LuaCs not initialized, use the console command cl_reloadluacs to force initialization."); - return; - } - - try - { - GameMain.LuaCs.Lua.DoString(string.Join(" ", args)); - } - catch(Exception ex) - { - LuaCsLogger.HandleException(ex, LuaCsMessageOrigin.LuaMod); - } - })); - - commands.Add(new Command("cl_reloadlua|cl_reloadcs|cl_reloadluacs", "Re-initializes the LuaCs environment.", (string[] args) => - { - GameMain.LuaCs.Initialize(); - })); - - commands.Add(new Command("cl_toggleluadebug", "Toggles the MoonSharp Debug Server.", (string[] args) => - { - int port = 41912; - - if (args.Length > 0) - { - int.TryParse(args[0], out port); - } - - GameMain.LuaCs.ToggleDebugger(port); - })); } - - private static void ReloadWearables(Character character, int variant = 0) { foreach (var limb in character.AnimController.Limbs) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/ConversationAction.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/ConversationAction.cs index 3a7988c21..e7538dc03 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/ConversationAction.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/ConversationAction.cs @@ -277,6 +277,14 @@ namespace Barotrauma int selectedOption = (userdata as int?) ?? 0; if (actionInstance != null) { + var option = actionInstance.Options[selectedOption]; + if (GameMain.Client == null && option.ForceSay) + { + Character.Controlled.ForceSay( + option.ForceSayText.IsNullOrEmpty() ? TextManager.Get(option.Text).Fallback(option.Text) : TextManager.Get(option.ForceSayText).Fallback(option.ForceSayText), + option.ForceSayInRadio, + option.ForceSayRemoveQuotes); + } actionInstance.selectedOption = selectedOption; DisableButtons(optionButtons, btn); btn.ExternalHighlight = true; @@ -340,7 +348,8 @@ namespace Barotrauma if (speaker?.Info != null && drawChathead) { // chathead - new GUICustomComponent(new RectTransform(new Vector2(0.15f, 0.8f), content.RectTransform), onDraw: (sb, customComponent) => + int chatHeadWidth = (int)(content.RectTransform.Rect.Width * 0.15f); + new GUICustomComponent(new RectTransform(new Point(chatHeadWidth, chatHeadWidth), content.RectTransform, isFixedSize: true), onDraw: (sb, customComponent) => { speaker.Info.DrawIcon(sb, customComponent.Rect.Center.ToVector2(), customComponent.Rect.Size.ToVector2()); }); @@ -382,7 +391,7 @@ namespace Barotrauma } textContent.RectTransform.MinSize = new Point(0, textContent.Children.Sum(c => c.Rect.Height + textContent.AbsoluteSpacing) + GUI.IntScale(16)); - content.RectTransform.MinSize = new Point(0, content.Children.Sum(c => c.Rect.Height)); + content.RectTransform.MinSize = textContent.RectTransform.MinSize; // Recalculate the text size as it is scaled up and no longer matching the text height due to the textContent's minSize increasing textBlock.CalculateHeightFromText(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/AbandonedOutpostMission.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/AbandonedOutpostMission.cs index e3a607d75..d8644061b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/AbandonedOutpostMission.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/AbandonedOutpostMission.cs @@ -61,17 +61,9 @@ namespace Barotrauma { Item.ReadSpawnData(msg); } - if (character.Submarine != null && character.AIController is EnemyAIController enemyAi) + if (character.AIController is EnemyAIController enemyAi && character.Submarine is Submarine ownSub) { - enemyAi.UnattackableSubmarines.Add(character.Submarine); - if (Submarine.MainSub != null) - { - enemyAi.UnattackableSubmarines.Add(Submarine.MainSub); - foreach (Submarine sub in Submarine.MainSub.DockedTo) - { - enemyAi.UnattackableSubmarines.Add(sub); - } - } + enemyAi.SetUnattackableSubmarines(ownSub); } } if (characters.Contains(null)) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/CustomMission.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/CustomMission.cs new file mode 100644 index 000000000..601d4ec09 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/CustomMission.cs @@ -0,0 +1,8 @@ +#nullable enable +namespace Barotrauma; + +internal sealed partial class CustomMission : Mission +{ + public override bool DisplayAsCompleted => State == SuccessState; + public override bool DisplayAsFailed => State == FailureState; +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/SalvageMission.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/SalvageMission.cs index 8b9d8c492..fbf3e7a16 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/SalvageMission.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/SalvageMission.cs @@ -14,7 +14,7 @@ namespace Barotrauma private void TryShowRetrievedMessage() { - if (DetermineCompleted()) + if (DetermineCompleted(CampaignMode.TransitionType.None)) { HandleMessage(ref allRetrievedMessage); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/ChatBox.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/ChatBox.cs index deb4201bf..5514fa1a0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/ChatBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/ChatBox.cs @@ -1,5 +1,6 @@ using Barotrauma.Extensions; using Barotrauma.Items.Components; +using Barotrauma.LuaCs.Events; using Barotrauma.Networking; using Microsoft.Xna.Framework; using System; @@ -413,7 +414,8 @@ namespace Barotrauma { if (GameMain.IsSingleplayer) { - var should = GameMain.LuaCs.Hook.Call("chatMessage", message.Text, message.SenderClient, message.Type, message); + bool? should = null; + LuaCsSetup.Instance.EventService.PublishEvent(x => should = x.OnChatMessage(message.Text, message.SenderClient, message.Type, message) ?? should); if (should != null && should.Value) { return; } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs index 918b67d16..af674c696 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs @@ -1435,8 +1435,15 @@ namespace Barotrauma Uri baseAddress = new Uri(url); Uri remoteDirectory = new Uri(baseAddress, "."); string remoteFileName = Path.GetFileName(baseAddress.LocalPath); - IRestClient client = new RestClient(remoteDirectory); - var response = client.Execute(new RestRequest(remoteFileName, Method.GET)); + var client = RestFactory.CreateClient(remoteDirectory.ToString()); + var request = RestFactory.CreateRequest(remoteFileName); + var response = client.Execute(request); + if (response.ErrorException != null) + { + DebugConsole.AddWarning($"Connection error: Failed to load remote sprite from {url} " + + $"({response.ErrorException.Message})."); + return null; + } if (response.ResponseStatus != ResponseStatus.Completed) { return null; } if (response.StatusCode != HttpStatusCode.OK) { return null; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIDropDown.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIDropDown.cs index 3116cfd81..2b0041c13 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIDropDown.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIDropDown.cs @@ -26,7 +26,9 @@ namespace Barotrauma public OnSelectedHandler OnDropped; - private readonly GUIButton button; + private readonly GUIButton button; + public GUIButton Button => button; + private readonly GUIImage icon; private readonly GUIListBox listBox; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/HRManagerUI.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/HRManagerUI.cs index 928ae3519..ea17ff7b5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/HRManagerUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/HRManagerUI.cs @@ -710,19 +710,24 @@ namespace Barotrauma if (listBox == pendingList || listBox == crewList) { - nameBlock.RectTransform.Resize(new Point(nameBlock.Rect.Width - nameBlock.Rect.Height, nameBlock.Rect.Height)); - nameBlock.Text = ToolBox.LimitString(characterName, nameBlock.Font, nameBlock.Rect.Width); - nameBlock.RectTransform.Resize(new Point((int)(nameBlock.Padding.X + nameBlock.TextSize.X + nameBlock.Padding.Z), nameBlock.Rect.Height)); - Point size = new Point((int)(0.7f * nameBlock.Rect.Height)); - new GUIImage(new RectTransform(size, nameGroup.RectTransform), "EditIcon") { CanBeFocused = false }; - size = new Point(3 * mainGroup.AbsoluteSpacing + icon.Rect.Width + nameAndJobGroup.Rect.Width, mainGroup.Rect.Height); - new GUIButton(new RectTransform(size, frame.RectTransform) { RelativeOffset = new Vector2(0.025f) }, style: null) + //if the character is already in the crew, only check permissions - reputation doesn't matter for renaming an already-hired bot + bool canRename = listBox == crewList ? HasPermissionToHire : CanHire(characterInfo); + if (canRename) { - Enabled = CanHire(characterInfo), - ToolTip = TextManager.GetWithVariable("campaigncrew.givenicknametooltip", "[mouseprimary]", PlayerInput.PrimaryMouseLabel), - UserData = characterInfo, - OnClicked = CreateRenamingComponent - }; + nameBlock.RectTransform.Resize(new Point(nameBlock.Rect.Width - nameBlock.Rect.Height, nameBlock.Rect.Height)); + nameBlock.Text = ToolBox.LimitString(characterName, nameBlock.Font, nameBlock.Rect.Width); + nameBlock.RectTransform.Resize(new Point((int)(nameBlock.Padding.X + nameBlock.TextSize.X + nameBlock.Padding.Z), nameBlock.Rect.Height)); + Point iconSize = new Point((int)(0.7f * nameBlock.Rect.Height)); + new GUIImage(new RectTransform(iconSize, nameGroup.RectTransform), "EditIcon") { CanBeFocused = false }; + Point buttonSize = new Point(3 * mainGroup.AbsoluteSpacing + icon.Rect.Width + nameAndJobGroup.Rect.Width + (int)(iconSize.X * 1.5f), mainGroup.Rect.Height); + new GUIButton(new RectTransform(buttonSize, frame.RectTransform) { RelativeOffset = new Vector2(0.025f) }, style: null) + { + ClampMouseRectToParent = false, + ToolTip = TextManager.GetWithVariable("campaigncrew.givenicknametooltip", "[mouseprimary]", PlayerInput.PrimaryMouseLabel), + UserData = characterInfo, + OnClicked = CreateRenamingComponent + }; + } } //recalculate everything and truncate texts if needed diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs b/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs index 953062de9..b8ddf8f72 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs @@ -18,12 +18,12 @@ using System.Reflection; using System.Threading; using Barotrauma.Extensions; using System.Collections.Immutable; +using Barotrauma.LuaCs.Events; namespace Barotrauma { class GameMain : Game { - public static LuaCsSetup LuaCs; public static bool ShowFPS; public static bool ShowPerf; public static bool DebugDraw; @@ -244,8 +244,6 @@ namespace Barotrauma throw new Exception("Content folder not found. If you are trying to compile the game from the source code and own a legal copy of the game, you can copy the Content folder from the game's files to BarotraumaShared/Content."); } - LuaCs = new LuaCsSetup(); - GameSettings.Init(); CreatureMetrics.Init(); @@ -297,6 +295,8 @@ namespace Barotrauma MainThread = Thread.CurrentThread; Window.FileDropped += OnFileDropped; + + LuaCsSetup.Instance.GetType(); } public static void ExecuteAfterContentFinishedLoading(Action action) @@ -636,9 +636,6 @@ namespace Barotrauma HasLoaded = true; log("LOADING COROUTINE FINISHED"); -#if CLIENT - LuaCsInstaller.CheckUpdate(); -#endif contentLoaded = true; while (postContentLoadActions.TryDequeue(out Action action)) @@ -986,8 +983,6 @@ namespace Barotrauma Screen.Selected.AddToGUIUpdateList(); - LuaCsLogger.AddToGUIUpdateList(); - Client?.AddToGUIUpdateList(); SubmarinePreview.AddToGUIUpdateList(); @@ -1054,8 +1049,6 @@ namespace Barotrauma SoundManager?.Update(); - GameMain.LuaCs.Update(); - Timing.Accumulator -= Timing.Step; updateCount++; @@ -1237,8 +1230,6 @@ namespace Barotrauma GUIMessageBox.CloseAll(); MainMenuScreen.Select(); GameSession = null; - - GameMain.LuaCs.Stop(); } public void ShowBugReporter() @@ -1301,6 +1292,18 @@ namespace Barotrauma { IsExiting = true; CreatureMetrics.Save(); + try + { + if (LuaCsSetup.Instance is not null) + { + LuaCsSetup.Instance.Dispose(); + } + } + catch (Exception e) + { + DebugConsole.ThrowError($"Error while disposing of LuaCsForBarotrauma: {e.Message} | {e.StackTrace}"); + } + DebugConsole.NewMessage("Exiting..."); Client?.Quit(); SteamManager.ShutDown(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/Tutorial.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/Tutorial.cs index 4dfea9e86..3b6ac9488 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/Tutorial.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/Tutorial.cs @@ -179,8 +179,6 @@ namespace Barotrauma.Tutorials public void Start() { - GameMain.LuaCs.CheckInitialize(); - GameMain.Instance.ShowLoading(Loading()); ObjectiveManager.ResetObjectives(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs index 0452c45e7..e33660954 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs @@ -487,7 +487,21 @@ namespace Barotrauma.Items.Components return 0.0f; } - public virtual bool ShouldDrawHUD(Character character) + public bool ShouldDrawHUD(Character character) + { + if (Character.Controlled?.SelectedItem != null) + { + Controller controller = item.GetComponent(); + if (controller != null && controller.User == Character.Controlled && controller.HideAllItemComponentHUDs) + { + return false; + } + } + + return ShouldDrawHUDComponentSpecific(character); + } + + protected virtual bool ShouldDrawHUDComponentSpecific(Character character) { return true; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs index c0a6ece27..31af8751a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs @@ -552,9 +552,9 @@ namespace Barotrauma.Items.Components if (flippedY) { origin.Y = contained.Item.Sprite.SourceRect.Height - origin.Y; } float containedSpriteDepth = ContainedSpriteDepth < 0.0f ? contained.Item.Sprite.Depth : ContainedSpriteDepth; - if (i < containedSpriteDepths.Length) + if (targetSlotIndex < containedSpriteDepths.Length) { - containedSpriteDepth = containedSpriteDepths[i]; + containedSpriteDepth = containedSpriteDepths[targetSlotIndex]; } containedSpriteDepth = itemDepth + (containedSpriteDepth - (item.Sprite?.Depth ?? item.SpriteDepth)) / 10000.0f; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs index 4e637d1dc..82183702e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs @@ -52,10 +52,12 @@ namespace Barotrauma.Items.Components partial void SetLightSourceTransformProjSpecific() { - Vector2 offset = Vector2.Zero; - if (LightOffset != Vector2.Zero) + Vector2 offset = LightOffset * item.Scale; + if (offset != Vector2.Zero) { - offset = Vector2.Transform(LightOffset, Matrix.CreateRotationZ(item.FlippedY ? -item.RotationRad - MathHelper.Pi : -item.RotationRad)) * item.Scale; + if (item.FlippedX) { offset.X *= -1; } + if (item.FlippedY) { offset.Y *= -1; } + offset = Vector2.Transform(offset, Matrix.CreateRotationZ(-item.RotationRad)); } if (ParentBody != null) @@ -101,7 +103,10 @@ namespace Barotrauma.Items.Components if (Light?.LightSprite == null) { return; } if ((item.body == null || item.body.Enabled) && lightBrightness > 0.0f && IsOn && Light.Enabled) { - Vector2 offset = Vector2.Transform(LightOffset, Matrix.CreateRotationZ(item.FlippedY ? -item.RotationRad - MathHelper.Pi : -item.RotationRad)) * item.Scale; + Vector2 offset = LightOffset * item.Scale; + if (item.FlippedX) { offset.X *= -1; } + if (item.FlippedY) { offset.Y *= -1; } + offset = Vector2.Transform(offset, Matrix.CreateRotationZ(-item.RotationRad)); Vector2 origin = Light.LightSprite.Origin; if ((Light.LightSpriteEffect & SpriteEffects.FlipHorizontally) == SpriteEffects.FlipHorizontally) { origin.X = Light.LightSprite.SourceRect.Width - origin.X; } @@ -114,6 +119,7 @@ namespace Barotrauma.Items.Components { color = new Color(lightColor, Light.OverrideLightSpriteAlpha.Value); } + Light.LightSprite.Draw(spriteBatch, new Vector2(drawPos.X, -drawPos.Y), color * lightBrightness, @@ -128,8 +134,16 @@ namespace Barotrauma.Items.Components { if (Light?.LightSprite != null && item.Prefab.CanSpriteFlipX) { - Light.LightSpriteEffect = Light.LightSpriteEffect == SpriteEffects.None ? - SpriteEffects.FlipHorizontally : SpriteEffects.None; + Light.LightSpriteEffect ^= SpriteEffects.FlipHorizontally; + } + SetLightSourceTransformProjSpecific(); + } + + public override void FlipY(bool relativeToSub) + { + if (Light?.LightSprite != null && item.Prefab.CanSpriteFlipY) + { + Light.LightSpriteEffect ^= SpriteEffects.FlipVertically; } SetLightSourceTransformProjSpecific(); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Controller.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Controller.cs index 710dcb9f6..bee454c79 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Controller.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Controller.cs @@ -8,6 +8,30 @@ namespace Barotrauma.Items.Components { private bool isHUDsHidden; + public void UpdateMsg() + { + if (Character.Controlled == null) { return; } + + if (!string.IsNullOrEmpty(KickOutCharacterMsg) && + SelectingKicksCharacterOut && + User != null && !User.Removed) + { + DisplayMsg = TextManager.ParseInputTypes(TextManager.Get(KickOutCharacterMsg)); + } + else if (!string.IsNullOrEmpty(PutOtherCharacterMsg) && + AllowPuttingInOtherCharacters && + CanPutSelectedCharacter(Character.Controlled.SelectedCharacter)) + { + DisplayMsg = TextManager.ParseInputTypes(TextManager.Get(PutOtherCharacterMsg)); + } + else + { + DisplayMsg = TextManager.ParseInputTypes(TextManager.Get(Msg)); + } + + CharacterHUD.RecreateHudTextsIfControlling(Character.Controlled); + } + public override void DrawHUD(SpriteBatch spriteBatch, Character character) { base.DrawHUD(spriteBatch, character); @@ -69,21 +93,33 @@ namespace Barotrauma.Items.Components ushort userID = msg.ReadUInt16(); if (userID == 0) { - if (user != null) + if (User != null) { IsActive = false; - CancelUsing(user); - user = null; + CancelUsing(User); + User = null; } } else { Character newUser = Entity.FindEntityByID(userID) as Character; - if (newUser != user) + if (newUser != User) { - CancelUsing(user); + CancelUsing(User); } - user = newUser; + User = newUser; + + // If the server assigned a user to this controller but the character is not selecting the item + // on the client-side, force the selection to prevent desync. This is required for force attaching, + // since the character placed into the controller may be unconscious, and in that state + // the server no longer syncs the current SelectedItem to clients. + if (ForceUserToStayAttached && + user != null && + !user.IsAnySelectedItem(Item)) + { + user.SelectedItem = Item; + } + IsActive = true; } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs index b791fcf87..5dca29d13 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs @@ -434,18 +434,13 @@ namespace Barotrauma.Items.Components foreach (FabricationRecipe fi in fabricationRecipes.Values) { - RichString recipeTooltip = - fi.RequiresRecipe ? - RichString.Rich(fi.TargetItem.Description + "\n\n" + $"‖color:{XMLExtensions.ToStringHex(GUIStyle.Red)}‖{TextManager.Get("fabricatorrequiresrecipe")}‖color:end‖") : - RichString.Rich(fi.TargetItem.Description); - var frame = new GUIFrame(new RectTransform(new Point(itemList.Content.Rect.Width, (int)(40 * GUI.yScale)), itemList.Content.RectTransform), style: null) { UserData = fi, HoverColor = Color.Gold * 0.2f, SelectedColor = Color.Gold * 0.5f, - ToolTip = recipeTooltip }; + SetRecipeTooltip(frame, fi); var container = new GUILayoutGroup(new RectTransform(Vector2.One, frame.RectTransform), childAnchor: Anchor.CenterLeft, isHorizontal: true) { RelativeSpacing = 0.02f }; @@ -457,7 +452,7 @@ namespace Barotrauma.Items.Components itemIcon, scaleToFit: true) { Color = itemIcon == fi.TargetItem.Sprite ? fi.TargetItem.SpriteColor : fi.TargetItem.InventoryIconColor, - ToolTip = recipeTooltip + CanBeFocused = false }; } @@ -466,7 +461,7 @@ namespace Barotrauma.Items.Components { Padding = Vector4.Zero, AutoScaleVertical = true, - ToolTip = recipeTooltip + CanBeFocused = false }; new GUITextBlock(new RectTransform(new Vector2(0.85f, 1f), frame.RectTransform, Anchor.BottomRight), @@ -478,6 +473,20 @@ namespace Barotrauma.Items.Components } } + private void SetRecipeTooltip(GUIComponent component, FabricationRecipe recipe) + { + if (!recipe.RequiresRecipe) + { + component.ToolTip = RichString.Rich(recipe.TargetItem.Description); + } + else + { + component.ToolTip = AnyOneHasRecipeForItem(Character.Controlled, recipe.TargetItem) ? + RichString.Rich(recipe.TargetItem.Description + "\n\n" + $"‖color:{XMLExtensions.ToStringHex(GUIStyle.Green)}‖{TextManager.Get("unlockedrecipe.true")}‖color:end‖") : + RichString.Rich(recipe.TargetItem.Description + "\n\n" + $"‖color:{XMLExtensions.ToStringHex(GUIStyle.Red)}‖{TextManager.Get("fabricatorrequiresrecipe")}‖color:end‖"); + } + } + private void InitInventoryUIs() { if (inputInventoryHolder != null) @@ -927,16 +936,24 @@ namespace Barotrauma.Items.Components } } - if (recipe.RequiresRecipe && recipe.HideIfNoRecipe) + if (recipe.RequiresRecipe) { - if (Character.Controlled != null) + if (recipe.HideIfNoRecipe) { - if (!AnyOneHasRecipeForItem(Character.Controlled, recipe.TargetItem)) + bool anyOneHasRecipe = AnyOneHasRecipeForItem(Character.Controlled, recipe.TargetItem); + if (Character.Controlled != null) { - child.Visible = false; - continue; + if (!anyOneHasRecipe) + { + child.Visible = false; + continue; + } } } + else + { + SetRecipeTooltip(child, recipe); + } } child.Visible = @@ -1147,7 +1164,16 @@ namespace Barotrauma.Items.Components var lines = description.WrappedText.Split('\n'); if (lines.Count <= 1) { break; } string newString = string.Join('\n', lines.Take(lines.Count - 1)); - description.Text = newString.Substring(0, newString.Length - 4) + "..."; + + if (newString.Length > 4) + { + description.Text = newString.Substring(0, newString.Length - 4) + "..."; + } + else + { + description.Text = newString + "..."; + } + description.CalculateHeightFromText(); description.ToolTip = richDescription; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs index 3c08f7fe7..f76b9506a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs @@ -443,6 +443,7 @@ namespace Barotrauma.Items.Components var wire = targetItem.GetComponent(); if (wire != null && wire.Connections.Any(c => c != null)) { return false; } + if (targetItem.Container is { NonInteractable: true }) { return false; } if (targetItem.Container?.GetComponent() is { DrawInventory: false } or { AllowAccess: false }) { return false; } if (targetItem.HasTag(Tags.TraitorMissionItem)) { return false; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs index b24371277..bbba38aea 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs @@ -575,6 +575,22 @@ namespace Barotrauma.Items.Components pos /= c.Resources.Count; MineralClusters.Add((center: pos, resources: c.Resources)); } + + if (GameMain.GameSession != null) + { + foreach (var mission in GameMain.GameSession.Missions) + { + if (mission is MineralMission mineralMission) + { + foreach (var minerals in mineralMission.SpawnedResources) + { + MineralClusters.Add(( + center: new Vector2(minerals.Average(m => m.WorldPosition.X), minerals.Average(m => m.WorldPosition.Y)), + resources: minerals)); + } + } + } + } } else { @@ -823,18 +839,20 @@ namespace Barotrauma.Items.Components if (t.Entity is Character c && !c.IsUnconscious && c.Params.HideInSonar) { continue; } if (t.SoundRange <= 0.0f || float.IsNaN(t.SoundRange) || float.IsInfinity(t.SoundRange)) { continue; } + float sonarSoundRange = t.SoundRange * t.SoundRangeOnSonarMultiplier; + float distSqr = Vector2.DistanceSquared(t.WorldPosition, transducerCenter); - if (distSqr > t.SoundRange * t.SoundRange * 2) { continue; } + if (distSqr > sonarSoundRange * sonarSoundRange * 2) { continue; } float dist = (float)Math.Sqrt(distSqr); if (dist > prevPassivePingRadius * Range && dist <= passivePingRadius * Range && Rand.Int(sonarBlips.Count) < 500) { Ping(t.WorldPosition, transducerCenter, - t.SoundRange * DisplayScale, 0, DisplayScale, range, + sonarSoundRange * DisplayScale, 0, DisplayScale, range, passive: true, pingStrength: 0.5f, needsToBeInSector: t); if (t.IsWithinSector(transducerCenter)) { - sonarBlips.Add(new SonarBlip(t.WorldPosition, fadeTimer: 1.0f, scale: MathHelper.Clamp(t.SoundRange / 2000, 1.0f, 5.0f))); + sonarBlips.Add(new SonarBlip(t.WorldPosition, fadeTimer: 1.0f, scale: MathHelper.Clamp(sonarSoundRange / 2000, 1.0f, 5.0f))); } } } @@ -977,7 +995,9 @@ namespace Barotrauma.Items.Components if (aiTarget.InDetectable) { continue; } if (aiTarget.SonarLabel.IsNullOrEmpty() || aiTarget.SoundRange <= 0.0f) { continue; } - if (Vector2.DistanceSquared(aiTarget.WorldPosition, transducerCenter) < aiTarget.SoundRange * aiTarget.SoundRange) + float sonarSoundRange = aiTarget.SoundRange * aiTarget.SoundRangeOnSonarMultiplier; + + if (Vector2.DistanceSquared(aiTarget.WorldPosition, transducerCenter) < sonarSoundRange * sonarSoundRange) { DrawMarker(spriteBatch, aiTarget.SonarLabel.Value, diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs index e86923ef7..ad7305e89 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs @@ -58,7 +58,7 @@ namespace Barotrauma.Items.Components get { return Vector2.Zero; } } - public override bool ShouldDrawHUD(Character character) + protected override bool ShouldDrawHUDComponentSpecific(Character character) { if (item.IsHidden) { return false; } if (!HasRequiredItems(character, false) || character.SelectedItem != item) { return false; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CircuitBox.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CircuitBox.cs index 9d8ba4214..cf4f06813 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CircuitBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CircuitBox.cs @@ -78,7 +78,7 @@ namespace Barotrauma.Items.Components } } - public override bool ShouldDrawHUD(Character character) + protected override bool ShouldDrawHUDComponentSpecific(Character character) => character == Character.Controlled && (character.SelectedItem == item || character.SelectedSecondaryItem == item); public override void UpdateHUDComponentSpecific(Character character, float deltaTime, Camera cam) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/ConnectionPanel.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/ConnectionPanel.cs index 50ee85985..d7d63d74d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/ConnectionPanel.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/ConnectionPanel.cs @@ -97,7 +97,7 @@ namespace Barotrauma.Items.Components MoveConnectedWires(amount); } - public override bool ShouldDrawHUD(Character character) + protected override bool ShouldDrawHUDComponentSpecific(Character character) { return character == Character.Controlled && character == user && (character.SelectedItem == item || character.SelectedSecondaryItem == item); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/StatusHUD.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/StatusHUD.cs index 9b999dbae..c6a7231fb 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/StatusHUD.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/StatusHUD.cs @@ -287,20 +287,24 @@ namespace Barotrauma.Items.Components texts.Add(target.CustomInteractHUDText); textColors.Add(GUIStyle.Green); } - if (!target.IsIncapacitated && target.IsPet) + if (equipper?.FocusedCharacter == target) { - texts.Add(CharacterHUD.GetCachedHudText("PlayHint", InputType.Use)); - textColors.Add(GUIStyle.Green); - } - if (equipper?.FocusedCharacter == target && target.CanBeHealedBy(equipper, checkFriendlyTeam: false)) - { - texts.Add(CharacterHUD.GetCachedHudText("HealHint", InputType.Health)); - textColors.Add(GUIStyle.Green); - } - if (target.CanBeDraggedBy(Character.Controlled)) - { - texts.Add(CharacterHUD.GetCachedHudText("GrabHint", InputType.Grab)); - textColors.Add(GUIStyle.Green); + if (!target.IsIncapacitated && target.IsPet && + target.AIController is EnemyAIController enemyAI && enemyAI.PetBehavior.CanPlayWith(Character.Controlled)) + { + texts.Add(CharacterHUD.GetCachedHudText("PlayHint", InputType.Use)); + textColors.Add(GUIStyle.Green); + } + if (target.CanBeHealedBy(equipper, checkFriendlyTeam: false)) + { + texts.Add(CharacterHUD.GetCachedHudText("HealHint", InputType.Health)); + textColors.Add(GUIStyle.Green); + } + if (target.CanBeDraggedBy(Character.Controlled)) + { + texts.Add(CharacterHUD.GetCachedHudText("GrabHint", InputType.Grab)); + textColors.Add(GUIStyle.Green); + } } if (target.IsUnconscious) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs index 88168faec..3ff96e2f6 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs @@ -1597,7 +1597,8 @@ namespace Barotrauma { if (DraggingSlot == null || (!DraggingSlot.MouseOn())) { - Sprite sprite = DraggingItems.First().Prefab.InventoryIcon ?? DraggingItems.First().Sprite; + Item firstDraggingItem = DraggingItems.First(); + Sprite sprite = firstDraggingItem.OverrideInventorySprite ?? firstDraggingItem.Prefab.InventoryIcon ?? firstDraggingItem.Sprite; int iconSize = (int)(64 * GUI.Scale); float scale = Math.Min(Math.Min(iconSize / sprite.size.X, iconSize / sprite.size.Y), 1.5f); @@ -1854,7 +1855,7 @@ namespace Barotrauma if (item != null && drawItem) { - Sprite sprite = item.Prefab.InventoryIcon ?? item.Sprite; + Sprite sprite = item.OverrideInventorySprite ?? item.Prefab.InventoryIcon ?? item.Sprite; float scale = Math.Min(Math.Min((rect.Width - 10) / sprite.size.X, (rect.Height - 10) / sprite.size.Y), 2.0f); Vector2 itemPos = rect.Center.ToVector2(); if (itemPos.Y > GameMain.GraphicsHeight) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs index eb371af3f..78be7c33a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs @@ -419,7 +419,7 @@ namespace Barotrauma if (fadeInBrokenSprite != null) { - float d = Math.Min(depth + (fadeInBrokenSprite.Sprite.Depth - activeSprite.Depth - 0.000001f), 0.999f); + float d = MathHelper.Clamp(depth + (fadeInBrokenSprite.Sprite.Depth - activeSprite.Depth - 0.000001f), 0.0f, 0.999f); fadeInBrokenSprite.Sprite.DrawTiled(spriteBatch, new Vector2(DrawPosition.X - rect.Width / 2, -(DrawPosition.Y + rect.Height / 2)) + fadeInBrokenSprite.Offset.ToVector2() * Scale, size, color: color * fadeInBrokenSpriteAlpha, textureScale: Vector2.One * Scale, depth: d); @@ -435,7 +435,7 @@ namespace Barotrauma activeSprite.Draw(spriteBatch, new Vector2(DrawPosition.X, -DrawPosition.Y) + drawOffset, color, origin, RotationRad, Scale, activeSprite.effects, depth); if (fadeInBrokenSprite != null) { - float d = Math.Min(depth + (fadeInBrokenSprite.Sprite.Depth - activeSprite.Depth - 0.000001f), 0.999f); + float d = MathHelper.Clamp(depth + (fadeInBrokenSprite.Sprite.Depth - activeSprite.Depth - 0.000001f), 0.0f, 0.999f); fadeInBrokenSprite.Sprite.Draw(spriteBatch, new Vector2(DrawPosition.X, -DrawPosition.Y) + fadeInBrokenSprite.Offset.ToVector2() * Scale, color * fadeInBrokenSpriteAlpha, origin, RotationRad, Scale, activeSprite.effects, d); } } @@ -885,7 +885,12 @@ namespace Barotrauma Spacing = (int)(25 * GUI.Scale) }; - var itemEditor = new SerializableEntityEditor(listBox.Content.RectTransform, this, inGame, showName: true, titleFont: GUIStyle.LargeFont) { UserData = this }; + var itemEditor = new SerializableEntityEditor(listBox.Content.RectTransform, this, inGame, showName: true, + titleFont: GUIStyle.LargeFont, + dimOutDefaultValues: false) + { + UserData = this + }; activeEditors.Add(itemEditor); itemEditor.Children.First().Color = Color.Black * 0.7f; if (!inGame) @@ -1045,7 +1050,12 @@ namespace Barotrauma new GUIFrame(new RectTransform(new Vector2(1.0f, 0.02f), listBox.Content.RectTransform), style: "HorizontalLine"); - var componentEditor = new SerializableEntityEditor(listBox.Content.RectTransform, ic, inGame, showName: !inGame, titleFont: GUIStyle.SubHeadingFont) { UserData = ic }; + var componentEditor = new SerializableEntityEditor(listBox.Content.RectTransform, ic, inGame, showName: !inGame, + titleFont: GUIStyle.SubHeadingFont, + dimOutDefaultValues: false) + { + UserData = ic + }; componentEditor.Children.First().Color = Color.Black * 0.7f; activeEditors.Add(componentEditor); @@ -1064,7 +1074,12 @@ namespace Barotrauma requiredItems.Add(relatedItem); } } - requiredItems.AddRange(ic.DisabledRequiredItems); + //if we have some actual requirements, no need to keep the empty requirement + //as a "placeholder" for the user to add requirements in the sub editor + if (ic.RequiredItems.None()) + { + requiredItems.AddRange(ic.DisabledRequiredItems); + } foreach (RelatedItem relatedItem in requiredItems) { @@ -1626,12 +1641,16 @@ namespace Barotrauma activeComponents.Clear(); activeComponents.AddRange(components); - foreach (MapEntity entity in linkedTo) + Controller controller = GetComponent(); + if (controller == null || controller.User != Character.Controlled || !controller.HideAllItemComponentHUDs) { - if (Prefab.IsLinkAllowed(entity.Prefab) && entity is Item i) + foreach (MapEntity entity in linkedTo) { - if (!i.DisplaySideBySideWhenLinked) { continue; } - activeComponents.AddRange(i.components); + if (Prefab.IsLinkAllowed(entity.Prefab) && entity is Item i) + { + if (!i.DisplaySideBySideWhenLinked) { continue; } + activeComponents.AddRange(i.components); + } } } @@ -1701,7 +1720,9 @@ namespace Barotrauma foreach (Character otherCharacter in Character.CharacterList) { if (otherCharacter != character && - otherCharacter.SelectedItem == this) + otherCharacter.SelectedItem == this && + // Prevent the in use message from being shown if a character is, for example, inside the deconstructor + !otherCharacter.IsAttachedToController()) { ItemInUseWarning.Visible = true; if (mergedHUDRect.Width > GameMain.GraphicsWidth / 2) { mergedHUDRect.Inflate(-GameMain.GraphicsWidth / 4, 0); } @@ -1751,6 +1772,11 @@ namespace Barotrauma } } + public void ClearActiveHUDs() + { + activeHUDs.Clear(); + } + readonly List texts = new(); public List GetHUDTexts(Character character, bool recreateHudTexts = true) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/ItemPrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/ItemPrefab.cs index 7a7701b9e..6f5041c4f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/ItemPrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/ItemPrefab.cs @@ -166,6 +166,14 @@ namespace Barotrauma subElement.GetAttributeBool("fadein", false), subElement.GetAttributePoint("offset", Point.Zero)); + if (brokenSprite.FadeIn && brokenSprite.MaxConditionPercentage <= 0.0f) + { + DebugConsole.AddWarning( + $"Potential error in item {Identifier}: a broken sprite that's set to fade in despite the max condition being 0."+ + " The sprite cannot fade in if it's set to only appear when the item is fully broken.", + ContentPackage); + } + int spriteIndex = 0; for (int i = 0; i < brokenSprites.Count && brokenSprites[i].MaxConditionPercentage < brokenSprite.MaxConditionPercentage; i++) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Data/IConfigInfo.cs b/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Data/IConfigInfo.cs new file mode 100644 index 000000000..5a902c753 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Data/IConfigInfo.cs @@ -0,0 +1,33 @@ +using Barotrauma.LuaCs.Data; + +namespace Barotrauma.LuaCs.Data; + +public partial interface IConfigInfo : IConfigDisplayInfo { } + +public interface IConfigDisplayInfo +{ + /// + /// Localization Token for display name. + /// + string DisplayName { get; } + /// + /// Localization Token for description. + /// + string Description { get; } + /// + /// The menu category to display under. Used for filtering. + /// + string DisplayCategory { get; } + /// + /// Should this config be displayed in end-user menus. + /// + bool ShowInMenus { get; } + /// + /// User-friendly on-hover tooltip text or Localization Token. + /// + string Tooltip { get; } + /// + /// Icon for display in menus, if available. + /// + ContentPath ImageIconPath { get; } +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Data/IDisplayable.cs b/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Data/IDisplayable.cs new file mode 100644 index 000000000..6d690ec50 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Data/IDisplayable.cs @@ -0,0 +1,9 @@ +using System; +using Microsoft.Xna.Framework; + +namespace Barotrauma.LuaCs.Data; + +public interface IDisplayable +{ + public void AddDisplayComponent(GUILayoutGroup layoutGroup, Vector2 relativeSize, Action onSerializedValue); +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Data/ISettingBase.cs b/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Data/ISettingBase.cs new file mode 100644 index 000000000..e61ad2e1d --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Data/ISettingBase.cs @@ -0,0 +1,6 @@ +namespace Barotrauma.LuaCs.Data; + +public partial interface ISettingBase : IDisplayable +{ + +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Data/ISettingControl.cs b/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Data/ISettingControl.cs new file mode 100644 index 000000000..c0202b2e8 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Data/ISettingControl.cs @@ -0,0 +1,11 @@ +using System; + +namespace Barotrauma.LuaCs.Data; + +public interface ISettingControl : ISettingBase +{ + KeyOrMouse Value { get; } + bool TrySetValue(KeyOrMouse value); + bool IsDown(); + bool IsHit(); +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Data/SettingControl.cs b/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Data/SettingControl.cs new file mode 100644 index 000000000..0314c6c5d --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Data/SettingControl.cs @@ -0,0 +1,252 @@ +using System; +using System.Globalization; +using System.Linq; +using System.Xml.Linq; +using Barotrauma.LuaCs.Data; +using Microsoft.Toolkit.Diagnostics; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Input; +using OneOf; + +namespace Barotrauma.LuaCs.Data; + +public sealed class SettingControl : SettingBase, ISettingControl +{ + public class Factory : ISettingBase.IFactory + { + public ISettingBase CreateInstance(IConfigInfo configInfo, Func, bool> valueChangePredicate) + { + Guard.IsNotNull(configInfo, nameof(configInfo)); + return new SettingControl(configInfo, valueChangePredicate); + } + } + + public SettingControl(IConfigInfo configInfo, Func, bool> valueChangePredicate) : base(configInfo) + { + _valueChangePredicate = valueChangePredicate; + TrySetSerializedValue(configInfo.Element); + } + + protected override void OnDispose() + { + OnValueChanged = null; + } + + private Func, bool> _valueChangePredicate; + public override Type GetValueType() => typeof(KeyOrMouse); + public override string GetStringValue() => Value.ToString(); + public override string GetDefaultStringValue() => new KeyOrMouse(Keys.NumLock).ToString(); + + public override bool TrySetSerializedValue(OneOf value) + { + var newVal = value.Match( + (string v) => GetKeyOrMouse(v), + (XElement e) => e.GetAttributeKeyOrMouse("Value", null)); + + if (newVal is null) + { + return false; + } + + if (_valueChangePredicate is not null && !_valueChangePredicate.Invoke(newVal)) + { + return false; + } + + Value = newVal; + OnValueChanged?.Invoke(this); + return true; + + KeyOrMouse GetKeyOrMouse(string strValue) + { + strValue ??= string.Empty; + if (Enum.TryParse(strValue, true, out Microsoft.Xna.Framework.Input.Keys key)) + { + return key; + } + else if (Enum.TryParse(strValue, out MouseButton mouseButton)) + { + return mouseButton; + } + else if (int.TryParse(strValue, NumberStyles.Any, CultureInfo.InvariantCulture, out int mouseButtonInt) && + Enum.GetValues().Contains((MouseButton)mouseButtonInt)) + { + return (MouseButton)mouseButtonInt; + } + else if (string.Equals(strValue, "LeftMouse", StringComparison.OrdinalIgnoreCase)) + { + return !PlayerInput.MouseButtonsSwapped() ? MouseButton.PrimaryMouse : MouseButton.SecondaryMouse; + } + else if (string.Equals(strValue, "RightMouse", StringComparison.OrdinalIgnoreCase)) + { + return !PlayerInput.MouseButtonsSwapped() ? MouseButton.SecondaryMouse : MouseButton.PrimaryMouse; + } + + return null; + } + + } + + public override event Action OnValueChanged; + public override OneOf GetSerializableValue() => Value.ToString(); + public KeyOrMouse Value { get; private set; } = new KeyOrMouse(Keys.NumLock); + + public bool TrySetValue(KeyOrMouse value) + { + Value = value; + OnValueChanged?.Invoke(this); + return true; + } + + public bool IsDown() + { + if (this.Value is null) + return false; + switch (this.Value.MouseButton) + { + case MouseButton.None: + return Barotrauma.PlayerInput.KeyDown(this.Value.Key); + case MouseButton.PrimaryMouse: + return Barotrauma.PlayerInput.PrimaryMouseButtonHeld(); + case MouseButton.SecondaryMouse: + return Barotrauma.PlayerInput.SecondaryMouseButtonHeld(); + case MouseButton.MiddleMouse: + return Barotrauma.PlayerInput.MidButtonHeld(); + case MouseButton.MouseButton4: + return Barotrauma.PlayerInput.Mouse4ButtonHeld(); + case MouseButton.MouseButton5: + return Barotrauma.PlayerInput.Mouse5ButtonHeld(); + case MouseButton.MouseWheelUp: + return Barotrauma.PlayerInput.MouseWheelUpClicked(); + case MouseButton.MouseWheelDown: + return Barotrauma.PlayerInput.MouseWheelDownClicked(); + } + return false; + } + + public bool IsHit() + { + if (this.Value is null) + return false; + switch (this.Value.MouseButton) + { + case MouseButton.None: + return Barotrauma.PlayerInput.KeyHit(this.Value.Key); + case MouseButton.PrimaryMouse: + return Barotrauma.PlayerInput.PrimaryMouseButtonClicked(); + case MouseButton.SecondaryMouse: + return Barotrauma.PlayerInput.SecondaryMouseButtonClicked(); + case MouseButton.MiddleMouse: + return Barotrauma.PlayerInput.MidButtonClicked(); + case MouseButton.MouseButton4: + return Barotrauma.PlayerInput.Mouse4ButtonClicked(); + case MouseButton.MouseButton5: + return Barotrauma.PlayerInput.Mouse5ButtonClicked(); + case MouseButton.MouseWheelUp: + return Barotrauma.PlayerInput.MouseWheelUpClicked(); + case MouseButton.MouseWheelDown: + return Barotrauma.PlayerInput.MouseWheelDownClicked(); + } + return false; + } + +#if CLIENT + private static GUICustomComponent InputListener; + + public override void AddDisplayComponent(GUILayoutGroup layoutGroup, Vector2 relativeSize, Action onSerializedValue) + { + var inputButton = new GUIButton(new RectTransform(relativeSize, layoutGroup.RectTransform), Alignment.Center, + style: "GUITextBoxNoIcon") + { + Text = this.Value.ToString(), + OnClicked = (btn, obj) => + { + if (InputListener is not null) + { + // Another button is active + return true; + } + CoroutineManager.Invoke(() => + { + CreateListener(btn); + }, 0f); // delay one frame for button inputs + return true; + } + }; + inputButton.OutlineColor = Color.PeachPuff; + inputButton.TextColor = Color.White; + + + void ClearListener() + { + InputListener?.Parent.RemoveChild(InputListener); + InputListener = null; + } + + void CreateListener(GUIButton button) + { + ClearListener(); + InputListener = new GUICustomComponent(new RectTransform(Vector2.Zero, layoutGroup.RectTransform), + onUpdate: (deltaTime, component) => + { + var pressedKeys = PlayerInput.GetKeyboardState.GetPressedKeys(); + if (pressedKeys?.Any() ?? false) + { + if (pressedKeys.Contains(Keys.Escape)) + { + ClearListener(); + return; + } + + ApplyValue(pressedKeys.First(), button); + return; + } + + if (PlayerInput.PrimaryMouseButtonClicked() && + (GUI.MouseOn == null || !(GUI.MouseOn is GUIButton) || GUI.MouseOn.IsChildOf(layoutGroup))) + { + ApplyValue(MouseButton.PrimaryMouse, button); + return; + } + else if (PlayerInput.SecondaryMouseButtonClicked()) + { + ApplyValue(MouseButton.SecondaryMouse, button); + return; + } + else if (PlayerInput.MidButtonClicked()) + { + ApplyValue(MouseButton.MiddleMouse, button); + return; + } + else if (PlayerInput.Mouse4ButtonClicked()) + { + ApplyValue(MouseButton.MouseButton4, button); + return; + } + else if (PlayerInput.Mouse5ButtonClicked()) + { + ApplyValue(MouseButton.MouseButton5, button); + return; + } + else if (PlayerInput.MouseWheelUpClicked()) + { + ApplyValue(MouseButton.MouseWheelUp, button); + return; + } + else if (PlayerInput.MouseWheelDownClicked()) + { + ApplyValue(MouseButton.MouseWheelDown, button); + return; + } + }); + } + + void ApplyValue(KeyOrMouse input, GUIButton button) + { + button.Text = input.ToString(); + onSerializedValue?.Invoke(input.ToString()); + ClearListener(); + } + } +#endif +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Data/StylesResources.cs b/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Data/StylesResources.cs new file mode 100644 index 000000000..a56d11112 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Data/StylesResources.cs @@ -0,0 +1,17 @@ +using System.Collections.Immutable; + +namespace Barotrauma.LuaCs.Data; + +public interface IStylesResourceInfo : IBaseResourceInfo { } + +public record StylesResourceInfo : BaseResourceInfo, IStylesResourceInfo { } + +public partial interface IModConfigInfo +{ + public ImmutableArray Styles { get; } +} + +public partial record ModConfigInfo +{ + public ImmutableArray Styles { get; init; } +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/LuaCs/GUIUtil.cs b/Barotrauma/BarotraumaClient/ClientSource/LuaCs/GUIUtil.cs new file mode 100644 index 000000000..f9f56ed67 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/LuaCs/GUIUtil.cs @@ -0,0 +1,143 @@ +using System; +using System.Collections.Generic; +using Barotrauma.Extensions; +using Microsoft.Xna.Framework; +#nullable enable + +namespace Barotrauma.LuaCs; + +/// +/// A collection of helper GUI functions. Mostly ripped from "Barotrauma/ClientSource/Settings/SettingsMenu.cs" +/// +public static class GUIUtil +{ + public static (GUILayoutGroup Left, GUILayoutGroup Right) CreateSidebars(GUIFrame parent, bool split = false) + { + GUILayoutGroup layout = new GUILayoutGroup(new RectTransform(Vector2.One, parent.RectTransform), isHorizontal: true); + GUILayoutGroup left = new GUILayoutGroup(new RectTransform((0.4875f, 1.0f), layout.RectTransform), isHorizontal: false); + var centerFrame = new GUIFrame(new RectTransform((0.025f, 1.0f), layout.RectTransform), style: null); + if (split) + { + new GUICustomComponent(new RectTransform(Vector2.One, centerFrame.RectTransform), + onDraw: (sb, c) => + { + sb.DrawLine((c.Rect.Center.X, c.Rect.Top), + (c.Rect.Center.X, c.Rect.Bottom), + GUIStyle.TextColorDim, + 2f); + }); + } + GUILayoutGroup right = new GUILayoutGroup(new RectTransform((0.4875f, 1.0f), layout.RectTransform), isHorizontal: false); + return (left, right); + } + + public static (GUILayoutGroup Left, GUILayoutGroup Right) CreateSidebars(GUILayoutGroup parent, bool split = false) + { + GUILayoutGroup layout = new GUILayoutGroup(new RectTransform(Vector2.One, parent.RectTransform), isHorizontal: true); + GUILayoutGroup left = new GUILayoutGroup(new RectTransform((0.4875f, 1.0f), layout.RectTransform), isHorizontal: false); + var centerFrame = new GUIFrame(new RectTransform((0.025f, 1.0f), layout.RectTransform), style: null); + if (split) + { + new GUICustomComponent(new RectTransform(Vector2.One, centerFrame.RectTransform), + onDraw: (sb, c) => + { + sb.DrawLine((c.Rect.Center.X, c.Rect.Top), + (c.Rect.Center.X, c.Rect.Bottom), + GUIStyle.TextColorDim, + 2f); + }); + } + GUILayoutGroup right = new GUILayoutGroup(new RectTransform((0.4875f, 1.0f), layout.RectTransform), isHorizontal: false); + return (left, right); + } + + public static GUILayoutGroup CreateCenterLayout(GUIFrame parent) + => new GUILayoutGroup(new RectTransform((0.5f, 1.0f), parent.RectTransform, Anchor.TopCenter, Pivot.TopCenter)) { ChildAnchor = Anchor.TopCenter }; + + public static RectTransform NewItemRectT(GUILayoutGroup parent, Vector2 adjustRatio) + => new RectTransform((1.0f * adjustRatio.X, 0.06f * adjustRatio.Y), parent.RectTransform, Anchor.CenterLeft); + + public static void Spacer(GUILayoutGroup parent, Vector2 adjustRatio) + => new GUIFrame(new RectTransform((1.0f * adjustRatio.X, 0.03f * adjustRatio.Y), parent.RectTransform, Anchor.CenterLeft), style: null); + + public static void ClearChildElements(GUIComponent component, bool clearSelfFromParent = false) + { + component.GetAllChildren().ForEachMod(c => + { + c.Visible = false; + component.RemoveChild(c); + }); + if (clearSelfFromParent && component.Parent is not null) + component.Parent.RemoveChild(component); + } + + public static GUITextBlock Label(GUILayoutGroup parent, LocalizedString str, GUIFont font, Vector2 adjustRatio) + => new GUITextBlock(NewItemRectT(parent, adjustRatio), str, font: font); + + public static GUIDropDown DropdownEnum(GUILayoutGroup parent, Func textFunc, Func? tooltipFunc, T currentValue, + Action setter, Vector2 adjustRatio) where T : Enum + => Dropdown(parent, textFunc, tooltipFunc, (T[])Enum.GetValues(typeof(T)), currentValue, setter, adjustRatio); + + public static GUIDropDown Dropdown(GUILayoutGroup parent, Func textFunc, Func? tooltipFunc, IReadOnlyList values, T currentValue, Action setter, Vector2 adjustRatio, float listBoxScale = 1) + { + var dropdown = new GUIDropDown(NewItemRectT(parent, adjustRatio), listBoxScale: listBoxScale); + values.ForEach(v => dropdown.AddItem(text: textFunc(v), userData: v, toolTip: tooltipFunc?.Invoke(v) ?? null)); + int childIndex = values.IndexOf(currentValue); + dropdown.Select(childIndex); + dropdown.ListBox.ForceLayoutRecalculation(); + dropdown.ListBox.ScrollToElement(dropdown.ListBox.Content.GetChild(childIndex)); + dropdown.OnSelected = (dd, obj) => + { + setter((T)obj); + return true; + }; + return dropdown; + } + + public static (GUIScrollBar, GUITextBlock) Slider(GUILayoutGroup parent, Vector2 range, int steps, Func labelFunc, float currentValue, Action setter, LocalizedString? tooltip, Vector2 adjustRatio) + { + var layout = new GUILayoutGroup(new RectTransform(adjustRatio, parent.RectTransform), isHorizontal: true); + var slider = new GUIScrollBar(new RectTransform((0.72f, 1.0f), layout.RectTransform), style: "GUISlider") + { + Range = range, + BarScrollValue = currentValue, + Step = 1.0f / (float)(steps - 1), + BarSize = 1.0f / steps + }; + if (tooltip != null) + { + slider.ToolTip = tooltip; + } + var label = new GUITextBlock(new RectTransform((0.28f, 1.0f), layout.RectTransform), + labelFunc(currentValue), wrap: false, textAlignment: Alignment.Center); + slider.OnMoved = (sb, val) => + { + label.Text = labelFunc(sb.BarScrollValue); + setter(sb.BarScrollValue); + return true; + }; + return (slider, label); + } + + public static GUITickBox Tickbox(GUILayoutGroup parent, LocalizedString label, LocalizedString tooltip, + bool currentValue, Action setter, Vector2 adjustRatio) + { + var tickbox = new GUITickBox(NewItemRectT(parent, adjustRatio), label) + { + Selected = currentValue, + ToolTip = tooltip, + OnSelected = (tb) => + { + setter(tb.Selected); + return true; + } + }; + return tickbox; + } + + public static string Percentage(float v) => ToolBox.GetFormattedPercentage(v); + + public static int Round(float v) => (int)MathF.Round(v); +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/LuaCs/LuaCsInstaller.cs b/Barotrauma/BarotraumaClient/ClientSource/LuaCs/LuaCsInstaller.cs index 422147beb..68fa823c7 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/LuaCs/LuaCsInstaller.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/LuaCs/LuaCsInstaller.cs @@ -8,115 +8,7 @@ namespace Barotrauma { public static void Uninstall() { - if (!File.Exists("Temp/Original/Barotrauma.dll")) - { - new GUIMessageBox("Error", "Error: Temp/Original/Barotrauma.dll not found, Github version? Use Steam validate files instead."); - return; - } - - var msg = new GUIMessageBox("Confirm", "Are you sure you want to remove Client-Side LuaCs?", new LocalizedString[2] { TextManager.Get("Yes"), TextManager.Get("Cancel") }); - - msg.Buttons[0].OnClicked = (GUIButton button, object obj) => - { - msg.Close(); - - string[] filesToRemove = new string[] - { - "Barotrauma.dll", "Barotrauma.deps.json", "Barotrauma.pdb", "BarotraumaCore.dll", "BarotraumaCore.pdb", - "System.Reflection.Metadata.dll", "System.Collections.Immutable.dll", - "System.Runtime.CompilerServices.Unsafe.dll" - }; - try - { - CreateMissingDirectory(); - - foreach (string file in filesToRemove) - { - File.Move(file, "Temp/ToDelete/" + file, true); - File.Move("Temp/Original/" + file, file, true); - } - } - catch (Exception e) - { - new GUIMessageBox("Error", $"{e} {e.InnerException} \nTry verifying files instead."); - return false; - } - - new GUIMessageBox("Restart", "Restart your game to apply the changes. If the mod continues to stay active after the restart, try verifying games instead."); - - return true; - }; - - msg.Buttons[1].OnClicked = (GUIButton button, object obj) => - { - msg.Close(); - return true; - }; - } - - public static void CheckUpdate() - { - if (!File.Exists(LuaCsSetup.VersionFile)) { return; } - - ContentPackage luaPackage = LuaCsSetup.GetPackage(LuaCsSetup.LuaForBarotraumaId); - - if (luaPackage == null) { return; } - - string luaCsPath = Path.GetDirectoryName(luaPackage.Path); - string clientVersion = File.ReadAllText(LuaCsSetup.VersionFile); - string workshopVersion = luaPackage.ModVersion; - - if (clientVersion == workshopVersion || File.Exists("debugsomething")) { return; } - - var msg = new GUIMessageBox($"LuaCs Update", $"Your LuaCs client version is different from the version found in the LuaCsForBarotrauma workshop files. Do you want to update?\n\n Client Version: {clientVersion}\n Workshop Version: {workshopVersion}", - new LocalizedString[2] { TextManager.Get("Yes"), TextManager.Get("Cancel") }); - - msg.Buttons[0].OnClicked = (GUIButton button, object obj) => - { - string[] filesToUpdate = trackingFiles.Concat(Directory.EnumerateFiles(luaCsPath, "*.dll", SearchOption.AllDirectories) - .Where(s => s.Contains("mscordaccore_amd64_amd64")).Select(s => Path.GetFileName(s))).ToArray(); - - try - { - CreateMissingDirectory(); - - foreach (string file in filesToUpdate) - { - try - { - File.Move(file, "Temp/Old/" + file, true); - File.Copy(Path.Combine(luaCsPath, "Binary", file), file, true); - } - catch (Exception e) - { - DebugConsole.ThrowError($"Failed to update file {e}"); - } - - } - - File.WriteAllText(LuaCsSetup.VersionFile, workshopVersion); - } - catch (Exception e) - { - new GUIMessageBox("Failed", $"Failed to update, error: {e}"); - - msg.Close(); - return true; - } - - new GUIMessageBox("Restart", $"LuaCs updated! Restart your game to apply the changes."); - - msg.Close(); - return true; - }; - - msg.Buttons[1].OnClicked = (GUIButton button, object obj) => - { - msg.Close(); - return true; - }; - } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/LuaCs/LuaCsNetworking.cs b/Barotrauma/BarotraumaClient/ClientSource/LuaCs/LuaCsNetworking.cs deleted file mode 100644 index 32c830bb4..000000000 --- a/Barotrauma/BarotraumaClient/ClientSource/LuaCs/LuaCsNetworking.cs +++ /dev/null @@ -1,143 +0,0 @@ -using Barotrauma.Networking; -using System.Collections.Generic; - -namespace Barotrauma -{ - partial class LuaCsNetworking - { - private Dictionary> receiveQueue = new Dictionary>(); - - public void SendSyncMessage() - { - if (GameMain.Client == null) { return; } - - WriteOnlyMessage message = new WriteOnlyMessage(); - message.WriteByte((byte)ClientPacketHeader.LUA_NET_MESSAGE); - message.WriteByte((byte)LuaCsClientToServer.RequestAllIds); - GameMain.Client.ClientPeer.Send(message, DeliveryMethod.Reliable); - } - - public void NetMessageReceived(IReadMessage netMessage, ServerPacketHeader header, Client client = null) - { - if (header != ServerPacketHeader.LUA_NET_MESSAGE) - { - GameMain.LuaCs.Hook.Call("netMessageReceived", netMessage, header, client); - return; - } - - LuaCsServerToClient luaCsHeader = (LuaCsServerToClient)netMessage.ReadByte(); - - switch (luaCsHeader) - { - case LuaCsServerToClient.NetMessageString: - HandleNetMessageString(netMessage); - break; - - case LuaCsServerToClient.NetMessageId: - HandleNetMessageId(netMessage); - break; - - case LuaCsServerToClient.ReceiveIds: - ReadIds(netMessage); - break; - } - } - - public IWriteMessage Start(string netMessageName) - { - var message = new WriteOnlyMessage(); - - message.WriteByte((byte)ClientPacketHeader.LUA_NET_MESSAGE); - - if (stringToId.ContainsKey(netMessageName)) - { - message.WriteByte((byte)LuaCsClientToServer.NetMessageId); - message.WriteUInt16(stringToId[netMessageName]); - } - else - { - message.WriteByte((byte)LuaCsClientToServer.NetMessageString); - message.WriteString(netMessageName); - } - - return message; - } - - public void Receive(string netMessageName, LuaCsAction callback) - { - RequestId(netMessageName); - - netReceives[netMessageName] = callback; - } - - public void RequestId(string netMessageName) - { - if (stringToId.ContainsKey(netMessageName)) { return; } - - if (GameMain.Client == null) { return; } - - WriteOnlyMessage message = new WriteOnlyMessage(); - message.WriteByte((byte)ClientPacketHeader.LUA_NET_MESSAGE); - message.WriteByte((byte)LuaCsClientToServer.RequestSingleId); - - message.WriteString(netMessageName); - - Send(message, DeliveryMethod.Reliable); - } - - public void Send(IWriteMessage netMessage, DeliveryMethod deliveryMethod = DeliveryMethod.Reliable) - { - GameMain.Client.ClientPeer.Send(netMessage, deliveryMethod); - } - - private void HandleNetMessageId(IReadMessage netMessage, Client client = null) - { - ushort id = netMessage.ReadUInt16(); - - if (idToString.ContainsKey(id)) - { - string name = idToString[id]; - - HandleNetMessage(netMessage, name, client); - } - else - { - if (!receiveQueue.ContainsKey(id)) { receiveQueue[id] = new Queue(); } - receiveQueue[id].Enqueue(netMessage); - - if (GameSettings.CurrentConfig.VerboseLogging) - { - LuaCsLogger.LogMessage($"Received NetMessage with unknown id {id} from server, storing in queue in case we receive the id later."); - } - } - } - - private void ReadIds(IReadMessage netMessage) - { - ushort size = netMessage.ReadUInt16(); - - for (int i = 0; i < size; i++) - { - ushort id = netMessage.ReadUInt16(); - string name = netMessage.ReadString(); - - idToString[id] = name; - stringToId[name] = id; - - if (!receiveQueue.ContainsKey(id)) - { - continue; - } - - while (receiveQueue[id].TryDequeue(out var queueMessage)) - { - if (netReceives.ContainsKey(name)) - { - netReceives[name](queueMessage, null); - } - } - } - } - } - -} diff --git a/Barotrauma/BarotraumaClient/ClientSource/LuaCs/LuaCsSettingsMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/LuaCs/LuaCsSettingsMenu.cs deleted file mode 100644 index 613a863d9..000000000 --- a/Barotrauma/BarotraumaClient/ClientSource/LuaCs/LuaCsSettingsMenu.cs +++ /dev/null @@ -1,115 +0,0 @@ -using Microsoft.Xna.Framework; - -namespace Barotrauma -{ - static class LuaCsSettingsMenu - { - private static GUIFrame frame; - - public static void Open(RectTransform rectTransform) - { - Close(); - - frame = new GUIFrame(new RectTransform(new Vector2(0.4f, 0.6f), rectTransform, Anchor.Center)); - - GUIListBox list = new GUIListBox(new RectTransform(new Vector2(0.95f, 0.95f), frame.RectTransform, Anchor.Center), false); - - new GUITextBlock(new RectTransform(new Vector2(1f, 0.1f), list.Content.RectTransform), "LuaCs Settings", textAlignment: Alignment.Center); - - - new GUITickBox(new RectTransform(new Vector2(0.8f, 0.1f), list.Content.RectTransform), "Enable CSharp Scripting") - { - Selected = GameMain.LuaCs.Config.EnableCsScripting, - ToolTip = "This enables CSharp Scripting for mods to use, WARNING: CSharp is NOT sandboxed, be careful with what mods you download.", - OnSelected = (GUITickBox tick) => - { - GameMain.LuaCs.Config.EnableCsScripting = tick.Selected; - GameMain.LuaCs.WriteSettings(); - - return true; - } - }; - - new GUITickBox(new RectTransform(new Vector2(0.8f, 0.1f), list.Content.RectTransform), "Treat Forced Mods As Normal") - { - Selected = GameMain.LuaCs.Config.TreatForcedModsAsNormal, - ToolTip = "This makes mods that were setup to run even when disabled to only run when enabled.", - OnSelected = (GUITickBox tick) => - { - GameMain.LuaCs.Config.TreatForcedModsAsNormal = tick.Selected; - GameMain.LuaCs.WriteSettings(); - - return true; - } - }; - - new GUITickBox(new RectTransform(new Vector2(0.8f, 0.1f), list.Content.RectTransform), "Prefer To Use Workshop Lua Setup") - { - Selected = GameMain.LuaCs.Config.PreferToUseWorkshopLuaSetup, - ToolTip = "This makes Lua look first for the Lua/LuaSetup.lua located in the Workshop package instead of the one located locally.", - OnSelected = (GUITickBox tick) => - { - GameMain.LuaCs.Config.PreferToUseWorkshopLuaSetup = tick.Selected; - GameMain.LuaCs.WriteSettings(); - - return true; - } - }; - - new GUITickBox(new RectTransform(new Vector2(0.8f, 0.1f), list.Content.RectTransform), "Disable Error GUI Overlay") - { - Selected = GameMain.LuaCs.Config.DisableErrorGUIOverlay, - ToolTip = "", - OnSelected = (GUITickBox tick) => - { - GameMain.LuaCs.Config.DisableErrorGUIOverlay = tick.Selected; - GameMain.LuaCs.WriteSettings(); - - return true; - } - }; - - new GUITickBox(new RectTransform(new Vector2(0.8f, 0.1f), list.Content.RectTransform), "Hide usernames In Error Logs") - { - Selected = GameMain.LuaCs.Config.HideUserNames, - ToolTip = "Hides the operating system username when displaying error logs (eg your username on windows).", - OnSelected = (GUITickBox tick) => - { - GameMain.LuaCs.Config.HideUserNames = tick.Selected; - GameMain.LuaCs.WriteSettings(); - - return true; - } - }; - - new GUIButton(new RectTransform(new Vector2(1f, 0.1f), list.Content.RectTransform), $"Remove Client-Side LuaCs", style: "GUIButtonSmall") - { - ToolTip = "Remove Client-Side LuaCs.", - OnClicked = (tb, userdata) => - { - LuaCsInstaller.Uninstall(); - return true; - } - }; - - new GUIButton(new RectTransform(new Vector2(0.8f, 0.01f), frame.RectTransform, Anchor.BottomCenter) - { - RelativeOffset = new Vector2(0f, 0.05f) - }, "Close") - { - OnClicked = (GUIButton button, object obj) => - { - Close(); - - return true; - } - }; - } - - public static void Close() - { - frame?.Parent.RemoveChild(frame); - frame = null; - } - } -} diff --git a/Barotrauma/BarotraumaClient/ClientSource/LuaCs/LuaCsSetup.cs b/Barotrauma/BarotraumaClient/ClientSource/LuaCs/LuaCsSetup.cs index b85175ded..fea455064 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/LuaCs/LuaCsSetup.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/LuaCs/LuaCsSetup.cs @@ -1,75 +1,184 @@ -using System.Collections.Generic; +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 static System.Collections.Specialized.BitVector32; + +// ReSharper disable ObjectCreationAsStatement namespace Barotrauma { partial class LuaCsSetup - { - public void AddToGUIUpdateList() + { + public void PromptCSharpMods(Action onSelection, bool joiningServer) { - if (!GameMain.LuaCs.Config.DisableErrorGUIOverlay) + ImmutableArray contentPackages = PackageManagementService.GetLoadedUnrestrictedPackages() + .Where(p => p.Name != PackageName) + .ToImmutableArray(); + + if (_csRunPolicy?.Value is "Enabled") { - LuaCsLogger.AddToGUIUpdateList(); + IsCsEnabledForSession = true; + onSelection(true); + return; } + else if (_csRunPolicy?.Value is "Disabled") + { + IsCsEnabledForSession = false; + onSelection(false); + return; + } + + if (contentPackages.None()) + { + onSelection(true); + return; + } + + GUIMessageBox messageBox = new GUIMessageBox( + TextManager.Get("warning"), + relativeSize: new Vector2(0.3f, 0.55f), + minSize: new Point(400, 500), + text: string.Empty, + buttons: []); + + GUILayoutGroup msgBoxLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.75f), messageBox.Content.RectTransform), isHorizontal: false, childAnchor: Anchor.TopCenter) + { + RelativeSpacing = 0.01f, + Stretch = true + }; + + 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)) + { + CurrentSelectMode = GUIListBox.SelectMode.None + }; + + foreach (ContentPackage package in contentPackages) + { + GUIFrame packageFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.15f), packageListBox.Content.RectTransform), style: "ListBoxElement"); + 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; + } + }; + } + + 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), "Enable C# for this session") + { + TextBlock = { AutoScaleHorizontal = true }, + OnClicked = (btn, userdata) => + { + IsCsEnabledForSession = true; + onSelection(true); + messageBox.Close(); + return true; + } + }; + + new GUIButton(new RectTransform(new Vector2(0.8f, 0.0f), buttonLayout.RectTransform), "Cancel") + { + OnClicked = (btn, userdata) => + { + IsCsEnabledForSession = false; + onSelection(false); + messageBox.Close(); + return true; + } + }; } - public void CheckInitialize() + private void SetupServicesProviderClient(IServicesProvider serviceProvider) { - List csharpMods = new List(); - foreach (ContentPackage cp in ContentPackageManager.EnabledPackages.All) + serviceProvider.RegisterServiceType(ServiceLifetime.Singleton); + // supplied via factory + //serviceProvider.RegisterServiceType(ServiceLifetime.Transient); + serviceProvider.RegisterServiceType, ModConfigFileParserService>(ServiceLifetime.Transient); + serviceProvider.RegisterServiceType(ServiceLifetime.Transient); + serviceProvider.RegisterServiceType(ServiceLifetime.Singleton); + } + + /// + /// Handles changes in game states tracked by screen changes. + /// + /// The new game screen. + public partial void OnScreenSelected(Screen screen) + { + /*Note: This logic needs to be run after the triggering event so that recursion scenarios (ie. resetting the EventService) + do not occur, so we delay it by one game tick.*/ + CoroutineManager.Invoke(() => { - if (Directory.Exists(cp.Dir + "/CSharp") || Directory.Exists(cp.Dir + "/bin")) + switch (screen) { - csharpMods.Add(cp); + // menus and navigation states + case MainMenuScreen: + case ModDownloadScreen: + case ServerListScreen: + SetRunState(RunState.Unloaded); + SetRunState(RunState.LoadedNoExec); + break; + // running lobby or editor states + case CampaignEndScreen: + case CharacterEditorScreen: + case EventEditorScreen: + case GameScreen: + case LevelEditorScreen: + case NetLobbyScreen: + case ParticleEditorScreen: + case RoundSummaryScreen: + case SpriteEditorScreen: + case SubEditorScreen: + case TestScreen: // notes: TestScreen is a Linux edge case editor screen and is deprecated. + + 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( + $"{nameof(LuaCsSetup)}: Received an unknown screen {screen?.GetType().Name ?? "'null screen'"}. Retarding load state to 'unloaded'."); + SetRunState(RunState.Unloaded); + break; } - } - - if (csharpMods.Count == 0 || ShouldRunCs) - { - Initialize(); - return; - } - - StringBuilder sb = new StringBuilder(); - - foreach (ContentPackage cp in csharpMods) - { - if (cp.UgcId.TryUnwrap(out ContentPackageId id)) - { - sb.AppendLine($"- {cp.Name} ({id})"); - } - else - { - sb.AppendLine($"- {cp.Name} (Not On Workshop)"); - } - } - - if (GameMain.Client == null || GameMain.Client.IsServerOwner) - { - new GUIMessageBox("", $"You have CSharp mods enabled but don't have the CSharp Scripting enabled, those mods might not work, go to the Main Menu, click on LuaCs Settings and check Enable CSharp Scripting.\n\n{sb}"); - Initialize(); - return; - } - - GUIMessageBox msg = new GUIMessageBox( - "Confirm", - $"This server has the following CSharp mods installed: \n{sb}\nDo you wish to run them? Cs mods are not sandboxed so make sure you trust these mods.", - new LocalizedString[2] { "Run", "Don't Run" }); - - msg.Buttons[0].OnClicked = (GUIButton button, object obj) => - { - Initialize(true); - msg.Close(); - return true; - }; - - msg.Buttons[1].OnClicked = (GUIButton button, object obj) => - { - Initialize(); - msg.Close(); - return true; - }; + }, delay: 0f); // min is one tick delay. } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/LuaCs/_Services/ConfigService.cs b/Barotrauma/BarotraumaClient/ClientSource/LuaCs/_Services/ConfigService.cs new file mode 100644 index 000000000..16a67e301 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/LuaCs/_Services/ConfigService.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Immutable; +using System.Linq; +using Barotrauma.LuaCs.Data; + +namespace Barotrauma.LuaCs; + +public sealed partial class ConfigService +{ + public ImmutableArray GetDisplayableConfigs() + { + using var _ = _operationLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult(); + IService.CheckDisposed(this); + + return _settingsInstances.Values + .Where(s => !s.IsDisposed) + .Where(s => s.GetDisplayInfo().ShowInMenus) + .Where(s => !GameMain.IsMultiplayer || s.GetConfigInfo().NetSync != NetSync.ServerAuthority) + .Where(s => s.GetConfigInfo().EditableStates >= _infoProvider.CurrentRunState) + .ToImmutableArray(); + } +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/LuaCs/_Services/LoggerService.cs b/Barotrauma/BarotraumaClient/ClientSource/LuaCs/_Services/LoggerService.cs new file mode 100644 index 000000000..bb1af88a4 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/LuaCs/_Services/LoggerService.cs @@ -0,0 +1,50 @@ +using Microsoft.Xna.Framework; + +namespace Barotrauma.LuaCs; + +public partial class LoggerService : ILoggerService, IClientLoggerService +{ + private GUIFrame _overlayFrame; + private GUITextBlock _textBlock; + private double _showTimer = 0; + + + private void CreateOverlay(string message) + { + _overlayFrame = new GUIFrame(new RectTransform(new Vector2(0.4f, 0.03f), null), null, new Color(50, 50, 50, 100)) + { + CanBeFocused = false + }; + + GUILayoutGroup layout = + new GUILayoutGroup( + new RectTransform(new Vector2(0.8f, 0.8f), _overlayFrame.RectTransform, Anchor.CenterLeft), false, + Anchor.Center); + + _textBlock = new GUITextBlock(new RectTransform(new Vector2(1f, 0f), layout.RectTransform), message); + _overlayFrame.RectTransform.MinSize = new Point((int)(_textBlock.TextSize.X * 1.2), 0); + + layout.Recalculate(); + } + + public void AddToGUIUpdateList() + { + if (_overlayFrame != null && Timing.TotalTime <= _showTimer) + { + _overlayFrame.AddToGUIUpdateList(); + } + } + + public void ShowErrorOverlay(string message, float time = 5f, float duration = 1.5f) + { + if (Timing.TotalTime <= _showTimer) + { + return; + } + + CreateOverlay(message); + + _overlayFrame.Flash(Color.Red, duration, true); + _showTimer = Timing.TotalTime + time; + } +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/LuaCs/_Services/ModConfigStylesFileParserService.cs b/Barotrauma/BarotraumaClient/ClientSource/LuaCs/_Services/ModConfigStylesFileParserService.cs new file mode 100644 index 000000000..ade7f4305 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/LuaCs/_Services/ModConfigStylesFileParserService.cs @@ -0,0 +1,44 @@ +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Threading.Tasks; +using Barotrauma.LuaCs.Data; +using FluentResults; + +namespace Barotrauma.LuaCs; + +public sealed partial class ModConfigFileParserService : + IParserServiceAsync +{ + async Task> IParserServiceAsync.TryParseResourceAsync(ResourceParserInfo src) + { + using var lck = await _operationsLock.AcquireReaderLock(); + IService.CheckDisposed(this); + + if (CheckThrowNullRefs(src, "Style") is { IsFailed: true } fail) + return fail; + + var runtimeEnv = GetRuntimeEnvironment(src.Element); + var fileResults = await UnsafeGetCheckedFiles(src.Element, src.Owner, ".xml"); + + if (fileResults.IsFailed) + return FluentResults.Result.Fail(fileResults.Errors); + + return new StylesResourceInfo() + { + SupportedPlatforms = runtimeEnv.Platform, + SupportedTargets = Target.Client, // clientside only + LoadPriority = src.Element.GetAttributeInt("LoadPriority", 0), + FilePaths = fileResults.Value, + Optional = src.Element.GetAttributeBool("Optional", false), + InternalName = src.Element.GetAttributeString("Name", string.Empty), + OwnerPackage = src.Owner, + RequiredPackages = src.Required, + IncompatiblePackages = src.Incompatible + }; + } + + public async Task>> TryParseResourcesAsync(IEnumerable sources) + { + return await this.TryParseGenericResourcesAsync(sources); + } +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/LuaCs/_Services/NetworkingService.cs b/Barotrauma/BarotraumaClient/ClientSource/LuaCs/_Services/NetworkingService.cs new file mode 100644 index 000000000..fa3b2905e --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/LuaCs/_Services/NetworkingService.cs @@ -0,0 +1,163 @@ +using Barotrauma.LuaCs; +using Barotrauma.LuaCs.Events; +using Barotrauma.Networking; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; + +namespace Barotrauma.LuaCs; + +partial class NetworkingService : INetworkingService, IEventServerConnected, IEventServerRawNetMessageReceived +{ + private ConcurrentDictionary> receiveQueue = new(); + + public void OnServerConnected() + { + ActivateNetVars(); + SendSyncMessage(); + } + + private void ActivateNetVars() + { + if (GameMain.Client == null) + { + return; + } + + // re-activate net vars + // todo: unregister net vars on client disconnect, currently handled by unloading the state machine. + foreach (var networkSyncVar in netVars.Keys) + { + networkSyncVar.SetNetworkOwner(this); + } + } + + public bool? OnReceivedServerNetMessage(IReadMessage netMessage, ServerPacketHeader serverPacketHeader) + { + if (serverPacketHeader != ServerHeader) + { + return null; + } + + ServerToClient luaCsHeader = (ServerToClient)netMessage.ReadByte(); + + switch (luaCsHeader) + { + case ServerToClient.NetMessageNetId: + HandleNetMessageString(netMessage); + break; + + case ServerToClient.NetMessageInternalId: + HandleNetMessageId(netMessage); + break; + + case ServerToClient.ReceiveNetIds: + ReadIds(netMessage); + break; + } + + return true; + } + + private void SendSyncMessage() + { + if (GameMain.Client == null) { return; } + + WriteOnlyMessage message = new WriteOnlyMessage(); + message.WriteByte((byte)ClientHeader); + message.WriteByte((byte)ClientToServer.RequestSync); + GameMain.Client.ClientPeer.Send(message, DeliveryMethod.Reliable); + } + + public IWriteMessage Start(NetId netId) + { + var message = new WriteOnlyMessage(); + + message.WriteByte((byte)ClientHeader); + + if (idToPacket.ContainsKey(netId)) + { + message.WriteByte((byte)ClientToServer.NetMessageInternalId); + message.WriteUInt16(idToPacket[netId]); + } + else + { + message.WriteByte((byte)ClientToServer.NetMessageNetId); + NetId.Write(message, netId); + } + + return message; + } + + public void SendToServer(IWriteMessage netMessage, DeliveryMethod deliveryMethod = DeliveryMethod.Reliable) + { + GameMain.Client.ClientPeer.Send(netMessage, deliveryMethod); + } + + public void Send(IWriteMessage netMessage, DeliveryMethod deliveryMethod = DeliveryMethod.Reliable) + => SendToServer(netMessage, deliveryMethod); + + private void RequestId(NetId netId) + { + if (idToPacket.ContainsKey(netId)) { return; } + + if (GameMain.Client == null) { return; } + + WriteOnlyMessage message = new WriteOnlyMessage(); + message.WriteByte((byte)ClientHeader); + message.WriteByte((byte)ClientToServer.RequestSingleNetId); + + NetId.Write(message, netId); + + SendToServer(message, DeliveryMethod.Reliable); + } + + private void HandleNetMessageId(IReadMessage netMessage, Client client = null) + { + ushort id = netMessage.ReadUInt16(); + + if (packetToId.ContainsKey(id)) + { + HandleNetMessage(netMessage, packetToId[id], client); + } + else + { + if (!receiveQueue.ContainsKey(id)) { receiveQueue[id] = new ConcurrentQueue(); } + receiveQueue[id].Enqueue(netMessage); + + if (GameSettings.CurrentConfig.VerboseLogging) + { + _loggerService.LogMessage($"Received NetMessage with unknown id {id} from server, storing in queue in case we receive the id later."); + } + } + } + + private void ReadIds(IReadMessage netMessage) + { + ushort size = netMessage.ReadUInt16(); + + for (int i = 0; i < size; i++) + { + ushort packetId = netMessage.ReadUInt16(); + NetId netId = NetId.Read(netMessage); + + packetToId[packetId] = netId; + idToPacket[netId] = packetId; + + if (!receiveQueue.ContainsKey(packetId)) + { + continue; + } + + // We could have received messages before receiving the sync message, so we need to process them now + + while (receiveQueue[packetId].TryDequeue(out var queueMessage)) + { + if (netReceives.ContainsKey(netId)) + { + netReceives[netId](queueMessage); + } + } + } + } +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/LuaCs/_Services/UIStylesCollection.cs b/Barotrauma/BarotraumaClient/ClientSource/LuaCs/_Services/UIStylesCollection.cs new file mode 100644 index 000000000..6bf134d8c --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/LuaCs/_Services/UIStylesCollection.cs @@ -0,0 +1,239 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Collections.Immutable; +using Barotrauma.Extensions; +using Barotrauma.LuaCs.Data; +using FluentResults; +using Microsoft.Toolkit.Diagnostics; + +namespace Barotrauma.LuaCs; + +public class UIStylesCollection : HashlessFile, IUIStylesCollection +{ + public class Factory : IUIStylesCollection.IFactory + { + public IEnumerable CreateInstance(IStylesResourceInfo info, IStorageService storageService) + { + Guard.IsNotNull(info, nameof(info)); + Guard.IsNotNull(info.OwnerPackage, nameof(info.OwnerPackage)); + if (info.FilePaths.IsDefaultOrEmpty) + { + return ImmutableArray.Empty; + } + + var builder = ImmutableArray.CreateBuilder(); + foreach (var contentPath in info.FilePaths) + { + builder.Add(new UIStylesCollection(contentPath, storageService)); + } + return builder.ToImmutable(); + } + + public void Dispose() + { + //ignore, stateless service + } + + public bool IsDisposed => false; + } + + private readonly ConcurrentDictionary _fonts = new(); + private readonly ConcurrentDictionary _sprites = new(); + private readonly ConcurrentDictionary _spriteSheets = new(); + private readonly ConcurrentDictionary _cursors = new(); + private readonly ConcurrentDictionary _colors = new(); + + /// + /// Only for internal reference. + /// + private UIStyleFile _fakeFile; + + private IStorageService _storageService; + + public UIStylesCollection(ContentPath path, IStorageService storageService) : base(path.ContentPackage, path) + { + Guard.IsNotNull(path, nameof(path)); + Guard.IsNotNull(path.ContentPackage, nameof(path.ContentPackage)); + _storageService = storageService; + _fakeFile = new UIStyleFile(path.ContentPackage, path); + } + + public new ContentPath Path => base.Path; + + public Result GetFont(string name) + { + using var lck = _lock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult(); + IService.CheckDisposed(this); + if (_fonts.TryGetValue(name, out var asset)) + { + return asset; + } + + return FluentResults.Result.Fail($"{nameof(GetFont)}: Failed to find the font with the name '{name}'"); + } + + public Result GetSprite(string name) + { + using var lck = _lock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult(); + IService.CheckDisposed(this); + if (_sprites.TryGetValue(name, out var asset)) + { + return asset; + } + + return FluentResults.Result.Fail($"{nameof(GetSprite)}: Failed to find the sprite with the name '{name}'"); + } + + public Result GetSpriteSheet(string name) + { + using var lck = _lock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult(); + IService.CheckDisposed(this); + if (_spriteSheets.TryGetValue(name, out var asset)) + { + return asset; + } + + return FluentResults.Result.Fail($"{nameof(GetSpriteSheet)}: Failed to find the spritesheet with the name '{name}'"); + } + + public Result GetCursor(string name) + { + using var lck = _lock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult(); + IService.CheckDisposed(this); + if (_cursors.TryGetValue(name, out var asset)) + { + return asset; + } + + return FluentResults.Result.Fail($"{nameof(GetCursor)}: Failed to find the cursor with the name '{name}'"); + } + + public Result GetColor(string name) + { + using var lck = _lock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult(); + IService.CheckDisposed(this); + if (_colors.TryGetValue(name, out var asset)) + { + return asset; + } + + return FluentResults.Result.Fail($"{nameof(GetColor)}: Failed to find the color with the name '{name}'"); + } + + public override void LoadFile() + { + using var lck = _lock.AcquireWriterLock().ConfigureAwait(false).GetAwaiter().GetResult(); + IService.CheckDisposed(this); + + if (_storageService.LoadPackageXml(Path) is not { IsSuccess: true } result) + { + DebugConsole.LogError($"Failed to load xml from {Path.FullPath}."); + ThrowHelper.ThrowArgumentException($"Failed to load xml from {Path.FullPath}."); + return; + } + + var root = result.Value.Root?.FromPackage(Path.ContentPackage); + if (root is null) + { + return; + } + + var styleElement = root.Name.LocalName.ToLowerInvariant() == "style" ? root : root.GetChildElement("style"); + if (styleElement is null) + return; + + var childElements = styleElement.GetChildElements("Font"); + if (childElements is not null) + AddToList(_fonts, childElements, _fakeFile); + + childElements = styleElement.GetChildElements("Sprite"); + if (childElements is not null) + AddToList(_sprites, childElements, _fakeFile); + + childElements = styleElement.GetChildElements("Spritesheet"); + if (childElements is not null) + AddToList(_spriteSheets, childElements, _fakeFile); + + childElements = styleElement.GetChildElements("Cursor"); + if (childElements is not null) + AddToList(_cursors, childElements, _fakeFile); + + childElements = styleElement.GetChildElements("Color"); + if (childElements is not null) + AddToList(_colors, childElements, _fakeFile); + + void AddToList(ConcurrentDictionary dict, IEnumerable elem, UIStyleFile file) where T1 : GUISelector where T2 : GUIPrefab + { + foreach (ContentXElement prefabElement in elem) + { + string name = prefabElement.GetAttributeString("name", string.Empty); + if (name != string.Empty) + { + var prefab = (T2)Activator.CreateInstance(typeof(T2), new object[]{ prefabElement, file })!; + if (!dict.ContainsKey(name)) + dict[name] = (T1)Activator.CreateInstance(typeof(T1), new object[] { name })!; + dict[name].Prefabs.Add(prefab, false); + } + } + } + } + + public override void UnloadFile() + { + using var lck = _lock.AcquireWriterLock().ConfigureAwait(false).GetAwaiter().GetResult(); + IService.CheckDisposed(this); + + _fonts.Values.ForEach(p => p.Prefabs.RemoveByFile(_fakeFile)); + _sprites.Values.ForEach(p => p.Prefabs.RemoveByFile(_fakeFile)); + _spriteSheets.Values.ForEach(p => p.Prefabs.RemoveByFile(_fakeFile)); + _cursors.Values.ForEach(p => p.Prefabs.RemoveByFile(_fakeFile)); + _colors.Values.ForEach(p => p.Prefabs.RemoveByFile(_fakeFile)); + } + + public override void Sort() + { + using var lck = _lock.AcquireWriterLock().ConfigureAwait(false).GetAwaiter().GetResult(); + IService.CheckDisposed(this); + + _fonts.Values.ForEach(p => p.Prefabs.Sort()); + _sprites.Values.ForEach(p => p.Prefabs.Sort()); + _spriteSheets.Values.ForEach(p => p.Prefabs.Sort()); + _cursors.Values.ForEach(p => p.Prefabs.Sort()); + _colors.Values.ForEach(p => p.Prefabs.Sort()); + } + + #region INTERNAL_DISPOSE + + private readonly AsyncReaderWriterLock _lock = new(); + + public void Dispose() + { + using var lck = _lock.AcquireWriterLock().ConfigureAwait(false).GetAwaiter().GetResult(); + if (!ModUtils.Threading.CheckIfClearAndSetBool(ref _isDisposed)) + { + return; + } + + _fonts.Values.ForEach(p => p.Prefabs.RemoveByFile(_fakeFile)); + _sprites.Values.ForEach(p => p.Prefabs.RemoveByFile(_fakeFile)); + _spriteSheets.Values.ForEach(p => p.Prefabs.RemoveByFile(_fakeFile)); + _cursors.Values.ForEach(p => p.Prefabs.RemoveByFile(_fakeFile)); + _colors.Values.ForEach(p => p.Prefabs.RemoveByFile(_fakeFile)); + + _fonts.Clear(); + _sprites.Clear(); + _spriteSheets.Clear(); + _cursors.Clear(); + _colors.Clear(); + } + + private int _isDisposed; + public bool IsDisposed + { + get => ModUtils.Threading.GetBool(ref _isDisposed); + private set => ModUtils.Threading.SetBool(ref _isDisposed, value); + } + + #endregion +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/LuaCs/_Services/UIStylesService.cs b/Barotrauma/BarotraumaClient/ClientSource/LuaCs/_Services/UIStylesService.cs new file mode 100644 index 000000000..2508d7376 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/LuaCs/_Services/UIStylesService.cs @@ -0,0 +1,350 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Immutable; +using System.Linq; +using Barotrauma.LuaCs.Data; +using FluentResults; +using Microsoft.Toolkit.Diagnostics; + +namespace Barotrauma.LuaCs; + +public class UIStylesService : IUIStylesService +{ + #region DISPOSAL + + public void Dispose() + { + using var lck = _lock.AcquireWriterLock().ConfigureAwait(false).GetAwaiter().GetResult(); + if (!ModUtils.Threading.CheckIfClearAndSetBool(ref _isDisposed)) + { + return; + } + + foreach (var collection in _stylesCollections.Values.SelectMany(c => c)) + { + try + { + collection.Dispose(); + } + catch + { + //ignored + } + } + + _stylesCollections.Clear(); + _storageService.Dispose(); + _stylesCollectionFactory.Dispose(); + + _storageService = null; + _stylesCollectionFactory = null; + } + + private int _isDisposed = 0; + public bool IsDisposed + { + get => ModUtils.Threading.GetBool(ref _isDisposed); + private set => ModUtils.Threading.SetBool(ref _isDisposed, value); + } + public FluentResults.Result Reset() + { + using var lck = _lock.AcquireWriterLock().ConfigureAwait(false).GetAwaiter().GetResult(); + IService.CheckDisposed(this); + + var result = FluentResults.Result.Ok(); + + foreach (var collection in _stylesCollections.Values.SelectMany(c => c)) + { + try + { + collection.Dispose(); + } + catch (Exception e) + { + result.WithError(new ExceptionalError(e)); + } + } + + _stylesCollections.Clear(); + + return result; + } + + private readonly AsyncReaderWriterLock _lock = new(); + + #endregion + + private IStorageService _storageService; + private IUIStylesCollection.IFactory _stylesCollectionFactory; + + private ConcurrentDictionary<(ContentPackage Package, string InternalName), ImmutableArray> + _stylesCollections = new(); + + public UIStylesService(IUIStylesCollection.IFactory stylesCollectionFactory, IStorageService storageService) + { + _stylesCollectionFactory = stylesCollectionFactory; + _storageService = storageService; + } + + public Result GetColor(ContentPackage package, string internalName, string assetName) + { + using var lck = _lock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult(); + IService.CheckDisposed(this); + Guard.IsNotNull(package, nameof(package)); + Guard.IsNotNullOrWhiteSpace(internalName, nameof(internalName)); + Guard.IsNotNullOrWhiteSpace(assetName, nameof(assetName)); + + if (!_stylesCollections.TryGetValue((package, internalName), out var collection) + || collection.IsDefaultOrEmpty) + { + return FluentResults.Result.Fail( + $"{nameof(UIStylesService)}: No styles loaded for [ContentPackage].[InternalName] of: [{package.Name}].[{internalName}]"); + } + + var failedResult = new FluentResults.Result(); + + foreach (var stylesCollection in collection) + { + var res = stylesCollection.GetColor(assetName); + if (res.IsSuccess) + { + return res; + } + + failedResult.WithErrors(res.Errors); + } + + return failedResult; + } + + public Result GetCursor(ContentPackage package, string internalName, string assetName) + { + using var lck = _lock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult(); + IService.CheckDisposed(this); + Guard.IsNotNull(package, nameof(package)); + Guard.IsNotNullOrWhiteSpace(internalName, nameof(internalName)); + Guard.IsNotNullOrWhiteSpace(assetName, nameof(assetName)); + + if (!_stylesCollections.TryGetValue((package, internalName), out var collection) + || collection.IsDefaultOrEmpty) + { + return FluentResults.Result.Fail( + $"{nameof(UIStylesService)}: No styles loaded for [ContentPackage].[InternalName] of: [{package.Name}].[{internalName}]"); + } + + var failedResult = new FluentResults.Result(); + + foreach (var stylesCollection in collection) + { + var res = stylesCollection.GetCursor(assetName); + if (res.IsSuccess) + { + return res; + } + + failedResult.WithErrors(res.Errors); + } + + return failedResult; + } + + public Result GetFont(ContentPackage package, string internalName, string assetName) + { + using var lck = _lock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult(); + IService.CheckDisposed(this); + Guard.IsNotNull(package, nameof(package)); + Guard.IsNotNullOrWhiteSpace(internalName, nameof(internalName)); + Guard.IsNotNullOrWhiteSpace(assetName, nameof(assetName)); + + if (!_stylesCollections.TryGetValue((package, internalName), out var collection) + || collection.IsDefaultOrEmpty) + { + return FluentResults.Result.Fail( + $"{nameof(UIStylesService)}: No styles loaded for [ContentPackage].[InternalName] of: [{package.Name}].[{internalName}]"); + } + + var failedResult = new FluentResults.Result(); + + foreach (var stylesCollection in collection) + { + var res = stylesCollection.GetFont(assetName); + if (res.IsSuccess) + { + return res; + } + + failedResult.WithErrors(res.Errors); + } + + return failedResult; + } + + public Result GetSprite(ContentPackage package, string internalName, string assetName) + { + using var lck = _lock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult(); + IService.CheckDisposed(this); + Guard.IsNotNull(package, nameof(package)); + Guard.IsNotNullOrWhiteSpace(internalName, nameof(internalName)); + Guard.IsNotNullOrWhiteSpace(assetName, nameof(assetName)); + + if (!_stylesCollections.TryGetValue((package, internalName), out var collection) + || collection.IsDefaultOrEmpty) + { + return FluentResults.Result.Fail( + $"{nameof(UIStylesService)}: No styles loaded for [ContentPackage].[InternalName] of: [{package.Name}].[{internalName}]"); + } + + var failedResult = new FluentResults.Result(); + + foreach (var stylesCollection in collection) + { + var res = stylesCollection.GetSprite(assetName); + if (res.IsSuccess) + { + return res; + } + + failedResult.WithErrors(res.Errors); + } + + return failedResult; + } + + public Result GetSpriteSheet(ContentPackage package, string internalName, string assetName) + { + using var lck = _lock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult(); + IService.CheckDisposed(this); + Guard.IsNotNull(package, nameof(package)); + Guard.IsNotNullOrWhiteSpace(internalName, nameof(internalName)); + Guard.IsNotNullOrWhiteSpace(assetName, nameof(assetName)); + + if (!_stylesCollections.TryGetValue((package, internalName), out var collection) + || collection.IsDefaultOrEmpty) + { + return FluentResults.Result.Fail( + $"{nameof(UIStylesService)}: No styles loaded for [ContentPackage].[InternalName] of: [{package.Name}].[{internalName}]"); + } + + var failedResult = new FluentResults.Result(); + + foreach (var stylesCollection in collection) + { + var res = stylesCollection.GetSpriteSheet(assetName); + if (res.IsSuccess) + { + return res; + } + + failedResult.WithErrors(res.Errors); + } + + return failedResult; + } + + public FluentResults.Result LoadAssets(ImmutableArray resources) + { + using var lck = _lock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult(); + IService.CheckDisposed(this); + if (resources.IsDefaultOrEmpty) + { + ThrowHelper.ThrowArgumentNullException(nameof(resources)); + } + + var operationSuccess = FluentResults.Result.Ok(); + + foreach (var resource in resources) + { + var builder = ImmutableArray.CreateBuilder(); + if (_stylesCollections.TryGetValue((resource.OwnerPackage, resource.InternalName), out var collection)) + { + builder.AddRange(collection); + } + + try + { + var newCollections = _stylesCollectionFactory.CreateInstance(resource, _storageService).ToImmutableArray(); + foreach (var stylesCollection in newCollections) + { + stylesCollection.LoadFile(); + } + builder.AddRange(newCollections); + } + catch (Exception e) + { + operationSuccess.WithError(new ExceptionalError(e)); + continue; + } + + _stylesCollections[(resource.OwnerPackage, resource.InternalName)] = builder.ToImmutable(); + } + + return operationSuccess; + } + + public FluentResults.Result UnloadPackages(ImmutableArray packages) + { + using var lck = _lock.AcquireWriterLock().ConfigureAwait(false).GetAwaiter().GetResult(); + IService.CheckDisposed(this); + + var toRemove = _stylesCollections + .Select(c => c.Key) + .Where(c => packages.Contains(c.Package)) + .ToImmutableArray(); + + var result = FluentResults.Result.Ok(); + + foreach (var key in toRemove) + { + if (_stylesCollections.TryRemove(key, out var collection) && !collection.IsDefaultOrEmpty) + { + foreach (var stylesCollection in collection) + { + try + { + stylesCollection.UnloadFile(); + } + catch (Exception e) + { + result.WithError(new ExceptionalError(e)); + } + } + } + } + + return result; + } + + public FluentResults.Result UnloadPackage(ContentPackage package) + { + // Yes, this is very cursed/inefficient. We don't care. + return UnloadPackages(new [] { package }.ToImmutableArray()); + } + + public FluentResults.Result UnloadAllPackages() + { + using var lck = _lock.AcquireWriterLock().ConfigureAwait(false).GetAwaiter().GetResult(); + IService.CheckDisposed(this); + + var result = FluentResults.Result.Ok(); + + foreach (var key in _stylesCollections.Keys.ToImmutableArray()) + { + if (_stylesCollections.TryRemove(key, out var collection) && !collection.IsDefaultOrEmpty) + { + foreach (var stylesCollection in collection) + { + try + { + stylesCollection.UnloadFile(); + } + catch (Exception e) + { + result.WithError(new ExceptionalError(e)); + } + } + } + } + + return result; + } +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/LuaCs/_Services/_Interfaces/IClientLoggerService.cs b/Barotrauma/BarotraumaClient/ClientSource/LuaCs/_Services/_Interfaces/IClientLoggerService.cs new file mode 100644 index 000000000..5c1ff7e0d --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/LuaCs/_Services/_Interfaces/IClientLoggerService.cs @@ -0,0 +1,7 @@ +namespace Barotrauma.LuaCs; + +public interface IClientLoggerService : IReusableService +{ + void AddToGUIUpdateList(); + void ShowErrorOverlay(string message, float time = 5f, float duration = 1.5f); +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/LuaCs/_Services/_Interfaces/IConfigService.cs b/Barotrauma/BarotraumaClient/ClientSource/LuaCs/_Services/_Interfaces/IConfigService.cs new file mode 100644 index 000000000..98a23dd6d --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/LuaCs/_Services/_Interfaces/IConfigService.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using Barotrauma.LuaCs.Data; +using Barotrauma.LuaCs; +using Barotrauma.Networking; + +namespace Barotrauma.LuaCs; + +public partial interface IConfigService +{ + ImmutableArray GetDisplayableConfigs(); +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/LuaCs/_Services/_Interfaces/ISettingsMenuService.cs b/Barotrauma/BarotraumaClient/ClientSource/LuaCs/_Services/_Interfaces/ISettingsMenuService.cs new file mode 100644 index 000000000..906fa7971 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/LuaCs/_Services/_Interfaces/ISettingsMenuService.cs @@ -0,0 +1,6 @@ +namespace Barotrauma.LuaCs; + +public interface ISettingsMenuSystem : ISystem +{ + +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/LuaCs/_Services/_Interfaces/IUIStylesCollection.cs b/Barotrauma/BarotraumaClient/ClientSource/LuaCs/_Services/_Interfaces/IUIStylesCollection.cs new file mode 100644 index 000000000..3a8ec99d8 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/LuaCs/_Services/_Interfaces/IUIStylesCollection.cs @@ -0,0 +1,75 @@ +using System.Collections.Generic; +using System.Collections.Immutable; +using Barotrauma.LuaCs.Data; +using FluentResults; + +namespace Barotrauma.LuaCs; + +public interface IUIStylesCollection : IService +{ + public interface IFactory : IService + { + /// + /// Returns a new for-each in the given + /// or empty is none. + /// + /// + /// + /// + IEnumerable CreateInstance(IStylesResourceInfo info, IStorageService storageService); + } + + /// + /// The assigned/target for this collection. + /// + public ContentPath Path { get; } + + /// + /// Gets the with the given name. + /// + /// + /// + public Result GetFont(string name); + /// + /// Gets the with the given name. + /// + /// + /// + public Result GetSprite(string name); + /// + /// Gets the with the given name. + /// + /// + /// + public Result GetSpriteSheet(string name); + /// + /// Gets the with the given name. + /// + /// + /// + public Result GetCursor(string name); + /// + /// Gets the with the given name. + /// + /// + /// + public Result GetColor(string name); + + #region BAROTRAUMA.UISTYLEFILE + + /// + /// Definition of + /// + internal void LoadFile(); + /// + /// Definition of + /// + internal void UnloadFile(); + /// + /// Definition of + /// + internal void Sort(); + + #endregion + +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/LuaCs/_Services/_Interfaces/IUIStylesService.cs b/Barotrauma/BarotraumaClient/ClientSource/LuaCs/_Services/_Interfaces/IUIStylesService.cs new file mode 100644 index 000000000..4d09f1284 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/LuaCs/_Services/_Interfaces/IUIStylesService.cs @@ -0,0 +1,57 @@ +using System.Collections.Immutable; +using Barotrauma.LuaCs.Data; +using FluentResults; + +namespace Barotrauma.LuaCs; + +public interface IUIStylesService : IReusableService +{ + /// + /// Gets the first loaded . + /// + /// The target + /// The targets as specified in the ModConfig.xml. + /// The asset's name as specified in the styles XML file. + /// A indicating success, and the target if succeeded. + public Result GetColor(ContentPackage package, string internalName, string assetName); + /// + /// Gets the loaded . + /// + /// The target + /// The targets as specified in the ModConfig.xml. + /// The asset's name as specified in the styles XML file. + /// A indicating success, and the target if succeeded. + public Result GetCursor(ContentPackage package, string internalName, string assetName); + /// + /// Gets the loaded . + /// + /// The target + /// The targets as specified in the ModConfig.xml. + /// The asset's name as specified in the styles XML file. + /// A indicating success, and the target if succeeded. + public Result GetFont(ContentPackage package, string internalName, string assetName); + /// + /// Gets the loaded . + /// + /// The target + /// The targets as specified in the ModConfig.xml. + /// The asset's name as specified in the styles XML file. + /// A indicating success, and the target if succeeded. + public Result GetSprite(ContentPackage package, string internalName, string assetName); + /// + /// Gets the loaded . + /// + /// The target + /// The targets as specified in the ModConfig.xml. + /// The asset's name as specified in the styles XML file. + /// A indicating success, and the target if succeeded. + public Result GetSpriteSheet(ContentPackage package, string internalName, string assetName); + + public FluentResults.Result LoadAssets(ImmutableArray resources); + + public FluentResults.Result UnloadPackages(ImmutableArray packages); + + public FluentResults.Result UnloadPackage(ContentPackage package); + + public FluentResults.Result UnloadAllPackages(); +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/LuaCs/_Services/_SettingsMenu/ModsControlsSettingsMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/LuaCs/_Services/_SettingsMenu/ModsControlsSettingsMenu.cs new file mode 100644 index 000000000..14b6f58d4 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/LuaCs/_Services/_SettingsMenu/ModsControlsSettingsMenu.cs @@ -0,0 +1,22 @@ +namespace Barotrauma.LuaCs; + +internal sealed class ModsControlsSettingsMenu : ModsSettingsMenuBase +{ + public ModsControlsSettingsMenu(GUIFrame contentFrame, + IPackageManagementService packageManagementService, + IConfigService configService, + SettingsMenu settingsMenuInstance) : base(contentFrame, packageManagementService, configService, settingsMenuInstance) + { + + } + + protected override void DisposeInternal() + { + // TODO: Finish this later. + } + + public override void ApplyInstalledModChanges() + { + // TODO: Finish this later. + } +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/LuaCs/_Services/_SettingsMenu/ModsGameplaySettingsMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/LuaCs/_Services/_SettingsMenu/ModsGameplaySettingsMenu.cs new file mode 100644 index 000000000..9cfa2667e --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/LuaCs/_Services/_SettingsMenu/ModsGameplaySettingsMenu.cs @@ -0,0 +1,458 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Collections.Immutable; +using Microsoft.Xna.Framework; +using System.Linq; +using System.Numerics; +using Barotrauma.LuaCs.Data; +using Vector2 = Microsoft.Xna.Framework.Vector2; +using Vector4 = Microsoft.Xna.Framework.Vector4; + +// ReSharper disable ObjectCreationAsStatement + +namespace Barotrauma.LuaCs; + +internal sealed class ModsGameplaySettingsMenu : ModsSettingsMenuBase +{ + private ImmutableArray _settingsInstancesGameplay; + // menu vars + private GUILayoutGroup _modCategoryDisplayGroup, _settingsDisplayGroup; + private string _selectedSearchQuery = string.Empty; + private ContentPackage _selectedContentPackage; + private string _selectedCategory = string.Empty; + private ImmutableArray _currentlyDisplayedSettings; + private ILoggerService _loggerService; + + private bool _promptOpen = false; + + + // Note: "static" instead of "const" for Hot Reload and to allow changing at runtime. + // ReSharper disable FieldCanBeMadeReadOnly.Local + + // --- UI controls --- + private static float MenuTitleHeight = 0.06f; // (ContentDisplayAreaHeightContainer + MenuTitleHeight) < 1f + private static float ContentDisplayAreaHeightContainer = 0.93f; + private static float ContentDisplayAreaHeightInnerCategories = 0.99f; + private static float ContentDisplayAreaHeightInnerSettings = 0.97f; + private static float ContentLeftRightSplitPosition = 0.3f; + + // Search Bar + private static float SearchBarLayoutHeight = 0.06f; + private static float SearchBarLabelWidth = 0.1f; + private static float SearchBarLabelBoxSpacing = 0.05f; + + private static float SearchBarTextBoxWidth = 1f - SearchBarLabelWidth - SearchBarLabelBoxSpacing; + + // Categories, Packages Display Area + private static float CategoriesDisplayListHeight = 0.945f; + private static float CategoryButtonHeightRelative = 0.122f; + private static float PackageSelectionButtonHeight = 0.07f; + + private static Color CategoryButtonHoverSelectColor = new Color(50, 50, 50, 255); + private static Color CategoryButtonTextColor = Color.PeachPuff; + private static Color CategoryButtonTextColorSelected = Color.White; + private static Color CategoryButtonColorPressed = Color.TransparentBlack; + + // Settings Display Area + private static float SettingLabelWidth = 0.6f; + private static float SettingControlWidth = 0.4f; + private static float SettingHeight = 0.05625f/ContentDisplayAreaHeightContainer/ContentDisplayAreaHeightInnerSettings; + private static Color SettingEntryLabelTextColor = Color.PeachPuff; + private static string SettingGUIFrameStyle = ""; + private static Color? SettingGUIFrameColor = null; + + // settings reset + private static Vector2 SettingsResetButtonTopSpacer = new Vector2(0f, 0.02f); + private static Vector2 SettingsResetButtonDimensions = new Vector2(0.3f, 0.05f); + private static string SettingsResetButtonStyle = "GUIButtonSmall"; + private static Color SettingsResetButtonColor = Color.DarkOliveGreen; + private static Color SettingsResetButtonHoverColor = Color.Olive; + private static Color SettingsResetButtonTextColor = Color.PeachPuff; + private static Color SettingsResetButtonTextColorSelected = Color.White; + + private static Vector2 ResetConfirmationPromptDimensions = new Vector2(0.15f, 0.2f); + + + // ReSharper restore FieldCanBeMadeReadOnly.Local + private const string SettingsResetButtonText = "LuaCsForBarotrauma.SettingsMenu.ResetVisibleSettings"; + private const string SettingsResetPromptTitle = "LuaCsForBarotrauma.SettingsMenu.ResetPrompt.Title"; + private const string SettingsResetPromptContents = "LuaCsForBarotrauma.SettingsMenu.ResetPrompt.Message"; + private const string SettingsResetPromptYesText = "LuaCsForBarotrauma.SettingsMenu.ResetPrompt.Yes"; + private const string SettingsResetPromptNoText = "LuaCsForBarotrauma.SettingsMenu.ResetPrompt.No"; + + + private event Action OnApplyInstalledModsChanges; + + public ModsGameplaySettingsMenu(GUIFrame contentFrame, + IPackageManagementService packageManagementService, + IConfigService configService, + ILoggerService loggerService, + SettingsMenu settingsMenuInstance) : base(contentFrame, packageManagementService, configService, settingsMenuInstance) + { + _settingsInstancesGameplay = configService.GetDisplayableConfigs() + .ToImmutableArray(); + + _loggerService = loggerService; + + var mainLayoutGroup = new GUILayoutGroup(new RectTransform(new Vector2(1f, 1f), contentFrame.RectTransform, Anchor.Center), false, Anchor.TopLeft); + // page title + var menuTitleLayoutGroup = new GUILayoutGroup( + new RectTransform(new Vector2(1f, MenuTitleHeight), mainLayoutGroup.RectTransform, Anchor.TopLeft), true, Anchor.TopLeft); + GUIUtil.Label(menuTitleLayoutGroup, + GetLocalizedString("LuaCsForBarotrauma.SettingsMenu.ModGameplayButton", "Mod Gameplay Settings"), + GUIStyle.LargeFont, new Vector2(1f, 1f)); + + // page contents + var contentAreaLayoutGroup = new GUILayoutGroup( + new RectTransform(new Vector2(1f, 0.94f), mainLayoutGroup.RectTransform, Anchor.BottomLeft), false, + Anchor.TopLeft); + + var searchBarLayoutGroup = new GUILayoutGroup( + 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(SearchBarTextBoxWidth, 0.1f), searchBarLayoutGroup.RectTransform, Anchor.TopLeft), + createClearButton: true) + { + OnTextChangedDelegate = (btn, txt) => + { + GenerateDisplayFromFilter(txt); + return true; + } + }; + + // main display area + 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(ContentLeftRightSplitPosition, ContentDisplayAreaHeightInnerCategories); + _settingsDisplayGroup.RectTransform.RelativeSize = new Vector2(1f-ContentLeftRightSplitPosition, ContentDisplayAreaHeightInnerSettings); + + // default category + _selectedCategory = "All"; + + OnApplyInstalledModsChanges = () => + { + _settingsInstancesGameplay = configService.GetDisplayableConfigs() + .ToImmutableArray(); + if (_selectedContentPackage is not null && !GetTargetPackagesList().Contains(_selectedContentPackage)) + { + _selectedContentPackage = null; + _selectedCategory = string.Empty; + } + + GenerateCategoryListDisplay(_modCategoryDisplayGroup, GetTargetPackagesList(), GetDisplayCategoriesList()); + GenerateSettingsListDisplay(_settingsDisplayGroup, GetDisplaySettingsList()); + }; + + GenerateCategoryListDisplay(_modCategoryDisplayGroup, GetTargetPackagesList(), GetDisplayCategoriesList()); + GenerateSettingsListDisplay(_settingsDisplayGroup, GetDisplaySettingsList()); + + void GenerateDisplayFromFilter(string text) + { + _selectedSearchQuery = text; + GenerateCategoryListDisplay(_modCategoryDisplayGroup, GetTargetPackagesList(), GetDisplayCategoriesList()); + GenerateSettingsListDisplay(_settingsDisplayGroup, GetDisplaySettingsList()); + } + + string GetLocalizedString(string identifier, string defaultValue) + { + var lstr = TextManager.Get(identifier); + return lstr.IsNullOrWhiteSpace() ? defaultValue : lstr.Value; + } + + // Filters by selected package and query text + ImmutableArray GetDisplayCategoriesList() + { + return GetFilteredSettingsList() + .Select(s => GetLocalizedString(s.GetDisplayInfo().DisplayCategory, "General")) + .Concat(new []{ "All" }) + .Distinct() + .OrderBy(s => s) + .ToImmutableArray(); + } + + // Filters by query text + ImmutableArray GetTargetPackagesList() + { + return _settingsInstancesGameplay + .Where(s => SettingMatchesQuery(s, _selectedSearchQuery)) + .Select(s => s.OwnerPackage) + .Concat(new[] { ContentPackageManager.VanillaCorePackage }) + .Distinct() + .OrderByDescending(p => p == ContentPackageManager.VanillaCorePackage ? 0 : 1) + .ThenBy(p => p.Name) + .ToImmutableArray(); + } + + // Filters by selected package, query text, and selected category. + ImmutableArray GetDisplaySettingsList() + { + return GetFilteredSettingsList() + .Where(s => _selectedCategory.IsNullOrWhiteSpace() + || _selectedCategory == "All" + || GetLocalizedString(s.GetDisplayInfo().DisplayCategory, "General") == _selectedCategory) + .OrderBy(s => GetLocalizedString(s.GetDisplayInfo().DisplayName, s.InternalName)) + .ToImmutableArray(); + } + + // Filters by selected package and by query text. + ImmutableArray GetFilteredSettingsList() + { + return _settingsInstancesGameplay + .Where(s => SettingMatchesQuery(s, _selectedSearchQuery)) + .Where(s => _selectedContentPackage is null + || _selectedContentPackage == ContentPackageManager.VanillaCorePackage // vanilla is treated as all packages + || s.OwnerPackage == _selectedContentPackage) + .OrderBy(s => GetLocalizedString(s.GetDisplayInfo().DisplayName, s.InternalName)) + .ToImmutableArray(); + } + + + bool SettingMatchesQuery(ISettingBase setting, string queryText) + { + if (queryText.IsNullOrWhiteSpace()) + { + return true; + } + + queryText = queryText.ToLowerInvariant().Trim(); + + if (setting.InternalName.ToLowerInvariant().Trim().Contains(queryText) || setting.OwnerPackage.Name.ToLowerInvariant().Trim().Contains(queryText)) + { + return true; + } + + var displayInfo = setting.GetDisplayInfo(); + return TextManager.Get(displayInfo.DisplayName).Value.ToLowerInvariant().Trim().Contains(queryText) + || TextManager.Get(displayInfo.DisplayCategory).Value.ToLowerInvariant().Trim().Contains(queryText) + || TextManager.Get(displayInfo.Description).Value.ToLowerInvariant().Trim().Contains(queryText) + || TextManager.Get(displayInfo.Tooltip).Value.ToLowerInvariant().Trim().Contains(queryText); + } + + string GetPackageName(ContentPackage package) + { + return package is null || package == ContentPackageManager.VanillaCorePackage ? "All" : package.Name; + } + + ContentPackage GetCurrentSelectedPackage(ImmutableArray packages) + { + if (_selectedContentPackage is null) + { + return ContentPackageManager.VanillaCorePackage; + } + + if (packages.Contains(_selectedContentPackage)) + { + return _selectedContentPackage; + } + + if (packages.Length > 0) + { + _selectedContentPackage = packages[0]; + return packages[0]; + } + + return null; + } + + void GenerateCategoryListDisplay(GUILayoutGroup layoutGroup, ImmutableArray packagesList, + ImmutableArray categories) + { + layoutGroup.ClearChildren(); + var packageSelectionList = GUIUtil.Dropdown(layoutGroup, cp => GetPackageName(cp), null, + packagesList, GetCurrentSelectedPackage(packagesList), cp => + { + _selectedContentPackage = cp; + _selectedCategory = string.Empty; + GenerateCategoryListDisplay(_modCategoryDisplayGroup, GetTargetPackagesList(), GetDisplayCategoriesList()); + GenerateSettingsListDisplay(_settingsDisplayGroup, GetDisplaySettingsList()); + }, 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 + }; + var displayCategoriesLayout = new GUILayoutGroup(new RectTransform(Vector2.One, displayedCategoriesFrame.RectTransform)); + + foreach (var category in categories) + { + var btn = new GUIButton(new RectTransform(new Vector2(1f, CategoryButtonHeightRelative), displayCategoriesLayout.RectTransform), + text: category, color: Color.TransparentBlack) + { + CanBeFocused = true, + CanBeSelected = true, + TextColor = CategoryButtonTextColor, + HoverColor = CategoryButtonHoverSelectColor, + HoverTextColor = CategoryButtonTextColorSelected, + PressedColor = CategoryButtonColorPressed, + SelectedColor = CategoryButtonHoverSelectColor, + SelectedTextColor = CategoryButtonHoverSelectColor, + OnClicked = (btn, obj) => + { + _selectedCategory = category; + GenerateSettingsListDisplay(_settingsDisplayGroup, GetDisplaySettingsList()); + return true; + } + }; + } + } + + void GenerateSettingsListDisplay(GUILayoutGroup layoutGroup, ImmutableArray settings) + { + layoutGroup.ClearChildren(); + _currentlyDisplayedSettings = settings; + + 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(SettingLabelWidth, 1f), + controlSize: new Vector2(SettingControlWidth, 1f)); + } + + 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), + style: SettingGUIFrameStyle, color: SettingGUIFrameColor) + { + Color = Color.DarkGray + }; + GUILayoutGroup entryLayoutGroup = new GUILayoutGroup(new RectTransform(Vector2.One, entryFrame.RectTransform), isHorizontal: true); + + // padding + 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: SettingEntryLabelTextColor, + font: GUIStyle.SmallFont, + textAlignment: Alignment.Left) + { + ToolTip = GetLocalizedString(setting.GetDisplayInfo().Tooltip, string.Empty) + }; + + setting.AddDisplayComponent(entryLayoutGroup, controlSize, newValue => + { + NewValuesCache[setting] = newValue; + }); + return (entryFrame, entryLayoutGroup); + } + + void DisplayResetConfirmationPrompt(ImmutableArray 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 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(); + } + } + + + protected override void DisposeInternal() + { + NewValuesCache.Clear(); + _modCategoryDisplayGroup?.Parent.RemoveChild(_modCategoryDisplayGroup); + _settingsDisplayGroup?.Parent.RemoveChild(_settingsDisplayGroup); + _modCategoryDisplayGroup = null; + _settingsDisplayGroup = null; + + } + + public override void ApplyInstalledModChanges() + { + foreach (var kvp in NewValuesCache) + { + if (kvp.Key.IsDisposed) + { + continue; + } + + 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(); + } +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/LuaCs/_Services/_SettingsMenu/ModsSettingsMenuBase.cs b/Barotrauma/BarotraumaClient/ClientSource/LuaCs/_Services/_SettingsMenu/ModsSettingsMenuBase.cs new file mode 100644 index 000000000..95c626c8f --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/LuaCs/_Services/_SettingsMenu/ModsSettingsMenuBase.cs @@ -0,0 +1,42 @@ +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; + +internal abstract class ModsSettingsMenuBase : IDisposable +{ + public GUIFrame ContentFrame { get; private set; } + protected IPackageManagementService PackageManagementService { get; private set; } + protected IConfigService ConfigService { get; private set; } + protected SettingsMenu SettingsMenuInstance { get; private set; } + protected readonly ConcurrentDictionary> NewValuesCache = new(); + + protected ModsSettingsMenuBase(GUIFrame contentFrame, + IPackageManagementService packageManagementService, + IConfigService configService, SettingsMenu settingsMenuInstance) + { + ContentFrame = contentFrame; + PackageManagementService = packageManagementService; + ConfigService = configService; + SettingsMenuInstance = settingsMenuInstance; + } + + protected abstract void DisposeInternal(); + public abstract void ApplyInstalledModChanges(); + + public void Dispose() + { + DisposeInternal(); + ContentFrame?.Parent.RemoveChild(ContentFrame); + SettingsMenuInstance = null; + ContentFrame = null; + PackageManagementService = null; + ConfigService = null; + NewValuesCache.Clear(); + } +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/LuaCs/_Services/_SettingsMenu/SettingsMenuSystem.cs b/Barotrauma/BarotraumaClient/ClientSource/LuaCs/_Services/_SettingsMenu/SettingsMenuSystem.cs new file mode 100644 index 000000000..1d5cb2d4d --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/LuaCs/_Services/_SettingsMenu/SettingsMenuSystem.cs @@ -0,0 +1,125 @@ +using System; +using System.Linq; +using Barotrauma.Extensions; +using HarmonyLib; +using Microsoft.Xna.Framework; + +namespace Barotrauma.LuaCs; + +public class SettingsMenuSystem : ISettingsMenuSystem +{ + + private ModsControlsSettingsMenu _controlsMenuInstance; + private ModsGameplaySettingsMenu _gameplayMenuInstance; + private GUIFrame _gameplayContentFrame; + private GUIFrame _controlsContentFrame; + private SettingsMenu _settingsMenuInstance; + + 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, ILoggerService loggerService) + { + _packageManagementService = packageManagementService; + _configService = configService; + _loggerService = loggerService; + SystemInstance = this; + _harmony = Harmony.CreateAndPatchAll(typeof(SettingsMenuSystem)); + } + + [HarmonyPatch(typeof(SettingsMenu), "CreateModsTab"), HarmonyPostfix] + private static void SettingsMenu_CreateModsTab_Post(SettingsMenu __instance) + { + SystemInstance._settingsMenuInstance = __instance; + SystemInstance.CreateSettingsMenu(__instance); + } + + private void CreateSettingsMenu(SettingsMenu __instance) + { + DisposeMenuFrames(); + + var tabCount = Enum.GetValues().Length; + var tabGameplayIndex = (SettingsMenu.Tab)tabCount; + var tabControlsIndex = (SettingsMenu.Tab)tabCount+1; + + _gameplayContentFrame = CreateNewContentTab(tabGameplayIndex, __instance, + GUIStyle.ComponentStyles.ContainsKey("SettingsMenuTab.LuaCsSettings") ? "SettingsMenuTab.LuaCsSettings" : "SettingsMenuTab.Mods", + "LuaCsForBarotrauma.SettingsMenu.ModGameplayButton"); + /*_controlsContentFrame = CreateNewContentTab(tabControlsIndex, __instance, + "SettingsMenuTab.Controls", "LuaCsForBarotrauma.SettingsMenu.ModControlsButton"); + */ + + _gameplayMenuInstance = new ModsGameplaySettingsMenu(_gameplayContentFrame, _packageManagementService, _configService, _loggerService, __instance); + //_controlsMenuInstance = new ModsControlsSettingsMenu(_controlsContentFrame, _packageManagementService, _configService, __instance); + } + + private GUIFrame CreateNewContentTab(SettingsMenu.Tab tab, SettingsMenu settingsMenuInstance, string settingsMenuTabName, string settingMenuHoverTextIdent) + { + if (settingsMenuInstance.tabContents.TryGetValue(tab, out (GUIButton Button, GUIFrame Content) tabContent)) + { + return tabContent.Content; + } + + var contentFr = new GUIFrame(new RectTransform(Vector2.One * 0.95f, settingsMenuInstance.contentFrame.RectTransform, Anchor.Center, Pivot.Center), style: null); + + var button = new GUIButton(new RectTransform(Vector2.One, settingsMenuInstance.tabber.RectTransform, + Anchor.TopLeft, Pivot.TopLeft, scaleBasis: ScaleBasis.Smallest), "", style: settingsMenuTabName) + { + ToolTip = TextManager.Get(settingMenuHoverTextIdent), + OnClicked = (b, _) => + { + settingsMenuInstance.SelectTab(tab); + return false; + } + }; + button.RectTransform.MaxSize = RectTransform.MaxPoint; + button.Children.ForEach(c => c.RectTransform.MaxSize = RectTransform.MaxPoint); + + settingsMenuInstance.tabContents.Add(tab, (button, contentFr)); + + return contentFr; + } + + + [HarmonyPatch(typeof(SettingsMenu), nameof(SettingsMenu.ApplyInstalledModChanges)), HarmonyPostfix] + private static void SettingsMenu_ApplyInstalledModChanges_Post() + { + SystemInstance._gameplayMenuInstance?.ApplyInstalledModChanges(); + SystemInstance._controlsMenuInstance?.ApplyInstalledModChanges(); + } + + private void DisposeMenuFrames() + { + _controlsMenuInstance?.Dispose(); + _gameplayMenuInstance?.Dispose(); + _controlsMenuInstance = null; + _gameplayMenuInstance = null; + } + + #region DISPOSAL + + public void Dispose() + { + if (!ModUtils.Threading.CheckIfClearAndSetBool(ref _isDisposed)) + { + return; + } + DisposeMenuFrames(); + GC.SuppressFinalize(this); + } + private int _isDisposed = 0; + public bool IsDisposed + { + get => ModUtils.Threading.GetBool(ref _isDisposed); + private set => ModUtils.Threading.SetBool(ref _isDisposed, value); + } + public FluentResults.Result Reset() + { + throw new NotImplementedException(); + } + + #endregion +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs index 129ef0958..49ab79cca 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs @@ -16,15 +16,17 @@ namespace Barotrauma { public readonly UInt32 DecalId; public readonly int SpriteIndex; - public Vector2 NormalizedPos; + public readonly Vector2 NormalizedPos; public readonly float Scale; + public readonly float DecalAlpha; - public RemoteDecal(UInt32 decalId, int spriteIndex, Vector2 normalizedPos, float scale) + public RemoteDecal(UInt32 decalId, int spriteIndex, Vector2 normalizedPos, float scale, float decalAlpha) { DecalId = decalId; SpriteIndex = spriteIndex; NormalizedPos = normalizedPos; Scale = scale; + DecalAlpha = decalAlpha; } } @@ -696,7 +698,7 @@ namespace Barotrauma var decal = decalEventData.Decal; int decalIndex = decals.IndexOf(decal); msg.WriteByte((byte)(decalIndex < 0 ? 255 : decalIndex)); - msg.WriteRangedSingle(decal.BaseAlpha, 0.0f, 1.0f, 8); + msg.WriteRangedSingle(decal.BaseAlpha, 0f, 1f, 8); break; default: throw new Exception($"Malformed hull event: did not expect {eventData.GetType().Name}"); @@ -752,7 +754,9 @@ namespace Barotrauma float normalizedXPos = msg.ReadRangedSingle(0.0f, 1.0f, 8); float normalizedYPos = msg.ReadRangedSingle(0.0f, 1.0f, 8); float decalScale = msg.ReadRangedSingle(0.0f, 2.0f, 12); - remoteDecals.Add(new RemoteDecal(decalId, spriteIndex, new Vector2(normalizedXPos, normalizedYPos), decalScale)); + float decalAlpha = msg.ReadRangedSingle(0f, 1f, 8); + + remoteDecals.Add(new RemoteDecal(decalId, spriteIndex, new Vector2(normalizedXPos, normalizedYPos), decalScale, decalAlpha)); } break; case EventType.BallastFlora: @@ -804,7 +808,8 @@ namespace Barotrauma decalPosX += Submarine.Position.X; decalPosY += Submarine.Position.Y; } - AddDecal(remoteDecal.DecalId, new Vector2(decalPosX, decalPosY), remoteDecal.Scale, isNetworkEvent: true, spriteIndex: remoteDecal.SpriteIndex); + Decal decal = AddDecal(remoteDecal.DecalId, new Vector2(decalPosX, decalPosY), remoteDecal.Scale, isNetworkEvent: true, spriteIndex: remoteDecal.SpriteIndex); + decal.BaseAlpha = remoteDecal.DecalAlpha; } remoteDecals.Clear(); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs index 8202240aa..9c145eca0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs @@ -296,13 +296,9 @@ namespace Barotrauma.Lights light.Priority = lightPriority(range, light); - int i = 0; - while (i < activeLights.Count && light.Priority < activeLights[i].Priority) - { - i++; - } - activeLights.Insert(i, light); + activeLights.Add(light); } + activeLights.Sort(static (a, b) => b.Priority.CompareTo(a.Priority)); ActiveLightCount = activeLights.Count; float lightPriority(float range, LightSource light) @@ -332,7 +328,7 @@ namespace Barotrauma.Lights activeLights.Remove(activeShadowCastingLights[i]); } } - activeLights.Sort((l1, l2) => l1.LastRecalculationTime.CompareTo(l2.LastRecalculationTime)); + activeLights.Sort(static (l1, l2) => l1.LastRecalculationTime.CompareTo(l2.LastRecalculationTime)); //draw light sprites attached to characters //render into a separate rendertarget using alpha blending (instead of on top of everything else with alpha blending) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs index e54f37f21..2912d88f2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs @@ -50,8 +50,14 @@ namespace Barotrauma.Lights [Serialize("0, 0", IsPropertySaveable.Yes), Editable(ValueStep = 1, DecimalCount = 1, MinValueFloat = -1000f, MaxValueFloat = 1000f)] public Vector2 Offset { get; set; } + public float RotationRad { get; private set; } + [Serialize(0f, IsPropertySaveable.Yes), Editable(MinValueFloat = -360, MaxValueFloat = 360, ValueStep = 1, DecimalCount = 0)] - public float Rotation { get; set; } + public float Rotation + { + get => MathHelper.ToDegrees(RotationRad); + set => RotationRad = MathHelper.ToRadians(value); + } [Serialize(false, IsPropertySaveable.Yes, "Directional lights only shine in \"one direction\", meaning no shadows are cast behind them."+ " Note that this does not affect how the light texture is drawn: if you want something like a conical spotlight, you should use an appropriate texture for that.")] @@ -314,6 +320,10 @@ namespace Barotrauma.Lights private float prevCalculatedRotation; private float rotation; + + /// + /// Current rotation in radians. Note that LightSourceParams.RotationRad also affects the final rotation of the light. + /// public float Rotation { get { return rotation; } @@ -322,7 +332,7 @@ namespace Barotrauma.Lights if (Math.Abs(value - rotation) < 0.001f) { return; } rotation = value; - dir = new Vector2(MathF.Cos(rotation), -MathF.Sin(rotation)); + RefreshDirection(); if (Math.Abs(rotation - prevCalculatedRotation) < RotationRecalculationThreshold && vertices != null) { @@ -486,6 +496,9 @@ namespace Barotrauma.Lights break; } } + //make sure the rotation defined in the parameters is taken into account + RefreshDirection(); + NeedsRecalculation = true; } public LightSource(LightSourceParams lightSourceParams) @@ -497,6 +510,9 @@ namespace Barotrauma.Lights { DeformableLightSprite = new DeformableSprite(lightSourceParams.DeformableLightSpriteElement, invert: true); } + //make sure the rotation defined in the parameters is taken into account + RefreshDirection(); + NeedsRecalculation = true; } public LightSource(Vector2 position, float range, Color color, Submarine submarine, bool addLight=true) @@ -511,6 +527,14 @@ namespace Barotrauma.Lights if (addLight) { GameMain.LightManager.AddLight(this); } } + /// + /// Refresh the direction vector of the light (which is used for calculating shadows) based on the rotation and + /// + private void RefreshDirection() + { + dir = new Vector2(MathF.Cos(rotation - LightSourceParams.RotationRad), -MathF.Sin(rotation - LightSourceParams.RotationRad)); + } + public void Update(float time) { float brightness = 1.0f; @@ -773,9 +797,6 @@ namespace Barotrauma.Lights float boundsExtended = TextureRange; if (OverrideLightTexture != null) { - float cosAngle = (float)Math.Cos(rotation); - float sinAngle = -(float)Math.Sin(rotation); - var overrideTextureDims = new Vector2(OverrideLightTexture.SourceRect.Width, OverrideLightTexture.SourceRect.Height); Vector2 origin = OverrideLightTextureOrigin; @@ -790,8 +811,11 @@ namespace Barotrauma.Lights origin *= TextureRange; - drawOffset.X = -origin.X * cosAngle - origin.Y * sinAngle; - drawOffset.Y = origin.X * sinAngle + origin.Y * cosAngle; + //rotate the origin based on the direction + float cos = dir.X; + float sin = dir.Y; + drawOffset.X = -origin.X * cos - origin.Y * sin; + drawOffset.Y = origin.X * sin + origin.Y * cos; } //add a square-shaped boundary to make sure we've got something to construct the triangles from @@ -1536,7 +1560,6 @@ namespace Barotrauma.Lights Vector2 offset = ParentSub == null ? Vector2.Zero : ParentSub.DrawPosition; lightEffect.World = Matrix.CreateTranslation(-new Vector3(position, 0.0f)) * - Matrix.CreateRotationZ(MathHelper.ToRadians(LightSourceParams.Rotation)) * Matrix.CreateTranslation(new Vector3(position + offset + translateVertices, 0.0f)) * transform; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs index e15d5bd48..4db180177 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs @@ -193,7 +193,12 @@ namespace Barotrauma { CanTakeKeyBoardFocus = false }; - var editor = new SerializableEntityEditor(listBox.Content.RectTransform, this, inGame, showName: true, titleFont: GUIStyle.LargeFont) { UserData = this }; + var editor = new SerializableEntityEditor(listBox.Content.RectTransform, this, inGame, showName: true, + titleFont: GUIStyle.LargeFont, + dimOutDefaultValues: false) + { + UserData = this + }; if (editor.Fields.TryGetValue(nameof(Scale).ToIdentifier(), out GUIComponent[] scaleFields) && scaleFields.FirstOrDefault() is GUINumberInput scaleInput) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs index a3fab020c..ed3ea6aed 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs @@ -1,6 +1,7 @@ using Barotrauma.Extensions; using Barotrauma.IO; using Barotrauma.Items.Components; +using Barotrauma.LuaCs.Events; using Barotrauma.Steam; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Input; @@ -603,8 +604,6 @@ namespace Barotrauma.Networking { ServerPacketHeader header = (ServerPacketHeader)inc.ReadByte(); - GameMain.LuaCs.Networking.NetMessageReceived(inc, header); - if (roundInitStatus == RoundInitStatus.WaitingForStartGameFinalize && header is not ( ServerPacketHeader.STARTGAMEFINALIZE @@ -2905,8 +2904,6 @@ namespace Barotrauma.Networking public void Quit() { - GameMain.LuaCs.Stop(); - ClientPeer?.Close(PeerDisconnectPacket.WithReason(DisconnectReason.Disconnected)); GUIMessageBox.MessageBoxes.RemoveAll(c => c?.UserData is RoundSummary); @@ -3006,7 +3003,8 @@ namespace Barotrauma.Networking public override void AddChatMessage(ChatMessage message) { - var should = GameMain.LuaCs.Hook.Call("chatMessage", message.Text, message.SenderClient, message.Type, message); + bool? should = null; + LuaCsSetup.Instance.EventService.PublishEvent(x => should = x.OnChatMessage(message.Text, message.SenderClient, message.Type, message) ?? should); if (should != null && should.Value) { return; } if (string.IsNullOrEmpty(message.Text)) { return; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/LidgrenClientPeer.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/LidgrenClientPeer.cs index c0656420b..96f56708a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/LidgrenClientPeer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/LidgrenClientPeer.cs @@ -30,6 +30,13 @@ namespace Barotrauma.Networking { DualStack = GameSettings.CurrentConfig.UseDualModeSockets }; + if (NetConfig.UseLenientHandshake) + { + // More lenient timeouts for local testing, so the server would start even without perfect conditions + netPeerConfiguration.ConnectionTimeout = 60.0f; + netPeerConfiguration.ResendHandshakeInterval = 5.0f; + netPeerConfiguration.MaximumHandshakeAttempts = 20; + } if (endpoint.NetEndpoint.Address.AddressFamily == AddressFamily.InterNetworkV6) { netPeerConfiguration.LocalAddress = System.Net.IPAddress.IPv6Any; diff --git a/Barotrauma/BarotraumaClient/ClientSource/PlayerInput.cs b/Barotrauma/BarotraumaClient/ClientSource/PlayerInput.cs index d3aa62499..593627a43 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/PlayerInput.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/PlayerInput.cs @@ -495,8 +495,6 @@ namespace Barotrauma allowInput = true; } - GameMain.LuaCs.Hook.Call("keyUpdate", deltaTime); - oldMouseState = mouseState; mouseState = latestMouseState; UpdateVariable(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs index 18184ae38..1e46bd75b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs @@ -352,7 +352,7 @@ namespace Barotrauma { Level.Loaded.DrawBack(graphics, spriteBatch, cam); } - else if (GameMain.GameSession.GameMode is TestGameMode testMode) + else if (GameMain.GameSession?.GameMode is TestGameMode testMode) { graphics.Clear(testMode.BackgroundParams.BackgroundColor); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen/MainMenuScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen/MainMenuScreen.cs index 31bfd266b..2d1f03114 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen/MainMenuScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen/MainMenuScreen.cs @@ -49,6 +49,9 @@ namespace Barotrauma private GUITextBox serverNameBox, passwordBox, maxPlayersBox; private GUITickBox isPublicBox, wrongPasswordBanBox, karmaBox; private GUIDropDown languageDropdown, serverExecutableDropdown; +#if DEBUG + private GUITickBox lenientHandshakeBox; +#endif private readonly GUIButton joinServerButton, hostServerButton; private readonly GUIFrame modsButtonContainer; @@ -531,23 +534,6 @@ namespace Barotrauma } }; #endif - new GUIButton(new RectTransform(new Point(300, 30), Frame.RectTransform, Anchor.TopLeft) { AbsoluteOffset = new Point(40, 50) }, - $"Open LuaCs Settings", style: "MainMenuGUIButton", color: GUIStyle.Red) - { - IgnoreLayoutGroups = true, - OnClicked = (tb, userdata) => - { - LuaCsSettingsMenu.Open(Frame.RectTransform); - return true; - } - }; - - string version = File.Exists(LuaCsSetup.VersionFile) ? File.ReadAllText(LuaCsSetup.VersionFile) : "Github"; - - new GUITextBlock(new RectTransform(new Point(300, 30), Frame.RectTransform, Anchor.TopLeft) { AbsoluteOffset = new Point(10, 10) }, $"Using LuaCsForBarotrauma revision {AssemblyInfo.GitRevision} version {version}", Color.Red) - { - IgnoreLayoutGroups = false - }; var minButtonSize = new Point(120, 20); var maxButtonSize = new Point(480, 80); @@ -703,8 +689,6 @@ namespace Barotrauma #region Selection public override void Select() { - GameMain.LuaCs.Stop(); - ResetModUpdateButton(); if (WorkshopItemsToUpdate.Any()) @@ -1044,7 +1028,7 @@ namespace Barotrauma else { StartServer(); - } + } } private IEnumerable WaitForSubmarineHashCalculations(GUIMessageBox messageBox) @@ -1095,7 +1079,7 @@ namespace Barotrauma "-public", isPublicBox.Selected.ToString(), "-playstyle", ((PlayStyle)playstyleBanner.UserData).ToString(), "-banafterwrongpassword", wrongPasswordBanBox.Selected.ToString(), - "-karmaenabled", (!karmaBox.Selected).ToString(), + "-karmaenabled", (karmaBox.Selected).ToString(), "-maxplayers", maxPlayersBox.Text, "-language", languageDropdown.SelectedData.ToString() }; @@ -1134,6 +1118,13 @@ namespace Barotrauma int ownerKey = Math.Max(CryptoRandom.Instance.Next(), 1); arguments.Add("-ownerkey"); arguments.Add(ownerKey.ToString()); +#if DEBUG + if (lenientHandshakeBox.Selected) + { + arguments.Add("-lenienthandshake"); + NetConfig.UseLenientHandshake = true; + } +#endif var processInfo = new ProcessStartInfo { @@ -1314,8 +1305,6 @@ namespace Barotrauma return; } - GameMain.LuaCs.CheckInitialize(); - selectedSub = new SubmarineInfo(Path.Combine(SaveUtil.TempPath, selectedSub.Name + ".sub")); GameMain.GameSession = new GameSession(selectedSub, Option.None, CampaignDataPath.CreateRegular(savePath), GameModePreset.SinglePlayerCampaign, settings, mapSeed); @@ -1331,8 +1320,6 @@ namespace Barotrauma { if (string.IsNullOrWhiteSpace(path)) return; - GameMain.LuaCs.CheckInitialize(); - try { CampaignDataPath dataPath = @@ -1392,7 +1379,7 @@ namespace Barotrauma } int maxPlayers = Math.Clamp(maxPlayersElement, min: 1, max: NetConfig.MaxPlayers); - var karmaEnabled = serverSettings.GetAttributeBool("karmaenabled", true); + var karmaEnabled = serverSettings.GetAttributeBool("karmaenabled", false); var selectedPlayStyle = serverSettings.GetAttributeEnum("playstyle", PlayStyle.Casual); Vector2 textLabelSize = new Vector2(1.0f, 0.05f); @@ -1603,10 +1590,18 @@ namespace Barotrauma karmaBox = new GUITickBox(new RectTransform(new Vector2(0.5f, 1.0f), tickboxAreaLower.RectTransform), TextManager.Get("HostServerKarmaSetting")) { - Selected = !karmaEnabled, + Selected = karmaEnabled, ToolTip = TextManager.Get("hostserverkarmasettingtooltip") }; +#if DEBUG + lenientHandshakeBox = new GUITickBox(new RectTransform(new Vector2(0.5f, 1.0f), tickboxAreaLower.RectTransform), "DEBUG: Lenient server startup timeouts") + { + Selected = true, + 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." + }; +#endif + tickboxAreaLower.RectTransform.IsFixedSize = true; //spacing @@ -1695,8 +1690,8 @@ namespace Barotrauma if (string.IsNullOrEmpty(remoteContentUrl)) { return; } try { - var client = new RestClient(remoteContentUrl); - var request = new RestRequest("MenuContent.xml", Method.GET); + var client = RestFactory.CreateClient(remoteContentUrl); + var request = RestFactory.CreateRequest("MenuContent.xml"); TaskPool.Add("RequestMainMenuRemoteContent", client.ExecuteAsync(request), RemoteContentReceived); } @@ -1717,12 +1712,17 @@ namespace Barotrauma try { if (!t.TryGetResult(out IRestResponse remoteContentResponse)) { throw new Exception("Task did not return a valid result"); } + if (remoteContentResponse.ErrorException != null) + { + DebugConsole.AddWarning($"Connection error: Failed to fetch remote main menu content " + + $"({remoteContentResponse.ErrorException.Message})."); + return; + } if (remoteContentResponse.StatusCode != HttpStatusCode.OK) { DebugConsole.AddWarning( "Failed to receive remote main menu content. " + - "There may be an issue with your internet connection, or the master server might be temporarily unavailable " + - $"(error code: {remoteContentResponse.StatusCode})"); + $"The master server might be temporarily unavailable (HTTP error: {remoteContentResponse.StatusCode})"); return; } string xml = remoteContentResponse.Content; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/ModDownloadScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/ModDownloadScreen.cs index e3d878e81..3af215c5e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/ModDownloadScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/ModDownloadScreen.cs @@ -5,6 +5,7 @@ using System.Diagnostics.CodeAnalysis; using System.Linq; using Barotrauma.Extensions; using Barotrauma.IO; +using Barotrauma.LuaCs.Events; using Barotrauma.Networking; using Barotrauma.Steam; using Microsoft.Xna.Framework; @@ -118,7 +119,6 @@ namespace Barotrauma ContentPackageManager.EnabledPackages.SetRegular(regularPackages); } GameMain.NetLobbyScreen.Select(); - GameMain.LuaCs.CheckInitialize(); return; } @@ -366,7 +366,7 @@ namespace Barotrauma ContentPackageManager.EnabledPackages.BackUp(); ContentPackageManager.EnabledPackages.SetCore(corePackage); ContentPackageManager.EnabledPackages.SetRegular(regularPackages); - + //see if any of the packages we enabled contain subs that we were missing previously, and update their paths foreach (var serverSub in GameMain.Client.ServerSubmarines) { @@ -379,7 +379,6 @@ namespace Barotrauma } GameMain.NetLobbyScreen.UpdateSubList(GameMain.NetLobbyScreen.SubList, GameMain.Client.ServerSubmarines); GameMain.NetLobbyScreen.Select(); - GameMain.LuaCs.CheckInitialize(); } } else if (GameMain.Client.FileReceiver.ActiveTransfers.None()) @@ -400,7 +399,7 @@ namespace Barotrauma string dir = path.RemoveFromEnd(ModReceiver.Extension, StringComparison.OrdinalIgnoreCase); SaveUtil.DecompressToDirectory(path, dir); - var result = ContentPackage.TryLoad(Path.Combine(dir, ContentPackage.FileListFileName)); + var result = ContentPackage.TryLoad(Path.Combine(dir, ContentPackage.FileListFileName).CleanUpPathCrossPlatform()); if (!result.TryUnwrapSuccess(out var newPackage)) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs index 37b5d6e5c..72ccad218 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs @@ -12,6 +12,7 @@ using System.Globalization; using System.Linq; using System.Threading; using System.Xml.Linq; +using Barotrauma.LuaCs.Events; using Barotrauma.Sounds; namespace Barotrauma @@ -733,6 +734,8 @@ namespace Barotrauma AutoHideScrollBar = false, OnSelected = (component, userdata) => { + //if we're clicking on a checkbox (toggle visibility) on the list, don't select the entry on the list + if (GUI.MouseOn is GUITickBox) { return false; } //toggling selection is not how listboxes normally work, need to do that manually here SoundPlayer.PlayUISound(GUISoundType.Select); if (layerList.SelectedData == userdata) @@ -1531,8 +1534,6 @@ namespace Barotrauma public override void Select() { Select(enableAutoSave: true); - - GameMain.LuaCs.CheckInitialize(); } public void Select(bool enableAutoSave = true) @@ -3255,6 +3256,20 @@ namespace Barotrauma = new GUITextBox(new RectTransform((1.0f, 0.15f), saveInPackageLayout.RectTransform), createClearButton: true); + packToSaveInFilter.OnTextChanged += (GUITextBox textBox, string text) => + { + + foreach (GUIComponent child in packageToSaveInList.Content.Children) + { + child.Visible = + // Get the pkgText from below + !(child.GetChild()?.GetChild() is GUITextBlock textBlock && + !textBlock.Text.Contains(packToSaveInFilter.Text, StringComparison.OrdinalIgnoreCase)); + } + + return true; + }; + GUILayoutGroup addItemToPackageToSaveList(LocalizedString itemText, ContentPackage p) { var listItem = new GUIFrame(new RectTransform((1.0f, 0.15f), packageToSaveInList.Content.RectTransform), @@ -3275,28 +3290,26 @@ namespace Barotrauma return retVal; } + ContentPackage ownerPkg = null; + #if DEBUG //this is a debug-only option so I won't bother submitting it for localization var modifyVanillaListItem = addItemToPackageToSaveList("Modify Vanilla content package", ContentPackageManager.VanillaCorePackage); var modifyVanillaListIcon = modifyVanillaListItem.GetChild(); GUIStyle.Apply(modifyVanillaListIcon, "WorkshopMenu.EditButton"); + + if (MainSub?.Info != null && IsVanillaSub(MainSub.Info)) + { + ownerPkg = ContentPackageManager.VanillaCorePackage; + } #endif var newPackageListItem = addItemToPackageToSaveList(TextManager.Get("CreateNewLocalPackage"), null); var newPackageListIcon = newPackageListItem.GetChild(); var newPackageListText = newPackageListItem.GetChild(); GUIStyle.Apply(newPackageListIcon, "NewContentPackageIcon"); - new GUICustomComponent(new RectTransform(Vector2.Zero, saveInPackageLayout.RectTransform), - onUpdate: (f, component) => - { - foreach (GUIComponent contentChild in packageToSaveInList.Content.Children) - { - contentChild.Visible &= !(contentChild.GetChild()?.GetChild() is GUITextBlock tb && - !tb.Text.Contains(packToSaveInFilter.Text, StringComparison.OrdinalIgnoreCase)); - } - }); - ContentPackage ownerPkg = null; - if (MainSub?.Info != null) { ownerPkg = GetLocalPackageThatOwnsSub(MainSub.Info); } + + if (ownerPkg == null && MainSub?.Info != null) { ownerPkg = GetLocalPackageThatOwnsSub(MainSub.Info); } foreach (var p in ContentPackageManager.LocalPackages) { var packageListItem = addItemToPackageToSaveList(p.Name, p); @@ -3851,6 +3864,10 @@ namespace Barotrauma return true; }; + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.1f), deleteButtonHolder.RectTransform), TextManager.Get("DragAndDropSubmarineTip").Fallback(LocalizedString.EmptyString), textAlignment: Alignment.Center, font: GUIStyle.Font) + { + Wrap = true + }; if (AutoSaveInfo?.Root != null) { @@ -4488,6 +4505,7 @@ namespace Barotrauma public void ReconstructLayers() { + Dictionary previousLayers = Layers.ToDictionary(); ClearLayers(); foreach (MapEntity entity in MapEntity.MapEntityList) { @@ -4496,6 +4514,13 @@ namespace Barotrauma Layers.TryAdd(entity.Layer, new LayerData(!entity.IsLayerHidden)); } } + foreach ((string layerName, LayerData data) in previousLayers) + { + if (Layers.ContainsKey(layerName)) + { + Layers[layerName] = data; + } + } UpdateLayerPanel(); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs b/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs index 668b1bde8..22b885e50 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs @@ -26,6 +26,8 @@ namespace Barotrauma public static DateTime NextCommandPush; public static Tuple CommandBuffer; + private bool dimOutDefaultValues; + private bool isReadonly; public bool Readonly { @@ -316,16 +318,17 @@ namespace Barotrauma } } - public SerializableEntityEditor(RectTransform parent, ISerializableEntity entity, bool inGame, bool showName, string style = "", int elementHeight = 24, GUIFont titleFont = null) + public SerializableEntityEditor(RectTransform parent, ISerializableEntity entity, bool inGame, bool showName, string style = "", int elementHeight = 24, GUIFont titleFont = null, bool dimOutDefaultValues = true) : this(parent, entity, inGame ? SerializableProperty.GetProperties(entity).Union(SerializableProperty.GetProperties(entity).Where(p => p.GetAttribute()?.IsEditable(entity) ?? false)) - : SerializableProperty.GetProperties(entity).Where(p => p.GetAttribute()?.IsEditable(entity) ?? true), showName, style, elementHeight, titleFont) + : SerializableProperty.GetProperties(entity).Where(p => p.GetAttribute()?.IsEditable(entity) ?? true), showName, style, elementHeight, titleFont, dimOutDefaultValues) { } - public SerializableEntityEditor(RectTransform parent, ISerializableEntity entity, IEnumerable properties, bool showName, string style = "", int elementHeight = 24, GUIFont titleFont = null) + public SerializableEntityEditor(RectTransform parent, ISerializableEntity entity, IEnumerable properties, bool showName, string style = "", int elementHeight = 24, GUIFont titleFont = null, bool dimOutDefaultValues = true) : base(style, new RectTransform(Vector2.One, parent)) { + this.dimOutDefaultValues = dimOutDefaultValues; elementHeight = (int)(elementHeight * GUI.Scale); var tickBoxStyle = GUIStyle.GetComponentStyle("GUITickBox"); var textBoxStyle = GUIStyle.GetComponentStyle("GUITextBox"); @@ -523,9 +526,67 @@ namespace Barotrauma { propertyField = CreateStringField(entity, property, value.ToString(), displayName, toolTip); } + if (propertyField != null && dimOutDefaultValues) + { + UpdateTextColors(property, entity, propertyField); + } return propertyField; } + + private void UpdateTextColors(SerializableProperty property, object parentObject, GUIComponent parentElement) + { + if (!dimOutDefaultValues) { return; } + + bool isSetToDefaultValue = false; + object currentValue = property.GetValue(parentObject); + foreach (var attribute in property.Attributes.OfType()) + { + if (XMLExtensions.DefaultValueEquals(attribute.DefaultValue, currentValue) || + //treat null and empty strings as identical, because there's no way to differentiate between those in the editor + (currentValue == null && attribute.DefaultValue is string defaultValueStr && defaultValueStr.IsNullOrEmpty())) + { + isSetToDefaultValue = true; + break; + } + } + foreach (var component in parentElement.GetAllChildren()) + { + UpdateTextColors(component, isSetToDefaultValue); + } + } + + private void UpdateTextColors(GUIComponent component, bool isSetToDefaultValue) + { + if (!dimOutDefaultValues) { return; } + + if (component is GUINumberInput numberInput) + { + SetTextColor(numberInput.TextBox.TextBlock); + } + else if (component is GUIDropDown dropDown) + { + SetTextColor(dropDown.Button.TextBlock); + } + else if (component is GUITextBox textBox) + { + SetTextColor(textBox.TextBlock); + } + else if (component is GUITextBlock textBlock) + { + SetTextColor(textBlock); + } + else if (component is GUITickBox tickBox) + { + SetTextColor(tickBox.TextBlock); + } + + void SetTextColor(GUITextBlock textBlock) + { + textBlock.TextColor = new Color(textBlock.TextColor, alpha: isSetToDefaultValue ? 0.5f : 1.0f); + } + } + public GUIComponent CreateBoolField(ISerializableEntity entity, SerializableProperty property, bool value, LocalizedString displayName, LocalizedString toolTip) { var editableAttribute = property.GetAttribute(); @@ -564,6 +625,7 @@ namespace Barotrauma tickBox.Selected = propertyValue; tickBox.Flash(Color.Red); } + UpdateTextColors(property, entity, tickBox); return true; } }; @@ -611,6 +673,7 @@ namespace Barotrauma { TrySendNetworkUpdate(entity, property); } + UpdateTextColors(property, entity, frame); }; refresh += () => { @@ -654,6 +717,7 @@ namespace Barotrauma { TrySendNetworkUpdate(entity, property); } + UpdateTextColors(property, entity, frame); }; HandleSetterValueTampering(numberInput, () => property.GetFloatValue(entity)); @@ -711,6 +775,7 @@ namespace Barotrauma { TrySendNetworkUpdate(entity, property); } + UpdateTextColors(property, entity, frame); return true; }; refresh += () => @@ -829,6 +894,7 @@ namespace Barotrauma TrySendNetworkUpdate(entity, property); textBox.Text = StripPrefabTags(property.GetValue(entity).ToString()); textBox.Flash(GUIStyle.Green, flashDuration: 1f); + UpdateTextColors(property, entity, frame); } //restore the entities that were selected before applying MapEntity.SelectedList.Clear(); @@ -973,6 +1039,7 @@ namespace Barotrauma { TrySendNetworkUpdate(entity, property); } + UpdateTextColors(property, entity, frame); }; fields[i] = numberInput; } @@ -1046,6 +1113,7 @@ namespace Barotrauma { TrySendNetworkUpdate(entity, property); } + UpdateTextColors(property, entity, frame); }; HandleSetterValueTampering(numberInput, () => { @@ -1126,6 +1194,7 @@ namespace Barotrauma { TrySendNetworkUpdate(entity, property); } + UpdateTextColors(property, entity, frame); }; fields[i] = numberInput; } @@ -1206,6 +1275,7 @@ namespace Barotrauma { TrySendNetworkUpdate(entity, property); } + UpdateTextColors(property, entity, frame); }; fields[i] = numberInput; } @@ -1299,6 +1369,7 @@ namespace Barotrauma TrySendNetworkUpdate(entity, property); colorBox.Color = colorBox.HoverColor = colorBox.PressedColor = colorBox.SelectedTextColor = newVal; } + UpdateTextColors(property, entity, frame); }; colorBox.Color = colorBox.HoverColor = colorBox.PressedColor = colorBox.SelectedTextColor = (Color)property.GetValue(entity); fields[i] = numberInput; @@ -1373,6 +1444,7 @@ namespace Barotrauma { TrySendNetworkUpdate(entity, property); } + UpdateTextColors(property, entity, frame); }; fields[i] = numberInput; } @@ -1437,6 +1509,7 @@ namespace Barotrauma TrySendNetworkUpdate(entity, property); textBox.Flash(color: GUIStyle.Green, flashDuration: 1f); } + UpdateTextColors(property, entity, frame); } else { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Settings/SettingsMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/Settings/SettingsMenu.cs index 34ebe4aca..537ad5b60 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Settings/SettingsMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Settings/SettingsMenu.cs @@ -33,10 +33,10 @@ namespace Barotrauma private GameSettings.Config unsavedConfig; - private readonly GUIFrame mainFrame; + public readonly GUIFrame mainFrame; - private readonly GUILayoutGroup tabber; - private readonly GUIFrame contentFrame; + public readonly GUILayoutGroup tabber; + public readonly GUIFrame contentFrame; private readonly GUILayoutGroup bottom; public readonly WorkshopMenu WorkshopMenu; @@ -103,7 +103,7 @@ namespace Barotrauma newContent.Visible = true; } - private readonly Dictionary tabContents; + public readonly Dictionary tabContents; public void SelectTab(Tab tab) { @@ -149,7 +149,7 @@ namespace Barotrauma return content; } - private static (GUILayoutGroup Left, GUILayoutGroup Right) CreateSidebars(GUIFrame parent, bool split = false) + public static (GUILayoutGroup Left, GUILayoutGroup Right) CreateSidebars(GUIFrame parent, bool split = false) { GUILayoutGroup layout = new GUILayoutGroup(new RectTransform(Vector2.One, parent.RectTransform), isHorizontal: true); GUILayoutGroup left = new GUILayoutGroup(new RectTransform((0.4875f, 1.0f), layout.RectTransform), isHorizontal: false); @@ -166,29 +166,29 @@ namespace Barotrauma return (left, right); } - private static GUILayoutGroup CreateCenterLayout(GUIFrame parent) + public static GUILayoutGroup CreateCenterLayout(GUIFrame parent) { return new GUILayoutGroup(new RectTransform((0.5f, 1.0f), parent.RectTransform, Anchor.TopCenter, Pivot.TopCenter)) { ChildAnchor = Anchor.TopCenter }; } - private static RectTransform NewItemRectT(GUILayoutGroup parent) + public static RectTransform NewItemRectT(GUILayoutGroup parent) => new RectTransform((1.0f, 0.06f), parent.RectTransform, Anchor.CenterLeft); - private 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); } - private static GUITextBlock Label(GUILayoutGroup parent, LocalizedString str, GUIFont font) + public static GUITextBlock Label(GUILayoutGroup parent, LocalizedString str, GUIFont font) { return new GUITextBlock(NewItemRectT(parent), str, font: font); } - private static void DropdownEnum(GUILayoutGroup parent, Func textFunc, Func? tooltipFunc, T currentValue, + public static void DropdownEnum(GUILayoutGroup parent, Func textFunc, Func? tooltipFunc, T currentValue, Action setter) where T : Enum => Dropdown(parent, textFunc, tooltipFunc, (T[])Enum.GetValues(typeof(T)), currentValue, setter); - private static GUIDropDown Dropdown(GUILayoutGroup parent, Func textFunc, Func? tooltipFunc, IReadOnlyList values, T currentValue, Action setter) + public static GUIDropDown Dropdown(GUILayoutGroup parent, Func textFunc, Func? tooltipFunc, IReadOnlyList values, T currentValue, Action setter) { var dropdown = new GUIDropDown(NewItemRectT(parent), elementCount: values.Count); values.ForEach(v => dropdown.AddItem(text: textFunc(v), userData: v, toolTip: tooltipFunc?.Invoke(v) ?? null)); @@ -204,7 +204,7 @@ namespace Barotrauma return dropdown; } - private static (GUIScrollBar slider, GUITextBlock label) Slider(GUILayoutGroup parent, Vector2 range, int steps, Func labelFunc, float currentValue, Action setter, LocalizedString? tooltip = null) + public static (GUIScrollBar slider, GUITextBlock label) Slider(GUILayoutGroup parent, Vector2 range, int steps, Func labelFunc, float currentValue, Action setter, LocalizedString? tooltip = null) { var layout = new GUILayoutGroup(NewItemRectT(parent), isHorizontal: true); var slider = new GUIScrollBar(new RectTransform((0.72f, 1.0f), layout.RectTransform), style: "GUISlider") @@ -229,7 +229,7 @@ namespace Barotrauma return (slider, label); } - private static GUITickBox Tickbox(GUILayoutGroup parent, LocalizedString label, LocalizedString tooltip, bool currentValue, Action setter) + public static GUITickBox Tickbox(GUILayoutGroup parent, LocalizedString label, LocalizedString tooltip, bool currentValue, Action setter) { return new GUITickBox(NewItemRectT(parent), label) { @@ -243,9 +243,9 @@ namespace Barotrauma }; } - private string Percentage(float v) => ToolBox.GetFormattedPercentage(v); + public string Percentage(float v) => ToolBox.GetFormattedPercentage(v); - private static int Round(float v) => MathUtils.RoundToInt(v); + public static int Round(float v) => MathUtils.RoundToInt(v); private void CreateGraphicsTab() { @@ -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); @@ -965,4 +1006,4 @@ namespace Barotrauma GUI.SettingsMenuOpen = false; } } -} \ No newline at end of file +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/SpamServerFilter.cs b/Barotrauma/BarotraumaClient/ClientSource/SpamServerFilter.cs index 2214848d4..47d3d1b58 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/SpamServerFilter.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/SpamServerFilter.cs @@ -387,13 +387,11 @@ These will hide all servers that have a discord.gg link in their name or descrip try { - var client = new RestClient($"{remoteContentUrl}spamfilter") - { - CachePolicy = new HttpRequestCachePolicy(HttpRequestCacheLevel.NoCacheNoStore) - }; + var client = RestFactory.CreateClient($"{remoteContentUrl}spamfilter"); + client.CachePolicy = new HttpRequestCachePolicy(HttpRequestCacheLevel.NoCacheNoStore); client.AddDefaultHeader("Cache-Control", "no-cache"); client.AddDefaultHeader("Pragma", "no-cache"); - var request = new RestRequest("serve_spamlist.php", Method.GET); + var request = RestFactory.CreateRequest("serve_spamlist.php"); TaskPool.Add("RequestGlobalSpamFilter", client.ExecuteAsync(request), RemoteContentReceived); } catch (Exception e) @@ -410,12 +408,18 @@ These will hide all servers that have a discord.gg link in their name or descrip try { if (!t.TryGetResult(out IRestResponse? remoteContentResponse)) { throw new Exception("Task did not return a valid result"); } + if (remoteContentResponse.ErrorException != null) + { + DebugConsole.AddWarning( + "Connection error: Failed to receive global spam filter " + + $"({remoteContentResponse.ErrorException.Message})."); + return; + } if (remoteContentResponse.StatusCode != HttpStatusCode.OK) { DebugConsole.AddWarning( - "Failed to receive global spam filter." + - "There may be an issue with your internet connection, or the master server might be temporarily unavailable " + - $"(error code: {remoteContentResponse.StatusCode})"); + "Failed to receive global spam filter. " + + $"The master server might be temporarily unavailable, HTTP status: {remoteContentResponse.StatusCode}"); return; } string data = remoteContentResponse.Content; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/Workshop.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/Workshop.cs index cc1ef9c79..dc31dc47c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/Workshop.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/Workshop.cs @@ -18,18 +18,36 @@ namespace Barotrauma.Steam { public const int MaxThumbnailSize = 1024 * 1024; + /// + /// 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()}". + /// public static readonly ImmutableArray Tags = new [] { "submarine", "item", "monster", - "art", "mission", + "outpost", + "beacon station", + "wreck", + "ruin", + "weapons", + "medical", + "equipment", + "art", "event set", "total conversion", + "game mode", + "gameplay mechanics", "environment", "item assembly", "language", + "qol", + "client-side", + "server-side", + "outdated", + "library" }.ToIdentifiers().ToImmutableArray(); public class ItemThumbnail : IDisposable @@ -113,10 +131,14 @@ namespace Barotrauma.Steam string? thumbnailUrl = item.PreviewImageUrl; if (thumbnailUrl.IsNullOrWhiteSpace()) { return null; } - var client = new RestClient(thumbnailUrl); - var request = new RestRequest(".", Method.GET); + var client = RestFactory.CreateClient(thumbnailUrl); + var request = RestFactory.CreateRequest("."); IRestResponse response = await client.ExecuteAsync(request, cancellationToken); - if (response is { StatusCode: System.Net.HttpStatusCode.OK, ResponseStatus: ResponseStatus.Completed }) + if (response.ErrorException != null) + { + DebugConsole.NewMessage($"Connection error: Failed to load workshop item thumbnail for {item.Id} ({response.ErrorException.Message})."); + } + else if (response is { StatusCode: System.Net.HttpStatusCode.OK, ResponseStatus: ResponseStatus.Completed }) { using var dataStream = new System.IO.MemoryStream(); await dataStream.WriteAsync(response.RawBytes, cancellationToken); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/ItemList.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/ItemList.cs index 91038dab5..3c7ec45b2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/ItemList.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/ItemList.cs @@ -535,9 +535,9 @@ namespace Barotrauma.Steam = new GUIListBox(rectT, style: null, isHorizontal: false) { UseGridLayout = true, - ScrollBarEnabled = false, + ScrollBarEnabled = true, ScrollBarVisible = false, - HideChildrenOutsideFrame = false, + HideChildrenOutsideFrame = true, Spacing = GUI.IntScale(4) }; tagsList.Content.ClampMouseRectToParent = false; diff --git a/Barotrauma/BarotraumaClient/LinuxClient.csproj b/Barotrauma/BarotraumaClient/LinuxClient.csproj index a74a96392..20e0c832f 100644 --- a/Barotrauma/BarotraumaClient/LinuxClient.csproj +++ b/Barotrauma/BarotraumaClient/LinuxClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 1.11.5.0 + 1.12.7.0 Copyright © FakeFish 2018-2024 AnyCPU;x64 Barotrauma @@ -14,6 +14,7 @@ Debug;Release;Unstable true ;NU1605;CS0114;CS0108;CS8597;CS8600;CS8601;CS8602;CS8603;CS8604;CS8605;CS8606;CS8607;CS8608;CS8609;CS8610;CS8611;CS8612;CS8613;CS8614;CS8615;CS8616;CS8617;CS8618;CS8619;CS8620;CS8621;CS8622;CS8624;CS8625;CS8626;CS8629;CS8631;CS8632;CS8633;CS8634;CS8638;CS8643;CS8644;CS8645;CS8653;CS8654;CS8655;CS8667;CS8669;CS8670;CS8714;CS8717;CS8765 + latest @@ -215,5 +216,5 @@ - + diff --git a/Barotrauma/BarotraumaClient/MacClient.csproj b/Barotrauma/BarotraumaClient/MacClient.csproj index 67009978a..cc877ab15 100644 --- a/Barotrauma/BarotraumaClient/MacClient.csproj +++ b/Barotrauma/BarotraumaClient/MacClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 1.11.5.0 + 1.12.7.0 Copyright © FakeFish 2018-2024 AnyCPU;x64 Barotrauma @@ -15,6 +15,7 @@ true ;NU1605;CS0114;CS0108;CS8597;CS8600;CS8601;CS8602;CS8603;CS8604;CS8605;CS8606;CS8607;CS8608;CS8609;CS8610;CS8611;CS8612;CS8613;CS8614;CS8615;CS8616;CS8617;CS8618;CS8619;CS8620;CS8621;CS8622;CS8624;CS8625;CS8626;CS8629;CS8631;CS8632;CS8633;CS8634;CS8638;CS8643;CS8644;CS8645;CS8653;CS8654;CS8655;CS8667;CS8669;CS8670;CS8714;CS8717;CS8765 false + latest @@ -220,5 +221,6 @@ + diff --git a/Barotrauma/BarotraumaClient/WindowsClient.csproj b/Barotrauma/BarotraumaClient/WindowsClient.csproj index 43f2ab06c..9bf9ca415 100644 --- a/Barotrauma/BarotraumaClient/WindowsClient.csproj +++ b/Barotrauma/BarotraumaClient/WindowsClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 1.11.5.0 + 1.12.7.0 Copyright © FakeFish 2018-2024 AnyCPU;x64 Barotrauma @@ -15,6 +15,7 @@ true app.manifest ;NU1605;CS0114;CS0108;CS8597;CS8600;CS8601;CS8602;CS8603;CS8604;CS8605;CS8606;CS8607;CS8608;CS8609;CS8610;CS8611;CS8612;CS8613;CS8614;CS8615;CS8616;CS8617;CS8618;CS8619;CS8620;CS8621;CS8622;CS8624;CS8625;CS8626;CS8629;CS8631;CS8632;CS8633;CS8634;CS8638;CS8643;CS8644;CS8645;CS8653;CS8654;CS8655;CS8667;CS8669;CS8670;CS8714;CS8717;CS8765 + latest @@ -247,5 +248,5 @@ - + diff --git a/Barotrauma/BarotraumaClient/WindowsClient.csproj.DotSettings b/Barotrauma/BarotraumaClient/WindowsClient.csproj.DotSettings new file mode 100644 index 000000000..051269cc3 --- /dev/null +++ b/Barotrauma/BarotraumaClient/WindowsClient.csproj.DotSettings @@ -0,0 +1,2 @@ + + True \ No newline at end of file diff --git a/Barotrauma/BarotraumaServer/LinuxServer.csproj b/Barotrauma/BarotraumaServer/LinuxServer.csproj index 18a89a450..411d2e5ee 100644 --- a/Barotrauma/BarotraumaServer/LinuxServer.csproj +++ b/Barotrauma/BarotraumaServer/LinuxServer.csproj @@ -6,14 +6,16 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 1.11.5.0 + 1.12.7.0 Copyright © FakeFish 2018-2023 AnyCPU;x64 DedicatedServer ..\BarotraumaShared\Icon.ico Debug;Release;Unstable true + latest ;NU1605;CS0114;CS0108;CS8597;CS8600;CS8601;CS8602;CS8603;CS8604;CS8605;CS8606;CS8607;CS8608;CS8609;CS8610;CS8611;CS8612;CS8613;CS8614;CS8615;CS8616;CS8617;CS8618;CS8619;CS8620;CS8621;CS8622;CS8624;CS8625;CS8626;CS8629;CS8631;CS8632;CS8633;CS8634;CS8638;CS8643;CS8644;CS8645;CS8653;CS8654;CS8655;CS8667;CS8669;CS8670;CS8714;CS8717;CS8765 + latest @@ -53,8 +55,9 @@ ..\bin\$(Configuration)Linux\ true - + + @@ -161,4 +164,6 @@ + + diff --git a/Barotrauma/BarotraumaServer/MacServer.csproj b/Barotrauma/BarotraumaServer/MacServer.csproj index fe5683a68..fb79c6495 100644 --- a/Barotrauma/BarotraumaServer/MacServer.csproj +++ b/Barotrauma/BarotraumaServer/MacServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 1.11.5.0 + 1.12.7.0 Copyright © FakeFish 2018-2023 AnyCPU;x64 DedicatedServer @@ -14,6 +14,7 @@ Debug;Release;Unstable true ;NU1605;CS0114;CS0108;CS8597;CS8600;CS8601;CS8602;CS8603;CS8604;CS8605;CS8606;CS8607;CS8608;CS8609;CS8610;CS8611;CS8612;CS8613;CS8614;CS8615;CS8616;CS8617;CS8618;CS8619;CS8620;CS8621;CS8622;CS8624;CS8625;CS8626;CS8629;CS8631;CS8632;CS8633;CS8634;CS8638;CS8643;CS8644;CS8645;CS8653;CS8654;CS8655;CS8667;CS8669;CS8670;CS8714;CS8717;CS8765 + latest @@ -166,4 +167,6 @@ + + diff --git a/Barotrauma/BarotraumaServer/ServerSource/Characters/Character.cs b/Barotrauma/BarotraumaServer/ServerSource/Characters/Character.cs index 4e3f0c5ec..62f25f357 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Characters/Character.cs @@ -68,7 +68,7 @@ namespace Barotrauma var owner = clients.FirstOrDefault(c => c.Character == this); if (owner != null) { - if (!GameMain.LuaCs.Game.overrideTraitors) + if (!LuaCsSetup.Instance.Game.overrideTraitors) { GameMain.Server.SendDirectChatMessage(TextManager.FormatServerMessage("KilledByTraitorNotification"), owner, ChatMessageType.ServerMessageBoxInGame); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs index 686a10dda..15d96d9eb 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs @@ -320,11 +320,7 @@ namespace Barotrauma if (TalentTree.IsViableTalentForCharacter(this, prefab.Identifier, talentSelection)) { - bool? should = GameMain.LuaCs.Hook.Call("character.updateTalent", this, prefab, c); - if (should == null) - { - GiveTalent(prefab.Identifier); - } + GiveTalent(prefab.Identifier); talentSelection.Add(prefab.Identifier); } } @@ -819,7 +815,7 @@ namespace Barotrauma var tempBuffer = new ReadWriteMessage(); WriteStatus(tempBuffer, forceAfflictionData: true); - if (msgLengthBeforeStatus + tempBuffer.LengthBytes >= 255 && restrictMessageSize && GameMain.LuaCs.Networking.RestrictMessageSize) + if (msgLengthBeforeStatus + tempBuffer.LengthBytes >= 255 && restrictMessageSize) { msg.WriteBoolean(false); if (msgLengthBeforeStatus < 255) diff --git a/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs b/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs index 063b61d08..dff16fe00 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs @@ -10,6 +10,7 @@ using System.Linq; using System.Text; using Barotrauma.Steam; using Barotrauma.Extensions; +using Barotrauma.LuaCs.Events; namespace Barotrauma { @@ -1287,41 +1288,6 @@ namespace Barotrauma GameMain.NetLobbyScreen.LevelSeed = string.Join(" ", args); })); - - commands.Add(new Command("lua", "lua: Runs a string.", (string[] args) => - { - try - { - GameMain.LuaCs.Lua.DoString(string.Join(" ", args)); - } - catch (Exception ex) - { - LuaCsLogger.HandleException(ex, LuaCsMessageOrigin.LuaMod); - } - })); - - commands.Add(new Command("reloadlua|reloadcs|reloadluacs", "Re-initializes the LuaCs environment.", (string[] args) => - { - GameMain.LuaCs.Initialize(); - })); - - commands.Add(new Command("toggleluadebug", "Toggles the MoonSharp Debug Server.", (string[] args) => - { - int port = 41912; - - if (args.Length > 0) - { - int.TryParse(args[0], out port); - } - - GameMain.LuaCs.ToggleDebugger(port); - })); - - commands.Add(new Command("install_cl_lua|install_cl|install_cl_cs|install_cl_luacs", "Installs Client-Side LuaCs into your client.", (string[] args) => - { - LuaCsInstaller.Install(); - })); - commands.Add(new Command("randomizeseed", "randomizeseed: Toggles level seed randomization on/off.", (string[] args) => { GameMain.Server.ServerSettings.RandomizeSeed = !GameMain.Server.ServerSettings.RandomizeSeed; @@ -2772,9 +2738,16 @@ namespace Barotrauma commands.Add(new Command("ShowServerPerf", "Immediately log server performance info in ServerMessage", (string[] args) => { GameServer.Log(PerformanceMonitor.PM.ToString(), ServerLog.MessageType.ServerMessage); - NewMessage(PerformanceMonitor.PM.ToString(), Color.Green); })); + 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) => { diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/EventManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/EventManager.cs index 78d1288cc..fc9a8aa55 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Events/EventManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/EventManager.cs @@ -79,6 +79,15 @@ namespace Barotrauma convAction.SelectedOption = selectedOption; if (convAction.Options.Any() && !convAction.GetEndingOptions().Contains(selectedOption)) { + var option = convAction.Options[selectedOption]; + if (option.ForceSay && sender.Character != null) + { + sender.Character.ForceSay( + option.ForceSayText.IsNullOrEmpty() ? TextManager.Get(option.Text).Fallback(option.Text) : TextManager.Get(option.ForceSayText).Fallback(option.ForceSayText), + option.ForceSayInRadio, + option.ForceSayRemoveQuotes); + } + foreach (Client c in convAction.TargetClients) { if (c == sender) { continue; } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/CombatMission.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/CombatMission.cs index 736c98542..836dc9f1c 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/CombatMission.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/CombatMission.cs @@ -133,7 +133,7 @@ namespace Barotrauma switch (winCondition) { case WinCondition.LastManStanding: - if (crews[0].Count == 0 || crews[1].Count == 0) + if (crews[0].Count == 0 && crews[1].Count == 0) { //if there are no characters in either crew, end the round teamDead[0] = teamDead[1] = true; diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs b/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs index 516bbba8f..d6ba7e980 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs @@ -14,6 +14,7 @@ using MoonSharp.Interpreter; using System.Net; using Barotrauma.Extensions; using System.Threading.Tasks; +using Barotrauma.LuaCs.Events; namespace Barotrauma { @@ -35,8 +36,6 @@ namespace Barotrauma set { world = value; } } - public static LuaCsSetup LuaCs; - public static GameServer Server; public static NetworkMember NetworkMember { @@ -113,6 +112,8 @@ namespace Barotrauma GameScreen = new GameScreen(); MainThread = Thread.CurrentThread; + + LuaCsSetup.Instance.GetType(); } public void Init() @@ -131,8 +132,6 @@ namespace Barotrauma NetLobbyScreen = new NetLobbyScreen(); CheckContentPackage(); - - LuaCs = new LuaCsSetup(); } @@ -244,6 +243,9 @@ namespace Barotrauma //handled in TryStartChildServerRelay i += 2; break; + case "-lenienthandshake": + NetConfig.UseLenientHandshake = true; + break; } } @@ -367,12 +369,16 @@ namespace Barotrauma TaskPool.Update(); CoroutineManager.Update(paused: false, (float)Timing.Step); - GameMain.LuaCs.Update(); performanceCounterTimer.Stop(); - if (GameMain.LuaCs.PerformanceCounter.EnablePerformanceCounter) + if (LuaCsSetup.Instance.PerformanceCounterService.EnablePerformanceCounter) { - GameMain.LuaCs.PerformanceCounter.UpdateElapsedTime = (double)performanceCounterTimer.ElapsedTicks / Stopwatch.Frequency; + LuaCsSetup.Instance.PerformanceCounterService.AddElapsedTicks(new SimplePerformanceData("Update", performanceCounterTimer.ElapsedTicks)); } + if (LuaCsSetup.Instance.PerformanceCounter.EnablePerformanceCounter) + { + LuaCsSetup.Instance.PerformanceCounter.UpdateElapsedTime = (double)performanceCounterTimer.ElapsedTicks / Stopwatch.Frequency; + } + performanceCounterTimer.Reset(); Timing.Accumulator -= Timing.Step; @@ -459,7 +465,17 @@ namespace Barotrauma public void Exit() { ShouldRun = false; - GameMain.LuaCs.Stop(); + try + { + if (LuaCsSetup.Instance is not null) + { + LuaCsSetup.Instance.Dispose(); + } + } + catch (Exception e) + { + DebugConsole.ThrowError($"Error while disposing of LuaCsForBarotrauma: {e.Message} | {e.StackTrace}"); + } } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Controller.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Controller.cs index 8d8fe2933..5a6f1708c 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Controller.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Controller.cs @@ -7,7 +7,7 @@ namespace Barotrauma.Items.Components public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) { msg.WriteBoolean(State); - msg.WriteUInt16(user == null ? (ushort)0 : user.ID); + msg.WriteUInt16(User == null || User.Removed ? (ushort)0 : User.ID); } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Repairable.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Repairable.cs index 089fc03df..f37295790 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Repairable.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Repairable.cs @@ -16,8 +16,11 @@ namespace Barotrauma.Items.Components public void ServerEventRead(IReadMessage msg, Client c) { if (c.Character == null) { return; } - var requestedFixAction = (FixActions)msg.ReadRangedInteger(0, 2); - var QTESuccess = msg.ReadBoolean(); + FixActions requestedFixAction = (FixActions)msg.ReadRangedInteger(0, 2); + bool QTESuccess = msg.ReadBoolean(); + + if (!item.CanClientAccess(c) || !HasRequiredItems(c.Character, addMessage: false)) { return; } + if (requestedFixAction != FixActions.None) { if (!c.Character.IsTraitor && requestedFixAction == FixActions.Sabotage) diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Inventory.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Inventory.cs index d2dfb2aad..b6acd3f1a 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Inventory.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Inventory.cs @@ -121,9 +121,9 @@ namespace Barotrauma if (shouldBeRemoved) { bool itemAccessDenied = prevItems.Contains(item) && // if the item was in the inventory before - !itemAccessibility[item] && // and the sender is not allowed to access it - (item.PreviousParentInventory == null || // and either the item has no previous inventory - !sender.Character.CanAccessInventory(item.PreviousParentInventory)); // or the sender can't access the previous inventory + !itemAccessibility[item] && // and the sender is not allowed to access it + (item.PreviousParentInventory == null || // and either the item has no previous inventory + !sender.Character.CanAccessInventory(item.PreviousParentInventory)); // or the sender can't access the previous inventory if (itemAccessDenied) { @@ -136,7 +136,7 @@ namespace Barotrauma Item droppedItem = item; Entity prevOwner = Owner; Inventory previousInventory = droppedItem.ParentInventory; - droppedItem.Drop(null); + droppedItem.Drop(sender.Character); droppedItem.PreviousParentInventory = previousInventory; var previousCharacterInventory = prevOwner switch @@ -188,9 +188,18 @@ namespace Barotrauma if (holdable != null && !holdable.CanBeDeattached()) { continue; } - bool itemAccessDenied = !prevItems.Contains(item) && !itemAccessibility[item] && - (sender.Character == null || item.PreviousParentInventory == null || !sender.Character.CanAccessInventory(item.PreviousParentInventory)); - + bool itemAccessDenied = !prevItems.Contains(item) && + !itemAccessibility[item] && + (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) { diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs index 0a66bbd08..216aeafa8 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs @@ -12,8 +12,6 @@ namespace Barotrauma { private CoroutineHandle logPropertyChangeCoroutine; - public Inventory PreviousParentInventory; - public override Sprite Sprite { get { return base.Prefab?.Sprite; } diff --git a/Barotrauma/BarotraumaServer/ServerSource/LuaCs/Lua/LuaBarotraumaAdditions.cs b/Barotrauma/BarotraumaServer/ServerSource/LuaCs/Lua/LuaBarotraumaAdditions.cs deleted file mode 100644 index 62289624f..000000000 --- a/Barotrauma/BarotraumaServer/ServerSource/LuaCs/Lua/LuaBarotraumaAdditions.cs +++ /dev/null @@ -1,107 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace Barotrauma.Networking -{ - partial class Client - { - public void SetClientCharacter(Character character) - { - GameMain.Server.SetClientCharacter(this, character); - } - - public void Kick(string reason = "") - { - GameMain.Server.KickClient(this.Connection, reason); - } - - public void Ban(string reason = "", float seconds = -1) - { - if (seconds == -1) - { - GameMain.Server.BanClient(this, reason, null); - } - else - { - GameMain.Server.BanClient(this, reason, TimeSpan.FromSeconds(seconds)); - } - } - - public static void UnbanPlayer(string playerName) - { - GameMain.Server.UnbanPlayer(playerName); - } - - public static void BanPlayer(string player, string reason, bool range = false, float seconds = -1) - { - if (seconds == -1) - { - GameMain.Server.BanPlayer(player, reason, null); - } - else - { - GameMain.Server.BanPlayer(player, reason, TimeSpan.FromSeconds(seconds)); - } - } - - public bool CheckPermission(ClientPermissions permissions) - { - return this.Permissions.HasFlag(permissions); - } - } -} - -namespace Barotrauma -{ - using Microsoft.Xna.Framework; - using System.Reflection; - - partial class Item - { - public object CreateServerEventString(string component) - { - var comp = GetComponentString(component); - - if (comp == null) - return null; - - MethodInfo method = typeof(Item).GetMethod(nameof(Item.CreateServerEvent), new Type[]{ Type.MakeGenericMethodParameter(0) }); - MethodInfo generic = method.MakeGenericMethod(comp.GetType()); - return generic.Invoke(this, new object[]{ comp }); - } - - public object CreateServerEventString(string component, object[] extraData) - { - var comp = GetComponentString(component); - - if (comp == null) - return null; - - MethodInfo method = typeof(Item).GetMethod(nameof(Item.CreateServerEvent), new Type[]{ Type.MakeGenericMethodParameter(0), typeof(object[]) }); - MethodInfo generic = method.MakeGenericMethod(comp.GetType()); - return generic.Invoke(this, new object[]{comp, extraData }); - } - } -} - -namespace Barotrauma.Items.Components -{ - using Barotrauma.Networking; - - partial struct Signal - { - public static Signal Create(string value, int stepsTaken = 0, Character sender = null, Item source = null, float power = 0.0f, float strength = 1.0f) - { - return new Signal(value, stepsTaken, sender, source, power, strength); - } - } - - partial class Quality - { - public void SetValue(StatType statType, float value) - { - statValues[statType] = value; - } - } -} diff --git a/Barotrauma/BarotraumaServer/ServerSource/LuaCs/LuaCsInstaller.cs b/Barotrauma/BarotraumaServer/ServerSource/LuaCs/LuaCsInstaller.cs index cd54fe591..2ed47b660 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/LuaCs/LuaCsInstaller.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/LuaCs/LuaCsInstaller.cs @@ -2,6 +2,7 @@ using System; using System.IO; using System.Linq; +using Barotrauma.LuaCs; namespace Barotrauma { @@ -9,11 +10,11 @@ namespace Barotrauma { public static void Install() { - ContentPackage luaPackage = LuaCsSetup.GetPackage(LuaCsSetup.LuaForBarotraumaId); + ContentPackage luaPackage = LuaCsSetup.GetLuaCsPackage(); if (luaPackage == null) { - GameMain.Server.SendChatMessage("Couldn't find the LuaCs For Barotrauma package.", ChatMessageType.ServerMessageBox); + GameMain.Server.SendChatMessage("Couldn't find the ProjectEP package.", ChatMessageType.ServerMessageBox); return; } @@ -45,15 +46,13 @@ namespace Barotrauma File.Copy(Path.Combine(path, "Binary", file), file, true); } - File.WriteAllText(LuaCsSetup.VersionFile, luaPackage.ModVersion); - #if WINDOWS File.WriteAllText("LuaCsDedicatedServer.bat", "\"%LocalAppData%/Daedalic Entertainment GmbH/Barotrauma/WorkshopMods/Installed/2559634234/Binary/DedicatedServer.exe\""); #endif } 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; } @@ -64,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); } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/LuaCs/LuaCsNetworking.cs b/Barotrauma/BarotraumaServer/ServerSource/LuaCs/LuaCsNetworking.cs deleted file mode 100644 index 62810ca56..000000000 --- a/Barotrauma/BarotraumaServer/ServerSource/LuaCs/LuaCsNetworking.cs +++ /dev/null @@ -1,194 +0,0 @@ -using Barotrauma.Networking; -using System.Collections.Generic; -using System.Linq; - -namespace Barotrauma -{ - partial class LuaCsNetworking - { - private const int MaxRegisterPerClient = 1000; - - private Dictionary clientRegisterCount = new Dictionary(); - - private ushort currentId = 0; - - public void NetMessageReceived(IReadMessage netMessage, ClientPacketHeader header, Client client = null) - { - if (header != ClientPacketHeader.LUA_NET_MESSAGE) - { - GameMain.LuaCs.Hook.Call("netMessageReceived", netMessage, header, client); - return; - } - - LuaCsClientToServer luaCsHeader = (LuaCsClientToServer)netMessage.ReadByte(); - - switch (luaCsHeader) - { - case LuaCsClientToServer.NetMessageString: - HandleNetMessageString(netMessage, client); - break; - - case LuaCsClientToServer.NetMessageId: - HandleNetMessageId(netMessage, client); - break; - - case LuaCsClientToServer.RequestAllIds: - WriteAllIds(client); - break; - - case LuaCsClientToServer.RequestSingleId: - RequestIdSingle(netMessage, client); - break; - } - } - - private void HandleNetMessageId(IReadMessage netMessage, Client client = null) - { - ushort id = netMessage.ReadUInt16(); - - if (idToString.ContainsKey(id)) - { - string name = idToString[id]; - - HandleNetMessage(netMessage, name, client); - } - else - { - if (GameSettings.CurrentConfig.VerboseLogging) - { - LuaCsLogger.LogError($"Received NetMessage for unknown id {id} from {GameServer.ClientLogName(client)}."); - } - } - } - - public IWriteMessage Start(string netMessageName) - { - var message = new WriteOnlyMessage(); - - message.WriteByte((byte)ServerPacketHeader.LUA_NET_MESSAGE); - - if (stringToId.ContainsKey(netMessageName)) - { - message.WriteByte((byte)LuaCsServerToClient.NetMessageId); - message.WriteUInt16(stringToId[netMessageName]); - } - else - { - message.WriteByte((byte)LuaCsServerToClient.NetMessageString); - message.WriteString(netMessageName); - } - - return message; - } - - public void Receive(string netMessageName, LuaCsAction callback) - { - RegisterId(netMessageName); - - netReceives[netMessageName] = callback; - } - - public ushort RegisterId(string name) - { - if (stringToId.ContainsKey(name)) - { - return stringToId[name]; - } - - if (currentId >= ushort.MaxValue) - { - LuaCsLogger.LogError($"Tried to register more than {ushort.MaxValue} network ids!"); - return 0; - } - - currentId++; - - idToString[currentId] = name; - stringToId[name] = currentId; - - WriteIdToAll(currentId, name); - - return currentId; - } - - private void RequestIdSingle(IReadMessage netMessage, Client client) - { - string name = netMessage.ReadString(); - - if (!stringToId.ContainsKey(name) && client.AccountId.TryUnwrap(out AccountId id)) - { - if (!clientRegisterCount.ContainsKey(id.StringRepresentation)) - { - clientRegisterCount[id.StringRepresentation] = 0; - } - - clientRegisterCount[id.StringRepresentation]++; - - if (clientRegisterCount[id.StringRepresentation] > MaxRegisterPerClient) - { - LuaCsLogger.Log($"{GameServer.ClientLogName(client)} Tried to register more than {MaxRegisterPerClient} Ids!"); - return; - } - } - - RegisterId(name); - } - - private void WriteIdToAll(ushort id, string name) - { - WriteOnlyMessage message = new WriteOnlyMessage(); - message.WriteByte((byte)ServerPacketHeader.LUA_NET_MESSAGE); - message.WriteByte((byte)LuaCsServerToClient.ReceiveIds); - - message.WriteUInt16(1); - message.WriteUInt16(id); - message.WriteString(name); - - Send(message, null, DeliveryMethod.Reliable); - } - - private void WriteAllIds(Client client) - { - WriteOnlyMessage message = new WriteOnlyMessage(); - message.WriteByte((byte)ServerPacketHeader.LUA_NET_MESSAGE); - message.WriteByte((byte)LuaCsServerToClient.ReceiveIds); - - message.WriteUInt16((ushort)idToString.Count()); - foreach ((ushort id, string name) in idToString) - { - message.WriteUInt16(id); - message.WriteString(name); - } - - Send(message, client.Connection, DeliveryMethod.Reliable); - } - - public void ClientWriteLobby(Client client) => GameMain.Server.ClientWriteLobby(client); - - public void Send(IWriteMessage netMessage, NetworkConnection connection = null, DeliveryMethod deliveryMethod = DeliveryMethod.Reliable) - { - if (connection == null) - { - foreach (NetworkConnection conn in Client.ClientList.Select(c => c.Connection)) - { - GameMain.Server.ServerPeer.Send(netMessage, conn, deliveryMethod); - } - } - else - { - GameMain.Server.ServerPeer.Send(netMessage, connection, deliveryMethod); - } - } - - public void UpdateClientPermissions(Client client) - { - GameMain.Server.UpdateClientPermissions(client); - } - - public int FileSenderMaxPacketsPerUpdate - { - get { return FileSender.FileTransferOut.MaxPacketsPerUpdate; } - set { FileSender.FileTransferOut.MaxPacketsPerUpdate = value; } - } - } -} diff --git a/Barotrauma/BarotraumaServer/ServerSource/LuaCs/LuaCsSetup.cs b/Barotrauma/BarotraumaServer/ServerSource/LuaCs/LuaCsSetup.cs new file mode 100644 index 000000000..7c33a0f64 --- /dev/null +++ b/Barotrauma/BarotraumaServer/ServerSource/LuaCs/LuaCsSetup.cs @@ -0,0 +1,25 @@ +using System; +using System.IO; +using Barotrauma.Networking; + +namespace Barotrauma; + +partial class LuaCsSetup +{ + partial void CheckReadyToRun(Action onReadyToRun) + { + onReadyToRun?.Invoke(); + } + + /// + /// Handles changes in game states tracked by screen changes. + /// + /// The new game screen. + public partial void OnScreenSelected(Screen screen) + { + // the server is always in the running state unless explicitly stopped. + if (screen == UnimplementedScreen.Instance) + SetRunState(RunState.Unloaded); + SetRunState(RunState.Running); + } +} diff --git a/Barotrauma/BarotraumaServer/ServerSource/LuaCs/_Services/NetworkingService.cs b/Barotrauma/BarotraumaServer/ServerSource/LuaCs/_Services/NetworkingService.cs new file mode 100644 index 000000000..5bd32a8a1 --- /dev/null +++ b/Barotrauma/BarotraumaServer/ServerSource/LuaCs/_Services/NetworkingService.cs @@ -0,0 +1,188 @@ +using Barotrauma.LuaCs.Events; +using Barotrauma.Networking; +using System; +using System.Collections.Generic; +using System.Linq; + +// ReSharper disable once CheckNamespace +namespace Barotrauma.LuaCs; + +partial class NetworkingService : INetworkingService, IEventClientRawNetMessageReceived +{ + private const int MaxRegisterPerClient = 1000; + + private Dictionary clientRegisterCount = new Dictionary(); + + private ushort currentId = 0; + + public IWriteMessage Start(NetId netId) + { + var message = new WriteOnlyMessage(); + + message.WriteByte((byte)ServerHeader); + + if (idToPacket.ContainsKey(netId)) + { + message.WriteByte((byte)ServerToClient.NetMessageInternalId); + message.WriteUInt16(idToPacket[netId]); + } + else + { + message.WriteByte((byte)ServerToClient.NetMessageNetId); + NetId.Write(message, netId); + } + + return message; + } + + public bool? OnReceivedClientNetMessage(IReadMessage netMessage, ClientPacketHeader clientPacketHeader, NetworkConnection sender) + { + if (clientPacketHeader != ClientHeader) + { + return null; + } + + Client client = GameMain.Server.ConnectedClients.First(c => c.Connection == sender); + + ClientToServer luaCsHeader = (ClientToServer)netMessage.ReadByte(); + + switch (luaCsHeader) + { + case ClientToServer.NetMessageNetId: + HandleNetMessageString(netMessage, client); + break; + + case ClientToServer.NetMessageInternalId: + HandleNetMessageId(netMessage, client); + break; + + case ClientToServer.RequestSync: + WriteSync(client); + break; + + case ClientToServer.RequestSingleNetId: + RequestIdSingle(netMessage, client); + break; + } + + return true; + } + + private void HandleNetMessageId(IReadMessage netMessage, Client client = null) + { + ushort id = netMessage.ReadUInt16(); + + if (packetToId.ContainsKey(id)) + { + NetId netId = packetToId[id]; + + HandleNetMessage(netMessage, netId, client); + } + else + { + if (GameSettings.CurrentConfig.VerboseLogging) + { + _loggerService.LogError($"Received NetMessage for unknown id {id} from {GameServer.ClientLogName(client)}."); + } + } + } + + private ushort RegisterId(NetId netId) + { + if (idToPacket.ContainsKey(netId)) + { + return idToPacket[netId]; + } + + if (currentId >= ushort.MaxValue) + { + _loggerService.LogError($"Tried to register more than {ushort.MaxValue} network ids!"); + return 0; + } + + currentId++; + + packetToId[currentId] = netId; + idToPacket[netId] = currentId; + + WriteIdToAll(currentId, netId); + + return currentId; + } + + private void RequestIdSingle(IReadMessage netMessage, Client client) + { + NetId netId = NetId.Read(netMessage); + + if (!idToPacket.ContainsKey(netId) && client.AccountId.TryUnwrap(out AccountId id)) + { + if (!clientRegisterCount.ContainsKey(id.StringRepresentation)) + { + clientRegisterCount[id.StringRepresentation] = 0; + } + + clientRegisterCount[id.StringRepresentation]++; + + if (clientRegisterCount[id.StringRepresentation] > MaxRegisterPerClient) + { + _loggerService.Log($"{GameServer.ClientLogName(client)} Tried to register more than {MaxRegisterPerClient} Ids!"); + return; + } + } + + RegisterId(netId); + } + + private void WriteIdToAll(ushort packet, NetId netId) + { + WriteOnlyMessage message = new WriteOnlyMessage(); + message.WriteByte((byte)ServerHeader); + message.WriteByte((byte)ServerToClient.ReceiveNetIds); + + message.WriteUInt16(1); + message.WriteUInt16(packet); + NetId.Write(message, netId); + + SendToClient(message, null, DeliveryMethod.Reliable); + } + + private void WriteSync(Client client) + { + WriteOnlyMessage message = new WriteOnlyMessage(); + message.WriteByte((byte)ServerHeader); + message.WriteByte((byte)ServerToClient.ReceiveNetIds); + + message.WriteUInt16((ushort)packetToId.Count()); + foreach ((ushort packet, NetId netId) in packetToId) + { + message.WriteUInt16(packet); + NetId.Write(message, netId); + } + + SendToClient(message, client.Connection, DeliveryMethod.Reliable); + + // TODO: when we move to using GUIDs for everything, this should combined into a single message + foreach (INetworkSyncVar netVar in netVars.Keys) + { + SendNetVar(netVar, client.Connection); + } + } + + public void SendToClient(IWriteMessage netMessage, NetworkConnection connection = null, DeliveryMethod deliveryMethod = DeliveryMethod.Reliable) + { + if (connection == null) + { + foreach (NetworkConnection conn in ModUtils.Client.ClientList.Select(c => c.Connection)) + { + GameMain.Server.ServerPeer.Send(netMessage, conn, deliveryMethod); + } + } + else + { + GameMain.Server.ServerPeer.Send(netMessage, connection, deliveryMethod); + } + } + + public void Send(IWriteMessage netMessage, NetworkConnection connection = null, DeliveryMethod deliveryMethod = DeliveryMethod.Reliable) + => SendToClient(netMessage, connection, deliveryMethod); +} diff --git a/Barotrauma/BarotraumaServer/ServerSource/Map/Hull.cs b/Barotrauma/BarotraumaServer/ServerSource/Map/Hull.cs index 1fa02b024..8e5cd018e 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Map/Hull.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Map/Hull.cs @@ -114,6 +114,7 @@ namespace Barotrauma msg.WriteRangedSingle(normalizedXPos, 0.0f, 1.0f, 8); msg.WriteRangedSingle(normalizedYPos, 0.0f, 1.0f, 8); msg.WriteRangedSingle(decal.Scale, 0f, 2f, 12); + msg.WriteRangedSingle(decal.BaseAlpha, 0f, 1f, 8); } break; case BallastFloraEventData ballastFloraEventData: @@ -253,7 +254,7 @@ namespace Barotrauma break; case EventType.Decal: byte decalIndex = msg.ReadByte(); - float decalAlpha = msg.ReadRangedSingle(0.0f, 1.0f, 255); + float decalAlpha = msg.ReadRangedSingle(0f, 1f, 8); if (decalIndex < 0 || decalIndex >= decals.Count) { return; } if (c.Character != null && c.Character.AllowInput && c.Character.HeldItems.Any(it => it.GetComponent() != null)) { diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/ChatMessage.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/ChatMessage.cs index c76385bf7..014225946 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/ChatMessage.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/ChatMessage.cs @@ -1,6 +1,8 @@ -using System; -using System.Text; +using Barotrauma.LuaCs.Events; using MoonSharp.Interpreter; +using MoonSharp.VsCodeDebugger.SDK; +using System; +using System.Text; namespace Barotrauma.Networking { @@ -67,6 +69,9 @@ namespace Barotrauma.Networking txt = msg.ReadString() ?? ""; } + // Sanitize incoming text message from client so they can't use RichString features + txt = txt.Replace('‖', ' '); + if (!NetIdUtils.IdMoreRecent(ID, c.LastSentChatMsgID)) { return; } c.LastSentChatMsgID = ID; @@ -86,12 +91,9 @@ namespace Barotrauma.Networking HandleSpamFilter(c, txt, out bool flaggedAsSpam, similarityMultiplier); if (flaggedAsSpam) { return; } - var should = GameMain.LuaCs.Hook.Call("chatMessage", txt, c, type); - - if (should != null && should.Value) - { - return; - } + bool? should = null; + LuaCsSetup.Instance.EventService.PublishEvent(x => should = x.OnChatMessage(txt, c, type, ChatMessage.Create(c.Name, txt, type, null, c)) ?? should); + if (should != null && should.Value) { return; } if (type == ChatMessageType.Order) { diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs index e0ad602a0..18d2cd374 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs @@ -1,19 +1,20 @@ using Barotrauma.Extensions; using Barotrauma.IO; using Barotrauma.Items.Components; +using Barotrauma.LuaCs.Events; +using Barotrauma.PerkBehaviors; using Barotrauma.Steam; using Lidgren.Network; using Microsoft.Xna.Framework; +using MoonSharp.Interpreter; using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics; using System.Linq; +using System.Net; using System.Threading; using System.Xml.Linq; -using MoonSharp.Interpreter; -using System.Net; -using Barotrauma.PerkBehaviors; namespace Barotrauma.Networking { @@ -245,7 +246,6 @@ namespace Barotrauma.Networking VoipServer = new VoipServer(serverPeer); - GameMain.LuaCs.Initialize(); Log("Server started", ServerLog.MessageType.ServerMessage); GameMain.NetLobbyScreen.Select(); @@ -339,8 +339,6 @@ namespace Barotrauma.Networking SendConsoleMessage("Granted all permissions to " + newClient.Name + ".", newClient); } - GameMain.LuaCs.Hook.Call("client.connected", newClient); - SendChatMessage($"ServerMessage.JoinedServer~[client]={ClientLogName(newClient)}", ChatMessageType.Server, changeType: PlayerConnectionChangeType.Joined); ServerSettings.ServerDetailsChanged = true; @@ -442,7 +440,7 @@ namespace Barotrauma.Networking (permadeathMode && (!character.IsDead || character.CauseOfDeath?.Type == CauseOfDeathType.Disconnected))); if (!character.IsDead) { - if (!GameMain.LuaCs.Game.disableDisconnectCharacter) + if (!LuaCsSetup.Instance.Game.disableDisconnectCharacter) { character.KillDisconnectedTimer += deltaTime; character.SetStun(1.0f); @@ -832,8 +830,6 @@ namespace Barotrauma.Networking using var _ = dosProtection.Start(connectedClient); ClientPacketHeader header = (ClientPacketHeader)inc.ReadByte(); - - GameMain.LuaCs.Networking.NetMessageReceived(inc, header, connectedClient); switch (header) { @@ -1149,7 +1145,9 @@ namespace Barotrauma.Networking } else { - KickClient(c, errorStr); + //Is it necessary to kick a client for a non-existing entity? + //there are plenty of things have been done if received an non-existing entity update. + //KickClient(c, errorStr); } } @@ -2307,7 +2305,6 @@ namespace Barotrauma.Networking segmentTable.StartNewSegment(ServerNetSegment.ClientList); outmsg.WriteUInt16(LastClientListUpdateID); - GameMain.LuaCs.Hook.Call("writeClientList", c, outmsg); outmsg.WriteByte((byte)Team1Count); outmsg.WriteByte((byte)Team2Count); @@ -2333,13 +2330,6 @@ namespace Barotrauma.Networking IsOwner = client.Connection == OwnerConnection, IsDownloading = FileSender.ActiveTransfers.Any(t => t.Connection == client.Connection) }; - - var result = GameMain.LuaCs.Hook.Call("writeClientList.modifyTempClientData", c, client, tempClientData, outmsg); - - if (result != null) - { - tempClientData = result.Value; - } outmsg.WriteNetSerializableStruct(tempClientData); outmsg.WritePadBits(); @@ -3192,7 +3182,7 @@ namespace Barotrauma.Networking } TraitorManager.Initialize(GameMain.GameSession.EventManager, Level.Loaded); - if (GameMain.LuaCs.Game.overrideTraitors) + if (LuaCsSetup.Instance.Game.overrideTraitors) { TraitorManager.Enabled = false; } @@ -3221,8 +3211,6 @@ namespace Barotrauma.Networking roundStartTime = DateTime.Now; - GameMain.LuaCs.Hook.Call("roundStart"); - startGameCoroutine = null; yield return CoroutineStatus.Success; } @@ -3395,15 +3383,6 @@ namespace Barotrauma.Networking GameMain.GameSession.EndRound(endMessage); } TraitorManager.TraitorResults? traitorResults = traitorManager?.GetEndResults() ?? null; - var result = GameMain.LuaCs.Hook.Call>("roundEnd"); - if (result != null) - { - foreach (var data in result) - { - if (data is TraitorManager.TraitorResults traitorResultData) { traitorResults = traitorResultData; } - if (data is string endMessageData) { endMessage = endMessageData; } - } - } EndRoundTimer = 0.0f; @@ -3536,7 +3515,8 @@ namespace Barotrauma.Networking return false; } - var result = GameMain.LuaCs.Hook.Call("tryChangeClientName", c, newName, newJob, newTeam); + bool? result = null; + LuaCsSetup.Instance.EventService.PublishEvent(x => result = x.OnTryClienChangeName(c, newName, newJob, newTeam) ?? result); if (result != null) { @@ -3740,8 +3720,6 @@ namespace Barotrauma.Networking { if (client == null) return; - GameMain.LuaCs.Hook.Call("client.disconnected", client); - if (client.Character != null) { client.Character.ClientDisconnected = true; @@ -3990,21 +3968,29 @@ namespace Barotrauma.Networking senderName = null; senderCharacter = null; } - else if (type == ChatMessageType.Radio && !GameMain.LuaCs.Game.overrideSignalRadio) + else if (type == ChatMessageType.Radio && !LuaCsSetup.Instance.Game.overrideSignalRadio) { //send to chat-linked wifi components Signal s = new Signal(message, sender: senderCharacter, source: senderRadio.Item); senderRadio.TransmitSignal(s, sentFromChat: true); - } - + } + var hookChatMsg = ChatMessage.Create(senderName, message, (ChatMessageType)type, senderCharacter, senderClient, changeType); - var should = GameMain.LuaCs.Hook.Call("modifyChatMessage", hookChatMsg, senderRadio); + bool shouldSkip = false; + LuaCsSetup.Instance.EventService.PublishEvent(sub => + { + if (sub.OnModifyMessagePredicate(hookChatMsg, senderRadio) is true) + { + shouldSkip = true; + } + }); - if (should != null && should.Value) + if (shouldSkip) + { return; + } - //check which clients can receive the message and apply distance effects foreach (Client client in ConnectedClients) { @@ -4675,8 +4661,6 @@ namespace Barotrauma.Networking $"No suitable jobs available for {c.Name} (karma {c.Karma}). Assigning a random job: {c.AssignedJob.Prefab.Name}."); } } - - GameMain.LuaCs.Hook.Call("jobsAssigned", unassigned); } public void AssignBotJobs(List bots, CharacterTeamType teamID, bool isPvP) @@ -4809,7 +4793,7 @@ namespace Barotrauma.Networking { if (GameMain.Server == null || !GameMain.Server.ServerSettings.SaveServerLogs) { return; } - GameMain.LuaCs?.Hook?.Call("serverLog", line, messageType); + LuaCsSetup.Instance?.EventService.PublishEvent(x => x.OnServerLog(line, messageType)); GameMain.Server.ServerSettings.ServerLog.WriteLine(line, messageType); var clients = GameMain.Server.ConnectedClients.ToArray(); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/KarmaManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/KarmaManager.cs index 9a325a12b..2853f79d6 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/KarmaManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/KarmaManager.cs @@ -244,6 +244,8 @@ namespace Barotrauma Character targetCharacter = inventory.Owner as Character; if (yoinker == null || item == null || thiefCharacter == null || targetCharacter == null || thiefCharacter == targetCharacter) { return; } + + if (thiefCharacter.TeamID != targetCharacter.TeamID) { return; } if (targetClient == null && (!DangerousItemStealBots || targetCharacter.AIController == null)) { return; } @@ -261,7 +263,7 @@ namespace Barotrauma } Item foundItem = null; - if (isValid(item)) + if (IsValid(item)) { foundItem = item; } @@ -269,7 +271,7 @@ namespace Barotrauma { foreach (Item containedItem in item.ContainedItems) { - if (isValid(containedItem)) + if (IsValid(containedItem)) { foundItem = containedItem; break; @@ -277,16 +279,19 @@ namespace Barotrauma } } - static bool isValid(Item item) + static bool IsValid(Item item) { - return item.GetComponent() != null || item.GetComponent() != null || item.GetComponent() != null; + return item.GetComponent() != null || IsWeapon(item); + } + static bool IsWeapon(Item item) + { + //a threshold of 10 excludes things like tools, all "proper weapons" seem to have a priority higher than that + return item.Components.Max(c => c.CombatPriority) > 10.0f || item.HasTag(Tags.Weapon); } if (foundItem == null) { return; } bool isIdCard = foundItem.GetComponent() != null; - bool isWeapon = foundItem.GetComponent() != null || foundItem.GetComponent() != null; - if (isIdCard) { string name = string.Empty; @@ -325,7 +330,7 @@ namespace Barotrauma JobPrefab clientJob = yoinker.CharacterInfo?.Job?.Prefab; // security officers receive less karma penalty - if (clientJob != null && clientJob.Identifier == "securityofficer" && isWeapon) + if (clientJob != null && clientJob.Identifier == "securityofficer" && IsWeapon(foundItem)) { karmaDecrease *= 0.5f; } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/LidgrenServerPeer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/LidgrenServerPeer.cs index e5cf2c975..f43919d12 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/LidgrenServerPeer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/LidgrenServerPeer.cs @@ -33,6 +33,13 @@ namespace Barotrauma.Networking DualStack = GameSettings.CurrentConfig.UseDualModeSockets, LocalAddress = serverSettings.ListenIPAddress, }; + if (NetConfig.UseLenientHandshake) + { + // More lenient timeouts for local testing, so the server would start even without perfect conditions + netPeerConfiguration.ConnectionTimeout = 60.0f; + netPeerConfiguration.ResendHandshakeInterval = 5.0f; + netPeerConfiguration.MaximumHandshakeAttempts = 20; + } netPeerConfiguration.DisableMessageType( NetIncomingMessageType.DebugMessage @@ -187,16 +194,7 @@ namespace Barotrauma.Networking { if (netServer == null) { return; } - var skipDeny = false; - { - var result = GameMain.LuaCs.Hook.Call("lidgren.handleConnection", inc); - if (result != null) { - if (result.Value) skipDeny = true; - else return; - } - } - - if (!skipDeny && connectedClients.Count >= serverSettings.MaxPlayers) + if (connectedClients.Count >= serverSettings.MaxPlayers) { inc.SenderConnection.Deny(PeerDisconnectPacket.WithReason(DisconnectReason.ServerFull).ToLidgrenStringRepresentation()); return; diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/ServerPeer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/ServerPeer.cs index 3e1a11c33..0003f1ef8 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/ServerPeer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/ServerPeer.cs @@ -257,12 +257,7 @@ namespace Barotrauma.Networking protected void UpdatePendingClient(PendingClient pendingClient) { - var skipRemove = false; - var result = GameMain.LuaCs.Hook.Call("handlePendingClient", pendingClient); - - if (result != null) skipRemove = result.Value; - - if (!skipRemove && connectedClients.Count >= serverSettings.MaxPlayers) + if (connectedClients.Count >= serverSettings.MaxPlayers) { RemovePendingClient(pendingClient, PeerDisconnectPacket.WithReason(DisconnectReason.ServerFull)); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs index 2b54023dd..c032b6e47 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs @@ -18,7 +18,7 @@ namespace Barotrauma.Networking MultiPlayerCampaign campaign = GameMain.GameSession.GameMode as MultiPlayerCampaign; foreach (Client c in networkMember.ConnectedClients) { - if (GameMain.LuaCs.Game.overrideRespawnSub) + if (LuaCsSetup.Instance.Game.overrideRespawnSub) continue; if (!c.InGame) { continue; } @@ -169,7 +169,7 @@ namespace Barotrauma.Networking private bool ShouldStartRespawnCountdown(int characterToRespawnCount) { - if (GameMain.LuaCs.Game.overrideRespawnSub) + if (LuaCsSetup.Instance.Game.overrideRespawnSub) { characterToRespawnCount = 0; } @@ -187,7 +187,7 @@ namespace Barotrauma.Networking var teamId = teamSpecificState.TeamID; var respawnShuttle = GetShuttle(teamId); - if (respawnShuttle != null && !GameMain.LuaCs.Game.overrideRespawnSub) + if (respawnShuttle != null && !LuaCsSetup.Instance.Game.overrideRespawnSub) { respawnShuttle.Velocity = Vector2.Zero; } @@ -240,7 +240,7 @@ namespace Barotrauma.Networking if (RespawnShuttles.Any()) { ResetShuttle(teamSpecificState); - if (GameMain.LuaCs.Game.overrideRespawnSub) + if (LuaCsSetup.Instance.Game.overrideRespawnSub) { teamSpecificState.CurrentState = State.Waiting; } @@ -596,6 +596,23 @@ namespace Barotrauma.Networking teamSpecificState.RespawnItems.AddRange(AutoItemPlacer.RegenerateLoot(respawnShuttle, respawnContainer)); } } + else if (character.InWater) + { + if (divingSuitPrefab != null) + { + var divingSuit = new Item(divingSuitPrefab, character.Position, respawnSub); + Spawner.CreateNetworkEvent(new EntitySpawner.SpawnEntity(divingSuit)); + character.Inventory.TryPutItem(divingSuit, user: null, allowedSlots: divingSuit.AllowedSlots); + teamSpecificState.RespawnItems.Add(divingSuit); + if (oxyPrefab != null && divingSuit.GetComponent() != null) + { + var oxyTank = new Item(oxyPrefab, character.Position, respawnSub); + Spawner.CreateNetworkEvent(new EntitySpawner.SpawnEntity(oxyTank)); + divingSuit.Combine(oxyTank, user: null); + teamSpecificState.RespawnItems.Add(oxyTank); + } + } + } var characterData = campaign?.GetClientCharacterData(clients[i]); // NOTE: This was where Reaper's tax got applied diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs index 4645a7a53..1e41951a5 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs @@ -46,7 +46,7 @@ namespace Barotrauma.Networking .Aggregate(NetFlags.None, (f1, f2) => f1 | f2); private bool IsFlagRequired(Client c, NetFlags flag) - => NetIdUtils.IdMoreRecent(LastUpdateIdForFlag[flag], c.LastRecvLobbyUpdate); + => NetIdUtils.IdMoreRecent(LastUpdateIdForFlag[flag], c.LastRecvLobbyUpdate) || !c.InitialLobbyUpdateSent; public NetFlags GetRequiredFlags(Client c) => LastUpdateIdForFlag.Keys diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/Voip/VoipServer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Voip/VoipServer.cs index 5349d2ea4..5970af2bf 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Voip/VoipServer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Voip/VoipServer.cs @@ -1,7 +1,10 @@ using Barotrauma.Items.Components; +using Barotrauma.LuaCs.Events; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; +using static Barotrauma.CharacterHealth; +using static Barotrauma.MedicalClinic; namespace Barotrauma.Networking { @@ -96,7 +99,8 @@ namespace Barotrauma.Networking ChatMessage.CanUseRadio(sender.Character, out WifiComponent senderRadio) && (recipientSpectating || ChatMessage.CanUseRadio(recipient.Character, out recipientRadio))) { - var canUse = GameMain.LuaCs.Hook.Call("canUseVoiceRadio", new object[] { sender, recipient }); + bool? canUse = null; + LuaCsSetup.Instance.EventService.PublishEvent(x => canUse = x.OnCanUseVoiceRadio(sender, recipient) ?? canUse); if (canUse != null) { @@ -116,7 +120,8 @@ namespace Barotrauma.Networking } } - float range = GameMain.LuaCs.Hook.Call("changeLocalVoiceRange", sender, recipient) ?? 1.0f; + float range = 1.0f; + LuaCsSetup.Instance.EventService.PublishEvent(x => range = x.OnChangeLocalVoiceRange(sender, recipient) ?? range); if (recipientSpectating) { diff --git a/Barotrauma/BarotraumaServer/ServerSource/PerformenceMonitor.cs b/Barotrauma/BarotraumaServer/ServerSource/PerformenceMonitor.cs index 442b91315..f67ce80ed 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/PerformenceMonitor.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/PerformenceMonitor.cs @@ -39,7 +39,7 @@ namespace Barotrauma } public int ConnectClients { - get { return Client.ClientList.Count; } + get { return GameMain.Server.ConnectedClients.Count; } } public double RealTickRate diff --git a/Barotrauma/BarotraumaServer/ServerSource/Program.cs b/Barotrauma/BarotraumaServer/ServerSource/Program.cs index 1f5a9727b..c5e19b32c 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Program.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Program.cs @@ -93,6 +93,7 @@ namespace Barotrauma private static bool hasShutDown = false; private static void ShutDown() { + SingleThreadWorker.Instance.Dispose(); if (hasShutDown) { return; } hasShutDown = true; diff --git a/Barotrauma/BarotraumaServer/WindowsServer.csproj b/Barotrauma/BarotraumaServer/WindowsServer.csproj index 857c49007..edf236deb 100644 --- a/Barotrauma/BarotraumaServer/WindowsServer.csproj +++ b/Barotrauma/BarotraumaServer/WindowsServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 1.11.5.0 + 1.12.7.0 Copyright © FakeFish 2018-2023 AnyCPU;x64 DedicatedServer @@ -14,6 +14,7 @@ Debug;Release;Unstable true ;NU1605;CS0114;CS0108;CS8597;CS8600;CS8601;CS8602;CS8603;CS8604;CS8605;CS8606;CS8607;CS8608;CS8609;CS8610;CS8611;CS8612;CS8613;CS8614;CS8615;CS8616;CS8617;CS8618;CS8619;CS8620;CS8621;CS8622;CS8624;CS8625;CS8626;CS8629;CS8631;CS8632;CS8633;CS8634;CS8638;CS8643;CS8644;CS8645;CS8653;CS8654;CS8655;CS8667;CS8669;CS8670;CS8714;CS8717;CS8765 + latest @@ -168,4 +169,5 @@ + diff --git a/Barotrauma/BarotraumaShared/LocalMods/LuaCsForBarotrauma/Config/SettingsShared.xml b/Barotrauma/BarotraumaShared/LocalMods/LuaCsForBarotrauma/Config/SettingsShared.xml new file mode 100644 index 000000000..99fdf1f3f --- /dev/null +++ b/Barotrauma/BarotraumaShared/LocalMods/LuaCsForBarotrauma/Config/SettingsShared.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/Barotrauma/BarotraumaShared/Lua/CompatibilityLib.lua b/Barotrauma/BarotraumaShared/LocalMods/LuaCsForBarotrauma/Lua/CompatibilityLib.lua similarity index 68% rename from Barotrauma/BarotraumaShared/Lua/CompatibilityLib.lua rename to Barotrauma/BarotraumaShared/LocalMods/LuaCsForBarotrauma/Lua/CompatibilityLib.lua index 524818c5f..6ca382f63 100644 --- a/Barotrauma/BarotraumaShared/Lua/CompatibilityLib.lua +++ b/Barotrauma/BarotraumaShared/LocalMods/LuaCsForBarotrauma/Lua/CompatibilityLib.lua @@ -2,11 +2,11 @@ local compatibilityLib = {} -local networking = LuaUserData.RegisterType("Barotrauma.LuaCsNetworking") +-- local networking = LuaUserData.RegisterType("Barotrauma.LuaCsNetworking") -LuaUserData.AddMethod(networking, "RequestGetHTTP", Networking.HttpGet) +-- LuaUserData.AddMethod(networking, "RequestGetHTTP", Networking.HttpGet) -LuaUserData.AddMethod(networking, "RequestPostHTTP", Networking.HttpPost) +-- LuaUserData.AddMethod(networking, "RequestPostHTTP", Networking.HttpPost) compatibilityLib.CreateVector2 = Vector2.__new compatibilityLib.CreateVector3 = Vector3.__new @@ -78,20 +78,4 @@ end compatibilityLib["Player"] = luaPlayer -Hook.Add("character.created", "compatibility.character.created", function (character) - Hook.Call("characterCreated", character) -end) - -Hook.Add("character.death", "compatibility.character.death", function (character, causeOfDeathAffliction) - Hook.Call("characterDeath", character, causeOfDeathAffliction) -end) - -Hook.Add("client.connected", "compatibility.client.connected", function (client) - Hook.Call("clientConnected", client) -end) - -Hook.Add("client.disconnected", "compatibility.client.disconnected", function (client) - Hook.Call("clientDisconnected", client) -end) - return compatibilityLib \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/Lua/DefaultHook.lua b/Barotrauma/BarotraumaShared/LocalMods/LuaCsForBarotrauma/Lua/DefaultHook.lua similarity index 100% rename from Barotrauma/BarotraumaShared/Lua/DefaultHook.lua rename to Barotrauma/BarotraumaShared/LocalMods/LuaCsForBarotrauma/Lua/DefaultHook.lua diff --git a/Barotrauma/BarotraumaShared/Lua/DefaultLib/LibClient.lua b/Barotrauma/BarotraumaShared/LocalMods/LuaCsForBarotrauma/Lua/DefaultLib/LibClient.lua similarity index 91% rename from Barotrauma/BarotraumaShared/Lua/DefaultLib/LibClient.lua rename to Barotrauma/BarotraumaShared/LocalMods/LuaCsForBarotrauma/Lua/DefaultLib/LibClient.lua index eb94d18f5..a6a6a1562 100644 --- a/Barotrauma/BarotraumaShared/Lua/DefaultLib/LibClient.lua +++ b/Barotrauma/BarotraumaShared/LocalMods/LuaCsForBarotrauma/Lua/DefaultLib/LibClient.lua @@ -1,8 +1,8 @@ local defaultLib = {} -local CreateStatic = LuaSetup.LuaUserData.CreateStatic -local CreateEnum = LuaSetup.LuaUserData.CreateEnumTable -local AddCallMetaTable = LuaSetup.LuaUserData.AddCallMetaTable +local CreateStatic = LuaUserData.CreateStatic +local CreateEnum = LuaUserData.CreateEnumTable +local AddCallMetaTable = LuaUserData.AddCallMetaTable local localizedStrings = { "LocalizedString", "LimitLString", "WrappedLString", "AddedPunctuationLString", "CapitalizeLString", "ConcatLString", "FallbackLString", "FormattedLString", "InputTypeLString", "JoinLString", "LowerLString", "RawLString", "ReplaceLString", "ServerMsgLString", "SplitLString", "TagLString", "TrimLString", "UpperLString", "StripRichTagsLString", @@ -79,13 +79,12 @@ defaultLib["GUI"] = { GUIStyle = CreateStatic("Barotrauma.GUIStyle", true), } +local guiFallback = defaultLib["GUI"].GUI + setmetatable(defaultLib["GUI"], { - __index = function (table, key) - return defaultLib["GUI"].GUI[key] + __index = function(_, key) + return guiFallback[key] end }) -AddCallMetaTable(defaultLib["GUI"].VideoPlayer.VideoSettings) -AddCallMetaTable(defaultLib["GUI"].VideoPlayer.TextSettings) - return defaultLib \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/Lua/DefaultLib/LibServer.lua b/Barotrauma/BarotraumaShared/LocalMods/LuaCsForBarotrauma/Lua/DefaultLib/LibServer.lua similarity index 81% rename from Barotrauma/BarotraumaShared/Lua/DefaultLib/LibServer.lua rename to Barotrauma/BarotraumaShared/LocalMods/LuaCsForBarotrauma/Lua/DefaultLib/LibServer.lua index 68bfe97ef..1076e90f6 100644 --- a/Barotrauma/BarotraumaShared/Lua/DefaultLib/LibServer.lua +++ b/Barotrauma/BarotraumaShared/LocalMods/LuaCsForBarotrauma/Lua/DefaultLib/LibServer.lua @@ -1,7 +1,7 @@ local defaultLib = {} -local CreateStatic = LuaSetup.LuaUserData.CreateStatic -local CreateEnum = LuaSetup.LuaUserData.CreateEnumTable +local CreateStatic = LuaUserData.CreateStatic +local CreateEnum = LuaUserData.CreateEnumTable local localizedStrings = { "LocalizedString", "AddedPunctuationLString", "CapitalizeLString", "ConcatLString", "FallbackLString", "FormattedLString", "InputTypeLString", "JoinLString", "LowerLString", "RawLString", "ReplaceLString", "ServerMsgLString", "SplitLString", "TagLString", "TrimLString", "UpperLString", "StripRichTagsLString", diff --git a/Barotrauma/BarotraumaShared/Lua/DefaultLib/LibShared.lua b/Barotrauma/BarotraumaShared/LocalMods/LuaCsForBarotrauma/Lua/DefaultLib/LibShared.lua similarity index 98% rename from Barotrauma/BarotraumaShared/Lua/DefaultLib/LibShared.lua rename to Barotrauma/BarotraumaShared/LocalMods/LuaCsForBarotrauma/Lua/DefaultLib/LibShared.lua index 1fde8456b..db8469158 100644 --- a/Barotrauma/BarotraumaShared/Lua/DefaultLib/LibShared.lua +++ b/Barotrauma/BarotraumaShared/LocalMods/LuaCsForBarotrauma/Lua/DefaultLib/LibShared.lua @@ -1,8 +1,8 @@ local defaultLib = {} -local AddCallMetaTable = LuaSetup.LuaUserData.AddCallMetaTable -local CreateStatic = LuaSetup.LuaUserData.CreateStatic -local CreateEnum = LuaSetup.LuaUserData.CreateEnumTable +local AddCallMetaTable = LuaUserData.AddCallMetaTable +local CreateStatic = LuaUserData.CreateStatic +local CreateEnum = LuaUserData.CreateEnumTable defaultLib["SByte"] = CreateStatic("Barotrauma.LuaSByte", true) defaultLib["Byte"] = CreateStatic("Barotrauma.LuaByte", true) diff --git a/Barotrauma/BarotraumaShared/Lua/DefaultLib/Utils/Math.lua b/Barotrauma/BarotraumaShared/LocalMods/LuaCsForBarotrauma/Lua/DefaultLib/Utils/Math.lua similarity index 100% rename from Barotrauma/BarotraumaShared/Lua/DefaultLib/Utils/Math.lua rename to Barotrauma/BarotraumaShared/LocalMods/LuaCsForBarotrauma/Lua/DefaultLib/Utils/Math.lua diff --git a/Barotrauma/BarotraumaShared/Lua/DefaultLib/Utils/SteamApi.lua b/Barotrauma/BarotraumaShared/LocalMods/LuaCsForBarotrauma/Lua/DefaultLib/Utils/SteamApi.lua similarity index 97% rename from Barotrauma/BarotraumaShared/Lua/DefaultLib/Utils/SteamApi.lua rename to Barotrauma/BarotraumaShared/LocalMods/LuaCsForBarotrauma/Lua/DefaultLib/Utils/SteamApi.lua index 4e9608f60..564d39bc7 100644 --- a/Barotrauma/BarotraumaShared/Lua/DefaultLib/Utils/SteamApi.lua +++ b/Barotrauma/BarotraumaShared/LocalMods/LuaCsForBarotrauma/Lua/DefaultLib/Utils/SteamApi.lua @@ -1,3 +1,5 @@ +if true then return end + local descriptor = LuaUserData.RegisterType("Barotrauma.LuaCsSteam") LuaUserData.AddMethod(descriptor, "GetWorkshopCollection", function (id, callback) diff --git a/Barotrauma/BarotraumaShared/Lua/DefaultLib/Utils/String.lua b/Barotrauma/BarotraumaShared/LocalMods/LuaCsForBarotrauma/Lua/DefaultLib/Utils/String.lua similarity index 100% rename from Barotrauma/BarotraumaShared/Lua/DefaultLib/Utils/String.lua rename to Barotrauma/BarotraumaShared/LocalMods/LuaCsForBarotrauma/Lua/DefaultLib/Utils/String.lua diff --git a/Barotrauma/BarotraumaShared/Lua/DefaultLib/Utils/Util.lua b/Barotrauma/BarotraumaShared/LocalMods/LuaCsForBarotrauma/Lua/DefaultLib/Utils/Util.lua similarity index 100% rename from Barotrauma/BarotraumaShared/Lua/DefaultLib/Utils/Util.lua rename to Barotrauma/BarotraumaShared/LocalMods/LuaCsForBarotrauma/Lua/DefaultLib/Utils/Util.lua diff --git a/Barotrauma/BarotraumaShared/LocalMods/LuaCsForBarotrauma/Lua/LuaSetup.lua b/Barotrauma/BarotraumaShared/LocalMods/LuaCsForBarotrauma/Lua/LuaSetup.lua new file mode 100644 index 000000000..08e139173 --- /dev/null +++ b/Barotrauma/BarotraumaShared/LocalMods/LuaCsForBarotrauma/Lua/LuaSetup.lua @@ -0,0 +1,38 @@ +LuaSetup = {} + +local path = ... + +local function AddTableToGlobal(tbl) + for k, v in pairs(tbl) do + _G[k] = v + end +end + +if SERVER then + AddTableToGlobal(dofile(path .. "/Lua/DefaultLib/LibServer.lua")) +else + AddTableToGlobal(dofile(path .. "/Lua/DefaultLib/LibClient.lua")) +end + +AddTableToGlobal(dofile(path .. "/Lua/DefaultLib/LibShared.lua")) + +AddTableToGlobal(dofile(path .. "/Lua/CompatibilityLib.lua")) + +dofile(path .. "/Lua/DefaultHook.lua") + +Descriptors = LuaUserData + +dofile(path .. "/Lua/DefaultLib/Utils/Math.lua") +dofile(path .. "/Lua/DefaultLib/Utils/String.lua") +dofile(path .. "/Lua/DefaultLib/Utils/Util.lua") +dofile(path .. "/Lua/DefaultLib/Utils/SteamApi.lua") + +if not CSActive then + for k, v in pairs(debug) do + if k ~= "getmetatable" and k ~= "setmetatable" and k ~= "traceback" then + debug[k] = nil + end + end +end + +LuaSetup = nil \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/LocalMods/LuaCsForBarotrauma/LuaCsSettingsIcon.png b/Barotrauma/BarotraumaShared/LocalMods/LuaCsForBarotrauma/LuaCsSettingsIcon.png new file mode 100644 index 000000000..434b6f2f2 Binary files /dev/null and b/Barotrauma/BarotraumaShared/LocalMods/LuaCsForBarotrauma/LuaCsSettingsIcon.png differ diff --git a/Barotrauma/BarotraumaShared/LocalMods/LuaCsForBarotrauma/ModConfig.xml b/Barotrauma/BarotraumaShared/LocalMods/LuaCsForBarotrauma/ModConfig.xml new file mode 100644 index 000000000..25afd73df --- /dev/null +++ b/Barotrauma/BarotraumaShared/LocalMods/LuaCsForBarotrauma/ModConfig.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/Barotrauma/BarotraumaShared/LocalMods/LuaCsForBarotrauma/Style.xml b/Barotrauma/BarotraumaShared/LocalMods/LuaCsForBarotrauma/Style.xml new file mode 100644 index 000000000..a364a50f3 --- /dev/null +++ b/Barotrauma/BarotraumaShared/LocalMods/LuaCsForBarotrauma/Style.xml @@ -0,0 +1,6 @@ + + \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/LocalMods/LuaCsForBarotrauma/Texts/English.xml b/Barotrauma/BarotraumaShared/LocalMods/LuaCsForBarotrauma/Texts/English.xml new file mode 100644 index 000000000..2108b69ad --- /dev/null +++ b/Barotrauma/BarotraumaShared/LocalMods/LuaCsForBarotrauma/Texts/English.xml @@ -0,0 +1,23 @@ + + + Mod Controls Settings + Mod Gameplay Settings + Reset Displayed Settings + Reset Visible Settings + Are you sure you want to reset the values for currently displayed settings? + Yes + No + + + Are C# Mods Allowed + Should unsandboxed scripts and dlls be allowed to run. + General + + Use Pre-Caching + Should mod files be preloaded to speed up loading. Should only be turned off if you have mods that have issues with this. + General + + Hide Local OS Account Name In Logs + If true, will replace your OS account name with 'USERNAME' in log files' paths. + General + diff --git a/Barotrauma/BarotraumaShared/LocalMods/LuaCsForBarotrauma/Texts/Portuguese.xml b/Barotrauma/BarotraumaShared/LocalMods/LuaCsForBarotrauma/Texts/Portuguese.xml new file mode 100644 index 000000000..4b149c002 --- /dev/null +++ b/Barotrauma/BarotraumaShared/LocalMods/LuaCsForBarotrauma/Texts/Portuguese.xml @@ -0,0 +1,3 @@ + + + diff --git a/Barotrauma/BarotraumaShared/LocalMods/LuaCsForBarotrauma/filelist.xml b/Barotrauma/BarotraumaShared/LocalMods/LuaCsForBarotrauma/filelist.xml new file mode 100644 index 000000000..679f8d4c8 --- /dev/null +++ b/Barotrauma/BarotraumaShared/LocalMods/LuaCsForBarotrauma/filelist.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]Character override and variant tests/Characters/Crawler/Crawler.xml b/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]Character override and variant tests/Characters/Crawler/Crawler.xml index f90685787..bb24f0573 100644 --- a/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]Character override and variant tests/Characters/Crawler/Crawler.xml +++ b/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]Character override and variant tests/Characters/Crawler/Crawler.xml @@ -45,6 +45,11 @@ + + + + + diff --git a/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]Lighting stress (10000 lights)/Lighting stress (10000 lights).sub b/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]Lighting stress (10000 lights)/Lighting stress (10000 lights).sub new file mode 100644 index 000000000..282a30b37 Binary files /dev/null and b/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]Lighting stress (10000 lights)/Lighting stress (10000 lights).sub differ diff --git a/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]Lighting stress (10000 lights)/filelist.xml b/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]Lighting stress (10000 lights)/filelist.xml new file mode 100644 index 000000000..f897a3c2b --- /dev/null +++ b/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]Lighting stress (10000 lights)/filelist.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]RotationAndFlippingTests/EthanolPowerGenerator.png b/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]RotationAndFlippingTests/EthanolPowerGenerator.png new file mode 100644 index 000000000..76d1aaf54 Binary files /dev/null and b/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]RotationAndFlippingTests/EthanolPowerGenerator.png differ diff --git a/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]RotationAndFlippingTests/OxygenDispenserTest.xml b/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]RotationAndFlippingTests/OxygenDispenserTest.xml new file mode 100644 index 000000000..82378a936 --- /dev/null +++ b/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]RotationAndFlippingTests/OxygenDispenserTest.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + diff --git a/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]RotationAndFlippingTests/RotationAndFlippingTests.sub b/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]RotationAndFlippingTests/RotationAndFlippingTests.sub index 0a09be029..10d816db3 100644 Binary files a/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]RotationAndFlippingTests/RotationAndFlippingTests.sub and b/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]RotationAndFlippingTests/RotationAndFlippingTests.sub differ diff --git a/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]RotationAndFlippingTests/StatusEffectAndLightTest.xml b/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]RotationAndFlippingTests/StatusEffectAndLightTest.xml new file mode 100644 index 000000000..4b58a87c8 --- /dev/null +++ b/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]RotationAndFlippingTests/StatusEffectAndLightTest.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]RotationAndFlippingTests/filelist.xml b/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]RotationAndFlippingTests/filelist.xml index fb1fd016e..be951b04d 100644 --- a/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]RotationAndFlippingTests/filelist.xml +++ b/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]RotationAndFlippingTests/filelist.xml @@ -1,4 +1,6 @@  - + + + \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]TestLuaMod/Lua/init.lua b/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]TestLuaMod/Lua/init.lua new file mode 100644 index 000000000..0c6c821fc --- /dev/null +++ b/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]TestLuaMod/Lua/init.lua @@ -0,0 +1,75 @@ +print("Hello!") + +Hook.Add("character.created", "test", function(character) + print("character.created: ", character) +end) + +Hook.Add("character.death", "test", function(character) + print("character.death: ", character) +end) + +Hook.Add("character.giveJobItems", "test", function(character) + print("character.giveJobItems: ", character) +end) + +Hook.Add("roundStart", "test", function() + print("roundStart") +end) + +Hook.Add("roundEnd", "test", function() + print("roundEnd") +end) + +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.Int32, package, "TestSynchroServer") +local success2, config2 = ConfigService.TryGetConfig(SettingBase.Int32, package, "TestSynchroClient") + +if not success or not success2 then + print("Failed to get configs.") + return +end + +config.OnValueChanged.add(OnChanged) +config2.OnValueChanged.add(OnChanged) + +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 + lastTime = Timer.Time + 10 + + 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) diff --git a/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]TestLuaMod/ModConfig.xml b/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]TestLuaMod/ModConfig.xml new file mode 100644 index 000000000..b5f96babd --- /dev/null +++ b/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]TestLuaMod/ModConfig.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]TestLuaMod/Settings.xml b/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]TestLuaMod/Settings.xml new file mode 100644 index 000000000..5c7e4458f --- /dev/null +++ b/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]TestLuaMod/Settings.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]TestLuaMod/SettingsClient.xml b/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]TestLuaMod/SettingsClient.xml new file mode 100644 index 000000000..902b2a77b --- /dev/null +++ b/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]TestLuaMod/SettingsClient.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]TestLuaMod/SettingsServer.xml b/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]TestLuaMod/SettingsServer.xml new file mode 100644 index 000000000..3e04dae6b --- /dev/null +++ b/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]TestLuaMod/SettingsServer.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]TestLuaMod/Texts/English.xml b/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]TestLuaMod/Texts/English.xml new file mode 100644 index 000000000..a6c96ff2f --- /dev/null +++ b/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]TestLuaMod/Texts/English.xml @@ -0,0 +1,13 @@ + + + <_x005B_DebugOnlyTest_x005D_TestLuaMod.TestTickbox.DisplayName>Test TickBox + <_x005B_DebugOnlyTest_x005D_TestLuaMod.TestTickbox.DisplayCategory>Tests + <_x005B_DebugOnlyTest_x005D_TestLuaMod.TestFloat.DisplayName>Test Float + <_x005B_DebugOnlyTest_x005D_TestLuaMod.TestFloat.DisplayCategory>Tests + <_x005B_DebugOnlyTest_x005D_TestLuaMod.TestRangeFloat.DisplayName>Test Range Float + <_x005B_DebugOnlyTest_x005D_TestLuaMod.TestRangeFloat.DisplayCategory>Tests + <_x005B_DebugOnlyTest_x005D_TestLuaMod.TestRangeInt.DisplayName>Test Range Int + <_x005B_DebugOnlyTest_x005D_TestLuaMod.TestRangeInt.DisplayCategory>Tests + <_x005B_DebugOnlyTest_x005D_TestLuaMod.TestString.DisplayName>Test String + <_x005B_DebugOnlyTest_x005D_TestLuaMod.TestString.DisplayCategory>Tests + diff --git a/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]TestLuaMod/dummy.xml b/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]TestLuaMod/dummy.xml new file mode 100644 index 000000000..ba75a446c --- /dev/null +++ b/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]TestLuaMod/dummy.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]TestLuaMod/filelist.xml b/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]TestLuaMod/filelist.xml new file mode 100644 index 000000000..1fc37166a --- /dev/null +++ b/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]TestLuaMod/filelist.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]TestPathFinding/Events.xml b/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]TestPathFinding/Events.xml new file mode 100644 index 000000000..fecc60223 --- /dev/null +++ b/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]TestPathFinding/Events.xml @@ -0,0 +1,159 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]TestPathFinding/[DebugOnlyTest]TestPathFinding.sub b/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]TestPathFinding/[DebugOnlyTest]TestPathFinding.sub new file mode 100644 index 000000000..b1620c419 Binary files /dev/null and b/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]TestPathFinding/[DebugOnlyTest]TestPathFinding.sub differ diff --git a/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]TestPathFinding/filelist.xml b/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]TestPathFinding/filelist.xml new file mode 100644 index 000000000..bc9dfb0f6 --- /dev/null +++ b/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]TestPathFinding/filelist.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/Lua/.vscode/launch.json b/Barotrauma/BarotraumaShared/Lua/.vscode/launch.json deleted file mode 100644 index bd4a5a056..000000000 --- a/Barotrauma/BarotraumaShared/Lua/.vscode/launch.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "version": "0.2.0", - "configurations": [ - { - "name": "MoonSharp Attach", - "type": "moonsharp-debug", - "debugServer": 41912, - "request": "attach" - } - ] -} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/Lua/.vscode/settings.json b/Barotrauma/BarotraumaShared/Lua/.vscode/settings.json deleted file mode 100644 index 237e6445e..000000000 --- a/Barotrauma/BarotraumaShared/Lua/.vscode/settings.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "Lua.diagnostics.globals": [ - "Game", - "Player", - "Random", - "Hook", - "Timer", - "bit32", - "TotalTime", - "DoFile", - "WayPoint", - "SpawnType", - "Level", - "Submarine", - "Vector2", - "PositionType", - "ServerLog_MessageType", - "Character", - "TraitorMessageType", - "ChatMessageType", - "CauseOfDeathType", - "CreateVector2", - "Item", - "ChatMessage", - "AfflictionPrefab", - "Gap", - "File", - "Networking", - "printNoLog", - "Client", - "SERVER", - "setmodulepaths", - "Type", - "BindingFlags", - "UserData", - "LuaUserData", - "CLIENT", - "ContentPackageManager" - ] -} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/Lua/DefaultRegister/RegisterClient.lua b/Barotrauma/BarotraumaShared/Lua/DefaultRegister/RegisterClient.lua deleted file mode 100644 index 6f074747b..000000000 --- a/Barotrauma/BarotraumaShared/Lua/DefaultRegister/RegisterClient.lua +++ /dev/null @@ -1,150 +0,0 @@ -local Register = LuaSetup.LuaUserData.RegisterType -local RegisterBarotrauma = LuaSetup.LuaUserData.RegisterTypeBarotrauma - -local localizedStrings = { - "LocalizedString", "LimitLString", "WrappedLString", "AddedPunctuationLString", "CapitalizeLString", "ConcatLString", "FallbackLString", "FormattedLString", "InputTypeLString", "JoinLString", "LowerLString", "RawLString", "ReplaceLString", "ServerMsgLString", "SplitLString", "TagLString", "TrimLString", "UpperLString", "StripRichTagsLString", -} - -for key, value in pairs(localizedStrings) do - RegisterBarotrauma(value) -end - -RegisterBarotrauma("EditorScreen") -RegisterBarotrauma("SubEditorScreen") -RegisterBarotrauma("EventEditorScreen") -RegisterBarotrauma("CharacterEditor.CharacterEditorScreen") -RegisterBarotrauma("SpriteEditorScreen") -RegisterBarotrauma("LevelEditorScreen") - -RegisterBarotrauma("Networking.ClientPeer") -RegisterBarotrauma("Networking.GameClient") -RegisterBarotrauma("Networking.VoipCapture") - -RegisterBarotrauma("Media.Video") - -RegisterBarotrauma("SoundsFile") -RegisterBarotrauma("SoundPrefab") -RegisterBarotrauma("PrefabCollection`1") -RegisterBarotrauma("PrefabSelector`1") -RegisterBarotrauma("BackgroundMusic") -RegisterBarotrauma("GUISound") -RegisterBarotrauma("DamageSound") - -RegisterBarotrauma("Sounds.SoundManager") -RegisterBarotrauma("Sounds.OggSound") -RegisterBarotrauma("Sounds.VideoSound") -RegisterBarotrauma("Sounds.VoipSound") -RegisterBarotrauma("Sounds.SoundChannel") -RegisterBarotrauma("Sounds.SoundBuffers") -RegisterBarotrauma("RoundSound") -RegisterBarotrauma("CharacterSound") -RegisterBarotrauma("SoundPlayer") -RegisterBarotrauma("Items.Components.ItemSound") - -RegisterBarotrauma("Sounds.LowpassFilter") -RegisterBarotrauma("Sounds.HighpassFilter") -RegisterBarotrauma("Sounds.BandpassFilter") -RegisterBarotrauma("Sounds.NotchFilter") -RegisterBarotrauma("Sounds.LowShelfFilter") -RegisterBarotrauma("Sounds.HighShelfFilter") -RegisterBarotrauma("Sounds.PeakFilter") - -RegisterBarotrauma("Particles.ParticleManager") -RegisterBarotrauma("Particles.Particle") -RegisterBarotrauma("Particles.ParticleEmitterProperties") -RegisterBarotrauma("Particles.ParticleEmitter") -RegisterBarotrauma("Particles.ParticlePrefab") - -RegisterBarotrauma("Lights.LightManager") -RegisterBarotrauma("Lights.LightSource") -RegisterBarotrauma("Lights.LightSourceParams") - -RegisterBarotrauma("LevelWallVertexBuffer") -RegisterBarotrauma("LevelRenderer") -RegisterBarotrauma("WaterRenderer") -RegisterBarotrauma("WaterVertexData") - -RegisterBarotrauma("ChatBox") -RegisterBarotrauma("GUICanvas") -RegisterBarotrauma("Anchor") -RegisterBarotrauma("Alignment") -RegisterBarotrauma("Pivot") -RegisterBarotrauma("Key") -RegisterBarotrauma("PlayerInput") -RegisterBarotrauma("ScalableFont") - -Register("Microsoft.Xna.Framework.Graphics.Effect") -Register("Microsoft.Xna.Framework.Graphics.EffectParameterCollection") -Register("Microsoft.Xna.Framework.Graphics.EffectParameter") - -Register("Microsoft.Xna.Framework.Graphics.SpriteBatch") -Register("Microsoft.Xna.Framework.Graphics.Texture2D") -Register("EventInput.KeyboardDispatcher") -Register("EventInput.KeyEventArgs") -Register("Microsoft.Xna.Framework.Input.Keys") -Register("Microsoft.Xna.Framework.Input.KeyboardState") - -RegisterBarotrauma("TextureLoader") -RegisterBarotrauma("Sprite") -RegisterBarotrauma("GUI") -RegisterBarotrauma("GUIStyle") -RegisterBarotrauma("GUIComponent") -RegisterBarotrauma("GUILayoutGroup") -RegisterBarotrauma("GUITextBox") -RegisterBarotrauma("GUITextBlock") -RegisterBarotrauma("GUIButton") -RegisterBarotrauma("RectTransform") -RegisterBarotrauma("GUIFrame") -RegisterBarotrauma("GUITickBox") -RegisterBarotrauma("GUIImage") -RegisterBarotrauma("GUIListBox") -RegisterBarotrauma("GUIScrollBar") -RegisterBarotrauma("GUIDropDown") -RegisterBarotrauma("GUINumberInput") -RegisterBarotrauma("GUIMessage") -RegisterBarotrauma("GUIMessageBox") -RegisterBarotrauma("GUIColorPicker") -RegisterBarotrauma("GUIProgressBar") -RegisterBarotrauma("GUICustomComponent") -RegisterBarotrauma("GUIScissorComponent") -RegisterBarotrauma("GUIComponentStyle") -RegisterBarotrauma("GUIFontPrefab") -RegisterBarotrauma("GUIFont") -RegisterBarotrauma("GUISpritePrefab") -RegisterBarotrauma("GUISprite") -RegisterBarotrauma("GUISpriteSheetPrefab") -RegisterBarotrauma("GUISpriteSheet") -RegisterBarotrauma("GUICursorPrefab") -RegisterBarotrauma("GUICursor") -RegisterBarotrauma("GUIRadioButtonGroup") -RegisterBarotrauma("GUIDragHandle") -RegisterBarotrauma("GUIContextMenu") -RegisterBarotrauma("ContextMenuOption") -RegisterBarotrauma("VideoPlayer") -RegisterBarotrauma("CreditsPlayer") -RegisterBarotrauma("SlideshowPlayer") -RegisterBarotrauma("SerializableEntityEditor") -RegisterBarotrauma("CircuitBoxWireRenderer") -RegisterBarotrauma("CircuitBoxLabel") -RegisterBarotrauma("CircuitBoxMouseDragSnapshotHandler") -RegisterBarotrauma("CircuitBoxUI") - -RegisterBarotrauma("SettingsMenu") -RegisterBarotrauma("TabMenu") -RegisterBarotrauma("Widget") -RegisterBarotrauma("UpgradeStore") -RegisterBarotrauma("VotingInterface") -RegisterBarotrauma("MedicalClinicUI") -RegisterBarotrauma("LoadingScreen") -RegisterBarotrauma("HUD") -RegisterBarotrauma("HUDLayoutSettings") -RegisterBarotrauma("HUDProgressBar") -RegisterBarotrauma("Graph") -RegisterBarotrauma("HRManagerUI") -RegisterBarotrauma("SubmarineSelection") -RegisterBarotrauma("Store") -RegisterBarotrauma("UISprite") -RegisterBarotrauma("ParamsEditor") - -RegisterBarotrauma("Inventory+SlotReference") -RegisterBarotrauma("VisualSlot") diff --git a/Barotrauma/BarotraumaShared/Lua/DefaultRegister/RegisterServer.lua b/Barotrauma/BarotraumaShared/Lua/DefaultRegister/RegisterServer.lua deleted file mode 100644 index 0de67a7f4..000000000 --- a/Barotrauma/BarotraumaShared/Lua/DefaultRegister/RegisterServer.lua +++ /dev/null @@ -1,20 +0,0 @@ -local Register = LuaSetup.LuaUserData.RegisterType -local RegisterBarotrauma = LuaSetup.LuaUserData.RegisterTypeBarotrauma - - -local localizedStrings = { - "LocalizedString", "AddedPunctuationLString", "CapitalizeLString", "ConcatLString", "FallbackLString", "FormattedLString", "InputTypeLString", "JoinLString", "LowerLString", "RawLString", "ReplaceLString", "ServerMsgLString", "SplitLString", "TagLString", "TrimLString", "UpperLString", "StripRichTagsLString", -} - -for key, value in pairs(localizedStrings) do - RegisterBarotrauma(value) -end - -Register("Steamworks.SteamServer") - -RegisterBarotrauma("Character+TeamChangeEventData") - -RegisterBarotrauma("Networking.GameServer") - -RegisterBarotrauma("Networking.ServerPeer") -RegisterBarotrauma("Networking.FileSender") diff --git a/Barotrauma/BarotraumaShared/Lua/DefaultRegister/RegisterShared.lua b/Barotrauma/BarotraumaShared/Lua/DefaultRegister/RegisterShared.lua deleted file mode 100644 index fc5ee9b2c..000000000 --- a/Barotrauma/BarotraumaShared/Lua/DefaultRegister/RegisterShared.lua +++ /dev/null @@ -1,479 +0,0 @@ -local Register = LuaSetup.LuaUserData.RegisterType -local RegisterExtension = LuaSetup.LuaUserData.RegisterExtensionType -local RegisterBarotrauma = LuaSetup.LuaUserData.RegisterTypeBarotrauma - -Register("System.TimeSpan") -Register("System.Exception") -Register("System.Console") -Register("System.Exception") - -RegisterBarotrauma("Success`2") -RegisterBarotrauma("Failure`2") - -RegisterBarotrauma("LuaSByte") -RegisterBarotrauma("LuaByte") -RegisterBarotrauma("LuaInt16") -RegisterBarotrauma("LuaUInt16") -RegisterBarotrauma("LuaInt32") -RegisterBarotrauma("LuaUInt32") -RegisterBarotrauma("LuaInt64") -RegisterBarotrauma("LuaUInt64") -RegisterBarotrauma("LuaSingle") -RegisterBarotrauma("LuaDouble") - -RegisterBarotrauma("GameMain") -RegisterBarotrauma("Networking.BanList") -RegisterBarotrauma("Networking.BannedPlayer") - -RegisterBarotrauma("Range`1") - -RegisterBarotrauma("RichString") -RegisterBarotrauma("Identifier") -RegisterBarotrauma("LanguageIdentifier") - -RegisterBarotrauma("Job") -RegisterBarotrauma("JobPrefab") -RegisterBarotrauma("JobVariant") - -Register("Voronoi2.DoubleVector2") -Register("Voronoi2.Site") -Register("Voronoi2.Edge") -Register("Voronoi2.Halfedge") -Register("Voronoi2.VoronoiCell") -Register("Voronoi2.GraphEdge") - -RegisterBarotrauma("WayPoint") -RegisterBarotrauma("Level") -RegisterBarotrauma("LevelData") -RegisterBarotrauma("Level+InterestingPosition") -RegisterBarotrauma("LevelGenerationParams") -RegisterBarotrauma("LevelObjectManager") -RegisterBarotrauma("LevelObject") -RegisterBarotrauma("LevelObjectPrefab") -RegisterBarotrauma("LevelTrigger") -RegisterBarotrauma("CaveGenerationParams") -RegisterBarotrauma("CaveGenerator") -RegisterBarotrauma("OutpostGenerationParams") -RegisterBarotrauma("OutpostGenerator") -RegisterBarotrauma("OutpostModuleInfo") -RegisterBarotrauma("BeaconStationInfo") -RegisterBarotrauma("NPCSet") -RegisterBarotrauma("RuinGeneration.Ruin") -RegisterBarotrauma("RuinGeneration.RuinGenerationParams") -RegisterBarotrauma("LevelWall") -RegisterBarotrauma("DestructibleLevelWall") -RegisterBarotrauma("Biome") -RegisterBarotrauma("Map") -RegisterBarotrauma("Networking.RespawnManager") -RegisterBarotrauma("Networking.RespawnManager+TeamSpecificState") - -RegisterBarotrauma("Character") -RegisterBarotrauma("CharacterPrefab") -RegisterBarotrauma("CharacterInfo") -RegisterBarotrauma("CharacterInfoPrefab") -RegisterBarotrauma("CharacterInfo+HeadPreset") -RegisterBarotrauma("CharacterInfo+HeadInfo") -RegisterBarotrauma("CharacterHealth") -RegisterBarotrauma("CharacterHealth+LimbHealth") -RegisterBarotrauma("DamageModifier") -RegisterBarotrauma("CharacterInventory") -RegisterBarotrauma("CharacterParams") -RegisterBarotrauma("CharacterParams+AIParams") -RegisterBarotrauma("CharacterParams+TargetParams") -RegisterBarotrauma("CharacterParams+InventoryParams") -RegisterBarotrauma("CharacterParams+HealthParams") -RegisterBarotrauma("CharacterParams+ParticleParams") -RegisterBarotrauma("CharacterParams+SoundParams") -RegisterBarotrauma("SteeringManager") -RegisterBarotrauma("IndoorsSteeringManager") -RegisterBarotrauma("SteeringPath") -RegisterBarotrauma("CreatureMetrics") - -RegisterBarotrauma("Item") -RegisterBarotrauma("DeconstructItem") -RegisterBarotrauma("PurchasedItem") -RegisterBarotrauma("PurchasedItemSwap") -RegisterBarotrauma("PurchasedUpgrade") -RegisterBarotrauma("SoldItem") -RegisterBarotrauma("StartItem") -RegisterBarotrauma("StartItemSet") -RegisterBarotrauma("RelatedItem") -RegisterBarotrauma("UpgradeManager") -RegisterBarotrauma("CargoManager") -RegisterBarotrauma("HireManager") -RegisterBarotrauma("FabricationRecipe") -RegisterBarotrauma("PreferredContainer") -RegisterBarotrauma("SwappableItem") -RegisterBarotrauma("FabricationRecipe+RequiredItemByIdentifier") -RegisterBarotrauma("FabricationRecipe+RequiredItemByTag") -RegisterBarotrauma("Submarine") - -RegisterBarotrauma("Networking.AccountInfo") -RegisterBarotrauma("Networking.AccountId") -RegisterBarotrauma("Networking.SteamId") -RegisterBarotrauma("Networking.EpicAccountId") -RegisterBarotrauma("Networking.Address") -RegisterBarotrauma("Networking.UnknownAddress") -RegisterBarotrauma("Networking.P2PAddress") -RegisterBarotrauma("Networking.EosP2PAddress") -RegisterBarotrauma("Networking.SteamP2PAddress") -RegisterBarotrauma("Networking.PipeAddress") -RegisterBarotrauma("Networking.LidgrenAddress") -RegisterBarotrauma("Networking.Endpoint") -RegisterBarotrauma("Networking.SteamP2PEndpoint") -RegisterBarotrauma("Networking.PipeEndpoint") -RegisterBarotrauma("Networking.LidgrenEndpoint") - -RegisterBarotrauma("INetSerializableStruct") -RegisterBarotrauma("Networking.Client") -RegisterBarotrauma("Networking.TempClient") -RegisterBarotrauma("Networking.NetworkConnection") -RegisterBarotrauma("Networking.LidgrenConnection") -RegisterBarotrauma("Networking.SteamP2PConnection") -RegisterBarotrauma("Networking.VoipQueue") -RegisterBarotrauma("Networking.ChatMessage") - -RegisterBarotrauma("AnimController") -RegisterBarotrauma("HumanoidAnimController") -RegisterBarotrauma("FishAnimController") -RegisterBarotrauma("Limb") -RegisterBarotrauma("Ragdoll") -RegisterBarotrauma("RagdollParams") - -RegisterBarotrauma("AfflictionPrefab") -RegisterBarotrauma("Affliction") -RegisterBarotrauma("AttackResult") -RegisterBarotrauma("Attack") -RegisterBarotrauma("Entity") -RegisterBarotrauma("EntityGrid") -RegisterBarotrauma("EntitySpawner") -RegisterBarotrauma("MapEntity") -RegisterBarotrauma("MapEntityPrefab") -RegisterBarotrauma("CauseOfDeath") -RegisterBarotrauma("Hull") -RegisterBarotrauma("WallSection") -RegisterBarotrauma("Structure") -RegisterBarotrauma("Gap") -RegisterBarotrauma("PhysicsBody") -RegisterBarotrauma("AbilityFlags") -RegisterBarotrauma("ItemPrefab") -RegisterBarotrauma("ItemAssemblyPrefab") -RegisterBarotrauma("InputType") - -RegisterBarotrauma("FireSource") -RegisterBarotrauma("SerializableProperty") -LuaUserData.MakeFieldAccessible(RegisterBarotrauma("StatusEffect"), "user") -RegisterBarotrauma("DurationListElement") -RegisterBarotrauma("PropertyConditional") -RegisterBarotrauma("DelayedListElement") -RegisterBarotrauma("DelayedEffect") - - -RegisterBarotrauma("ContentPackageManager") -RegisterBarotrauma("ContentPackageManager+PackageSource") -RegisterBarotrauma("ContentPackageManager+EnabledPackages") -RegisterBarotrauma("ContentPackage") -RegisterBarotrauma("RegularPackage") -RegisterBarotrauma("CorePackage") -RegisterBarotrauma("ContentXElement") -RegisterBarotrauma("ContentPath") -RegisterBarotrauma("ContentPackageId") -RegisterBarotrauma("SteamWorkshopId") -RegisterBarotrauma("Md5Hash") - -RegisterBarotrauma("AfflictionsFile") -RegisterBarotrauma("BackgroundCreaturePrefabsFile") -RegisterBarotrauma("BallastFloraFile") -RegisterBarotrauma("BeaconStationFile") -RegisterBarotrauma("CaveGenerationParametersFile") -RegisterBarotrauma("CharacterFile") -RegisterBarotrauma("ContentFile") -RegisterBarotrauma("CorpsesFile") -RegisterBarotrauma("DecalsFile") -RegisterBarotrauma("EnemySubmarineFile") -RegisterBarotrauma("EventManagerSettingsFile") -RegisterBarotrauma("FactionsFile") -RegisterBarotrauma("ItemAssemblyFile") -RegisterBarotrauma("ItemFile") -RegisterBarotrauma("JobsFile") -RegisterBarotrauma("LevelGenerationParametersFile") -RegisterBarotrauma("LevelObjectPrefabsFile") -RegisterBarotrauma("LocationTypesFile") -RegisterBarotrauma("MapGenerationParametersFile") -RegisterBarotrauma("MissionsFile") -RegisterBarotrauma("NPCConversationsFile") -RegisterBarotrauma("NPCPersonalityTraitsFile") -RegisterBarotrauma("NPCSetsFile") -RegisterBarotrauma("OrdersFile") -RegisterBarotrauma("OtherFile") -RegisterBarotrauma("OutpostConfigFile") -RegisterBarotrauma("OutpostFile") -RegisterBarotrauma("OutpostModuleFile") -RegisterBarotrauma("ParticlesFile") -RegisterBarotrauma("RandomEventsFile") -RegisterBarotrauma("RuinConfigFile") -RegisterBarotrauma("ServerExecutableFile") -RegisterBarotrauma("SkillSettingsFile") -RegisterBarotrauma("SoundsFile") -RegisterBarotrauma("StartItemsFile") -RegisterBarotrauma("StructureFile") -RegisterBarotrauma("SubmarineFile") -RegisterBarotrauma("TalentsFile") -RegisterBarotrauma("TalentTreesFile") -RegisterBarotrauma("TextFile") -RegisterBarotrauma("TutorialsFile") -RegisterBarotrauma("UIStyleFile") -RegisterBarotrauma("UpgradeModulesFile") -RegisterBarotrauma("WreckAIConfigFile") -RegisterBarotrauma("WreckFile") - -Register("System.Xml.Linq.XElement") -Register("System.Xml.Linq.XName") -Register("System.Xml.Linq.XAttribute") -Register("System.Xml.Linq.XContainer") -Register("System.Xml.Linq.XDocument") -Register("System.Xml.Linq.XNode") - - -RegisterBarotrauma("SubmarineBody") -RegisterBarotrauma("Explosion") -RegisterBarotrauma("Networking.ServerSettings") -RegisterBarotrauma("Networking.ServerSettings+SavedClientPermission") -RegisterBarotrauma("Inventory") -RegisterBarotrauma("ItemInventory") -RegisterBarotrauma("Inventory+ItemSlot") -RegisterBarotrauma("FireSource") -RegisterBarotrauma("AutoItemPlacer") -RegisterBarotrauma("CircuitBoxConnection") -RegisterBarotrauma("CircuitBoxComponent") -RegisterBarotrauma("CircuitBoxNode") -RegisterBarotrauma("CircuitBoxWire") -RegisterBarotrauma("CircuitBoxInputOutputNode") -RegisterBarotrauma("CircuitBoxSelectable") -RegisterBarotrauma("CircuitBoxSizes") - -local componentsToRegister = { "DockingPort", "Door", "GeneticMaterial", "Growable", "Holdable", "LevelResource", "ItemComponent", "ItemLabel", "LightComponent", "Controller", "Deconstructor", "Engine", "Fabricator", "OutpostTerminal", "Pump", "Reactor", "Steering", "PowerContainer", "Projectile", "Repairable", "Rope", "Scanner", "ButtonTerminal", "ConnectionPanel", "CustomInterface", "MemoryComponent", "Terminal", "WifiComponent", "Wire", "TriggerComponent", "ElectricalDischarger", "EntitySpawnerComponent", "ProducedItem", "VineTile", "GrowthSideExtension", "IdCard", "MeleeWeapon", "Pickable", "AbilityItemPickingTime", "Propulsion", "RangedWeapon", "AbilityRangedWeapon", "RepairTool", "Sprayer", "Throwable", "ItemContainer", "AbilityItemContainer", "Ladder", "LimbPos", "AbilityDeconstructedItem", "AbilityItemCreationMultiplier", "AbilityItemDeconstructedInventory", "MiniMap", "OxygenGenerator", "Sonar", "SonarTransducer", "Vent", "NameTag", "Planter", "Powered", "PowerTransfer", "Quality", "RemoteController", "AdderComponent", "AndComponent", "ArithmeticComponent", "ColorComponent", "ConcatComponent", "Connection", "CircuitBox", "DelayComponent", "DivideComponent", "EqualsComponent", "ExponentiationComponent", "FunctionComponent", "GreaterComponent", "ModuloComponent", "MotionSensor", "MultiplyComponent", "NotComponent", "OrComponent", "OscillatorComponent", "OxygenDetector", "RegExFindComponent", "RelayComponent", "SignalCheckComponent", "SmokeDetector", "StringComponent", "SubtractComponent", "TrigonometricFunctionComponent", "WaterDetector", "XorComponent", "StatusHUD", "Turret", "Wearable", -"GridInfo", "PowerSourceGroup" -} - -for key, value in pairs(componentsToRegister) do - RegisterBarotrauma("Items.Components." .. value) -end - -LuaUserData.MakeFieldAccessible(RegisterBarotrauma("Items.Components.CustomInterface"), "customInterfaceElementList") -RegisterBarotrauma("Items.Components.CustomInterface+CustomInterfaceElement") - -RegisterBarotrauma("WearableSprite") - -RegisterBarotrauma("AIController") -RegisterBarotrauma("EnemyAIController") -RegisterBarotrauma("HumanAIController") -RegisterBarotrauma("AICharacter") -RegisterBarotrauma("AITarget") -RegisterBarotrauma("AITargetMemory") -RegisterBarotrauma("AIChatMessage") -RegisterBarotrauma("AIObjectiveManager") -RegisterBarotrauma("WreckAI") -RegisterBarotrauma("WreckAIConfig") - -RegisterBarotrauma("AIObjectiveChargeBatteries") -RegisterBarotrauma("AIObjective") -RegisterBarotrauma("AIObjectiveCleanupItem") -RegisterBarotrauma("AIObjectiveCleanupItems") -RegisterBarotrauma("AIObjectiveCombat") -RegisterBarotrauma("AIObjectiveContainItem") -RegisterBarotrauma("AIObjectiveDeconstructItem") -RegisterBarotrauma("AIObjectiveDeconstructItems") -RegisterBarotrauma("AIObjectiveEscapeHandcuffs") -RegisterBarotrauma("AIObjectiveExtinguishFire") -RegisterBarotrauma("AIObjectiveExtinguishFires") -RegisterBarotrauma("AIObjectiveFightIntruders") -RegisterBarotrauma("AIObjectiveFindDivingGear") -RegisterBarotrauma("AIObjectiveFindSafety") -RegisterBarotrauma("AIObjectiveFixLeak") -RegisterBarotrauma("AIObjectiveFixLeaks") -RegisterBarotrauma("AIObjectiveGetItem") -RegisterBarotrauma("AIObjectiveGoTo") -RegisterBarotrauma("AIObjectiveIdle") -RegisterBarotrauma("AIObjectiveOperateItem") -RegisterBarotrauma("AIObjectivePumpWater") -RegisterBarotrauma("AIObjectiveRepairItem") -RegisterBarotrauma("AIObjectiveRepairItems") -RegisterBarotrauma("AIObjectiveRescue") -RegisterBarotrauma("AIObjectiveRescueAll") -RegisterBarotrauma("AIObjectiveReturn") - -RegisterBarotrauma("Order") -RegisterBarotrauma("OrderPrefab") -RegisterBarotrauma("OrderTarget") - -RegisterBarotrauma("TalentPrefab") -RegisterBarotrauma("TalentOption") -RegisterBarotrauma("TalentSubTree") -RegisterBarotrauma("TalentTree") -RegisterBarotrauma("CharacterTalent") -RegisterBarotrauma("Upgrade") -RegisterBarotrauma("UpgradeCategory") -RegisterBarotrauma("UpgradePrefab") -RegisterBarotrauma("UpgradeManager") - -RegisterBarotrauma("Screen") -RegisterBarotrauma("GameScreen") -RegisterBarotrauma("GameSession") -RegisterBarotrauma("GameSettings") -RegisterBarotrauma("CrewManager") -RegisterBarotrauma("KarmaManager") - -RegisterBarotrauma("GameMode") -RegisterBarotrauma("MissionMode") -RegisterBarotrauma("PvPMode") -RegisterBarotrauma("Mission") -RegisterBarotrauma("AbandonedOutpostMission") -RegisterBarotrauma("EliminateTargetsMission") -RegisterBarotrauma("EndMission") -RegisterBarotrauma("BeaconMission") -RegisterBarotrauma("CargoMission") -RegisterBarotrauma("CombatMission") -RegisterBarotrauma("EscortMission") -RegisterBarotrauma("GoToMission") -RegisterBarotrauma("MineralMission") -RegisterBarotrauma("MonsterMission") -RegisterBarotrauma("NestMission") -RegisterBarotrauma("PirateMission") -RegisterBarotrauma("SalvageMission") -RegisterBarotrauma("ScanMission") -RegisterBarotrauma("MissionPrefab") -RegisterBarotrauma("CampaignMode") -RegisterBarotrauma("CoOpMode") -RegisterBarotrauma("MultiPlayerCampaign") -RegisterBarotrauma("Radiation") - -RegisterBarotrauma("CampaignMetadata") -RegisterBarotrauma("Wallet") - -RegisterBarotrauma("Faction") -RegisterBarotrauma("FactionPrefab") -RegisterBarotrauma("Reputation") - -RegisterBarotrauma("Location") -RegisterBarotrauma("LocationConnection") -RegisterBarotrauma("LocationType") -RegisterBarotrauma("LocationTypeChange") - -RegisterBarotrauma("DebugConsole") -RegisterBarotrauma("DebugConsole+Command") - -RegisterBarotrauma("TextManager") -RegisterBarotrauma("TextPack") - -local descriptor = RegisterBarotrauma("NetLobbyScreen") - -if SERVER then - LuaUserData.MakeFieldAccessible(descriptor, "subs") -end - -RegisterBarotrauma("EventManager") -RegisterBarotrauma("EventManagerSettings") -RegisterBarotrauma("Event") -RegisterBarotrauma("ArtifactEvent") -RegisterBarotrauma("MonsterEvent") -RegisterBarotrauma("ScriptedEvent") -RegisterBarotrauma("MalfunctionEvent") -RegisterBarotrauma("EventSet") -RegisterBarotrauma("EventPrefab") - -RegisterBarotrauma("Networking.NetConfig") -RegisterBarotrauma("Networking.IWriteMessage") -RegisterBarotrauma("Networking.IReadMessage") -RegisterBarotrauma("Networking.NetEntityEvent") -RegisterBarotrauma("Networking.INetSerializable") -Register("Lidgren.Network.NetIncomingMessage") -Register("Lidgren.Network.NetConnection") -Register("System.Net.IPEndPoint") -Register("System.Net.IPAddress") - -RegisterBarotrauma("Skill") -RegisterBarotrauma("SkillPrefab") -RegisterBarotrauma("SkillSettings") - -RegisterBarotrauma("TraitorManager") -RegisterBarotrauma("TraitorEvent") -RegisterBarotrauma("TraitorEventPrefab") -RegisterBarotrauma("TraitorManager+TraitorResults") - -Register("FarseerPhysics.Dynamics.Body") -Register("FarseerPhysics.Dynamics.World") -Register("FarseerPhysics.Dynamics.Fixture") -Register("FarseerPhysics.ConvertUnits") -Register("FarseerPhysics.Collision.AABB") -Register("FarseerPhysics.Collision.ContactFeature") -Register("FarseerPhysics.Collision.ManifoldPoint") -Register("FarseerPhysics.Collision.ContactID") -Register("FarseerPhysics.Collision.Manifold") -Register("FarseerPhysics.Collision.RayCastInput") -Register("FarseerPhysics.Collision.ClipVertex") -Register("FarseerPhysics.Collision.RayCastOutput") -Register("FarseerPhysics.Collision.EPAxis") -Register("FarseerPhysics.Collision.ReferenceFace") -Register("FarseerPhysics.Collision.Collision") - -RegisterBarotrauma("Physics") - -local toolBox = RegisterBarotrauma("ToolBox") -if CLIENT then - LuaUserData.RemoveMember(toolBox, "OpenFileWithShell") -end - -RegisterBarotrauma("Camera") -RegisterBarotrauma("Key") - -RegisterBarotrauma("PrefabCollection`1") - -RegisterBarotrauma("PrefabSelector`1") - -RegisterBarotrauma("Pair`2") - -RegisterBarotrauma("Items.Components.Signal") -RegisterBarotrauma("SubmarineInfo") - -RegisterBarotrauma("MapCreatures.Behavior.BallastFloraBehavior") -RegisterBarotrauma("MapCreatures.Behavior.BallastFloraBranch") - -RegisterBarotrauma("PetBehavior") -RegisterBarotrauma("SwarmBehavior") -RegisterBarotrauma("LatchOntoAI") - -RegisterBarotrauma("Decal") -RegisterBarotrauma("DecalPrefab") -RegisterBarotrauma("DecalManager") - -RegisterBarotrauma("PriceInfo") - -RegisterBarotrauma("Voting") - -Register("Microsoft.Xna.Framework.Vector2") -Register("Microsoft.Xna.Framework.Vector3") -Register("Microsoft.Xna.Framework.Vector4") -Register("Microsoft.Xna.Framework.Color") -Register("Microsoft.Xna.Framework.Point") -Register("Microsoft.Xna.Framework.Rectangle") -Register("Microsoft.Xna.Framework.Matrix") - -local friend = Register("Steamworks.Friend") - -LuaUserData.RemoveMember(friend, "InviteToGame") -LuaUserData.RemoveMember(friend, "SendMessage") - -local workshopItem = Register("Steamworks.Ugc.Item") - -LuaUserData.RemoveMember(workshopItem, "Subscribe") -LuaUserData.RemoveMember(workshopItem, "DownloadAsync") -LuaUserData.RemoveMember(workshopItem, "Unsubscribe") -LuaUserData.RemoveMember(workshopItem, "AddFavorite") -LuaUserData.RemoveMember(workshopItem, "RemoveFavorite") -LuaUserData.RemoveMember(workshopItem, "Vote") -LuaUserData.RemoveMember(workshopItem, "GetUserVote") -LuaUserData.RemoveMember(workshopItem, "Edit") - -RegisterExtension("Barotrauma.MathUtils") -RegisterExtension("Barotrauma.XMLExtensions") \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/Lua/LuaSetup.lua b/Barotrauma/BarotraumaShared/Lua/LuaSetup.lua deleted file mode 100644 index 05163a0dd..000000000 --- a/Barotrauma/BarotraumaShared/Lua/LuaSetup.lua +++ /dev/null @@ -1,50 +0,0 @@ -LuaSetup = {} - -local path = table.pack(...)[1] - -package.path = {path .. "/?.lua"} - -setmodulepaths(package.path) - --- Setup Libraries -require("LuaUserData") - -require("DefaultRegister/RegisterShared") - -if SERVER then - require("DefaultRegister/RegisterServer") -else - require("DefaultRegister/RegisterClient") -end - -local function AddTableToGlobal(tbl) - for k, v in pairs(tbl) do - _G[k] = v - end -end - -if SERVER then - AddTableToGlobal(require("DefaultLib/LibServer")) -else - AddTableToGlobal(require("DefaultLib/LibClient")) -end - -AddTableToGlobal(require("DefaultLib/LibShared")) - -AddTableToGlobal(require("CompatibilityLib")) - -require("DefaultHook") - -Descriptors = LuaSetup.LuaUserData.Descriptors -LuaUserData = LuaSetup.LuaUserData - -require("DefaultLib/Utils/Math") -require("DefaultLib/Utils/String") -require("DefaultLib/Utils/Util") -require("DefaultLib/Utils/SteamApi") - -require("PostSetup") - -LuaSetup = nil - -require("ModLoader") \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/Lua/LuaUserData.lua b/Barotrauma/BarotraumaShared/Lua/LuaUserData.lua deleted file mode 100644 index 387fc7f2b..000000000 --- a/Barotrauma/BarotraumaShared/Lua/LuaUserData.lua +++ /dev/null @@ -1,97 +0,0 @@ -local clrLuaUserData = LuaUserData -local luaUserData = {} - -luaUserData.Descriptors = {} - -LuaSetup.LuaUserData = luaUserData - -luaUserData.IsRegistered = clrLuaUserData.IsRegistered -luaUserData.UnregisterType = clrLuaUserData.UnregisterType -luaUserData.RegisterGenericType = clrLuaUserData.RegisterGenericType -luaUserData.RegisterExtensionType = clrLuaUserData.RegisterExtensionType -luaUserData.UnregisterGenericType = clrLuaUserData.UnregisterGenericType -luaUserData.IsTargetType = clrLuaUserData.IsTargetType -luaUserData.TypeOf = clrLuaUserData.TypeOf -luaUserData.GetType = clrLuaUserData.GetType -luaUserData.CreateEnumTable = clrLuaUserData.CreateEnumTable -luaUserData.MakeFieldAccessible = clrLuaUserData.MakeFieldAccessible -luaUserData.MakeMethodAccessible = clrLuaUserData.MakeMethodAccessible -luaUserData.MakePropertyAccessible = clrLuaUserData.MakePropertyAccessible -luaUserData.AddMethod = clrLuaUserData.AddMethod -luaUserData.AddField = clrLuaUserData.AddField -luaUserData.RemoveMember = clrLuaUserData.RemoveMember -luaUserData.CreateUserDataFromDescriptor = clrLuaUserData.CreateUserDataFromDescriptor -luaUserData.CreateUserDataFromType = clrLuaUserData.CreateUserDataFromType -luaUserData.HasMember = clrLuaUserData.HasMember - -luaUserData.RegisterType = function(typeName) - local success, result = pcall(clrLuaUserData.RegisterType, typeName) - - if not success then - error(result, 2) - end - - luaUserData.Descriptors[typeName] = result - - return result -end - -luaUserData.RegisterTypeBarotrauma = function(typeName) - typeName = "Barotrauma." .. typeName - local success, result = pcall(luaUserData.RegisterType, typeName) - - if not success then - error(result, 2) - end - - return result -end - -luaUserData.AddCallMetaTable = function (userdata) - if userdata == nil then - error("Attempted to add a call metatable to a nil value.", 2) - end - - if not LuaUserData.HasMember(userdata, ".ctor") then - error("Attempted to add a call metatable to a userdata that does not have a constructor.", 2) - end - - debug.setmetatable(userdata, { - __call = function(obj, ...) - if userdata == nil then - error("userdata was nil.", 2) - end - - local success, result = pcall(userdata.__new, ...) - - - if not success then - error(result, 2) - end - - return result - end - }) -end - -luaUserData.CreateStatic = function(typeName) - if type(typeName) ~= "string" then - error("Expected a string for typeName, got " .. type(typeName) .. ".", 2) - end - - local success, result = pcall(clrLuaUserData.CreateStatic, typeName) - - if not success then - error(result, 2) - end - - if result == nil then - return - end - - if LuaUserData.HasMember(result, ".ctor") then - luaUserData.AddCallMetaTable(result) - end - - return result -end \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/Lua/ModLoader.lua b/Barotrauma/BarotraumaShared/Lua/ModLoader.lua deleted file mode 100644 index 828b56205..000000000 --- a/Barotrauma/BarotraumaShared/Lua/ModLoader.lua +++ /dev/null @@ -1,193 +0,0 @@ -local LUA_MOD_REQUIRE_PATH = "/Lua/?.lua" -local LUA_MOD_AUTORUN_PATH = "/Lua/Autorun" -local LUA_MOD_FORCEDAUTORUN_PATH = "/Lua/ForcedAutorun" - -local function EndsWith(str, suffix) - return str:sub(-string.len(suffix)) == suffix -end - -local function GetFileName(file) - return file:match("^.+/(.+)$") -end - -local function ExecuteProtected(s, folder) - loadfile(s)(folder) -end - -local function RunFolder(folder, rootFolder, package) - local search = File.DirSearch(folder) - for i = 1, #search, 1 do - local s = search[i]:gsub("\\", "/") - - if EndsWith(s, ".lua") then - local time = os.clock() - local ok, result = pcall(ExecuteProtected, s, rootFolder) - local diff = os.clock() - time - - print(string.format(" - %s (Took %.5fms)", GetFileName(s), diff)) - if not ok then - printerror(result) - end - end - - end -end - -local function AssertTypes(expectedTypes, ...) - local args = table.pack(...) - assert( - #args == #expectedTypes, - string.format( - "Assertion failed: incorrect number of args\n\texpected = %s\n\tgot = %s", - #expectedTypes, #args - ) - ) - for i = 1, #args do - local arg = args[i] - local expectedType = expectedTypes[i] - assert( - type(arg) == expectedType, - string.format( - "Assertion failed: incorrect argument type (arg #%d)\n\texpected = %s\n\tgot = %s", - i, expectedType, type(arg) - ) - ) - end -end - -local function ExecutionQueue() - local executionQueue = {} - executionQueue.Queue = {} - - executionQueue.Process = function() - while executionQueue.Queue[1] ~= nil do - local folder, rootFolder, package = table.unpack(table.remove(executionQueue.Queue, 1)) - print(string.format("%s %s", package.Name, package.ModVersion)) - RunFolder(folder, rootFolder, package) - end - end - - executionQueue.Add = function(...) - AssertTypes({ 'string', 'string', 'userdata' }, ...) - table.insert(executionQueue.Queue, table.pack(...)) - end - - return executionQueue -end - -local QueueAutorun = ExecutionQueue() -local QueueForcedAutorun = ExecutionQueue() - -local function nocase(s) - s = string.gsub(s, "%a", function(c) - return string.format("[%s%s]", string.lower(c), string.upper(c)) - end) - return s -end - -local function ProcessPackages(packages, fn) - for pkg in packages do - if pkg then - local pkgPath = pkg.Path - :gsub("\\", "/") - :gsub(nocase("/filelist.xml"), "") - fn(pkg, pkgPath) - end - end -end - -ProcessPackages(ContentPackageManager.EnabledPackages.All, function(pkg, pkgPath) - table.insert(package.path, pkgPath .. LUA_MOD_REQUIRE_PATH) - local autorunPath = pkgPath .. LUA_MOD_AUTORUN_PATH - if File.DirectoryExists(autorunPath) then - QueueAutorun.Add(autorunPath, pkgPath, pkg) - end -end) - --- we don't want to execute workshop ForcedAutorun if we have a local Package -local executedLocalPackages = {} - -ProcessPackages(ContentPackageManager.EnabledPackages.All, function(pkg, pkgPath) - table.insert(package.path, pkgPath .. LUA_MOD_REQUIRE_PATH) - local forcedAutorunPath = pkgPath .. LUA_MOD_FORCEDAUTORUN_PATH - if File.DirectoryExists(forcedAutorunPath) then - QueueForcedAutorun.Add(forcedAutorunPath, pkgPath, pkg) - executedLocalPackages[pkg.Name] = true - end -end) - -if not LuaCsConfig.TreatForcedModsAsNormal then - ProcessPackages(ContentPackageManager.LocalPackages, function(pkg, pkgPath) - if not executedLocalPackages[pkg.Name] then - table.insert(package.path, pkgPath .. LUA_MOD_REQUIRE_PATH) - local forcedAutorunPath = pkgPath .. LUA_MOD_FORCEDAUTORUN_PATH - if File.DirectoryExists(forcedAutorunPath) then - QueueForcedAutorun.Add(forcedAutorunPath, pkgPath, pkg) - executedLocalPackages[pkg.Name] = true - end - end - end) - - ProcessPackages(ContentPackageManager.AllPackages, function(pkg, pkgPath) - if not executedLocalPackages[pkg.Name] then - table.insert(package.path, pkgPath .. LUA_MOD_REQUIRE_PATH) - local forcedAutorunPath = pkgPath .. LUA_MOD_FORCEDAUTORUN_PATH - if File.DirectoryExists(forcedAutorunPath) then - QueueForcedAutorun.Add(forcedAutorunPath, pkgPath, pkg) - end - end - end) -end - -setmodulepaths(package.path) -setmodulepaths = nil - -local allExecuted = {} -for key, value in pairs(QueueAutorun.Queue) do table.insert(allExecuted, value[3]) end -for key, value in pairs(QueueForcedAutorun.Queue) do table.insert(allExecuted, value[3]) 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") - - message.WriteUInt16(#allExecuted) - - for key, package in pairs(allExecuted) 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 - -QueueAutorun.Process() -QueueForcedAutorun.Process() - -Hook.Add("stop", "luaSetup.stop", function() - print("Stopping Lua...") -end) - -Hook.Call("loaded") \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/Lua/PostSetup.lua b/Barotrauma/BarotraumaShared/Lua/PostSetup.lua deleted file mode 100644 index f54417259..000000000 --- a/Barotrauma/BarotraumaShared/Lua/PostSetup.lua +++ /dev/null @@ -1,75 +0,0 @@ -if CSActive then - return -end - -local function IsAllowed(typeName) - if string.startsWith(typeName, "Barotrauma.Lua") or string.startsWith(typeName, "Barotrauma.Cs") or string.startsWith(typeName, "Barotrauma.LuaCs") then - return false - end - - if string.startsWith(typeName, "System.Collections") then return true end - - if string.startsWith(typeName, "Microsoft.Xna") then return true end - - if string.startsWith(typeName, "Barotrauma.IO") then return false end - - if string.startsWith(typeName, "Barotrauma.ToolBox") then return false end - if string.startsWith(typeName, "Barotrauma.SaveUtil") then return false end - - if string.startsWith(typeName, "Barotrauma.") then return true end - - return false -end - -local function CanBeReRegistered(typeName) - if string.startsWith(typeName, "Barotrauma.Lua") or string.startsWith(typeName, "Barotrauma.Cs") or string.startsWith(typeName, "Barotrauma.LuaCs") then - return false - end - - return true -end - -local originalRegisterType = LuaUserData.RegisterType -LuaUserData.RegisterType = function (typeName) - if not (CanBeReRegistered(typeName) and LuaUserData.IsRegistered(typeName)) and not IsAllowed(typeName) then - error("Couldn't register type " .. typeName .. ".", 2) - end - - local success, result = pcall(originalRegisterType, typeName) - - if not success then - error(result, 2) - end - - return result -end - -local originalRegisterGenericType = LuaUserData.RegisterType -LuaUserData.RegisterGenericType = function (typeName, ...) - if not (CanBeReRegistered(typeName) and LuaUserData.IsRegistered(typeName)) and not IsAllowed(typeName) then - error("Couldn't register generic type " .. typeName .. ".", 2) - end - - local success, result = pcall(originalRegisterGenericType, typeName, ...) - - if not success then - error(result, 2) - end - - return result -end - -local originalCreateStatic = LuaUserData.CreateStatic -LuaUserData.CreateStatic = function (typeName, addCallMethod) - if not (CanBeReRegistered(typeName) and LuaUserData.IsRegistered(typeName)) and not IsAllowed(typeName) then - error("Couldn't create static type " .. typeName .. ".", 2) - end - - local success, result = pcall(originalCreateStatic, typeName, addCallMethod) - - if not success then - error(result, 2) - end - - return result -end \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/Luatrauma.props b/Barotrauma/BarotraumaShared/Luatrauma.props index cf9a48490..cbcdb4bcc 100644 --- a/Barotrauma/BarotraumaShared/Luatrauma.props +++ b/Barotrauma/BarotraumaShared/Luatrauma.props @@ -1,20 +1,23 @@ - - - - - - - - - - - - - en - + --> + + en + diff --git a/Barotrauma/BarotraumaShared/LuatraumaBuild.props b/Barotrauma/BarotraumaShared/LuatraumaBuild.props new file mode 100644 index 000000000..54a01bdad --- /dev/null +++ b/Barotrauma/BarotraumaShared/LuatraumaBuild.props @@ -0,0 +1,30 @@ + + + + + + + + + + + + diff --git a/Barotrauma/BarotraumaShared/README.txt b/Barotrauma/BarotraumaShared/README.txt deleted file mode 100644 index 6ba44b3f5..000000000 --- a/Barotrauma/BarotraumaShared/README.txt +++ /dev/null @@ -1,38 +0,0 @@ -BAROTRAUMA - -http://www.barotraumagame.com - -© 2017-2024 FakeFish Ltd. All rights reserved. -© 2019-2024 Daedalic Entertainment GmbH. The Daedalic logo is a trademark of Daedalic Entertainment GmbH, Germany. All rights reserved. -Privacy policy: http://privacypolicy.daedalic.com - -See the wiki for more detailed info and instructions: -http://barotraumagame.com/wiki - ------------------------------------------------------------------------- - -Port forwarding: -You may try to forward ports on your router using UPnP (Universal Plug and -Play) port forwarding by selecting "Attempt UPnP port forwarding" in the -"Host Server" menu. - -However, UPnP isn't supported by all routers, so you may need to setup port -forwards manually. The exact steps for forwarding a port depend on your -router's model, but you may be able to find a port forwarding guide for -your particular router/application on portforward.com or by practicing -your google-fu skills. - -These are the values that you should use when forwarding a port to your -Barotrauma server: - -Game port (used to communicate with clients) - Service/Application: barotrauma - External Port: The port you have selected for your server (27015 by default) - Internal Port: The port you have selected for your server (27015 by default) - Protocol: UDP - -Query port (used to communicate with Steam) - Service/Application: barotrauma - External Port: The port you have selected for your server (27016 by default) - Internal Port: The port you have selected for your server (27016 by default) - Protocol: UDP \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/AchievementManager.cs b/Barotrauma/BarotraumaShared/SharedSource/AchievementManager.cs index 6e06d8b33..517d909d1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/AchievementManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/AchievementManager.cs @@ -552,9 +552,14 @@ namespace Barotrauma private static void UnlockKillAchievement(Character killer, Character target, Identifier identifier) { - if (killer != null && - target.Params.UnlockKillAchievementForWholeCrew && - GameSession.GetSessionCrewCharacters(CharacterType.Player).Contains(killer)) + bool alwaysUnlockForWholeCrew = false; +#if CLIENT + alwaysUnlockForWholeCrew = GameMain.GameSession?.Campaign is SinglePlayerCampaign; +#endif + + if (killer != null && + (alwaysUnlockForWholeCrew || target.Params.UnlockKillAchievementForWholeCrew) && + GameSession.GetSessionCrewCharacters(CharacterType.Both).Contains(killer)) { UnlockAchievement(identifier, unlockClients: true, characterConditions: c => c != null); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AITarget.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AITarget.cs index 8c246a8c5..e044e515e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AITarget.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AITarget.cs @@ -103,6 +103,13 @@ namespace Barotrauma } } + + /// + /// A multiplier for the sound range for the purposes of displaying the target on sonar. + /// E.g. a value of 10 would mean the sonar can detect the target from x10 further than monsters. + /// + public float SoundRangeOnSonarMultiplier { get; private set; } = 1.0f; + public float SightRange { get { return sightRange; } @@ -260,6 +267,7 @@ namespace Barotrauma MinSoundRange = element.GetAttributeFloat("minsoundrange", 0f); MaxSightRange = element.GetAttributeFloat("maxsightrange", SightRange); MaxSoundRange = element.GetAttributeFloat("maxsoundrange", SoundRange); + SoundRangeOnSonarMultiplier = element.GetAttributeFloat(nameof(SoundRangeOnSonarMultiplier), 1.0f); FadeOutTime = element.GetAttributeFloat("fadeouttime", FadeOutTime); Static = element.GetAttributeBool("static", Static); StaticSight = element.GetAttributeBool("staticsight", StaticSight); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs index 49389c008..eeab5c9b6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs @@ -242,16 +242,44 @@ namespace Barotrauma } /// - /// 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. /// - public HashSet UnattackableSubmarines + private readonly HashSet unattackableSubmarines = []; + + /// + /// 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. + /// + public void SetUnattackableSubmarines(Submarine submarine, bool includeOwnSub = true, bool includeConnectedSubs = true, bool clearExisting = true) { - get; - private set; - } = new HashSet(); + if (clearExisting) + { + unattackableSubmarines.Clear(); + } + if (submarine != null) + { + AddSubs(submarine); + } + if (includeOwnSub && Character.Submarine is Submarine ownSub && ownSub != submarine) + { + AddSubs(ownSub); + } + + void AddSubs(Submarine sub) + { + unattackableSubmarines.Add(sub); + if (includeConnectedSubs) + { + foreach (Submarine connectedSub in sub.DockedTo) + { + unattackableSubmarines.Add(connectedSub); + } + } + } + } public static bool IsTargetBeingChasedBy(Character target, Character character) => character?.AIController is EnemyAIController enemyAI && enemyAI.SelectedAiTarget?.Entity == target && enemyAI.State is AIState.Attack or AIState.Aggressive; + public bool IsBeingChasedBy(Character c) => IsTargetBeingChasedBy(Character, c); private bool IsBeingChased => IsBeingChasedBy(SelectedAiTarget?.Entity as Character); @@ -539,26 +567,7 @@ namespace Barotrauma //doesn't do anything usually, but events may sometimes change monsters' (or pets' that use enemy AI) teams Character.UpdateTeam(); - bool ignorePlatforms = Character.AnimController.TargetMovement.Y < -0.5f && (-Character.AnimController.TargetMovement.Y > Math.Abs(Character.AnimController.TargetMovement.X)); - if (steeringManager == insideSteering) - { - var currPath = PathSteering.CurrentPath; - if (currPath != null && currPath.CurrentNode != null) - { - if (currPath.CurrentNode.SimPosition.Y < Character.AnimController.GetColliderBottom().Y) - { - // Don't allow to jump from too high. - float allowedJumpHeight = Character.AnimController.ImpactTolerance / 2; - float height = Math.Abs(currPath.CurrentNode.SimPosition.Y - Character.SimPosition.Y); - ignorePlatforms = height < allowedJumpHeight; - } - } - if (Character.IsClimbing && PathSteering.IsNextLadderSameAsCurrent) - { - Character.AnimController.TargetMovement = new Vector2(0.0f, Math.Sign(Character.AnimController.TargetMovement.Y)); - } - } - Character.AnimController.IgnorePlatforms = ignorePlatforms; + HandleLaddersAndPlatforms(deltaTime); if (Math.Abs(Character.AnimController.movement.X) > 0.1f && !Character.AnimController.InWater && (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer || Character.Controlled == Character)) @@ -986,6 +995,69 @@ namespace Barotrauma } } + //how often the character can try ragdolling to drop down + private const float MaxDroppingInterval = 5.0f; + + //last time the character tried ragdolling to drop down + private double lastDroppingTime; + + //how long the character can stay ragdolled to drop down + private const float MaxDroppingTime = 1.0f; + + //timer for the duration of the ragdolling + private float droppingTimer; + + private void HandleLaddersAndPlatforms(float deltaTime) + { + bool ignorePlatforms = Character.AnimController.TargetMovement.Y < -0.5f && (-Character.AnimController.TargetMovement.Y > Math.Abs(Character.AnimController.TargetMovement.X)); + if (steeringManager == insideSteering) + { + var currPath = PathSteering.CurrentPath; + if (currPath is { CurrentNode: WayPoint currentNode }) + { + Vector2 colliderBottom = Character.AnimController.GetColliderBottom(); + if (Character.Submarine != currentNode.Submarine) + { + colliderBottom = Submarine.GetRelativeSimPosition(colliderBottom, currentNode.Submarine, Character.Submarine); + } + if (currentNode.SimPosition.Y < colliderBottom.Y) + { + // Don't allow to jump from too high. + float allowedJumpHeight = Character.AnimController.ImpactTolerance / 2; + Vector2 diff = currentNode.WorldPosition - Character.WorldPosition; + float height = ConvertUnits.ToSimUnits(Math.Abs(diff.Y)); + ignorePlatforms = height < allowedJumpHeight; + + //trying to head down ladders, but can't climb -> periodically try ragdolling to get down + //(may be required by large monsters like mudraptors to fit through hatches) + if (ignorePlatforms && !Character.CanClimb && PathSteering.IsCurrentNodeLadder && + ConvertUnits.ToSimUnits(Math.Abs(diff.X)) < Character.AnimController.Collider.GetMaxExtent()) + { + if (lastDroppingTime < Timing.TotalTime - MaxDroppingInterval) + { + Character.IsRagdolled = true; + Character.SetInput(InputType.Ragdoll, hit: false, held: true); + droppingTimer += deltaTime; + if (droppingTimer > MaxDroppingTime) + { + lastDroppingTime = Timing.TotalTime; + } + } + else + { + droppingTimer = 0.0f; + } + } + } + } + if (Character.IsClimbing && PathSteering.IsNextLadderSameAsCurrent) + { + Character.AnimController.TargetMovement = new Vector2(0.0f, Math.Sign(Character.AnimController.TargetMovement.Y)); + } + } + Character.AnimController.IgnorePlatforms = ignorePlatforms; + } + #region Idle private void UpdateIdle(float deltaTime, bool followLastTarget = true) @@ -1229,6 +1301,8 @@ namespace Barotrauma return; } + if (Character.IsAttachedToController()) { return; } + attackWorldPos = SelectedAiTarget.WorldPosition; attackSimPos = SelectedAiTarget.SimPosition; @@ -1751,6 +1825,7 @@ namespace Barotrauma { SelectTarget(door.Item.AiTarget, currentTargetMemory.Priority); State = AIState.Attack; + AttackLimb = null; return; } } @@ -1761,12 +1836,20 @@ namespace Barotrauma float margin = AttackLimb != null ? Math.Min(AttackLimb.attack.Range * 0.9f, max) : max; if ((!canAttack || distance > margin) && !IsTryingToSteerThroughGap) { + bool useManualSteering = false; // Steer towards the target if in the same room and swimming // Ruins have walls/pillars inside hulls and therefore we should navigate around them using the path steering. if (Character.CurrentHull != null && Character.Submarine != null && !Character.Submarine.Info.IsRuin && (Character.AnimController.InWater || pursue || !Character.AnimController.CanWalk) && targetCharacter != null && VisibleHulls.Contains(targetCharacter.CurrentHull)) + { + if (CanSeeTarget(targetCharacter)) + { + useManualSteering = true; + } + } + if (useManualSteering) { Vector2 myPos = Character.AnimController.SimplePhysicsEnabled ? Character.SimPosition : steeringLimb.SimPosition; SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(attackSimPos - myPos)); @@ -2311,18 +2394,49 @@ namespace Barotrauma { float prio = 1 + limb.attack.Priority; if (Character.AnimController.SimplePhysicsEnabled) { return prio; } - float dist = Vector2.Distance(limb.WorldPosition, attackPos); - float distanceFactor = 1; + float distance = Vector2.Distance(limb.WorldPosition, attackPos); + float maxDistance = Math.Max(limb.attack.Range * 3, 1000); + if (distance > maxDistance) + { + // Far enough to ignore the attack. + return 0; + } + // Not in range, but relatively close. Let's use the distance factor as a multiplier. + float distanceFactor; if (limb.attack.Ranged) { float min = 100; - distanceFactor = MathHelper.Lerp(1, 0, MathUtils.InverseLerp(min, Math.Max(limb.attack.Range / 2, min), dist)); + if (distance < min) + { + // Too close -> smoothly but steeply reduce the preference (and prefer other attacks, like melee instead) + float t = MathUtils.InverseLerp(0, min, distance); + distanceFactor = MathHelper.Lerp(0.01f, 1, t * t); + } + else + { + distanceFactor = MathHelper.Lerp(1, 0, MathUtils.InverseLerp(min, maxDistance, distance)); + } } else { - // The limb is ignored if the target is not close. Prevents character going in reverse if very far away from it. - // We also need a max value that is more than the actual range. - distanceFactor = MathHelper.Lerp(1, 0, MathUtils.InverseLerp(0, limb.attack.Range * 3, dist)); + if (distance <= limb.attack.Range) + { + // In range. + if (!Character.InWater) + { + // On dry land vertical distance works a bit differently, as we can't necessarily reach the target above/below us. + float verticalDistance = Math.Abs(limb.WorldPosition.Y - attackPos.Y); + if (verticalDistance > limb.attack.DamageRange) + { + // Most likely can't reach. + return 0; + } + } + // Highly prefer attacks which we can use to hit immediately. + return prio * 10; + } + float min = limb.attack.Range; + distanceFactor = MathHelper.Lerp(1, 0, MathUtils.InverseLerp(min, maxDistance, distance)); } return prio * distanceFactor; } @@ -2521,6 +2635,7 @@ namespace Barotrauma { SelectTarget(aiTarget, GetTargetMemory(SelectedAiTarget, addIfNotFound: true).Priority); State = AIState.Attack; + AttackLimb = null; return true; } } @@ -2555,14 +2670,10 @@ namespace Barotrauma return true; } } - if (damageTarget != null) - { - Character.SetInput(item.IsShootable ? InputType.Shoot : InputType.Use, false, true); - item.Use(deltaTime, user: Character); - } + Character.SetInput(item.IsShootable ? InputType.Shoot : InputType.Use, false, true); + item.Use(deltaTime, user: Character); } } - if (damageTarget == null) { return true; } //simulate attack input to get the character to attack client-side Character.SetInput(InputType.Attack, true, true); if (!ActiveAttack.IsRunning) @@ -2609,10 +2720,24 @@ namespace Barotrauma } return true; } + + private const float VisibilityCheckStep = 0.2f; + private double lastVisibilityCheckTime; + private bool canSeeTarget; + /// + /// This method uses and caches the results. + /// + private bool CanSeeTarget(ISpatialEntity target) + { + if (Timing.TotalTime > lastVisibilityCheckTime + VisibilityCheckStep) + { + canSeeTarget = Character.CanSeeTarget(target); + lastVisibilityCheckTime = Timing.TotalTime; + } + return canSeeTarget; + } private float aimTimer; - private float visibilityCheckTimer; - private bool canSeeTarget; private float sinTime; private bool Aim(float deltaTime, ISpatialEntity target, Item weapon) { @@ -2630,13 +2755,7 @@ namespace Barotrauma { Character.CursorPosition -= Character.Submarine.Position; } - visibilityCheckTimer -= deltaTime; - if (visibilityCheckTimer <= 0.0f) - { - canSeeTarget = Character.CanSeeTarget(target); - visibilityCheckTimer = 0.2f; - } - if (!canSeeTarget) + if (!CanSeeTarget(target)) { SetAimTimer(); return false; @@ -2817,7 +2936,10 @@ namespace Barotrauma } } steeringManager.SteeringManual(deltaTime, Vector2.Normalize(limbDiff) * 3); - Character.AnimController.Collider.ApplyForce(limbDiff * mouthLimb.Mass * 50.0f, mouthPos); + if (Character.AnimController.OnGround || Character.InWater) + { + Character.AnimController.Collider.ApplyForce(limbDiff * mouthLimb.Mass * 50.0f, maxVelocity: 10.0f); + } } } else @@ -2956,12 +3078,18 @@ 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 || - UnattackableSubmarines.Contains(aiTarget.Entity.Submarine)) + aiTarget.Entity.Submarine.Info.IsBeacon) + { + continue; + } + if (aiTarget.Entity is Structure or Hull or Item { body: null } && + unattackableSubmarines.Contains(aiTarget.Entity.Submarine)) { continue; } @@ -3509,13 +3637,16 @@ namespace Barotrauma { if (targetCharacter.Submarine != null) { - // Target is inside -> reduce the priority - valueModifier *= 0.5f; - if (Character.Submarine != null) + if (Character.Submarine != null && !targetCharacter.Submarine.IsConnectedTo(Character.Submarine)) { - // Both inside different submarines -> can ignore safely + // Both inside different, unconnected submarines -> can ignore safely continue; } + else + { + // Target is inside a submarine that we are not -> reduce the priority + valueModifier *= 0.5f; + } } else if (Character.CurrentHull != null) { @@ -4402,6 +4533,7 @@ namespace Barotrauma { SelectTarget(doorAiTarget, CurrentTargetMemory.Priority); State = AIState.Attack; + AttackLimb = null; return false; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs index e1eb0c40e..c729bfcbc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs @@ -1386,7 +1386,7 @@ namespace Barotrauma } else { - isAttackerInfected = attacker.CharacterHealth.GetAfflictionStrengthByType(AfflictionPrefab.AlienInfectedType) > 0; + isAttackerInfected = attacker.CharacterHealth.GetAfflictionStrengthByType(AfflictionPrefab.AlienInfectionType) > 0; // Inform other NPCs if (isAttackerInfected || cumulativeDamage > minorDamageThreshold || totalDamage > minorDamageThreshold) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs index 60ad0799d..5c7447555 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs @@ -4,6 +4,7 @@ using Microsoft.Xna.Framework; using System; using System.Linq; using FarseerPhysics; +using System.Diagnostics; namespace Barotrauma { @@ -50,7 +51,7 @@ namespace Barotrauma } /// - /// Returns true if any node in the path is in stairs + /// Returns true if any node in the path is on stairs /// public bool PathHasStairs => currentPath != null && currentPath.Nodes.Any(n => n.Stairs != null); @@ -285,14 +286,17 @@ namespace Barotrauma } } - Vector2 diff = DiffToCurrentNode(); + Vector2 diff = GetDiffAndAdvance(); if (diff == Vector2.Zero) { return Vector2.Zero; } return Vector2.Normalize(diff) * weight; } protected override Vector2 DoSteeringSeek(Vector2 target, float weight) => CalculateSteeringSeek(target, weight); - - private Vector2 DiffToCurrentNode() + + /// + /// Decides whether and when we should skip to the next node. Returns the difference to the current node (after skipping). + /// + private Vector2 GetDiffAndAdvance() { if (currentPath == null || currentPath.Unreachable) { @@ -320,26 +324,37 @@ namespace Barotrauma Reset(); return Vector2.Zero; } - Vector2 pos = host.WorldPosition; - Vector2 diff = currentPath.CurrentNode.WorldPosition - pos; + WayPoint currentNode = currentPath.CurrentNode; + WayPoint nextNode = currentPath.NextNode; + Vector2 diff = currentNode.WorldPosition - host.WorldPosition; + float horizontalDistance = Math.Abs(diff.X); + float verticalDistance = Math.Abs(diff.Y); bool isDiving = character.AnimController.InWater && character.AnimController.HeadInWater; bool canClimb = character.CanClimb; Ladder currentLadder = GetCurrentLadder(); Ladder nextLadder = GetNextLadder(); - var ladders = currentLadder ?? nextLadder; + Ladder ladders = currentLadder ?? nextLadder; bool useLadders = canClimb && ladders != null; var collider = character.AnimController.Collider; - Vector2 colliderSize = collider.GetSize(); + Vector2 colliderSize = ConvertUnits.ToDisplayUnits(collider.GetSize()); + float colliderHeight = colliderSize.Y; + if (character.AnimController.CurrentAnimationParams is FishGroundedParams fishGrounded) + { + // On monsters, the main collider might be rotated, so we need to take that into account here. + float standAngle = fishGrounded.ColliderStandAngleInRadians * character.AnimController.Dir; + Vector2 transformedColliderSize = PhysicsBody.RotateVector(colliderSize, standAngle); + colliderHeight = Math.Abs(transformedColliderSize.Y); + } if (useLadders) { - if (character.IsClimbing && Math.Abs(diff.X) - ConvertUnits.ToDisplayUnits(colliderSize.X) > Math.Abs(diff.Y)) + if (character.IsClimbing && Math.Abs(diff.X) - colliderSize.X > Math.Abs(diff.Y)) { // If the current node is horizontally farther from us than vertically, we don't want to keep climbing the ladders. useLadders = false; } - else if (!character.IsClimbing && currentPath.NextNode != null && nextLadder == null) + else if (!character.IsClimbing && nextNode != null && nextLadder == null) { - Vector2 diffToNextNode = currentPath.NextNode.WorldPosition - pos; + Vector2 diffToNextNode = nextNode.WorldPosition - host.WorldPosition; if (Math.Abs(diffToNextNode.X) > Math.Abs(diffToNextNode.Y)) { // If the next node is horizontally farther from us than vertically, we don't want to start climbing. @@ -356,7 +371,7 @@ namespace Barotrauma { if (currentPath.IsAtEndNode && canClimb && ladders != null) { - // Don't release the ladders when ending a path in ladders. + // Don't release the ladders when ending a path on ladders. useLadders = true; } else @@ -388,20 +403,18 @@ namespace Barotrauma if (currentLadder == null && nextLadder != null && character.SelectedSecondaryItem == nextLadder.Item) { // Climbing a ladder but the path is still on the node next to the ladder -> Skip the node. - NextNode(!doorsChecked); + return NextNode(!doorsChecked); } else { bool nextLadderSameAsCurrent = currentLadder == nextLadder; - float colliderHeight = collider.Height / 2 + collider.Radius; - float heightDiff = currentPath.CurrentNode.SimPosition.Y - collider.SimPosition.Y; - float distanceMargin = ConvertUnits.ToDisplayUnits(colliderSize.X); + float distanceMargin = colliderSize.X; if (currentLadder != null && nextLadder != null) { //climbing ladders -> don't move horizontally diff.X = 0.0f; } - if (Math.Abs(heightDiff) < colliderHeight * 1.25f) + if (verticalDistance < colliderHeight / 2 * 1.25f) { if (nextLadder != null && !nextLadderSameAsCurrent) { @@ -410,7 +423,7 @@ namespace Barotrauma { if (nextLadder.Item.TryInteract(character, forceSelectKey: true)) { - NextNode(!doorsChecked); + return NextNode(!doorsChecked); } } } @@ -432,9 +445,9 @@ namespace Barotrauma } if (isAboveFloor) { - if (Math.Abs(diff.Y) < distanceMargin) + if (verticalDistance < distanceMargin) { - NextNode(!doorsChecked); + return NextNode(!doorsChecked); } else if (!currentPath.IsAtEndNode && (nextLadder == null || (currentLadder != null && Math.Abs(currentLadder.Item.WorldPosition.X - nextLadder.Item.WorldPosition.X) > distanceMargin))) { @@ -443,14 +456,21 @@ namespace Barotrauma } } } - else if (currentLadder != null && currentPath.NextNode != null) + else if (currentLadder != null && nextNode != null) { - if (Math.Sign(currentPath.CurrentNode.WorldPosition.Y - character.WorldPosition.Y) != Math.Sign(currentPath.NextNode.WorldPosition.Y - character.WorldPosition.Y)) + if (Math.Sign(currentNode.WorldPosition.Y - character.WorldPosition.Y) != Math.Sign(nextNode.WorldPosition.Y - character.WorldPosition.Y)) { //if the current node is below the character and the next one is above (or vice versa) //and both are on ladders, we can skip directly to the next one //e.g. no point in going down to reach the starting point of a path when we could go directly to the one above - NextNode(!doorsChecked); + return NextNode(!doorsChecked); + } + //heading towards a ladder waypoint below the character, but the next waypoint is above it on the same ladder + // -> allow skipping to that waypoint. + // Otherwise the character may get stuck trying to move to a waypoint near the floor at the bottom of the ladder, failing to get close enough because they can't move any lower. + else if (nextLadderSameAsCurrent && diff.Y < 0 && nextNode.WorldPosition.Y > currentNode.WorldPosition.Y) + { + return NextNode(!doorsChecked); } } } @@ -458,21 +478,20 @@ namespace Barotrauma else if (character.AnimController.InWater) { // Swimming - var door = currentPath.CurrentNode.ConnectedDoor; + var door = currentNode.ConnectedDoor; if (door == null || door.CanBeTraversed) { - float margin = MathHelper.Lerp(1, 5, MathHelper.Clamp(collider.LinearVelocity.Length() / 10, 0, 1)); - float targetDistance = Math.Max(Math.Max(colliderSize.X, colliderSize.Y) / 2 * margin, 0.5f); - float horizontalDistance = Math.Abs(character.WorldPosition.X - currentPath.CurrentNode.WorldPosition.X); - float verticalDistance = Math.Abs(character.WorldPosition.Y - currentPath.CurrentNode.WorldPosition.Y); - if (character.CurrentHull != currentPath.CurrentNode.CurrentHull) + float distanceMultiplier = MathHelper.Lerp(1, 5, MathHelper.Clamp(collider.LinearVelocity.Length() / 10, 0, 1)); + float targetDistance = Math.Max(Math.Max(colliderSize.X, colliderSize.Y) / 2 * distanceMultiplier, 0.5f); + float modifiedVerticalDist = verticalDistance; + if (character.CurrentHull != currentNode.CurrentHull) { - verticalDistance *= 2; + modifiedVerticalDist *= 2; } - float distance = horizontalDistance + verticalDistance; - if (ConvertUnits.ToSimUnits(distance) < targetDistance) + float distance = horizontalDistance + modifiedVerticalDist; + if (distance < targetDistance) { - NextNode(!doorsChecked); + return NextNode(!doorsChecked); } } } @@ -480,6 +499,10 @@ namespace Barotrauma { // Walking horizontally Vector2 colliderBottom = character.AnimController.GetColliderBottom(); + if (character.Submarine != currentNode.Submarine) + { + colliderBottom = Submarine.GetRelativeSimPosition(colliderBottom, currentNode.Submarine, character.Submarine); + } Vector2 velocity = collider.LinearVelocity; // If the character is very short, it would fail to use the waypoint nodes because they are always too high. // If the character is very thin, it would often fail to reach the waypoints, because the horizontal distance is too small. @@ -487,60 +510,113 @@ namespace Barotrauma float minHeight = 1.6125001f; float minWidth = 0.3225f; // Cannot use the head position, because not all characters have head or it can be below the total height of the character - float characterHeight = Math.Max(colliderSize.Y + character.AnimController.ColliderHeightFromFloor, minHeight); - float horizontalDistance = Math.Abs(collider.SimPosition.X - currentPath.CurrentNode.SimPosition.X); - bool isTargetTooHigh = currentPath.CurrentNode.SimPosition.Y > colliderBottom.Y + characterHeight; - bool isTargetTooLow = currentPath.CurrentNode.SimPosition.Y < colliderBottom.Y; - var door = currentPath.CurrentNode.ConnectedDoor; - float margin = MathHelper.Lerp(1, 10, MathHelper.Clamp(Math.Abs(velocity.X) / 5, 0, 1)); - float colliderHeight = collider.Height / 2 + collider.Radius; - if (currentPath.CurrentNode.Stairs == null) + float characterHeight = Math.Max(ConvertUnits.ToSimUnits(colliderHeight) + character.AnimController.ColliderHeightFromFloor, minHeight); + bool isTargetTooHigh = currentNode.SimPosition.Y > colliderBottom.Y + characterHeight; + bool isTargetTooLow = currentNode.SimPosition.Y < colliderBottom.Y; + var door = currentNode.ConnectedDoor; + float targetDistanceMultiplier = MathHelper.Lerp(1, 10, MathHelper.Clamp(Math.Abs(velocity.X) / 5, 0, 1)); + if (currentNode.Stairs == null) { - float heightDiff = currentPath.CurrentNode.SimPosition.Y - collider.SimPosition.Y; - if (heightDiff < colliderHeight) + // Only attempt dropping if the node is below the collider bottom. + // Using the next node position here, because the current node might be on the top of the ladder, which can be at the same level with the character or even above it. + bool isBelowEnough = (nextNode ?? currentNode).WorldPosition.Y < character.WorldPosition.Y - colliderHeight / 2; + bool drop = false; + if (isBelowEnough) { - // Original comment: - //the waypoint is between the top and bottom of the collider, no need to move vertically. - // Note that the waypoint can be below collider too! This might be incorrect. + if (!canClimb) + { + // Can't climb -> check if we should drop. + Door nextDoor = door ?? nextNode?.ConnectedDoor; + if (nextDoor is Door { IsHorizontal: true, CanBeTraversed: true } openHatch) + { + bool isHatchBelowCharacter = openHatch.LinkedGap.WorldPosition.Y < character.WorldPosition.Y; + if (isHatchBelowCharacter) + { + // Trying to go through an open hatch below us -> drop. + drop = true; + } + } + else if (currentLadder != null && !isTargetTooLow && nextDoor == null) + { + // On ladders -> drop. + drop = true; + } + } + } + if (drop) + { + return NextNode(!doorsChecked); + } + else if (verticalDistance < colliderHeight / 2) + { + // The waypoint is between the top and bottom of the collider, and we don't intend to drop -> no need to move vertically. diff.Y = 0.0f; } } else { - // In stairs - bool isNextNodeInSameStairs = currentPath.NextNode?.Stairs == currentPath.CurrentNode.Stairs; + // On stairs + bool isNextNodeInSameStairs = nextNode?.Stairs == currentNode.Stairs; if (!isNextNodeInSameStairs) { - margin = 1; - if (currentPath.CurrentNode.SimPosition.Y < colliderBottom.Y + character.AnimController.ColliderHeightFromFloor * 0.25f) + targetDistanceMultiplier = 1; + if (currentNode.SimPosition.Y < colliderBottom.Y + character.AnimController.ColliderHeightFromFloor * 0.25f) { isTargetTooLow = true; } + Structure nextStairs = nextNode?.Stairs; + if (character.AnimController.Stairs != null && nextStairs != null) + { + //currently on stairs, and the next node is not in the same stairs + // -> we must get off the current stairs first before we can skip to the next node, otherwise the character + // would attempt to get "through the stairs" to the next ones + if (character.AnimController.Stairs.StairDirection == Direction.Right) + { + //the direction in which the bot should keep moving depends on the direction of the stairs and whether we're going up or down + diff = nextStairs.WorldPosition.Y > character.AnimController.Stairs.WorldPosition.Y ? Vector2.UnitX : -Vector2.UnitX; + } + else + { + diff = nextStairs.WorldPosition.Y > character.AnimController.Stairs.WorldPosition.Y ? -Vector2.UnitX : Vector2.UnitX; + } + } } } - float targetDistance = Math.Max(colliderSize.X / 2 * margin, minWidth / 2); - if (horizontalDistance < targetDistance && !isTargetTooHigh && !isTargetTooLow) + // Walking horizontally, check whether we are close enough to the current node. + float targetDistance = Math.Max(colliderSize.X / 2 * targetDistanceMultiplier, ConvertUnits.ToDisplayUnits(minWidth / 2)); + Debug.Assert(targetDistance < 500, "Target distance too large (a character is trying to skip on their path to a waypoint far away), something is probably off here."); + if (!isTargetTooHigh && !isTargetTooLow && horizontalDistance < targetDistance) { - if (door is not { CanBeTraversed: false } && (currentLadder == null || nextLadder == null)) + bool isBlockedByDoor = door is { CanBeTraversed: false }; + // If both the current ladder and the next ladder are not null, we are in the middle of ladders and should let the code above handle advancing the nodes. + // However, if either one is null, and we get here, we are probably walking to or from ladders. + bool notOnLadders = currentLadder == null || nextLadder == null; + if (!isBlockedByDoor && notOnLadders) { - NextNode(!doorsChecked); + return NextNode(!doorsChecked); } } } - if (currentPath.CurrentNode == null) + return ReturnDiff(); + + Vector2 NextNode(bool checkDoors) { - return Vector2.Zero; + if (checkDoors) + { + CheckDoorsInPath(); + } + currentPath.SkipToNextNode(); + return ReturnDiff(); } - return ConvertUnits.ToSimUnits(diff); - } - - private void NextNode(bool checkDoors) - { - if (checkDoors) + + Vector2 ReturnDiff() { - CheckDoorsInPath(); + if (currentPath.CurrentNode == null) + { + return Vector2.Zero; + } + return ConvertUnits.ToSimUnits(diff); } - currentPath.SkipToNextNode(); } public bool CanAccessDoor(Door door, Func buttonFilter = null) @@ -600,8 +676,6 @@ namespace Barotrauma } } - private Vector2 GetColliderSize() => ConvertUnits.ToDisplayUnits(character.AnimController.Collider.GetSize()); - private float GetColliderLength() { Vector2 colliderSize = character.AnimController.Collider.GetSize(); @@ -676,7 +750,7 @@ namespace Barotrauma if (door.LinkedGap.IsHorizontal) { int dir = Math.Sign(nextWaypoint.WorldPosition.X - door.Item.WorldPosition.X); - float size = character.AnimController.InWater ? colliderLength : GetColliderSize().X; + float size = character.AnimController.InWater ? colliderLength : ConvertUnits.ToDisplayUnits(character.AnimController.Collider.GetSize()).X; shouldBeOpen = (door.Item.WorldPosition.X - character.WorldPosition.X) * dir > -size; } else @@ -794,12 +868,17 @@ namespace Barotrauma if (character == null) { return 0.0f; } float? penalty = GetSingleNodePenalty(nextNode); if (penalty == null) { return null; } + Vector2 nextNodePosition = nextNode.Position; + if (nextNode.Waypoint.Submarine != node.Waypoint.Submarine) + { + nextNodePosition = Submarine.GetRelativeSimPosition(nextNodePosition, node.Waypoint.Submarine, nextNode.Waypoint.Submarine); + } bool nextNodeAboveWaterLevel = nextNode.Waypoint.CurrentHull != null && nextNode.Waypoint.CurrentHull.Surface < nextNode.Waypoint.Position.Y; if (!character.CanClimb && node.Waypoint.Stairs == null && nextNode.Waypoint.Stairs == null) { if (node.Waypoint.Ladders != null && nextNode.Waypoint.Ladders != null && (!nextNode.Waypoint.Ladders.Item.IsInteractable(character) || character.LockHands) || - (nextNode.Position.Y - node.Position.Y > 1.0f && //more than one sim unit to climb up - nextNodeAboveWaterLevel)) //upper node not underwater + (nextNodePosition.Y - node.Position.Y > 1.0f && //more than one sim unit to climb up + nextNodeAboveWaterLevel)) //upper node not underwater { return null; } @@ -830,7 +909,7 @@ namespace Barotrauma } } - float yDist = Math.Abs(node.Position.Y - nextNode.Position.Y); + float yDist = Math.Abs(node.Position.Y - nextNodePosition.Y); if (nextNodeAboveWaterLevel && node.Waypoint.Ladders == null && nextNode.Waypoint.Ladders == null && node.Waypoint.Stairs == null && nextNode.Waypoint.Stairs == null) { penalty += yDist * 10.0f; @@ -898,18 +977,14 @@ namespace Barotrauma //steer away from edges of the hull bool wander = false; bool inWater = character.AnimController.InWater; - Hull currentHull = character.CurrentHull; - // TODO: disabled for now, because seems to cause bots to walk towards walls/doors in some places. In some places it's because how the hulls are defined, but there is probably something else too, is it seems to happen also elsewhere. - // if (!inWater) - // { - // Vector2 colliderBottomPos = ConvertUnits.ToDisplayUnits(character.AnimController.GetColliderBottom()); - // if (Hull.FindHull(colliderBottomPos, guess: currentHull, useWorldCoordinates: false) is Hull lowestHull) - // { - // // Use the hull found at the collider bottom, if found. - // // Makes difference in some rooms that have multiple hulls, of which the lowest hull where the feet are might not be the same as where the center position of the main collider is. - // currentHull = lowestHull; - // } - // } + + //use the hull the legs are in (if one is found), so the character won't walk against the wall when their torso is in a different hull where there'd be room to walk further + //(e.g. if the character is in a shallow pool-type room, like in ResearchModule_01_Colony) + Hull currentHull = + character.AnimController.GetLimb(LimbType.RightLeg)?.Hull ?? + character.AnimController.GetLimb(LimbType.LeftLeg)?.Hull ?? + character.CurrentHull; + if (currentHull != null && !inWater) { float roomWidth = currentHull.Rect.Width; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs index e27bf7a04..664813c08 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs @@ -103,9 +103,19 @@ namespace Barotrauma } } - // For temporarily forcing walking. Will reset after each priority calculation, so it will need to be kept alive by something. - // The intention of this boolean to allow walking even when the priority is higher than AIObjectiveManager.RunPriority. - public bool ForceWalk { get; set; } + /// + /// For temporarily forcing walking. Will reset after each priority calculation, so it will need to be kept alive by something. + /// The intention of this boolean to allow walking even when the priority is higher than AIObjectiveManager.RunPriority. + /// + public bool ForceWalkTemporarily { get; set; } + + /// + /// Forces the character to walk when executing this objective, even if the priority is above . + /// Unlike , this value is not automatically reset. + /// + public bool ForceWalkPermanently { get; set; } + + public bool ForceWalk => ForceWalkTemporarily || ForceWalkPermanently; public bool IgnoreAtOutpost { get; set; } @@ -313,7 +323,7 @@ namespace Barotrauma /// public float CalculatePriority() { - ForceWalk = false; + ForceWalkTemporarily = false; Priority = GetPriority(); ForceHighestPriority = false; return Priority; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItems.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItems.cs index 89fea879b..394e2c378 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItems.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItems.cs @@ -40,7 +40,7 @@ namespace Barotrauma if (subObjectives.All(so => so.SubObjectives.None())) { // If none of the subobjectives have subobjectives, no valid container was found. Don't allow running. - ForceWalk = true; + ForceWalkTemporarily = true; } return prio; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs index 42e35c1b2..093cf34a2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs @@ -258,13 +258,14 @@ namespace Barotrauma protected override bool CheckObjectiveState() { - if (character.Submarine is { TeamID: CharacterTeamType.FriendlyNPC } && character.Submarine == Enemy.Submarine) + // In a friendly outpost, and the target is still in the outpost + if (character.Submarine is { Info.IsOutpost: true } && character.IsOnFriendlyTeam(character.Submarine.TeamID) && + character.Submarine == Enemy.Submarine) { - // Target still in the outpost + // Outpost guards shouldn't lose the target in friendly outposts, + // However, if we are not a guard, let's ensure that we allow the cooldown. if (character.TeamID == CharacterTeamType.FriendlyNPC && !character.IsSecurity) { - // Outpost guards shouldn't lose the target in friendly outposts, - // However, if we are not a guard, let's ensure that we allow the cooldown. allowCooldown = true; } } @@ -286,7 +287,8 @@ namespace Barotrauma { allowCooldown = true; // Target not in the outpost anymore. - if (character.CanSeeTarget(Enemy)) + if (character.Submarine.IsConnectedTo(Enemy.Submarine) && + character.CanSeeTarget(Enemy)) { allowCooldown = false; coolDownTimer = DefaultCoolDown; @@ -389,7 +391,7 @@ namespace Barotrauma HumanAIController.AutoFaceMovement = false; if (!gotoObjective.ShouldRun(true)) { - ForceWalk = true; + ForceWalkTemporarily = true; } } } @@ -468,7 +470,7 @@ namespace Barotrauma isMoving = true; if (!IsEnemyClose(MeleeDistance)) { - ForceWalk = true; + ForceWalkTemporarily = true; } HumanAIController.FaceTarget(Enemy); HumanAIController.AutoFaceMovement = false; @@ -1234,7 +1236,7 @@ namespace Barotrauma } if (isAimBlocked) { - ForceWalk = true; + ForceWalkTemporarily = true; } if (!followTargetObjective.IsCloseEnough) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveDeconstructItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveDeconstructItem.cs index 2f0f8ec71..c3fd16668 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveDeconstructItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveDeconstructItem.cs @@ -95,7 +95,11 @@ namespace Barotrauma if (potentialDeconstructor?.InputContainer == null) { continue; } if (!potentialDeconstructor.InputContainer.Inventory.CanBePut(Item)) { continue; } if (!potentialDeconstructor.Item.HasAccess(character)) { continue; } - if (Item.Prefab.DeconstructItems.None(it => it.IsValidDeconstructor(otherItem))) { continue; } + if (Item.Prefab.DeconstructItems.Any() && + Item.Prefab.DeconstructItems.None(it => it.IsValidDeconstructor(otherItem))) + { + continue; + } float distFactor = GetDistanceFactor(Item.WorldPosition, potentialDeconstructor.Item.WorldPosition, factorAtMaxDistance: 0.2f); if (distFactor > bestDistFactor) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveDeconstructItems.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveDeconstructItems.cs index 781ed4746..391bbc4dd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveDeconstructItems.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveDeconstructItems.cs @@ -64,7 +64,11 @@ namespace Barotrauma if (target == null || target.Removed) { return false; } //bots can't handle deconstructing items that require another item to deconstruct, let's not try to do that //in the vanilla game, this means unidentified genetic materials, which we don't want to "deconstruct" anyway - if (target.Prefab.DeconstructItems.All(d => d.RequiredOtherItem.Length > 0)) { return false; } + if (target.Prefab.DeconstructItems.Any() && + target.Prefab.DeconstructItems.All(d => d.RequiredOtherItem.Length > 0)) + { + return false; + } // If the target was selected as a valid target, we'll have to accept it so that the objective can be completed. // The validity changes when a character picks the item up. if (!IsValidTarget(target, character, checkInventory: true)) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFire.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFire.cs index 53ec38880..0a57628a2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFire.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFire.cs @@ -148,7 +148,7 @@ namespace Barotrauma character.Speak(TextManager.GetWithVariable("DialogPutOutFire", "[roomname]", targetHull.DisplayName, FormatCapitals.Yes).Value, null, 0, "putoutfire".ToIdentifier(), 10.0f); } // Prevents running into the flames. - objectiveManager.CurrentObjective.ForceWalk = true; + objectiveManager.CurrentObjective.ForceWalkTemporarily = true; } if (moveCloser) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFires.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFires.cs index af532273c..c5790c08d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFires.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFires.cs @@ -11,6 +11,8 @@ namespace Barotrauma public override Identifier Identifier { get; set; } = "extinguish fires".ToIdentifier(); public override bool ForceRun => true; protected override bool AllowInAnySub => true; + // Periodically clear the ignore list so that fires abandoned when fumbling with finding an extinguisher, navigating etc get reconsidered + protected override float IgnoreListClearInterval => 30; public AIObjectiveExtinguishFires(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier) { } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs index 94a876ca7..b2f0ae419 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs @@ -49,6 +49,13 @@ namespace Barotrauma public const float DefaultReach = 100; public const float MaxReach = 150; + /// + /// How long it takes for the objective to be abandoned if no suitable item is found. + /// Intended to be an optimization: if the bots are constantly trying to find some item (like a diving suit), + /// it can easily lead to performance issues when e.g. AIObjectiveFindDivingGear constantly starts up new GetItem objectives. + /// + private float abandonDelayIfItemNotFound = 5.0f; + /// /// Is the goal of this objective to get diving gear (i.e. has it been created by )? /// If so, the objective won't attempt to create another objective if the path requires diving gear @@ -213,7 +220,7 @@ namespace Barotrauma { if (isDoneSeeking) { - HandlePotentialItems(); + HandlePotentialItems(deltaTime); } if (objectiveManager.CurrentOrder is not AIObjectiveGoTo) { @@ -389,6 +396,8 @@ namespace Barotrauma // If the root container changes, the item is no longer where it was (taken by someone -> need to find another item) AbortCondition = obj => targetItem == null || (targetItem.GetRootInventoryOwner() is Entity owner && owner != moveToTarget && owner != character), SpeakIfFails = false, + ForceWalkTemporarily = this.ForceWalkTemporarily, + ForceWalkPermanently = this.ForceWalkPermanently, endNodeFilter = CreateEndNodeFilter(moveToTarget) }; }, @@ -598,7 +607,7 @@ namespace Barotrauma } } - private void HandlePotentialItems() + private void HandlePotentialItems(float deltaTime) { Debug.Assert(isDoneSeeking); if (itemCandidates.Any()) @@ -652,10 +661,14 @@ namespace Barotrauma } else { -#if DEBUG - DebugConsole.NewMessage($"{character.Name}: Cannot find an item with the following identifier(s) or tag(s): {string.Join(", ", IdentifiersOrTags)}", Color.Yellow); -#endif - Abandon = true; + abandonDelayIfItemNotFound -= deltaTime; + if (abandonDelayIfItemNotFound <= 0.0f) + { + #if DEBUG + DebugConsole.NewMessage($"{character.Name}: Cannot find an item with the following identifier(s) or tag(s): {string.Join(", ", IdentifiersOrTags)}", Color.Yellow); + #endif + Abandon = true; + } } } } @@ -718,13 +731,15 @@ namespace Barotrauma private bool CheckItem(Item item) { + bool matchesIdentifiersOrTags = item.HasIdentifierOrTags(IdentifiersOrTags) || (AllowVariants && !item.Prefab.VariantOf.IsEmpty && IdentifiersOrTags.Contains(item.Prefab.VariantOf)); + if (!matchesIdentifiersOrTags) { return false; } if (!item.HasAccess(character)) { return false; } if (ignoredItems.Contains(item)) { return false; }; if (ignoredIdentifiersOrTags != null && item.HasIdentifierOrTags(ignoredIdentifiersOrTags)) { return false; } if (item.Condition < TargetCondition) { return false; } if (ItemFilter != null && !ItemFilter(item)) { return false; } if (RequireNonEmpty && item.Components.Any(i => i.IsEmpty(character))) { return false; } - return item.HasIdentifierOrTags(IdentifiersOrTags) || (AllowVariants && !item.Prefab.VariantOf.IsEmpty && IdentifiersOrTags.Contains(item.Prefab.VariantOf)); + return true; } public override void Reset() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs index fd5ade494..79786bcea 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs @@ -958,6 +958,7 @@ namespace Barotrauma public bool ShouldRun(bool run) { + if (ForceWalk) { return false; } if (run && objectiveManager.ForcedOrder == this && IsWaitOrder && !character.IsOnPlayerTeam) { // NPCs with a wait order don't run. diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs index efa495d6c..1a2d14048 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs @@ -267,7 +267,10 @@ namespace Barotrauma if (node.Waypoint.CurrentHull != character.CurrentHull && HumanAIController.UnsafeHulls.Contains(node.Waypoint.CurrentHull)) { return false; } return true; //don't stop at ladders when idling - }, endNodeFilter: node => node.Waypoint.Stairs == null && node.Waypoint.Ladders == null && (!isCurrentHullAllowed || !IsForbidden(node.Waypoint.CurrentHull))); + }, endNodeFilter: node => + node.Waypoint.Stairs == null && node.Waypoint.CurrentHull == currentTarget && node.Waypoint.Ladders == null && + (!isCurrentHullAllowed || !IsForbidden(node.Waypoint.CurrentHull))); + if (path.Unreachable) { //can't go to this room, remove it from the list and try another room diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveInspectNoises.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveInspectNoises.cs index b8639dd08..6439708bc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveInspectNoises.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveInspectNoises.cs @@ -78,9 +78,10 @@ namespace Barotrauma if (item.GetRootInventoryOwner() is Character targetCharacter && AIObjectiveFightIntruders.IsValidTarget(targetCharacter, character, targetCharactersInOtherSubs: false)) { - float dist = character.CurrentHull.GetApproximateDistance(character.Position, targetCharacter.Position, targetCharacter.CurrentHull, aiTarget.SoundRange, distanceMultiplierPerClosedDoor: 2); - if (dist * HumanAIController.Hearing > aiTarget.SoundRange) { continue; } - + float range = aiTarget.SoundRange * HumanAIController.Hearing; + float dist = character.CurrentHull.GetApproximateDistance(character.Position, targetCharacter.Position, targetCharacter.CurrentHull, range, distanceMultiplierPerClosedDoor: 2); + if (dist > range) { continue; } + character.Speak(TextManager.Get("dialogheardenemy").Value, identifier: "heardenemy".ToIdentifier(), minDurationBetweenSimilar: 30.0f); if (inspectNoiseObjective != null && subObjectives.Contains(inspectNoiseObjective)) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoadItems.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoadItems.cs index 894f27e60..65d0e110a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoadItems.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoadItems.cs @@ -112,7 +112,7 @@ namespace Barotrauma float prio = objectiveManager.GetOrderPriority(this); if (subObjectives.All(so => so.SubObjectives.None() || so.Priority <= 0)) { - ForceWalk = true; + ForceWalkTemporarily = true; } return prio; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs index ead5b6a87..a9ed43140 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs @@ -103,10 +103,6 @@ namespace Barotrauma public void AddObjective(T objective) where T : AIObjective { - var result = GameMain.LuaCs.Hook.Call("AI.addObjective", this, objective); - - if (result != null && result.Value) return; - if (objective == null) { #if DEBUG diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveOperateItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveOperateItem.cs index ad38f8cee..04a9bafa4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveOperateItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveOperateItem.cs @@ -257,6 +257,7 @@ namespace Barotrauma { DialogueIdentifier = AIObjectiveGoTo.DialogCannotReachTarget, TargetName = target.Item.Name, + ForceWalkPermanently = ForceWalk, endNodeFilter = EndNodeFilter ?? AIObjectiveGetItem.CreateEndNodeFilter(target.Item) }, onAbandon: () => Abandon = true, diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePumpWater.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePumpWater.cs index 6c1a7a37b..7aa3d1e87 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePumpWater.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePumpWater.cs @@ -29,7 +29,7 @@ namespace Barotrauma { if (pump?.Item == null || pump.Item.Removed) { return false; } if (pump.Item.IgnoreByAI(character)) { return false; } - if (!pump.Item.IsInteractable(character)) { return false; } + if (!pump.Item.IsInteractable(character) || !pump.CanBeSelected) { return false; } if (pump.IsAutoControlled) { return false; } if (pump.Item.ConditionPercentage <= 0) { return false; } if (pump.Item.CurrentHull == null) { return false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescueAll.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescueAll.cs index 42cbd4de6..8963eeb34 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescueAll.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescueAll.cs @@ -136,7 +136,7 @@ namespace Barotrauma public static bool IsValidTarget(Character target, Character character, out bool ignoredAsMinorWounds) { ignoredAsMinorWounds = false; - if (target == null || target.IsDead || target.Removed) { return false; } + if (target == null || target.IsDead || target.Removed || target.InvisibleTimer > 0.0f) { return false; } if (target.IsInstigator) { return false; } if (target.IsPet) { return false; } if (!HumanAIController.IsFriendly(character, target, onlySameTeam: true)) { return false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AICharacter.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AICharacter.cs index 879c4197f..669bc3a0a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AICharacter.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AICharacter.cs @@ -42,7 +42,9 @@ namespace Barotrauma { enemyAi.PetBehavior?.Update(deltaTime); } - if (IsDead || IsUnconscious || Stun > 0.0f || IsIncapacitated) + if (IsDead || IsUnconscious || IsIncapacitated || + //only check "real" stuns here, ignoring ragdolling, so the AI can run and decide whether to ragdoll or unragdoll + CharacterHealth.Stun > 0.0f) { //don't enable simple physics on dead/incapacitated characters //the ragdoll controls the movement of incapacitated characters instead of the collider, diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs index 316d3db5d..db9322c49 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs @@ -685,7 +685,7 @@ namespace Barotrauma { movement = MathUtils.SmoothStep(movement, TargetMovement, 0.2f); - if (Collider.BodyType == BodyType.Dynamic) + if (Collider.BodyType == BodyType.Dynamic && onGround) { Collider.LinearVelocity = new Vector2( movement.X, diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs index fb8754e55..3c3bd5c48 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs @@ -1,5 +1,6 @@ using Barotrauma.Extensions; using Barotrauma.Items.Components; +using Barotrauma.LuaCs.Events; using Barotrauma.Networking; using FarseerPhysics; using Microsoft.Xna.Framework; @@ -1305,11 +1306,11 @@ namespace Barotrauma //increase oxygen and clamp it above zero // -> the character should be revived if there are no major afflictions in addition to lack of oxygen target.Oxygen = Math.Max(target.Oxygen + 10.0f, 10.0f); - GameMain.LuaCs.Hook.Call("human.CPRSuccess", this); + LuaCsSetup.Instance.EventService.PublishEvent(x => x.OnCharacterCPRSuccess(this)); } else { - GameMain.LuaCs.Hook.Call("human.CPRFailed", this); + LuaCsSetup.Instance.EventService.PublishEvent(x => x.OnCharacterCPRFailed(this)); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs index 74ddd1820..38141c698 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs @@ -1,19 +1,20 @@ -using Barotrauma.Networking; +using Barotrauma.Extensions; +using Barotrauma.LuaCs.Events; +using Barotrauma.Networking; using FarseerPhysics; using FarseerPhysics.Dynamics; using FarseerPhysics.Dynamics.Contacts; using FarseerPhysics.Dynamics.Joints; using Microsoft.Xna.Framework; +using MoonSharp.Interpreter; using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Xml.Linq; -using Barotrauma.Extensions; -using LimbParams = Barotrauma.RagdollParams.LimbParams; using JointParams = Barotrauma.RagdollParams.JointParams; -using MoonSharp.Interpreter; +using LimbParams = Barotrauma.RagdollParams.LimbParams; namespace Barotrauma { @@ -59,6 +60,7 @@ namespace Barotrauma { public Fixture F1, F2; public Vector2 LocalNormal; + public Vector2 WorldNormal; public Vector2 Velocity; public Vector2 ImpactPos; @@ -68,7 +70,7 @@ namespace Barotrauma F2 = f2; Velocity = velocity; LocalNormal = contact.Manifold.LocalNormal; - contact.GetWorldManifold(out _, out FarseerPhysics.Common.FixedArray2 points); + contact.GetWorldManifold(out WorldNormal, out FarseerPhysics.Common.FixedArray2 points); ImpactPos = points[0]; } } @@ -850,7 +852,7 @@ namespace Barotrauma return true; } - private void ApplyImpact(Fixture f1, Fixture f2, Vector2 localNormal, Vector2 impactPos, Vector2 velocity) + private void ApplyImpact(Fixture f1, Fixture f2, Vector2 worldNormal, Vector2 impactPos, Vector2 velocity) { if (character.DisableImpactDamageTimer > 0.0f) { return; } @@ -862,7 +864,7 @@ namespace Barotrauma return; } - Vector2 normal = localNormal; + Vector2 normal = worldNormal; float impact = Vector2.Dot(velocity, -normal); if (f1.Body == Collider.FarseerBody || !Collider.Enabled) { @@ -880,7 +882,8 @@ namespace Barotrauma float impactDamage = GetImpactDamage(impact, impactTolerance); - var should = GameMain.LuaCs.Hook.Call("changeFallDamage", impactDamage, character, impactPos, velocity); + float? should = null; + LuaCsSetup.Instance.EventService.PublishEvent(x => should = x.OnChangeFallDamage(impactDamage, character, impactPos, velocity) ?? should); if (should != null) { @@ -1100,9 +1103,12 @@ namespace Barotrauma } Hull newHull = Hull.FindHull(findPos, currentHull); - if (setInWater && newHull == null) + if (setInWater) { - inWater = true; + if (newHull == null || findPos.Y < newHull.WorldSurface) + { + inWater = true; + } } if (newHull == currentHull) { return; } @@ -1145,7 +1151,10 @@ namespace Barotrauma { //don't teleport out yet if the character is going through a gap if (Gap.FindAdjacent(Gap.GapList.Where(g => g.Submarine == currentHull.Submarine), findPos, 150.0f, allowRoomToRoom: true) != null) { return; } - if (Limbs.Any(l => Gap.FindAdjacent(currentHull.ConnectedGaps, l.WorldPosition, ConvertUnits.ToDisplayUnits(l.body.GetSize().Combine()), allowRoomToRoom: true) != null)) { return; } + if (Limbs.Any(l => !l.IsSevered && Gap.FindAdjacent(currentHull.ConnectedGaps, l.WorldPosition, ConvertUnits.ToDisplayUnits(l.body.GetSize().Combine()), allowRoomToRoom: true) != null)) + { + return; + } character.MemLocalState?.Clear(); Teleport(ConvertUnits.ToSimUnits(currentHull.Submarine.Position), currentHull.Submarine.Velocity); } @@ -1207,6 +1216,10 @@ namespace Barotrauma public void Teleport(Vector2 moveAmount, Vector2 velocityChange, bool detachProjectiles = true) { + // Hopefully this will fix some crashes :( + // If Collider was null then no need to procced: nothing is there already + if (Collider == null) { return; } + foreach (Limb limb in Limbs) { if (limb.IsSevered) { continue; } @@ -1228,6 +1241,7 @@ namespace Barotrauma character.DisableImpactDamageTimer = 0.25f; + // Why they did null check below but didn't do it here???? SetPosition(Collider.SimPosition + moveAmount); character.CursorPosition += moveAmount; @@ -1277,6 +1291,9 @@ namespace Barotrauma private float BodyInRestDelay = 1.0f; + /// + /// Controls the sleeping state of this character + /// public bool BodyInRest { get { return bodyInRestTimer > BodyInRestDelay; } @@ -1342,9 +1359,18 @@ namespace Barotrauma } float MaxVel = NetConfig.MaxPhysicsBodyVelocity; - Collider.LinearVelocity = new Vector2( - NetConfig.Quantize(Collider.LinearVelocity.X, -MaxVel, MaxVel, 12), - NetConfig.Quantize(Collider.LinearVelocity.Y, -MaxVel, MaxVel, 12)); + if (GameMain.NetworkMember != null) + { + Collider.LinearVelocity = new Vector2( + NetConfig.Quantize(Collider.LinearVelocity.X, -MaxVel, MaxVel, 12), + NetConfig.Quantize(Collider.LinearVelocity.Y, -MaxVel, MaxVel, 12)); + } + else + { + Collider.LinearVelocity = new Vector2( + MathHelper.Clamp(Collider.LinearVelocity.X, -MaxVel, MaxVel), + MathHelper.Clamp(Collider.LinearVelocity.Y, -MaxVel, MaxVel)); + } if (forceStanding) { @@ -1398,9 +1424,19 @@ namespace Barotrauma UpdateHullFlowForces(deltaTime); - if (currentHull == null || + bool applyWaterForces = + currentHull == null || currentHull.WaterVolume > currentHull.Volume * 0.95f || - ConvertUnits.ToSimUnits(currentHull.Surface) > Collider.SimPosition.Y) + ConvertUnits.ToSimUnits(currentHull.Surface) > Collider.SimPosition.Y; +#if CLIENT + if (Screen.Selected is CharacterEditor.CharacterEditorScreen && + this is AnimController animController) + { + applyWaterForces = animController.CurrentAnimationParams is SwimParams; + } +#endif + + if (applyWaterForces) { Collider.ApplyWaterForces(); } @@ -1490,10 +1526,10 @@ namespace Barotrauma else { // Falling -> ragdoll briefly if we are not moving at all, because we are probably stuck. - if (Collider.LinearVelocity == Vector2.Zero && !character.IsRemotePlayer) + if (Collider.LinearVelocity == Vector2.Zero && GameMain.NetworkMember is not { IsClient: true }) { character.IsRagdolled = true; - if (character.IsBot) + if (!character.IsPlayer) { // Seems to work without this on player controlled characters -> not sure if we should call it always or just for the bots. character.SetInput(InputType.Ragdoll, hit: false, held: true); @@ -1853,7 +1889,13 @@ namespace Barotrauma { floorFixture = standOnFloorFixture; standOnFloorY = rayStart.Y + (rayEnd.Y - rayStart.Y) * standOnFloorFraction; - if (rayStart.Y - standOnFloorY < Collider.Height * 0.5f + Collider.Radius + ColliderHeightFromFloor * 1.2f) + + //allow the floor to be just a bit below the bottom of the collider for the character to be "on ground" + //there is some inaccuracy in the physics simulation (and floats), the collider isn't usually precisely ColliderHeightFromFloor above the floor + const float Tolerance = 0.1f; + float standHeight = Collider.Height * 0.5f + Collider.Radius + ColliderHeightFromFloor; + + if (rayStart.Y - standOnFloorY <= standHeight + Tolerance) { onGround = true; if (standOnFloorFixture.CollisionCategories == Physics.CollisionStairs) @@ -2220,8 +2262,9 @@ namespace Barotrauma if (limb == null) { // Didn't seek or find a (valid) limb of the matching type. If there's multiple limbs of the same type, check the other limbs. - foreach (var l in limbs) + foreach (var l in Limbs) { + if (l == null) { continue; } if (l.Removed) { continue; } if (useSecondaryType) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs index 763c677ca..aa22cee73 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs @@ -250,6 +250,11 @@ namespace Barotrauma set => Params.Health.DoesBleed = value; } + /// + /// Can this character be contained inside a controller? + /// + public bool IsContainable { get; set; } + public readonly Dictionary Properties; public Dictionary SerializableProperties { @@ -746,6 +751,11 @@ namespace Barotrauma get { return AnimController.Mass; } } + /// + /// The position the character was at when we previously set the transforms of the items in the character's inventory. + /// + private Vector2 lastInventoryItemSetTransformPosition; + public CharacterInventory Inventory { get; private set; } /// @@ -851,7 +861,24 @@ namespace Barotrauma set { if (value == selectedCharacter) { return; } - if (selectedCharacter != null) { selectedCharacter.selectedBy = null; } + //deselect the currently selected character + if (selectedCharacter != null) + { + selectedCharacter.selectedBy = null; + //check if some other character has selected the currently selected character too, + //and set selectedBy to that other character (otherwise the currently selected character would be unaware they're still being dragged by someone) + foreach (var otherCharacter in CharacterList) + { + if (otherCharacter != this && otherCharacter.selectedCharacter == selectedCharacter) + { + selectedCharacter.selectedBy = otherCharacter; + break; + } + } + } + + CharacterHUD.RecreateHudTextsIfControlling(this); + selectedCharacter = value; if (selectedCharacter != null) { selectedCharacter.selectedBy = this; } #if CLIENT @@ -1493,8 +1520,6 @@ namespace Barotrauma } #endif - GameMain.LuaCs.Hook.Call("character.created", new object[] { newCharacter }); - return newCharacter; } @@ -1708,8 +1733,10 @@ namespace Barotrauma AnimController.FindHull(setInWater: true); if (AnimController.CurrentHull != null) { Submarine = AnimController.CurrentHull.Submarine; } + IsContainable = prefab.ConfigElement.GetAttributeBool(nameof(IsContainable), def: Mass <= 30.0f); + CharacterList.Add(this); - + Enabled = GameMain.NetworkMember == null; if (info != null) @@ -1949,7 +1976,6 @@ namespace Barotrauma } } info.Job?.GiveJobItems(this, isPvPMode, spawnPoint); - GameMain.LuaCs.Hook.Call("character.giveJobItems", this, spawnPoint, isPvPMode); } public void GiveIdCardTags(WayPoint spawnPoint, bool createNetworkEvent = false) @@ -2335,6 +2361,12 @@ namespace Barotrauma } } + // Try to detach from the controller if we are currently attached to something that is dangerous for our character + if (aiControlled && Stun <= 0f && !IsKnockedDownOrRagdolled && !LockHands && ShouldAvoidStayingAttachedToController()) + { + SelectedItem = null; + } + if (GameMain.NetworkMember != null) { if (GameMain.NetworkMember.IsServer) @@ -2383,7 +2415,7 @@ namespace Barotrauma { attackCoolDown -= deltaTime; } - else if (IsKeyDown(InputType.Attack)) + else if (IsKeyDown(InputType.Attack) && !IsAttachedToController()) { //normally the attack target, where to aim the attack and such is handled by EnemyAIController, //but in the case of player-controlled monsters, we handle it here @@ -2911,14 +2943,14 @@ namespace Barotrauma #if CLIENT if (Screen.Selected == GameMain.SubEditorScreen) { hidden = false; } #endif - if (!CanInteract || hidden || !item.IsInteractable(this)) { return false; } - Controller controller = item.GetComponent(); if (controller != null && IsAnySelectedItem(item) && controller.IsAttachedUser(this)) { return true; } + if (!CanInteract || hidden || !item.IsInteractable(this)) { return false; } + if (item.ParentInventory != null) { return CanAccessInventory(item.ParentInventory); @@ -3040,7 +3072,9 @@ namespace Barotrauma } } - if (!item.Prefab.InteractThroughWalls && Screen.Selected != GameMain.SubEditorScreen && !insideTrigger) + //note that the distance to item should be set to 0 above if the character is within the item's bounding box + bool closeEnoughToIgnoreVisibilityCheck = distanceToItem <= 0.1f; + if (!item.Prefab.InteractThroughWalls && Screen.Selected != GameMain.SubEditorScreen && !insideTrigger && !closeEnoughToIgnoreVisibilityCheck) { var body = Submarine.CheckVisibility(SimPosition, itemPosition, ignoreLevel: true); bool itemCenterVisible = CheckBody(body, item); @@ -3069,7 +3103,6 @@ namespace Barotrauma { return itemCenterVisible; } - } return true; @@ -3159,7 +3192,11 @@ namespace Barotrauma if (!CanInteract) { - SelectedItem = SelectedSecondaryItem = null; + if (!IsAttachedToController()) + { + SelectedItem = null; + } + SelectedSecondaryItem = null; focusedItem = null; if (!AllowInput) { @@ -3178,8 +3215,16 @@ namespace Barotrauma { if (!PlayerInput.PrimaryMouseButtonHeld() || Barotrauma.Inventory.DraggingItemToWorld) { - FocusedCharacter = CanInteract || CanEat ? FindCharacterAtPosition(mouseSimPos) : null; - if (FocusedCharacter != null && !CanSeeTarget(FocusedCharacter)) { FocusedCharacter = null; } + //don't allow focusing on anyone when the health window is open (avoids accidentally selecting someone when closing the window) + if (CharacterHealth.OpenHealthWindow != null) + { + FocusedCharacter = null; + } + else + { + FocusedCharacter = CanInteract || CanEat ? FindCharacterAtPosition(mouseSimPos) : null; + if (FocusedCharacter != null && !CanSeeTarget(FocusedCharacter)) { FocusedCharacter = null; } + } float aimAssist = GameSettings.CurrentConfig.AimAssistAmount * (AnimController.InWater ? 1.5f : 1.0f); if (HeldItems.Any(it => it?.GetComponent()?.IsActive ?? false)) { @@ -3450,7 +3495,7 @@ namespace Barotrauma for (int i = 0; i < CharacterList.Count; i++) { - CharacterList[i].Update(deltaTime , cam); + CharacterList[i].Update(deltaTime, cam); } #if CLIENT @@ -3504,7 +3549,7 @@ namespace Barotrauma obstructVisionAmount = Math.Max(obstructVisionAmount - deltaTime, 0.0f); - if (Inventory != null) + if (Inventory != null && Vector2.DistanceSquared(lastInventoryItemSetTransformPosition, Position) > 0.1f) { //do not check for duplicates: this is code is called very frequently, and duplicates don't matter here, //so it's better just to avoid the relatively expensive duplicate check @@ -3513,6 +3558,7 @@ namespace Barotrauma if (item.body == null || item.body.Enabled) { continue; } item.SetTransform(SimPosition, 0.0f, forceSubmarine: Submarine); } + lastInventoryItemSetTransformPosition = Position; } HideFace = false; @@ -3639,7 +3685,7 @@ namespace Barotrauma { wasRagdolled = IsRagdolled; IsRagdolled = IsKeyDown(InputType.Ragdoll); - if (IsRagdolled && IsBot && GameMain.NetworkMember is not { IsClient: true }) + if (IsRagdolled && !IsPlayer && GameMain.NetworkMember is not { IsClient: true }) { ClearInput(InputType.Ragdoll); } @@ -3691,7 +3737,19 @@ namespace Barotrauma AnimController.IgnorePlatforms = true; } AnimController.ResetPullJoints(); - SelectedItem = SelectedSecondaryItem = null; + + // Prevent us from detaching from the controller if we are attached to it OR detach if we + // manually ragdoll, in this case it should be similar to us deselecting the controller + if (!IsAttachedToController() || + (IsKeyDown(InputType.Ragdoll) + // Let only the server do this check since the Ragdoll input for other clients is set to be held + // for stunned characters even if a character isn't manually ragdolling + && (GameMain.NetworkMember == null || GameMain.NetworkMember is { IsServer: true } ))) + { + SelectedItem = null; + } + + SelectedSecondaryItem = null; SelectedCharacter = null; return; } @@ -3720,6 +3778,13 @@ namespace Barotrauma bool MustDeselect(Item item) { if (item == null) { return false; } + + // Prevent creatures from deselecting the controller if they are attached to it + if (IsAIControlled && !CanInteract && IsAttachedToController()) + { + return false; + } + if (!CanInteractWith(item)) { return true; } bool hasSelectableComponent = false; foreach (var component in item.Components) @@ -4445,6 +4510,41 @@ namespace Barotrauma } } + public void ForceSay(LocalizedString messageToSay, bool sayInRadio, bool removeQuotes = false, float delay = 0.0f) + { + if (messageToSay.IsNullOrEmpty() || SpeechImpediment >= 100.0f || IsDead) + { + return; + } + + if (removeQuotes) + { + messageToSay = new TrimLString(messageToSay, + TrimLString.Mode.Both, ['"', '”', '“', ' ']); + } + + ChatMessageType messageType = ChatMessageType.Default; + bool canUseRadio = ChatMessage.CanUseRadio(this, out WifiComponent radio); + if (canUseRadio && sayInRadio) + { + messageType = ChatMessageType.Radio; + } + + CoroutineManager.Invoke(() => + { +#if SERVER + GameMain.Server?.SendChatMessage(messageToSay.Value, messageType, senderClient: null, this); +#elif CLIENT + // no need to create the message when playing as a client, the server will send it to us + if (GameMain.Client == null) + { + AIChatMessage message = new AIChatMessage(messageToSay.Value, messageType); + SendSinglePlayerMessage(message, canUseRadio, radio); + } +#endif + }, delay); + } + public void SetAllDamage(float damageAmount, float bleedingDamageAmount, float burnDamageAmount) { CharacterHealth.SetAllDamage(damageAmount, bleedingDamageAmount, burnDamageAmount); @@ -4657,12 +4757,6 @@ namespace Barotrauma public AttackResult DamageLimb(Vector2 worldPosition, Limb hitLimb, IEnumerable afflictions, float stun, bool playSound, Vector2 attackImpulse, Character attacker = null, float damageMultiplier = 1, bool allowStacking = true, float penetration = 0f, bool shouldImplode = false, bool ignoreDamageOverlay = false, bool recalculateVitality = true) { if (Removed) { return new AttackResult(); } - - AttackResult? retAttackResult = GameMain.LuaCs.Hook.Call("character.damageLimb", this, worldPosition, hitLimb, afflictions, stun, playSound, attackImpulse, attacker, damageMultiplier, allowStacking, penetration, shouldImplode); - if (retAttackResult != null) - { - return retAttackResult.Value; - } SetStun(stun); @@ -4835,6 +4929,10 @@ namespace Barotrauma { if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient && !isNetworkMessage) { return; } if (Screen.Selected != GameMain.GameScreen) { return; } + //don't allow stunning for less than one frame + //fixes monsters/enemies that take some minuscule amount of stun from a weapon still being noticeable affected by the stun, + //because even a one-frame stun briefly disables the animations and makes the character stop + if (newStun < Timing.Step && Stun <= 0.0f) { return; } if (GodMode) { CharacterHealth.Stun = 0; @@ -4862,7 +4960,12 @@ namespace Barotrauma CharacterHealth.Stun = newStun; if (newStun > 0.0f) { - SelectedItem = SelectedSecondaryItem = null; + if (!IsAttachedToController()) + { + SelectedItem = null; + } + + SelectedSecondaryItem = null; if (SelectedCharacter != null) { DeselectCharacter(); } } HealthUpdateInterval = 0.0f; @@ -5056,6 +5159,37 @@ namespace Barotrauma } } + public bool IsAttachedToController() + { + if (SelectedItem == null) { return false; } + + var controller = SelectedItem.GetComponent(); + if (controller == null) { return false; } + + return controller.IsAttachedUser(this); + } + + public bool ShouldAvoidStayingAttachedToController() + { + if (!IsAttachedToController()) { return false; } + + var deconstructor = SelectedItem.GetComponent(); + if (deconstructor != null) + { + return true; + } + + // Character is being carried by an enemy! + if (IsHuman && + SelectedItem.GetRootInventoryOwner() is Character carryingCharacter && + TeamID != carryingCharacter.TeamID) + { + return true; + } + + return false; + } + public void Kill(CauseOfDeathType causeOfDeath, Affliction causeOfDeathAffliction, bool isNetworkMessage = false, bool log = true) { if (IsDead || CharacterHealth.Unkillable || GodMode || Removed) { return; } @@ -5183,7 +5317,6 @@ namespace Barotrauma AchievementManager.OnCharacterKilled(this, CauseOfDeath); } - GameMain.LuaCs.Hook.Call("character.death", this, causeOfDeathAffliction); KillProjSpecific(causeOfDeath, causeOfDeathAffliction, log); if (info != null) @@ -5194,7 +5327,7 @@ namespace Barotrauma AnimController.movement = Vector2.Zero; AnimController.TargetMovement = Vector2.Zero; - if (!LockHands) + if (!LockHands && causeOfDeath != CauseOfDeathType.Disconnected) { foreach (Item heldItem in HeldItems.ToList()) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterPrefab.cs index 93e4b1bf6..68110740b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterPrefab.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using System.Xml.Linq; using Microsoft.Xna.Framework; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs index 2dbe048f5..1202d80f4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs @@ -482,7 +482,6 @@ namespace Barotrauma { GrainEffectStrength -= amount; } - GameMain.LuaCs.Hook.Call("afflictionUpdate", new object[] { this, characterHealth, targetLimb, deltaTime }); } public void ApplyStatusEffects(ActionType type, float deltaTime, CharacterHealth characterHealth, Limb targetLimb) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs index 31abf6789..37b2f35f5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs @@ -4,6 +4,7 @@ using System.Xml.Linq; using System; using Barotrauma.Extensions; using Microsoft.Xna.Framework; +using Barotrauma.LuaCs.Events; namespace Barotrauma { @@ -337,13 +338,13 @@ namespace Barotrauma if (Prefab is AfflictionPrefabHusk huskPrefab) { - if (huskPrefab.ControlHusk || GameMain.LuaCs.Game.enableControlHusk) + if (huskPrefab.ControlHusk || LuaCsSetup.Instance.Game.enableControlHusk) { #if SERVER if (client != null) { GameMain.Server.SetClientCharacter(client, husk); - GameMain.LuaCs.Hook.Call("husk.clientControlHusk", new object[] { client, husk }); + LuaCsSetup.Instance.EventService.PublishEvent(x => x.OnClientControlHusk(client, husk)); } #else if (!character.IsRemotelyControlled && character == Character.Controlled) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs index cb36b1196..8008179fb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs @@ -629,7 +629,7 @@ namespace Barotrauma public static readonly Identifier StunType = "stun".ToIdentifier(); public static readonly Identifier EMPType = "emp".ToIdentifier(); public static readonly Identifier SpaceHerpesType = "spaceherpes".ToIdentifier(); - public static readonly Identifier AlienInfectedType = "alieninfected".ToIdentifier(); + public static readonly Identifier AlienInfectionType = "alieninfection".ToIdentifier(); public static readonly Identifier InvertControlsType = "invertcontrols".ToIdentifier(); public static readonly Identifier DisguisedAsHuskType = "disguiseashusk".ToIdentifier(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs index 03fecbc34..0193d3cad 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs @@ -1,11 +1,17 @@ using Barotrauma.Abilities; +using Barotrauma.Abilities; using Barotrauma.Extensions; +using Barotrauma.Extensions; +using Barotrauma.LuaCs.Events; +using Barotrauma.Networking; using Barotrauma.Networking; using Microsoft.Xna.Framework; +using MoonSharp.Interpreter; using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Globalization; +using System.Globalization; using System.Linq; using System.Threading; using System.Xml.Linq; @@ -662,7 +668,8 @@ namespace Barotrauma return; } - var should = GameMain.LuaCs.Hook.Call("character.applyDamage", this, attackResult, hitLimb, allowStacking); + bool? should = null; + LuaCsSetup.Instance.EventService.PublishEvent(x => should = x.OnCharacterApplyDamage(this, attackResult, hitLimb, allowStacking) ?? should); if (should != null && should.Value) { return; } foreach (Affliction newAffliction in attackResult.Afflictions) @@ -843,10 +850,9 @@ namespace Barotrauma if (newAffliction.Prefab.TargetSpecies.Any() && newAffliction.Prefab.TargetSpecies.None(s => s == Character.SpeciesName)) { return; } if (Character.Params.Health.ImmunityIdentifiers.Contains(newAffliction.Identifier)) { return; } - var should = GameMain.LuaCs.Hook.Call("character.applyAffliction", this, limbHealth, newAffliction, allowStacking); - - if (should != null && should.Value) - return; + bool? should = null; + LuaCsSetup.Instance.EventService.PublishEvent(x => should = x.OnCharacterApplyAffliction(this, limbHealth, newAffliction, allowStacking) ?? should); + if (should != null && should.Value) { return; } Affliction existingAffliction = null; foreach ((Affliction affliction, LimbHealth value) in afflictions) @@ -858,9 +864,21 @@ namespace Barotrauma } } + float modifiedStrength = newAffliction.Strength * (100.0f / MaxVitality) * (1f - GetResistance(newAffliction.Prefab, limbType)); + if (newAffliction.Prefab.AfflictionType == AfflictionPrefab.StunType) + { + //don't allow stunning for less than one frame + //fixes monsters/enemies that take some minuscule amount of stun from a weapon still being noticeable affected by the stun, + //because even a one-frame stun briefly disables the animations and makes the character stop + if (modifiedStrength < Timing.Step && Stun <= 0.0f) + { + return; + } + } + if (existingAffliction != null) { - float newStrength = newAffliction.Strength * (100.0f / MaxVitality) * (1f - GetResistance(existingAffliction.Prefab, limbType)); + float newStrength = modifiedStrength; if (allowStacking) { // Add the existing strength @@ -882,7 +900,7 @@ namespace Barotrauma //create a new instance of the affliction to make sure we don't use the same instance for multiple characters //or modify the affliction instance of an Attack or a StatusEffect var copyAffliction = newAffliction.Prefab.Instantiate( - Math.Min(newAffliction.Prefab.MaxStrength, newAffliction.Strength * (100.0f / MaxVitality) * (1f - GetResistance(newAffliction.Prefab, limbType))), + Math.Min(newAffliction.Prefab.MaxStrength, modifiedStrength), newAffliction.Source); afflictions.TryAdd(copyAffliction, limbHealth); AchievementManager.OnAfflictionReceived(copyAffliction, Character); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs index 680d29279..d3e69b401 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs @@ -190,7 +190,7 @@ namespace Barotrauma idleObjective.PreferredOutpostModuleTypes.Add(moduleType); } } - humanAI.ReportRange = Hearing; + humanAI.Hearing = Hearing; humanAI.ReportRange = ReportRange; humanAI.FindWeaponsRange = FindWeaponsRange; humanAI.AimSpeed = AimSpeed; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs index 79c09a34c..a6ab75891 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs @@ -1300,7 +1300,7 @@ namespace Barotrauma if (!statusEffects.TryGetValue(actionType, out var statusEffectList)) { return; } foreach (StatusEffect statusEffect in statusEffectList) { - if (statusEffect.ShouldWaitForInterval(character, deltaTime)) { return; } + if (statusEffect.ShouldWaitForInterval(character, deltaTime)) { continue; } statusEffect.sourceBody = body; if (statusEffect.type == ActionType.OnDamaged) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs index 14c8e83c8..675faa777 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs @@ -728,7 +728,9 @@ namespace Barotrauma [Serialize(true, IsPropertySaveable.Yes, description: "Should the character target or ignore walls when it's outside the submarine."), Editable] public bool TargetOuterWalls { get; private set; } - [Serialize(false, IsPropertySaveable.Yes, description: "If enabled, the character chooses randomly from the available attacks. The priority is used as a weight for weighted random."), Editable] + [Serialize(false, IsPropertySaveable.Yes, description: "If disabled (default), the character selects the limb based on a formula where the parameters are a) the priority of the attack b) the distance to the target, and c) the range of the attack" + + "If enabled, the character chooses randomly from the available attacks. The priority is used as a weight for weighted random. The distance to the target is in this case ignored." + ), Editable] public bool RandomAttack { get; private set; } [Serialize(false, IsPropertySaveable.Yes, description:"Does the creature know how to open doors (still requires a proper ID card). Humans can always open doors (They don't use this AI definition)."), Editable] diff --git a/Barotrauma/BarotraumaShared/SharedSource/CircuitBox/CircuitBoxInputOutputNode.cs b/Barotrauma/BarotraumaShared/SharedSource/CircuitBox/CircuitBoxInputOutputNode.cs index e589401bc..99b619886 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/CircuitBox/CircuitBoxInputOutputNode.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/CircuitBox/CircuitBoxInputOutputNode.cs @@ -77,8 +77,8 @@ namespace Barotrauma } else { - conn.SetLabel(conn.Connection.DisplayName, this); conn.Connection.DisplayNameOverride = null; + conn.SetLabel(conn.Connection.DisplayName, this); } } #endif diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/CharacterFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/CharacterFile.cs index e608d06ba..0e67a57ef 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/CharacterFile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/CharacterFile.cs @@ -106,9 +106,10 @@ namespace Barotrauma void AddTexturePath(string path) { if (string.IsNullOrEmpty(path)) { return; } + var contentPath = ContentPath.FromRaw(characterPrefab.ContentPackage, ragdollParams.Texture); //if the path contains a gender variable, we can't load it yet because we don't know which gender we need - if (path.Contains("[GENDER]")) { return; } - texturePaths.Add(ContentPath.FromRaw(characterPrefab.ContentPackage, ragdollParams.Texture)); + if (contentPath.FullPath.Contains("[GENDER]")) { return; } + texturePaths.Add(contentPath); } } #endif diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackage.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackage.cs index 2bee7b3c1..12967f125 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackage.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackage.cs @@ -199,9 +199,16 @@ namespace Barotrauma try { - return success(doc.Root.GetAttributeBool("corepackage", false) + ContentPackage contentPackage = doc.Root.GetAttributeBool("corepackage", false) ? new CorePackage(doc, path) - : new RegularPackage(doc, path)); + : new RegularPackage(doc, path); + + if (System.IO.Path.GetFileNameWithoutExtension(path)?.Any(char.IsUpper) is true) + { + DebugConsole.ThrowError($"Invalid filename casing. Please rename \"filelist.xml\" so it is entirely lowercase.", contentPackage: contentPackage); + } + + return success(contentPackage); } catch (Exception e) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackageManager.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackageManager.cs index 79ce7be3f..50cf39e5b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackageManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackageManager.cs @@ -9,8 +9,10 @@ using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Xml.Linq; using Barotrauma.IO; +using Barotrauma.LuaCs.Events; using Barotrauma.Steam; using Microsoft.Xna.Framework; +using OneOf.Types; namespace Barotrauma { @@ -48,7 +50,10 @@ namespace Barotrauma public static ImmutableArray? Regular; } - public static void SetCore(CorePackage newCore) => SetCoreEnumerable(newCore).Consume(); + public static void SetCore(CorePackage newCore) + { + SetCoreEnumerable(newCore).Consume(); + } public static IEnumerable SetCoreEnumerable(CorePackage newCore) { @@ -85,7 +90,9 @@ namespace Barotrauma } public static void SetRegular(IReadOnlyList newRegular) - => SetRegularEnumerable(newRegular).Consume(); + { + SetRegularEnumerable(newRegular).Consume(); + } public static IEnumerable SetRegularEnumerable(IReadOnlyList inNewRegular) { @@ -583,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); } }); @@ -597,4 +609,4 @@ namespace Barotrauma } } } -} \ No newline at end of file +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPath.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPath.cs index 0d9a4a112..2661aef1d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPath.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPath.cs @@ -99,12 +99,13 @@ namespace Barotrauma public static ContentPath FromRaw(ContentPackage? contentPackage, string? rawValue) { var newRaw = new ContentPath(contentPackage, rawValue); - if (prevCreatedRaw is not null && prevCreatedRaw.ContentPackage == contentPackage && + // Removed as this almost never happens but makes the constructor not thread-safe. + /*if (prevCreatedRaw is not null && prevCreatedRaw.ContentPackage == contentPackage && prevCreatedRaw.RawValue == rawValue) { newRaw.cachedValue = prevCreatedRaw.Value; } - prevCreatedRaw = newRaw; + prevCreatedRaw = newRaw;*/ return newRaw; } @@ -158,4 +159,4 @@ namespace Barotrauma public override string? ToString() => Value; } -} \ No newline at end of file +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs index fb2cacc66..4c5a1e339 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs @@ -2311,6 +2311,8 @@ namespace Barotrauma NewMessage($"Start item set changed to \"{AutoItemPlacer.DefaultStartItemSet}\""); }, isCheat: false)); + + //"dummy commands" that only exist so that the server can give clients permissions to use them //TODO: alphabetical order? commands.Add(new Command("control", "control [character name]: Start controlling the specified character (client-only).", null, () => @@ -3020,7 +3022,10 @@ namespace Barotrauma switch (args[argIndex].ToLowerInvariant()) { case "inside": - spawnPoint = WayPoint.GetRandom(SpawnType.Human, job, Submarine.MainSub); + spawnPoint = + WayPoint.GetRandom(SpawnType.Human, job, Submarine.MainSub) ?? + //try a non-job-specific spawnpoint if a job-specific one can't be found + WayPoint.GetRandom(SpawnType.Human, assignedJob: null, Submarine.MainSub); break; case "outside": spawnPoint = WayPoint.GetRandom(SpawnType.Enemy); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Decals/Decal.cs b/Barotrauma/BarotraumaShared/SharedSource/Decals/Decal.cs index b42f1131c..281022315 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Decals/Decal.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Decals/Decal.cs @@ -34,11 +34,12 @@ namespace Barotrauma get { return Prefab.LifeTime; } } + private float baseAlpha = 1.0f; public float BaseAlpha { - get; - set; - } = 1.0f; + get => baseAlpha; + set => baseAlpha = MathHelper.Clamp(value, 0f, 1f); + } public Color Color { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Enums.cs b/Barotrauma/BarotraumaShared/SharedSource/Enums.cs index 79a25913a..3f2cb74c6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Enums.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Enums.cs @@ -131,6 +131,14 @@ namespace Barotrauma /// OnRemoved = 25, /// + /// Executes continuously while the item/character is being deconstructed. + /// + OnDeconstructing = 26, + /// + /// Executed once when the item/character is deconstructed. + /// + OnDeconstructed = 27, + /// /// Executes when the character dies. Only valid for characters. /// OnDeath = OnBroken diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Event.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Event.cs index 08abda9ed..1c2cc3a87 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Event.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Event.cs @@ -12,7 +12,11 @@ namespace Barotrauma public readonly int RandomSeed; protected readonly EventPrefab prefab; - + +#nullable enable + public Mission? TriggeringMission; +#nullable restore + public EventPrefab Prefab => prefab; public EventSet ParentSet { get; private set; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckConditionalAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckConditionalAction.cs index 04d517c2f..b1391bd64 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckConditionalAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckConditionalAction.cs @@ -29,6 +29,9 @@ namespace Barotrauma [Serialize("", IsPropertySaveable.Yes, description: "Tag to apply to the target (or all targets if there's multiple) when the check succeeds.")] public Identifier ApplyTagToTarget { get; set; } + [Serialize(true, IsPropertySaveable.Yes, description: "Should the check fail if no targets matching the specified tag are found?")] + public bool FailIfTargetNotFound { get; set; } + public CheckConditionalAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { if (TargetTag.IsEmpty) @@ -79,11 +82,10 @@ namespace Barotrauma if (targets.None()) { - DebugConsole.LogError($"{nameof(CheckConditionalAction)} error: {GetEventDebugName()} uses a {nameof(CheckConditionalAction)} but no valid target was found for tag \"{TargetTag}\"! This will cause the check to automatically succeed.", - contentPackage: ParentEvent.Prefab.ContentPackage); + return !FailIfTargetNotFound; } - if (targets.None() || Conditionals.None()) + if (Conditionals.None()) { foreach (var target in targets) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ConversationAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ConversationAction.cs index f6ff09a03..bc40eab16 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ConversationAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ConversationAction.cs @@ -14,6 +14,33 @@ namespace Barotrauma /// partial class ConversationAction : EventAction { + public class OptionActionGroup : SubactionGroup + { + [Serialize("", IsPropertySaveable.Yes, description: "The text to display in the option.")] + public string Text { get; set; } + + [Serialize(false, IsPropertySaveable.Yes, description: "Should this option end the conversation (closing the conversation prompt?). " + + "By default, options that don't have any actions inside them, or that only have a GoTo action, end the conversation. " + + "But if there are other actions inside the option, the game assumes there may be some kind of a follow-up coming to the conversation, " + + "and by default leaves it open.")] + public bool EndConversation { get; set; } + + [Serialize(false, IsPropertySaveable.Yes, description: $"If enabled, the player will send the {nameof(Text)} in chat when selecting the option, or if {nameof(ForceSayText)} is not empty, will send that instead.")] + public bool ForceSay { get; set; } + + [Serialize(false, IsPropertySaveable.Yes, description: "If enabled, the message sent in chat will be sent in radio chat instead.")] + public bool ForceSayInRadio { get; set; } + + [Serialize("", IsPropertySaveable.Yes, description: $"Message sent in chat, if empty, {nameof(Text)} is used instead.")] + public string ForceSayText { get; set; } + + [Serialize(true, IsPropertySaveable.Yes, description: "Should the chat message be stripped of any quotation mark characters?")] + public bool ForceSayRemoveQuotes { get; set; } + + public OptionActionGroup(ScriptedEvent scriptedEvent, ContentXElement element) : base(scriptedEvent, element) + { + } + } public enum DialogTypes { @@ -33,6 +60,18 @@ namespace Barotrauma [Serialize("", IsPropertySaveable.Yes, description: "The text to display in the prompt. Can be the text as-is, or a tag referring to a line in a text file.")] public string Text { get; set; } + [Serialize(false, IsPropertySaveable.Yes, description: $"If enabled, the speaker will send the {nameof(Text)} in chat, or if {nameof(ForceSayText)} is not empty, will send that instead. Note: requires a valid SpeakerTag to be defined.")] + public bool ForceSay { get; set; } + + [Serialize(false, IsPropertySaveable.Yes, description: "If enabled, the message sent in chat by the speaker will be sent in radio chat instead.")] + public bool ForceSayInRadio { get; set; } + + [Serialize("", IsPropertySaveable.Yes, description: $"Message sent in chat by the speaker, if empty, {nameof(Text)} is used instead.")] + public string ForceSayText { get; set; } + + [Serialize(true, IsPropertySaveable.Yes, description: "Should the chat message be stripped of any quotation mark characters?")] + public bool ForceSayRemoveQuotes { get; set; } + [Serialize("", IsPropertySaveable.Yes, description: "Tag of the character who's speaking. Makes a speech bubble icon appear above the character to indicate you can speak with them, and stops the character in place when the conversation triggers. Also allows the conversation to be interrupted if the speaker dies or becomes incapacitated mid-conversation.")] public Identifier SpeakerTag { get; set; } @@ -75,7 +114,7 @@ namespace Barotrauma private AIObjective prevIdleObjective, prevGotoObjective; private AIObjective npcWaitObjective; - public List Options { get; private set; } + public List Options { get; private set; } public SubactionGroup Interrupted { get; private set; } @@ -99,12 +138,12 @@ namespace Barotrauma { actionCount++; Identifier = actionCount; - Options = new List(); + Options = new List(); foreach (var elem in element.Elements()) { if (elem.Name.LocalName.Equals("option", StringComparison.OrdinalIgnoreCase)) { - Options.Add(new SubactionGroup(ParentEvent, elem)); + Options.Add(new OptionActionGroup(ParentEvent, elem)); } else if (elem.Name.LocalName.Equals("interrupt", StringComparison.OrdinalIgnoreCase)) { @@ -215,6 +254,10 @@ namespace Barotrauma interrupt = false; dialogOpened = false; Speaker = null; +#if CLIENT + dialogBox?.Close(); + dialogBox = null; +#endif } /// @@ -292,6 +335,7 @@ namespace Barotrauma if (dialogOpened) { lastActiveTime = Timing.TotalTime; + #if CLIENT if (GUIMessageBox.MessageBoxes.Any(mb => mb.UserData as string == "ConversationAction")) { @@ -350,7 +394,7 @@ namespace Barotrauma } else { - TryStartConversation(null); + TryStartConversation(Speaker); } } else @@ -467,11 +511,26 @@ namespace Barotrauma ParentEvent.AddTarget(InvokerTag, targetCharacter); } - ShowDialog(speaker, targetCharacter); + if (ForceSay) + { + speaker?.ForceSay( + ForceSayText.IsNullOrEmpty() ? TextManager.Get(Text).Fallback(Text) : TextManager.Get(ForceSayText).Fallback(ForceSayText), + ForceSayInRadio, + ForceSayRemoveQuotes, + // Small delay so the speaking character doesn't talk at the same time as the player + delay: 0.7f); + } + + ShowDialog(Speaker, targetCharacter); dialogOpened = true; - if (speaker != null) + if (Speaker != null) { + Speaker = speaker; + + // Set the Speaker of the child conversation actions so they know which character is speaking + Options.SelectMany(static op => op.Actions).OfType().ForEach(action => action.Speaker = speaker); + speaker.CampaignInteractionType = CampaignMode.InteractionType.None; speaker.SetCustomInteract(null, null); #if SERVER diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CountTargetsAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CountTargetsAction.cs index 13d3b6859..ba10a1f7e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CountTargetsAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CountTargetsAction.cs @@ -99,6 +99,7 @@ namespace Barotrauma else { int compareToTargetCount = ParentEvent.GetTargets(CompareToTarget).Count(); + if (compareToTargetCount == 0) { return false; } float percentage = MathUtils.Percentage(targetCount, compareToTargetCount); if (MinPercentageRelativeToTarget > -1 && percentage < MinPercentageRelativeToTarget) { return false; } if (MaxPercentageRelativeToTarget > -1 && percentage > MaxPercentageRelativeToTarget) { return false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/EventAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/EventAction.cs index f1a54e742..bf96cb286 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/EventAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/EventAction.cs @@ -9,14 +9,7 @@ namespace Barotrauma { public class SubactionGroup { - public string Text; public List Actions; - /// - /// Should this option end the conversation (closing the conversation prompt?). By default, options that don't have any actions inside them, or that only have a GoTo action, end the conversation. - /// But if there are other actions inside the option, the game assumes there may be some kind of a follow-up coming to the conversation, and by default leaves it open. - /// - public bool EndConversation; - private int currentSubAction = 0; public EventAction CurrentSubAction @@ -31,17 +24,17 @@ namespace Barotrauma } } - public SubactionGroup(ScriptedEvent scriptedEvent, ContentXElement elem) + public SubactionGroup(ScriptedEvent scriptedEvent, ContentXElement element) { - Text = elem.GetAttribute("text")?.Value ?? ""; + SerializableProperty.DeserializeProperties(this, element); + Actions = new List(); - EndConversation = elem.GetAttributeBool("endconversation", false); - foreach (var e in elem.Elements()) + foreach (var e in element.Elements()) { if (e.Name.ToString().Equals("statuseffect", StringComparison.OrdinalIgnoreCase)) { - DebugConsole.ThrowError($"Error in event prefab \"{scriptedEvent.Prefab.Identifier}\". Status effect configured as a sub action (text: \"{Text}\"). Please configure status effects as child elements of a StatusEffectAction.", - contentPackage: elem.ContentPackage); + 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; } var action = Instantiate(scriptedEvent, e); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ForceSayAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ForceSayAction.cs new file mode 100644 index 000000000..1e5b933a2 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ForceSayAction.cs @@ -0,0 +1,62 @@ +using Barotrauma.Items.Components; +using Barotrauma.Networking; +using System.Linq; + +namespace Barotrauma +{ + /// + /// Forces a specific character to say a message in chat. + /// + class ForceSayAction : EventAction + { + [Serialize("", IsPropertySaveable.Yes, description: "Tag of the character that should say the message.")] + public Identifier TargetTag { get; set; } + + [Serialize("", IsPropertySaveable.Yes, description: "The message that the character should say. Can be the text as-is, or a tag referring to a line in a text file.")] + public string Message { get; set; } + + [Serialize(false, IsPropertySaveable.Yes, description: "Should the message that the character says be sent in radio?")] + public bool SayInRadio { get; set; } + + [Serialize(true, IsPropertySaveable.Yes, description: "Should the message be stripped of any quotation mark characters?")] + public bool RemoveQuotes { get; set; } + + public ForceSayAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { } + + private bool isFinished = false; + + public override bool IsFinished(ref string goTo) + { + return isFinished; + } + + public override void Reset() + { + isFinished = false; + } + + public override void Update(float deltaTime) + { + if (isFinished) { return; } + + var targets = ParentEvent.GetTargets(TargetTag); + + LocalizedString messageToSay = TextManager.Get(Message).Fallback(Message); + foreach (var target in targets) + { + if (target != null && target is Character character) + { + character.ForceSay(messageToSay, SayInRadio, RemoveQuotes); + } + } + + isFinished = true; + } + + public override string ToDebugString() + { + return $"{ToolBox.GetDebugSymbol(isFinished)} {nameof(ForceSayAction)} -> (TargetTag: {TargetTag.ColorizeObject()}, " + + $"Message: {Message})"; + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MissionStateAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MissionStateAction.cs index a6dd612be..92ddc1ee3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MissionStateAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MissionStateAction.cs @@ -1,84 +1,77 @@ -namespace Barotrauma +#nullable enable +namespace Barotrauma; + +/// Changes the state of missions. The way the states are used depends on the type of mission. +internal sealed class MissionStateAction : EventAction { - - /// - /// Changes the state of a specific active mission. The way the states are used depends on the type of mission. - /// - class MissionStateAction : EventAction + /// The operation to perform on missions' states. + public enum OperationType { - [Serialize("", IsPropertySaveable.Yes, description: "Identifier of the mission whose state to change.")] - public Identifier MissionIdentifier { get; set; } + /// Sets the missions' states to . + Set, + /// Adds to the missions' states. + Add + } - public enum OperationType + [Serialize("", IsPropertySaveable.Yes, "Identifiers of the missions whose states to change. Leave blank to only set the state of the mission that triggered the parent event.")] + public Identifier MissionIdentifier { get; set; } + + [Serialize(OperationType.Set, IsPropertySaveable.Yes, "The operation to perform on missions' states.")] + public OperationType Operation { get; set; } + + [Serialize(0, IsPropertySaveable.Yes, "The value to apply to missions' states.")] + public int State { get; set; } + + [Serialize(false, IsPropertySaveable.Yes, "If set to true, missions are forced to fail without a chance of retrying them.")] + public bool ForceFailure { get; set; } + + public MissionStateAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) + { + State = element.GetAttributeInt("value", State); + if (Operation == OperationType.Add && State == 0 && !ForceFailure) { - Set, - Add + DebugConsole.AddWarning($"Potential error in event \"{parentEvent.Prefab.Identifier}\": {nameof(MissionStateAction)} is set to only add 0 to the mission state, which will do nothing.", + contentPackage: element.ContentPackage); } + } - [Serialize(OperationType.Set, IsPropertySaveable.Yes, description: "Should the value be added to the state of the mission, or should the state be set to the specified value.")] - public OperationType Operation { get; set; } + private bool isFinished; + public override bool IsFinished(ref string goTo) => isFinished; + public override void Reset() => isFinished = false; - [Serialize(0, IsPropertySaveable.Yes, description: "The state to set the mission to, or how much to add to the state of the mission.")] - public int State { get; set; } + public override void Update(float deltaTime) + { + if (isFinished) { return; } - [Serialize(false, IsPropertySaveable.Yes, description: "If set to true, the mission is forced to fail without a chance of retrying it.")] - public bool ForceFailure { get; set; } - - private bool isFinished; - - public MissionStateAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) + if (!MissionIdentifier.IsEmpty) { - State = element.GetAttributeInt("value", State); - if (MissionIdentifier.IsEmpty) - { - DebugConsole.ThrowError($"Error in event \"{parentEvent.Prefab.Identifier}\": MissionIdentifier has not been configured.", - contentPackage: element.ContentPackage); - } - if (Operation == OperationType.Add && State == 0 && !ForceFailure) - { - DebugConsole.AddWarning($"Potential error in event \"{parentEvent.Prefab.Identifier}\": {nameof(MissionStateAction)} is set to add 0 to the mission state, which will do nothing.", - contentPackage: element.ContentPackage); - } - } - - public override bool IsFinished(ref string goTo) - { - return isFinished; - } - public override void Reset() - { - isFinished = false; - } - - public override void Update(float deltaTime) - { - if (isFinished) { return; } - foreach (Mission mission in GameMain.GameSession.Missions) { if (mission.Prefab.Identifier != MissionIdentifier) { continue; } - if (ForceFailure) - { - mission.ForceFailure = true; - } - - switch (Operation) - { - case OperationType.Set: - mission.State = State; - break; - case OperationType.Add: - mission.State += State; - break; - } + SetMissionState(mission); } - - isFinished = true; + } + else if (ParentEvent.TriggeringMission != null) + { + SetMissionState(ParentEvent.TriggeringMission); } - public override string ToDebugString() + isFinished = true; + } + + private void SetMissionState(Mission mission) + { + if (ForceFailure) { mission.ForceFailure = true; } + switch (Operation) { - return $"{ToolBox.GetDebugSymbol(isFinished)} {nameof(MissionStateAction)} -> ({(Operation == OperationType.Set ? State : '+' + State)})"; + case OperationType.Set: + mission.State = State; + break; + case OperationType.Add: + mission.State += State; + break; } } + + public override string ToDebugString() => $"{ToolBox.GetDebugSymbol(isFinished)} {nameof(MissionStateAction)} -> ({(Operation == OperationType.Set ? State : '+' + State)})"; } \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCFollowAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCFollowAction.cs index 968e8c988..ed8fa1590 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCFollowAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCFollowAction.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; @@ -18,6 +18,9 @@ namespace Barotrauma [Serialize(true, IsPropertySaveable.Yes, description: "Should the NPC start or stop following the target?")] public bool Follow { get; set; } + [Serialize(false, IsPropertySaveable.Yes, description: "Should the NPC be forced to walk towards the target?")] + public bool ForceWalk { get; set; } + [Serialize(-1, IsPropertySaveable.Yes, description: "Maximum number of NPCs to target (e.g. you could choose to only make a specific number of security officers follow the player.)")] public int MaxTargets { get; set; } @@ -65,7 +68,8 @@ namespace Barotrauma var newObjective = new AIObjectiveGoTo(target, npc, humanAiController.ObjectiveManager, repeat: true) { OverridePriority = Priority, - IsFollowOrder = true + IsFollowOrder = true, + ForceWalkPermanently = ForceWalk }; humanAiController.ObjectiveManager.AddObjective(newObjective); humanAiController.ObjectiveManager.WaitTimer = 0.0f; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs index 3b28cd186..6accd385e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs @@ -271,6 +271,10 @@ namespace Barotrauma ParentEvent.AddTarget(TargetTag, newCharacter); } spawnedEntity = newCharacter; + if (newCharacter is { AIController: EnemyAIController enemyAi, Submarine: Submarine ownSub }) + { + enemyAi.SetUnattackableSubmarines(ownSub); + } }); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs index 517298a9b..e39f17441 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs @@ -313,42 +313,45 @@ namespace Barotrauma CreateEvents(eventSet); } - if (level?.LevelData != null) + bool isOutpostLevel = level?.LevelData is { Type: LevelData.LevelType.Outpost } || + (GameMain.GameSession?.GameMode is TestGameMode && Submarine.MainSub?.Info?.Type == SubmarineType.Outpost); + if (isOutpostLevel) { - if (level.LevelData.Type == LevelData.LevelType.Outpost) + //if the outpost is connected to a locked connection, create an event to unlock it + if (level?.StartLocation?.Connections.Any(c => c.Locked && level.StartLocation.MapPosition.X < c.OtherLocation(level.StartLocation).MapPosition.X) ?? false) { - //if the outpost is connected to a locked connection, create an event to unlock it - if (level.StartLocation?.Connections.Any(c => c.Locked && level.StartLocation.MapPosition.X < c.OtherLocation(level.StartLocation).MapPosition.X) ?? false) + var unlockPathEventPrefab = EventPrefab.GetUnlockPathEvent(level.LevelData.Biome.Identifier, level.StartLocation.Faction); + if (unlockPathEventPrefab != null) { - var unlockPathEventPrefab = EventPrefab.GetUnlockPathEvent(level.LevelData.Biome.Identifier, level.StartLocation.Faction); - if (unlockPathEventPrefab != null) + var newEvent = unlockPathEventPrefab.CreateInstance(RandomSeed); + activeEvents.Add(newEvent); + } + else + { + //if no event that unlocks the path can be found, unlock it automatically + level.StartLocation.Connections.ForEach(c => c.Locked = false); + } + } + Submarine outpost = level?.StartOutpost ?? Submarine.MainSub; + if (GameMain.NetworkMember is not { IsClient: true } && outpost != null) + { + foreach (var eventTag in outpost.Info.TriggerOutpostMissionEvents) + { + EventPrefab eventPrefab = EventPrefab.FindEventPrefab(identifier: Identifier.Empty, tag: eventTag, outpost.ContentPackage); + if (eventPrefab == null) { - var newEvent = unlockPathEventPrefab.CreateInstance(RandomSeed); - AddActiveEvent(newEvent); + DebugConsole.ThrowError($"Outpost {outpost.Info.DisplayName} failed to trigger an event (tag: {eventTag}).", contentPackage: outpost.ContentPackage); } else { - //if no event that unlocks the path can be found, unlock it automatically - level.StartLocation.Connections.ForEach(c => c.Locked = false); + var newEvent = eventPrefab.CreateInstance(RandomSeed); + ActivateEvent(newEvent); } } - if (GameMain.NetworkMember is not { IsClient: true } && level.StartOutpost != null) - { - foreach (var eventTag in level.StartOutpost.Info.TriggerOutpostMissionEvents) - { - EventPrefab eventPrefab = EventPrefab.FindEventPrefab(identifier: Identifier.Empty, tag: eventTag, level.StartOutpost.ContentPackage); - if (eventPrefab == null) - { - DebugConsole.ThrowError($"Outpost {level.StartOutpost.Info.DisplayName} failed to trigger an event (tag: {eventTag}).", contentPackage: level.StartOutpost.ContentPackage); - } - else - { - var newEvent = eventPrefab.CreateInstance(RandomSeed); - ActivateEvent(newEvent); - } - } - } - } + } + } + if (level?.LevelData != null) + { RegisterNonRepeatableChildEvents(initialEventSet); void RegisterNonRepeatableChildEvents(EventSet eventSet) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AbandonedOutpostMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AbandonedOutpostMission.cs index 730d77e44..69f224663 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AbandonedOutpostMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AbandonedOutpostMission.cs @@ -233,7 +233,7 @@ namespace Barotrauma } - protected override bool DetermineCompleted() + protected override bool DetermineCompleted(CampaignMode.TransitionType transitionType) { return State > 0 && State != HostagesKilledState; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/BeaconMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/BeaconMission.cs index f9b102a87..70fc86b00 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/BeaconMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/BeaconMission.cs @@ -171,7 +171,7 @@ namespace Barotrauma #endif } - protected override bool DetermineCompleted() + protected override bool DetermineCompleted(CampaignMode.TransitionType transitionType) { return level.CheckBeaconActive(); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CargoMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CargoMission.cs index f62efa2ef..4768696f5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CargoMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CargoMission.cs @@ -331,7 +331,7 @@ namespace Barotrauma } } - protected override bool DetermineCompleted() + protected override bool DetermineCompleted(CampaignMode.TransitionType transitionType) { if (Submarine.MainSub != null && Submarine.MainSub.AtEndExit) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CombatMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CombatMission.cs index 64bf201c7..6b45f0ef9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CombatMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CombatMission.cs @@ -204,7 +204,7 @@ namespace Barotrauma } } - protected override bool DetermineCompleted() + protected override bool DetermineCompleted(CampaignMode.TransitionType transitionType) { return Winner != CharacterTeamType.None; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CustomMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CustomMission.cs new file mode 100644 index 000000000..d18349dbd --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CustomMission.cs @@ -0,0 +1,18 @@ +#nullable enable +namespace Barotrauma; + +/// +/// Defines a mission where the success and failure are determined solely by its state. +/// Intended to be used alongside . +/// +internal sealed partial class CustomMission(MissionPrefab prefab, Location[] locations, Submarine sub) : Mission(prefab, locations, sub) +{ + public readonly int SuccessState = prefab.ConfigElement.GetAttributeInt(nameof(SuccessState), +1); + public readonly int FailureState = prefab.ConfigElement.GetAttributeInt(nameof(FailureState), -1); + + public bool RequireDestinationReached = prefab.ConfigElement.GetAttributeBool(nameof(RequireDestinationReached), false); + + protected override bool DetermineCompleted(CampaignMode.TransitionType transitionType) => + State == SuccessState && + (!RequireDestinationReached || transitionType is CampaignMode.TransitionType.ProgressToNextLocation or CampaignMode.TransitionType.ProgressToNextEmptyLocation); +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/EliminateTargetsMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/EliminateTargetsMission.cs index cc7855701..1bfeaea1f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/EliminateTargetsMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/EliminateTargetsMission.cs @@ -1,4 +1,4 @@ -using System; +using System; using Barotrauma.Extensions; using Barotrauma.RuinGeneration; using Microsoft.Xna.Framework; @@ -199,7 +199,7 @@ namespace Barotrauma private static bool IsEnemyDefeated(Character enemy) => enemy == null ||enemy.Removed || enemy.IsDead; - protected override bool DetermineCompleted() + protected override bool DetermineCompleted(CampaignMode.TransitionType transitionType) { bool exitingLevel = GameMain.GameSession?.GameMode is CampaignMode campaign ? campaign.GetAvailableTransition() != CampaignMode.TransitionType.None : diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/EndMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/EndMission.cs index 513cdd8c5..35c9c40be 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/EndMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/EndMission.cs @@ -1,4 +1,4 @@ -using Barotrauma.Extensions; +using Barotrauma.Extensions; using Barotrauma.Items.Components; using Microsoft.Xna.Framework; using System; @@ -301,7 +301,7 @@ namespace Barotrauma partial void OnStateChangedProjSpecific(); - protected override bool DetermineCompleted() + protected override bool DetermineCompleted(CampaignMode.TransitionType transitionType) { return Phase == MissionPhase.BossKilled; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/EscortMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/EscortMission.cs index 473006b60..3a770fffa 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/EscortMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/EscortMission.cs @@ -343,7 +343,7 @@ namespace Barotrauma return character != null && !character.Removed && !character.IsDead; } - protected override bool DetermineCompleted() + protected override bool DetermineCompleted(CampaignMode.TransitionType transitionType) { if (Submarine.MainSub != null && Submarine.MainSub.AtEndExit) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/GoToMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/GoToMission.cs index a1924db58..c35e7b789 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/GoToMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/GoToMission.cs @@ -17,7 +17,7 @@ namespace Barotrauma } } - protected override bool DetermineCompleted() + protected override bool DetermineCompleted(CampaignMode.TransitionType transitionType) { if (Level.Loaded?.Type == LevelData.LevelType.Outpost) { @@ -25,7 +25,7 @@ namespace Barotrauma } else { - return Submarine.MainSub is { AtEndExit: true }; + return transitionType == CampaignMode.TransitionType.ProgressToNextLocation; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MineralMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MineralMission.cs index 10d174170..d75e61fa6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MineralMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MineralMission.cs @@ -1,4 +1,4 @@ -using Barotrauma.Extensions; +using Barotrauma.Extensions; using Barotrauma.Items.Components; using Microsoft.Xna.Framework; using System; @@ -47,6 +47,12 @@ namespace Barotrauma } } + /// + /// Minerals spawned by the mission. Note that minerals that were already present in the level may have also been used as targets. + /// Each list of items represents a separate cluster of minerals. + /// + public IEnumerable> SpawnedResources => spawnedResources.Values; + public override LocalizedString SuccessMessage => ModifyMessage(base.SuccessMessage); public override LocalizedString FailureMessage => ModifyMessage(base.FailureMessage); public override LocalizedString Description => ModifyMessage(description); @@ -169,7 +175,7 @@ namespace Barotrauma } } - protected override bool DetermineCompleted() + protected override bool DetermineCompleted(CampaignMode.TransitionType transitionType) { return EnoughHaveBeenCollected(); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs index e6629022b..6dcfd314f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs @@ -401,14 +401,9 @@ namespace Barotrauma { characterItems.Add(spawnedCharacter, spawnedCharacter.Inventory.FindAllItems(recursive: true)); } - if (submarine != null && spawnedCharacter.AIController is EnemyAIController enemyAi) + if (spawnedCharacter.AIController is EnemyAIController enemyAi && submarine != null) { - enemyAi.UnattackableSubmarines.Add(submarine); - enemyAi.UnattackableSubmarines.Add(Submarine.MainSub); - foreach (Submarine sub in Submarine.MainSub.DockedTo) - { - enemyAi.UnattackableSubmarines.Add(sub); - } + enemyAi.SetUnattackableSubmarines(submarine); } InitCharacter(spawnedCharacter, element); return spawnedCharacter; @@ -532,6 +527,7 @@ namespace Barotrauma if (GameMain.GameSession?.EventManager != null) { var newEvent = eventPrefab.CreateInstance(GameMain.GameSession.EventManager.RandomSeed); + newEvent.TriggeringMission = this; GameMain.GameSession.EventManager.ActivateEvent(newEvent); } } @@ -539,13 +535,13 @@ namespace Barotrauma /// /// End the mission and give a reward if it was completed successfully /// - public void End() + public void End(CampaignMode.TransitionType transitionType) { if (GameMain.NetworkMember is not { IsClient: true }) { completed = !ForceFailure && - DetermineCompleted() && + DetermineCompleted(transitionType) && (completeCheckDataAction == null || completeCheckDataAction.GetSuccess()); } if (completed) @@ -578,7 +574,7 @@ namespace Barotrauma } } - protected abstract bool DetermineCompleted(); + protected abstract bool DetermineCompleted(CampaignMode.TransitionType transitionType); protected virtual void EndMissionSpecific(bool completed) { } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs index cd32310eb..28c5e4a26 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs @@ -30,7 +30,8 @@ namespace Barotrauma { "GoTo".ToIdentifier(), typeof(GoToMission) }, { "ScanAlienRuins".ToIdentifier(), typeof(ScanMission) }, { "EliminateTargets".ToIdentifier(), typeof(EliminateTargetsMission) }, - { "End".ToIdentifier(), typeof(EndMission) } + { "End".ToIdentifier(), typeof(EndMission) }, + { "Custom".ToIdentifier(), typeof(CustomMission) } }; /// @@ -64,6 +65,7 @@ namespace Barotrauma public Type MissionClass { get; private set; } + public bool CampaignOnly { get; private set; } public bool MultiplayerOnly { get; private set; } public bool SingleplayerOnly { get; private set; } @@ -319,8 +321,9 @@ namespace Barotrauma SonarIconIdentifier = ConfigElement.GetAttributeIdentifier("sonaricon", ""); - MultiplayerOnly = ConfigElement.GetAttributeBool("multiplayeronly", false); - SingleplayerOnly = ConfigElement.GetAttributeBool("singleplayeronly", false); + CampaignOnly = ConfigElement.GetAttributeBool(nameof(CampaignOnly), false); + MultiplayerOnly = ConfigElement.GetAttributeBool(nameof(MultiplayerOnly), false); + SingleplayerOnly = ConfigElement.GetAttributeBool(nameof(SingleplayerOnly), false); AchievementIdentifier = ConfigElement.GetAttributeIdentifier("achievementidentifier", ""); @@ -543,7 +546,7 @@ namespace Barotrauma } /// - /// Returns all mission types that can be selected e.g. in the server lobby, excluding any special, hidden ones like EndMission + /// Returns all mission types that can be selected in the server lobby, excluding any special, hidden ones like EndMission /// (the mission at the end of the campaign) /// public static IEnumerable GetAllMultiplayerSelectableMissionTypes() @@ -552,6 +555,7 @@ namespace Barotrauma foreach (var missionPrefab in Prefabs) { if (missionPrefab.Commonness <= 0.0f) { continue; } + if (missionPrefab.CampaignOnly) { continue; } if (missionPrefab.SingleplayerOnly) { continue; } if (HiddenMissionTypes.Contains(missionPrefab.Type)) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MonsterMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MonsterMission.cs index 941b7c4dd..656764b14 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MonsterMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MonsterMission.cs @@ -242,7 +242,7 @@ namespace Barotrauma } } - protected override bool DetermineCompleted() + protected override bool DetermineCompleted(CampaignMode.TransitionType transitionType) { return state > 0; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/NestMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/NestMission.cs index 3ad3effe1..41dbb356a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/NestMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/NestMission.cs @@ -337,7 +337,7 @@ namespace Barotrauma return true; } - protected override bool DetermineCompleted() + protected override bool DetermineCompleted(CampaignMode.TransitionType transitionType) { return AllItemsDestroyedOrRetrieved(); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/PirateMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/PirateMission.cs index 5a31acbdf..ee5d1eda7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/PirateMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/PirateMission.cs @@ -547,7 +547,7 @@ namespace Barotrauma return character == null || character.Removed || character.Submarine == null || (character.LockHands && character.Submarine == Submarine.MainSub) || character.IsIncapacitated; } - protected override bool DetermineCompleted() + protected override bool DetermineCompleted(CampaignMode.TransitionType transitionType) { return state == 2; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs index 602e4dd0d..e0a7aa4af 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs @@ -715,7 +715,7 @@ namespace Barotrauma } } - protected override bool DetermineCompleted() + protected override bool DetermineCompleted(CampaignMode.TransitionType transitionType) { if (requiredDeliveryAmount < 1.0f) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/ScanMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/ScanMission.cs index e388b2908..a16c556fd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/ScanMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/ScanMission.cs @@ -1,4 +1,4 @@ -using System; +using System; using Barotrauma.Extensions; using Barotrauma.Items.Components; using Barotrauma.RuinGeneration; @@ -17,7 +17,7 @@ namespace Barotrauma private readonly Dictionary parentInventoryIDs = new Dictionary(); private readonly Dictionary inventorySlotIndices = new Dictionary(); private readonly Dictionary parentItemContainerIndices = new Dictionary(); - private readonly int targetsToScan; + private readonly int totalTargetsToScan; private readonly Dictionary scanTargets = new Dictionary(); private readonly HashSet newTargetsScanned = new HashSet(); private readonly float minTargetDistance; @@ -44,7 +44,7 @@ namespace Barotrauma public ScanMission(MissionPrefab prefab, Location[] locations, Submarine sub) : base(prefab, locations, sub) { itemConfig = prefab.ConfigElement.GetChildElement("Items"); - targetsToScan = prefab.ConfigElement.GetAttributeInt("targets", 1); + totalTargetsToScan = prefab.ConfigElement.GetAttributeInt("targets", 1); minTargetDistance = prefab.ConfigElement.GetAttributeFloat("mintargetdistance", 0.0f); } @@ -77,57 +77,60 @@ namespace Barotrauma var ruinWaypoints = TargetRuin.Submarine.GetWaypoints(false); ruinWaypoints.RemoveAll(wp => wp.CurrentHull == null); - if (ruinWaypoints.Count < targetsToScan) + if (ruinWaypoints.Count < totalTargetsToScan) { - DebugConsole.ThrowError($"Failed to initialize a Scan mission: target ruin has less waypoints than required as scan targets ({ruinWaypoints.Count} < {targetsToScan})", + DebugConsole.ThrowError($"Failed to initialize a Scan mission: target ruin has less waypoints than required as scan targets ({ruinWaypoints.Count} < {totalTargetsToScan})", contentPackage: Prefab.ContentPackage); return; } + + //the distance we'll use if we otherwise fail to place the targets far enough from each other + //(smallest extent should be large enough to fit the targets and one extra to be safe) + float guaranteedDistance = Math.Min(TargetRuin.Area.Width, TargetRuin.Area.Height) / (totalTargetsToScan + 1); + var availableWaypoints = new List(); - float minTargetDistanceSquared = minTargetDistance * minTargetDistance; - for (int tries = 0; tries < 15; tries++) + const int MaxTries = 15; + for (int tries = 0; tries < MaxTries; tries++) { + float triesNormalized = tries / (float)(MaxTries - 1); // 0.0 -> 1.0 + float desperationFactor = MathF.Pow(triesNormalized, 2); + //try placing the targets the desired minimum distance apart, gradually lowering the distance requirement on each try + float currentMinDistance = MathHelper.Lerp(minTargetDistance, guaranteedDistance, desperationFactor); + float currentMinDistanceSquared = currentMinDistance * currentMinDistance; + scanTargets.Clear(); availableWaypoints.Clear(); availableWaypoints.AddRange(ruinWaypoints); - for (int i = 0; i < targetsToScan; i++) + for (int i = 0; i < totalTargetsToScan; i++) { var selectedWaypoint = availableWaypoints.GetRandom(randSync: Rand.RandSync.ServerAndClient); scanTargets.Add(selectedWaypoint, false); availableWaypoints.Remove(selectedWaypoint); - if (i < (targetsToScan - 1)) + if (i < (totalTargetsToScan - 1)) { availableWaypoints.RemoveAll(wp => wp.CurrentHull == selectedWaypoint.CurrentHull); - availableWaypoints.RemoveAll(wp => Vector2.DistanceSquared(wp.WorldPosition, selectedWaypoint.WorldPosition) < minTargetDistanceSquared); + availableWaypoints.RemoveAll(wp => Vector2.DistanceSquared(wp.WorldPosition, selectedWaypoint.WorldPosition) < currentMinDistanceSquared); if (availableWaypoints.None()) { #if DEBUG - DebugConsole.ThrowError($"Error initializing a Scan mission: not enough targets available on try #{tries + 1} to reach the required scan target count (current targets: {scanTargets.Count}, required targets: {targetsToScan})", + DebugConsole.ThrowError($"Error initializing a Scan mission: not enough targets available on try #{tries + 1} to reach the required scan target count (current targets: {scanTargets.Count}, required targets: {totalTargetsToScan})", contentPackage: Prefab.ContentPackage); #endif break; } } } - if (scanTargets.Count >= targetsToScan) + if (scanTargets.Count >= totalTargetsToScan) { #if DEBUG DebugConsole.NewMessage($"Successfully initialized a Scan mission: targets set on try #{tries + 1}", Color.Green); #endif break; } - if ((tries + 1) % 5 == 0) - { - float reducedMinTargetDistance = (1.0f - (((tries + 1) / 5) * 0.1f)) * minTargetDistance; - minTargetDistanceSquared = reducedMinTargetDistance * reducedMinTargetDistance; -#if DEBUG - DebugConsole.NewMessage($"Reducing minimum distance between Scan mission targets (new min: {reducedMinTargetDistance}) to reach the required target count", Color.Yellow); -#endif - } } - if (scanTargets.Count < targetsToScan) + if (scanTargets.Count < totalTargetsToScan) { - DebugConsole.ThrowError($"Error initializing a Scan mission: not enough targets (current targets: {scanTargets.Count}, required targets: {targetsToScan})", + DebugConsole.ThrowError($"Error initializing a Scan mission: not enough targets (current targets: {scanTargets.Count}, required targets: {totalTargetsToScan})", contentPackage: Prefab.ContentPackage); } } @@ -241,9 +244,9 @@ namespace Barotrauma State = Math.Max(State, scanTargets.Count(kvp => kvp.Value)); } - private bool AllTargetsScanned() => State >= targetsToScan; + private bool AllTargetsScanned() => State >= totalTargetsToScan; - protected override bool DetermineCompleted() => AllTargetsScanned(); + protected override bool DetermineCompleted(CampaignMode.TransitionType transitionType) => AllTargetsScanned(); protected override void EndMissionSpecific(bool completed) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameAnalytics/GameAnalyticsConsent.cs b/Barotrauma/BarotraumaShared/SharedSource/GameAnalytics/GameAnalyticsConsent.cs index 129fa3d25..6edea3673 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameAnalytics/GameAnalyticsConsent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameAnalytics/GameAnalyticsConsent.cs @@ -104,7 +104,7 @@ namespace Barotrauma return authTicket.TryUnwrap(out var ticketUnwrapped) && ticketUnwrapped.Data is { Length: > 0 } ? new AuthTicket(ToolBoxCore.ByteArrayToHexString(ticketUnwrapped.Data), Platform.Steam) //convert byte array to hex - : throw new Exception("Could not retrieve Steamworks authentication ticket for GameAnalytics"); + : throw new Exception("Could not retrieve Steam authentication ticket, possibly due to connection issues. GameAnalytics logging will be disabled."); } private static async Task GetEOSAuthTicket() @@ -215,9 +215,8 @@ namespace Barotrauma IRestResponse response; try { - var client = new RestClient(consentServerUrl); - - var request = new RestRequest(consentServerFile, Method.GET); + var client = RestFactory.CreateClient(consentServerUrl); + var request = RestFactory.CreateRequest(consentServerFile); request.AddParameter("authticket", authTicket.Token); if (consent == Consent.Ask) { @@ -321,7 +320,7 @@ namespace Barotrauma RestClient client; try { - client = new RestClient(consentServerUrl); + client = RestFactory.CreateClient(consentServerUrl); } catch (Exception e) { @@ -329,7 +328,7 @@ namespace Barotrauma return Consent.Error; } - var request = new RestRequest(consentServerFile, Method.GET); + var request = RestFactory.CreateRequest(consentServerFile); request.AddParameter("authticket", authTicket.Token); request.AddParameter("action", "getconsent"); request.AddParameter("request_version", RemoteRequestVersion); diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs index daa20659a..ae18a5a21 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs @@ -1017,7 +1017,7 @@ namespace Barotrauma UpdateStoreStock(); } - GameMain.GameSession.EndMissions(); + GameMain.GameSession.EndMissions(TransitionType.None); GameMain.GameSession.EventManager?.StoreEventDataAtRoundEnd(registerFinishedOnly: true); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MissionMode.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MissionMode.cs index e00bf540b..d19250e5e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MissionMode.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MissionMode.cs @@ -53,6 +53,7 @@ namespace Barotrauma { foreach (MissionPrefab missionPrefab in missionPrefabs) { + if (missionPrefab.CampaignOnly) { continue; } if (!missionClasses.ContainsValue(missionPrefab.MissionClass)) { throw new InvalidOperationException($"Cannot start gamemode with a {missionPrefab.MissionClass} mission."); @@ -68,7 +69,7 @@ namespace Barotrauma { return missionTypes.Where(type => MissionPrefab.Prefabs.OrderBy(missionPrefab => missionPrefab.UintIdentifier) - .Any(missionPrefab => missionPrefab.Type == type && missionClasses.ContainsValue(missionPrefab.MissionClass))); + .Any(missionPrefab => missionPrefab.Type == type && !missionPrefab.CampaignOnly && missionClasses.ContainsValue(missionPrefab.MissionClass))); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs index c23a81644..89b118e05 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs @@ -1,16 +1,17 @@ #nullable enable +using Barotrauma.Extensions; using Barotrauma.IO; using Barotrauma.Items.Components; +using Barotrauma.Networking; +using Barotrauma.PerkBehaviors; +using FarseerPhysics; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using System.Xml.Linq; -using Barotrauma.Networking; -using Barotrauma.Extensions; -using Barotrauma.PerkBehaviors; namespace Barotrauma { @@ -408,7 +409,6 @@ namespace Barotrauma public void LoadPreviousSave() { - GameMain.LuaCs.Hook.Call("roundEnd"); AchievementManager.OnRoundEnded(this, roundInterrupted: true); Submarine.Unload(); SaveUtil.LoadGame(DataPath); @@ -761,7 +761,6 @@ namespace Barotrauma GUI.PreventPauseMenuToggle = false; HintManager.OnRoundStarted(); - GameMain.LuaCs.Hook.Call("roundStart"); EnableEventLogNotificationIcon(enabled: false); LogStartRoundStats(); @@ -955,14 +954,6 @@ namespace Barotrauma sub.SetPosition(spawnPos); myPort.Dock(outPostPort); myPort.Lock(isNetworkMessage: true, applyEffects: false); - foreach (var item in sub.GetItems(alsoFromConnectedSubs: true)) - { - //need to refresh position to maintain since the sub was moved to the docking port - if (item.GetComponent() is { MaintainPos: true } steering) - { - steering.RefreshPosToMaintain(); - } - } } else { @@ -985,6 +976,16 @@ namespace Barotrauma sub.EnableMaintainPosition(); } + foreach (var item in sub.GetItems(alsoFromConnectedSubs: true)) + { + // Refresh pos to maintain in all steering components maintaining + // position, including ones in shuttles, since the submarines moved + if (item.GetComponent() is { MaintainPos: true } steering) + { + steering.RefreshPosToMaintain(); + } + } + // Make sure that linked subs which are NOT docked to the main sub // (but still close enough to NOT be considered as 'left behind') // are also moved to keep their relative position to the main sub @@ -1048,9 +1049,6 @@ namespace Barotrauma /// public static ImmutableHashSet GetSessionCrewCharacters(CharacterType type) { - var result = GameMain.LuaCs.Hook.Call("getSessionCrewCharacters", type); - if (result != null) return ImmutableHashSet.Create(result); - if (GameMain.GameSession?.CrewManager is not { } crewManager) { return ImmutableHashSet.Empty; } IEnumerable players; @@ -1089,9 +1087,6 @@ namespace Barotrauma { RoundEnding = true; -#if CLIENT - GameMain.LuaCs.Hook.Call("roundEnd"); -#endif //Clear the grids to allow for garbage collection Powered.Grids.Clear(); Powered.ClearChangedConnections(); @@ -1103,15 +1098,13 @@ namespace Barotrauma ImmutableHashSet crewCharacters = GetSessionCrewCharacters(CharacterType.Both); int prevMoney = GetAmountOfMoney(crewCharacters); - EndMissions(); + EndMissions(transitionType); foreach (Character character in crewCharacters) { character.CheckTalents(AbilityEffectType.OnRoundEnd); } - GameMain.LuaCs.Hook.Call("missionsEnded", missions); - #if CLIENT if (GUI.PauseMenuOpen) { @@ -1209,12 +1202,12 @@ namespace Barotrauma } } - public void EndMissions() + public void EndMissions(CampaignMode.TransitionType transitionType) { ImmutableHashSet crewCharacters = GetSessionCrewCharacters(CharacterType.Both); foreach (Mission mission in missions) { - mission.End(); + mission.End(transitionType); } if (missions.Any()) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs index 98b9bd60a..146f6b603 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs @@ -43,8 +43,12 @@ namespace Barotrauma public InvSlotType[] SlotTypes { get; - private set; } + + /// + /// Optimization for fast access of by . + /// + private readonly Dictionary> slotsByType = []; public static readonly List AnySlot = new List { InvSlotType.Any }; public static readonly List BagSlot = new List { InvSlotType.Bag }; @@ -106,9 +110,20 @@ namespace Barotrauma case InvSlotType.RightHand: slots[i].HideIfEmpty = true; break; - } + } } - + + for (int i = 0; i < capacity; i++) + { + InvSlotType slotType = SlotTypes[i]; + if (!slotsByType.TryGetValue(slotType, out List slotList)) + { + slotList = []; + slotsByType[SlotTypes[i]] = slotList; + } + slotList.Add(slots[i]); + } + InitProjSpecific(element); var itemElements = element.Elements().Where(e => e.Name.ToString().Equals("item", StringComparison.OrdinalIgnoreCase)); @@ -198,39 +213,55 @@ namespace Barotrauma public Item GetItemInLimbSlot(InvSlotType limbSlot) { - for (int i = 0; i < slots.Length; i++) + if (slotsByType.TryGetValue(limbSlot, out List slotList)) { - if (SlotTypes[i] == limbSlot) { return slots[i].FirstOrDefault(); } + return slotList.First().FirstOrDefault(); } return null; } + public IEnumerable GetItemsInLimbSlot(InvSlotType limbSlot) + { + if (slotsByType.TryGetValue(limbSlot, out List slotList)) + { + foreach (var slot in slotList) + { + foreach (Item item in slot.Items) + { + yield return item; + } + } + } + } public bool IsInLimbSlot(Item item, InvSlotType limbSlot) { if (limbSlot == (InvSlotType.LeftHand | InvSlotType.RightHand)) { - int rightHandSlot = FindLimbSlot(InvSlotType.RightHand); - int leftHandSlot = FindLimbSlot(InvSlotType.LeftHand); - if (rightHandSlot > -1 && slots[rightHandSlot].Contains(item) && - leftHandSlot > -1 && slots[leftHandSlot].Contains(item)) + if (GetItemsInLimbSlot(InvSlotType.RightHand).Contains(item) && + GetItemsInLimbSlot(InvSlotType.LeftHand).Contains(item)) { return true; } } - - for (int i = 0; i < slots.Length; i++) + else if (slotsByType.TryGetValue(limbSlot, out List slotList)) { - if (SlotTypes[i] == limbSlot && slots[i].Contains(item)) { return true; } + foreach (ItemSlot slot in slotList) + { + if (slot.Contains(item)) { return true; } + } } return false; } public bool IsSlotEmpty(InvSlotType limbSlot) { - for (int i = 0; i < slots.Length; i++) + if (slotsByType.TryGetValue(limbSlot, out List slotList)) { - if (SlotTypes[i] == limbSlot && slots[i].Empty()) { return true; } + foreach (ItemSlot slot in slotList) + { + if (slot.Empty()) { return true; } + } } return false; } @@ -370,7 +401,7 @@ namespace Barotrauma /// /// If there is room, puts the item in the inventory and returns true, otherwise returns false /// - public override bool TryPutItem(Item item, Character user, IEnumerable allowedSlots = null, bool createNetworkEvent = true, bool ignoreCondition = false) + public override bool TryPutItem(Item item, Character user, IEnumerable allowedSlots = null, bool createNetworkEvent = true, bool ignoreCondition = false, bool triggerOnInsertedEffects = true) { if (allowedSlots == null || !allowedSlots.Any()) { return false; } if (item == null) @@ -494,8 +525,6 @@ namespace Barotrauma return placedInSlot > -1; } - - public bool IsAnySlotAvailable(Item item) => GetFreeAnySlot(item, inWrongSlot: false) > -1; private int GetFreeAnySlot(Item item, bool inWrongSlot) @@ -542,7 +571,7 @@ namespace Barotrauma return -1; } - public override bool TryPutItem(Item item, int index, bool allowSwapping, bool allowCombine, Character user, bool createNetworkEvent = true, bool ignoreCondition = false) + public override bool TryPutItem(Item item, int index, bool allowSwapping, bool allowCombine, Character user, bool createNetworkEvent = true, bool ignoreCondition = false, bool triggerOnInsertedEffects = true) { if (index < 0 || index >= slots.Length) { @@ -590,9 +619,9 @@ namespace Barotrauma return TryPutItem(item, user, new List() { placeToSlots }, createNetworkEvent, ignoreCondition); } - protected override void PutItem(Item item, int i, Character user, bool removeItem = true, bool createNetworkEvent = true) + protected override void PutItem(Item item, int i, Character user, bool removeItem = true, bool createNetworkEvent = true, bool triggerOnInsertedEffects = true) { - base.PutItem(item, i, user, removeItem, createNetworkEvent); + base.PutItem(item, i, user, removeItem, createNetworkEvent, triggerOnInsertedEffects); #if CLIENT CreateSlots(); if (character == Character.Controlled) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs index 405b0c9b8..7e03796ab 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs @@ -769,6 +769,12 @@ namespace Barotrauma.Items.Components { picker.Inventory.FlashAllowedSlots(item, Color.Red); } + else + { + //normally this would be done in the base.OnPicked method, but clients don't call it, + //but instead rely on the server telling them to put the item in the inventory + SoundPlayer.PlayUISound(GUISoundType.PickItem); + } return false; } #endif diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs index 15fe42183..35d06fc8f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs @@ -1,4 +1,5 @@ -using FarseerPhysics; +using Barotrauma.LuaCs.Events; +using FarseerPhysics; using FarseerPhysics.Dynamics; using FarseerPhysics.Dynamics.Contacts; using Microsoft.Xna.Framework; @@ -347,18 +348,9 @@ namespace Barotrauma.Items.Components } else if (f2.Body.UserData is Character targetCharacter) { - if (targetCharacter == picker || targetCharacter == User) { return false; } - if (targetCharacter.IgnoreMeleeWeapons) { return false; } - if (HitFriendlyTarget(targetCharacter)) { return false; } - if (AllowHitMultiple) - { - if (hitTargets.Contains(targetCharacter)) { return false; } - } - else - { - if (hitTargets.Any(t => t is Character)) { return false; } - } - hitTargets.Add(targetCharacter); + //only allow hitting limbs, not the main collider + //otherwise it's difficult to make certain parts of the ragdoll not take hits by making them ignore collisions or melee weapons + return false; } else if (!HitOnlyCharacters) { @@ -435,7 +427,7 @@ namespace Barotrauma.Items.Components Structure targetStructure = target.UserData as Structure ?? targetFixture.UserData as Structure; Item targetItem = target.UserData is Holdable h ? h.Item : target.UserData as Item ?? targetFixture.UserData as Item; Entity targetEntity = targetCharacter ?? targetStructure ?? targetItem ?? target.UserData as Entity; - GameMain.LuaCs.Hook.Call("meleeWeapon.handleImpact", this, target); + LuaCsSetup.Instance.EventService.PublishEvent(x => x.OnMeleeWeaponHandleImpact(this, target)); if (Attack != null) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs index 0ae99cd9e..3263531c7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs @@ -380,7 +380,7 @@ namespace Barotrauma.Items.Components break; case "requireditem": case "requireditems": - SetRequiredItems(subElement); + SetRequiredItems(subElement, allowEmpty: true); break; case "requiredskill": case "requiredskills": @@ -1103,6 +1103,9 @@ namespace Barotrauma.Items.Components foreach (RelatedItem ri in DisabledRequiredItems) { XElement newElement = new XElement("requireditem"); + //if we have some actual requirements, no need to keep the empty requirement + //as a "placeholder" for the user to add requirements in the sub editor + if (ri.Identifiers.IsEmpty && RequiredItems.Any()) { continue; } ri.Save(newElement); componentElement.Add(newElement); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs index 992b4bfc6..64b919848 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs @@ -425,7 +425,7 @@ namespace Barotrauma.Items.Components partial void InitProjSpecific(ContentXElement element); - public void OnItemContained(Item containedItem) + public void OnItemContained(Item containedItem, bool triggerOnInsertedEffects = true) { int index = Inventory.FindIndex(containedItem); RelatedItem relatedItem = null; @@ -444,14 +444,20 @@ namespace Barotrauma.Items.Components ActiveContainedItem activeContainedItem = new(containedItem, effect, containableItem.ExcludeBroken, containableItem.ExcludeFullCondition, containableItem.BlameEquipperForDeath); activeContainedItems.Add(activeContainedItem); - if (!ShouldApplyEffects(activeContainedItem) || item.Submarine is { Loading: true} || initializingLoadedItems || - containedItem.OnInsertedEffectsApplied) - { - continue; + if (triggerOnInsertedEffects) + { + if (!ShouldApplyEffects(activeContainedItem) || item.Submarine is { Loading: true} || initializingLoadedItems || + containedItem.OnInsertedEffectsApplied) + { + continue; + } + activeContainedItem.StatusEffect.Apply(ActionType.OnInserted, deltaTime: 1, item, targets); } - activeContainedItem.StatusEffect.Apply(ActionType.OnInserted, deltaTime: 1, item, targets); } - containedItem.OnInsertedEffectsApplied = true; + if (triggerOnInsertedEffects) + { + containedItem.OnInsertedEffectsApplied = true; + } } } } @@ -1124,6 +1130,16 @@ namespace Barotrauma.Items.Components } else { + if (item.GetComponent() 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) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/LinkedControllerCharacterComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/LinkedControllerCharacterComponent.cs new file mode 100644 index 000000000..6faa6db36 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/LinkedControllerCharacterComponent.cs @@ -0,0 +1,121 @@ +#nullable enable + +using Barotrauma.Networking; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Xml.Linq; + +namespace Barotrauma.Items.Components +{ + /// + /// Item component used by for keeping a reference to the character that is currently + /// selecting the controller. Also provides functionality for changing the inventory sprite of the item based on the linked character. + /// + partial class LinkedControllerCharacterComponent : ItemComponent, IServerSerializable + { +#if CLIENT + private class SpriteOverride + { + public readonly Sprite? Sprite; + public readonly Identifier SpeciesName; + public readonly Identifier SpeciesGroup; + public SpriteOverride(ContentXElement element) + { + if (element.GetChildElement("Sprite") is ContentXElement spriteElement) + { + Sprite = new Sprite(spriteElement); + } + SpeciesName = element.GetAttributeIdentifier("speciesname", Identifier.Empty); + SpeciesGroup = element.GetAttributeIdentifier("speciesgroup", Identifier.Empty); + } + } + + private readonly ImmutableArray spriteOverrides; +#endif + + [Serialize(0.5f, IsPropertySaveable.No, description: $"Maximum value which {nameof(DeconstructTimeMultiplier)} can be.")] + public float MaxDeconstructTimeMultiplier + { + get; + set; + } + + public Character? Character { get; private set; } + + public bool DoesBleed => Character?.DoesBleed == true; + + public float DeconstructTimeMultiplier { get; private set; } = 1f; + + public LinkedControllerCharacterComponent(Item item, ContentXElement element) : base(item, element) + { +#if CLIENT + spriteOverrides = element.Elements() + .Where(static e => e.Name.LocalName.ToLowerInvariant() == "spriteoverride") + .Select(static e => new SpriteOverride(e)) + .ToImmutableArray(); +#endif + } + + public void UpdateLinkedCharacter(Character? character) + { + Character = character; + + if (character != null) + { + var animController = character.AnimController; + float totalLimbs = animController.Limbs.Length; + float nonSeveredLimbs = animController.Limbs.Count(static l => !l.IsSevered); + + // Decrease deconstruction time if the character is missing some limbs + DeconstructTimeMultiplier *= MathF.Max(MaxDeconstructTimeMultiplier, nonSeveredLimbs / totalLimbs); + } + +#if CLIENT + if (character != null) + { + SpriteOverride? spriteOverride = + spriteOverrides.Where(s => s.SpeciesName == character.SpeciesName).FirstOrDefault() + ?? spriteOverrides.Where(s => s.SpeciesGroup == character.Group).FirstOrDefault(); + + if (spriteOverride != null) + { + item.OverrideInventorySprite = spriteOverride.Sprite; + } + } + else + { + item.OverrideInventorySprite = null; + } +#elif SERVER + Item.CreateServerEvent(this); +#endif + } + + public void ClientEventRead(IReadMessage msg, float sendingTime) + { + UInt16 characterId = msg.ReadUInt16(); + if (characterId == Entity.NullEntityID) + { + UpdateLinkedCharacter(null); + } + else if (Entity.FindEntityByID(characterId) is Character character) + { + UpdateLinkedCharacter(character); + } + } + + public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData? extraData = null) + { + if (Character != null) + { + msg.WriteUInt16(Character.ID); + } + else + { + msg.WriteUInt16(Entity.NullEntityID); + } + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs index 9d032f85d..028098575 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs @@ -1,9 +1,12 @@ -using FarseerPhysics; -using Barotrauma.Networking; +using Barotrauma.Networking; +using FarseerPhysics; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; +using System.Collections.Immutable; +using System.ComponentModel; using System.Globalization; +using System.Linq; using System.Xml.Linq; using System.Linq; @@ -46,6 +49,39 @@ namespace Barotrauma.Items.Components private Camera cam; private Character user; + public Character User + { + get { return user; } + private set + { + if (user == value) + { + return; + } + + user = value; + + if (user != null) + { + teleportTransition = 0f; + teleportStartPosition = user.WorldPosition; + } +#if SERVER + item.CreateServerEvent(this); +#endif + +#if CLIENT + UpdateMsg(); + + if (HideAllItemComponentHUDs && Character.Controlled == user) + { + // Prevents any UIs in this item from briefly showing up when you select this controller, since + // activeHUDs would take a single frame to be updated to not contain any other item component HUD + Item.ClearActiveHUDs(); + } +#endif + } + } private Item focusTarget; private float targetRotation; @@ -56,11 +92,6 @@ namespace Barotrauma.Items.Components set { userPos = value; } } - public Character User - { - get { return user; } - } - public IEnumerable LimbPositions { get { return limbPositions; } } [Editable, Serialize(false, IsPropertySaveable.No, description: "When enabled, the item will continuously send out a signal and interacting with it will flip the signal (making the item behave like a switch). When disabled, the item will simply send out a signal when interacted with.", alwaysUseInstanceValues: true)] @@ -124,6 +155,13 @@ namespace Barotrauma.Items.Components set; } + [Serialize(false, IsPropertySaveable.No, description: "Should the HUDs of all item components in this item be hidden when a character is using this controller.")] + public bool HideAllItemComponentHUDs + { + get; + set; + } + public enum UseEnvironment { Air, Water, Both @@ -153,6 +191,49 @@ namespace Barotrauma.Items.Components set; } + [Serialize(false, IsPropertySaveable.No, description: "Can a character put another character into this controller by dragging them and selecting this controller?")] + public bool AllowPuttingInOtherCharacters + { + get; + set; + } + + [Serialize(true, IsPropertySaveable.No, description: "Can a character select this controller by themselves?")] + public bool CanBeSelectedByCharacters + { + get; + set; + } + + [Serialize(false, IsPropertySaveable.No, description: "If a character selects this controller, but another character already has it selected, should it be kicked out?")] + public bool SelectingKicksCharacterOut + { + get; + set; + } + + [Serialize("", IsPropertySaveable.No, description: "Message displayed when there's a character inside this controller.")] + public string KickOutCharacterMsg + { + get; + set; + } + + [Serialize("", IsPropertySaveable.No, description: "Message displayed when you are putting a character into the controller.")] + public string PutOtherCharacterMsg + { + get; + set; + } + + + [Serialize("", IsPropertySaveable.No, description: "Spawns this item in the first available item container slot when a character selects this controller, if the item container is full, the character will not be able to select the controller.")] + public Identifier SpawnItemOnSelected + { + get; + private set; + } + public bool ControlCharacterPose { get { return limbPositions.Count > 0; } @@ -205,6 +286,23 @@ namespace Barotrauma.Items.Components set; } + /// + /// Used to determine how fast the character is teleported + /// to the item when they first select the controller. + /// Only relevant for + /// + private const float TeleportTransitionSpeed = 8f; + private float teleportTransition = 0f; + private Vector2 teleportStartPosition; + + private readonly ItemPrefab spawnItemOnSelectedPrefab; + private readonly ItemContainer containerToSpawnOnSelectedItem; + + /// + /// Item spawned by + /// + private Item spawnedItemOnSelected = null; + public Controller(Item item, ContentXElement element) : base(item, element) { @@ -212,6 +310,18 @@ namespace Barotrauma.Items.Components Enum.TryParse(element.GetAttributeString("direction", "None"), out dir); LoadLimbPositions(element); IsActive = true; + + containerToSpawnOnSelectedItem = item.GetComponent(); + + if (!SpawnItemOnSelected.IsEmpty && !ItemPrefab.Prefabs.TryGet(SpawnItemOnSelected, out spawnItemOnSelectedPrefab)) + { + DebugConsole.ThrowError($"Failed to find item prefab \"{SpawnItemOnSelected}\""); + } + + if (containerToSpawnOnSelectedItem == null && !SpawnItemOnSelected.IsEmpty) + { + DebugConsole.ThrowError($"Error - Controller has a {nameof(SpawnItemOnSelected)} but no ItemContainer defined"); + } } /// @@ -237,58 +347,77 @@ namespace Barotrauma.Items.Components item.SendSignal(signal, "trigger_out"); } - if (forceSelectNextFrame && user != null) + if (forceSelectNextFrame && User != null) { - user.SelectedItem = item; + User.SelectedItem = item; } forceSelectNextFrame = false; userCanInteractCheckTimer -= deltaTime; - if (user == null - || user.Removed - || !user.IsAnySelectedItem(item) - || (item.ParentInventory != null && !IsAttachedUser(user)) - || (UsableIn == UseEnvironment.Water && !user.AnimController.InWater) - || (UsableIn == UseEnvironment.Air && user.AnimController.InWater) - || !CheckUserCanInteract()) + if (User == null + || User.Removed + || (((User.Stun <= 0f && !User.IsKnockedDownOrRagdolled && !User.LockHands) || !ForceUserToStayAttached) && (!User.IsAnySelectedItem(item) || !CheckUserCanInteract())) + || (item.ParentInventory != null && !IsAttachedUser(User)) + || (UsableIn == UseEnvironment.Water && !User.AnimController.InWater) + || (UsableIn == UseEnvironment.Air && User.AnimController.InWater) + || !CheckSpawnItem() + ) { - if (user != null) + if (User != null) { - CancelUsing(user); - user = null; + CancelUsing(User); + User = null; } if (item.Connections == null || !IsToggle || string.IsNullOrEmpty(signal)) { IsActive = false; } return; } - if (ForceUserToStayAttached && Vector2.DistanceSquared(item.WorldPosition, user.WorldPosition) > 0.1f) + if (ForceUserToStayAttached) { - user.TeleportTo(item.WorldPosition); - user.AnimController.Collider.ResetDynamics(); - foreach (var limb in user.AnimController.Limbs) + teleportTransition = MathF.Min(teleportTransition + deltaTime * TeleportTransitionSpeed, 1f); + + if (teleportTransition >= 1f) { - if (limb.Removed || limb.IsSevered) { continue; } - limb.body?.ResetDynamics(); + // Transition is complete, if someone was holding this character, force them to deselect + // so they aren't holding the character that is now attached to the controller + if (User.SelectedBy != null) + { + User.SelectedBy.SelectedCharacter = null; + } + } + + if (User == Character.Controlled + || teleportTransition < 1f + || Vector2.DistanceSquared(item.WorldPosition, User.WorldPosition) > 0.1f) + { + var targetPosition = Vector2.Lerp(teleportStartPosition, item.WorldPosition, teleportTransition); + User.TeleportTo(targetPosition); + User.AnimController.Collider.ResetDynamics(); + foreach (var limb in User.AnimController.Limbs) + { + if (limb.Removed || limb.IsSevered) { continue; } + limb.body?.ResetDynamics(); + } } } - user.AnimController.StartUsingItem(); + User.AnimController.StartUsingItem(); if (userPos != Vector2.Zero) { - Vector2 diff = (item.WorldPosition + userPos) - user.WorldPosition; + Vector2 diff = (item.WorldPosition + userPos) - User.WorldPosition; - if (user.AnimController.InWater) + if (User.AnimController.InWater) { if (diff.LengthSquared() > 30.0f * 30.0f) { - user.AnimController.TargetMovement = Vector2.Clamp(diff * 0.01f, -Vector2.One, Vector2.One); - user.AnimController.TargetDir = diff.X > 0.0f ? Direction.Right : Direction.Left; + User.AnimController.TargetMovement = Vector2.Clamp(diff * 0.01f, -Vector2.One, Vector2.One); + User.AnimController.TargetDir = diff.X > 0.0f ? Direction.Right : Direction.Left; } else { - user.AnimController.TargetMovement = Vector2.Zero; + User.AnimController.TargetMovement = Vector2.Zero; UserInCorrectPosition = true; } } @@ -296,10 +425,10 @@ namespace Barotrauma.Items.Components { // Secondary items (like ladders or chairs) will control the character position over primary items // Only control the character position if the character doesn't have another secondary item already controlling it - if (!user.HasSelectedAnotherSecondaryItem(Item)) + if (!User.HasSelectedAnotherSecondaryItem(Item)) { diff.Y = 0.0f; - if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient && user != Character.Controlled) + if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient && User != Character.Controlled) { if (Math.Abs(diff.X) > 20.0f) { @@ -309,48 +438,48 @@ namespace Barotrauma.Items.Components else if (Math.Abs(diff.X) > 0.1f) { //aim to keep the collider at the correct position once close enough - user.AnimController.Collider.LinearVelocity = new Vector2( + User.AnimController.Collider.LinearVelocity = new Vector2( diff.X * 0.1f, - user.AnimController.Collider.LinearVelocity.Y); + User.AnimController.Collider.LinearVelocity.Y); } } else if (Math.Abs(diff.X) > 10.0f) { - user.AnimController.TargetMovement = Vector2.Normalize(diff); - user.AnimController.TargetDir = diff.X > 0.0f ? Direction.Right : Direction.Left; + User.AnimController.TargetMovement = Vector2.Normalize(diff); + User.AnimController.TargetDir = diff.X > 0.0f ? Direction.Right : Direction.Left; return; } - user.AnimController.TargetMovement = Vector2.Zero; + User.AnimController.TargetMovement = Vector2.Zero; } UserInCorrectPosition = true; } } - ApplyStatusEffects(ActionType.OnActive, deltaTime, user); + ApplyStatusEffects(ActionType.OnActive, deltaTime, User); if (limbPositions.Count == 0) { return; } - user.AnimController.StartUsingItem(); + User.AnimController.StartUsingItem(); - if (user.SelectedItem != null) + if (User.SelectedItem != null) { - user.AnimController.ResetPullJoints(l => l.IsLowerBody); + User.AnimController.ResetPullJoints(l => l.IsLowerBody); } else { - user.AnimController.ResetPullJoints(); + User.AnimController.ResetPullJoints(); } - if (dir != 0) { user.AnimController.TargetDir = dir; } + if (dir != 0) { User.AnimController.TargetDir = dir; } foreach (LimbPos lb in limbPositions) { - Limb limb = user.AnimController.GetLimb(lb.LimbType); + Limb limb = User.AnimController.GetLimb(lb.LimbType); if (limb == null || !limb.body.Enabled) { continue; } // Don't move lower body limbs if there's another selected secondary item that should control them - if (limb.IsLowerBody && user.HasSelectedAnotherSecondaryItem(Item)) { continue; } + if (limb.IsLowerBody && User.HasSelectedAnotherSecondaryItem(Item)) { continue; } // Don't move hands if there's a selected primary item that should control them - if (limb.IsArm && Item == user.SelectedSecondaryItem && user.SelectedItem != null) { continue; } + if (limb.IsArm && Item == User.SelectedSecondaryItem && User.SelectedItem != null) { continue; } if (lb.AllowUsingLimb) { switch (lb.LimbType) @@ -358,12 +487,12 @@ namespace Barotrauma.Items.Components case LimbType.RightHand: case LimbType.RightForearm: case LimbType.RightArm: - if (user.Inventory.GetItemInLimbSlot(InvSlotType.RightHand) != null) { continue; } + if (User.Inventory.GetItemInLimbSlot(InvSlotType.RightHand) != null) { continue; } break; case LimbType.LeftHand: case LimbType.LeftForearm: case LimbType.LeftArm: - if (user.Inventory.GetItemInLimbSlot(InvSlotType.LeftHand) != null) { continue; } + if (User.Inventory.GetItemInLimbSlot(InvSlotType.LeftHand) != null) { continue; } break; } } @@ -375,15 +504,77 @@ namespace Barotrauma.Items.Components } } + private bool IsSpawnContainerFull() + { + if (spawnItemOnSelectedPrefab == null || containerToSpawnOnSelectedItem == null) + { + return false; + } + + if (containerToSpawnOnSelectedItem.Inventory.IsFull()) + { + return true; + } + + return false; + } + + private bool CheckSpawnItem() + { + if (spawnItemOnSelectedPrefab == null || containerToSpawnOnSelectedItem == null) + { + return true; + } + + if (containerToSpawnOnSelectedItem.Inventory.AllItems.Any(x => x.Prefab == spawnItemOnSelectedPrefab)) + { + return true; + } + + if (spawnedItemOnSelected != null && !spawnedItemOnSelected.Removed) + { + if (spawnedItemOnSelected.ParentInventory != containerToSpawnOnSelectedItem.Inventory) + { + // Item was moved or dropped, force the user in this controller out + return false; + } + else + { + return true; + } + } + + if (IsSpawnContainerFull()) + { + return false; + } + + if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer) + { + Entity.Spawner.AddItemToSpawnQueue(spawnItemOnSelectedPrefab, containerToSpawnOnSelectedItem.Inventory, onSpawned: spawnedItem => + { + spawnedItemOnSelected = spawnedItem; + + var linkedCharacterComponent = spawnedItem.GetComponent(); + if (linkedCharacterComponent != null) + { + linkedCharacterComponent.UpdateLinkedCharacter(User); + } + }); + } + + return true; + } + private bool CheckUserCanInteract() { //optimization: CanInteractWith is relatively heavy (can involve visibility checks for example), let's not do it every frame - if (user != null) + if (User != null) { if (userCanInteractCheckTimer <= 0.0f) { userCanInteractCheckTimer = UserCanInteractCheckInterval; - return user.CanInteractWith(item); + return User.CanInteractWith(item); } } //we only do the actual check every UserCanInteractCheckInterval seconds @@ -395,13 +586,13 @@ namespace Barotrauma.Items.Components public override bool Use(float deltaTime, Character activator = null) { - if (activator != user) + if (activator != User) { return false; } - if (user == null || user.Removed || !user.IsAnySelectedItem(item) || !user.CanInteractWith(item)) + if (User == null || User.Removed || !User.IsAnySelectedItem(item) || !User.CanInteractWith(item)) { - user = null; + User = null; return false; } @@ -420,7 +611,7 @@ namespace Barotrauma.Items.Components } else if (!string.IsNullOrEmpty(output)) { - item.SendSignal(new Signal(output, sender: user), "trigger_out"); + item.SendSignal(new Signal(output, sender: User), "trigger_out"); } lastUsed = Timing.TotalTime; @@ -429,13 +620,13 @@ namespace Barotrauma.Items.Components public override bool SecondaryUse(float deltaTime, Character character = null) { - if (user != character) + if (User != character) { return false; } - if (user == null || character.Removed || !user.IsAnySelectedItem(item) || !character.CanInteractWith(item)) + if (User == null || character.Removed || !User.IsAnySelectedItem(item) || !character.CanInteractWith(item)) { - user = null; + User = null; return false; } if (character == null) @@ -496,7 +687,7 @@ namespace Barotrauma.Items.Components if (IsOutOfPower()) { return null; } - item.SendSignal(new Signal(MathHelper.ToDegrees(targetRotation).ToString("G", CultureInfo.InvariantCulture), sender: user), positionOut); + item.SendSignal(new Signal(MathHelper.ToDegrees(targetRotation).ToString("G", CultureInfo.InvariantCulture), sender: User), positionOut); // Use ToList() snapshot for thread-safe iteration var signalRecipients = item.LastSentSignalRecipients.ToList(); @@ -550,8 +741,20 @@ namespace Barotrauma.Items.Components private void CancelUsing(Character character) { + if (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient) + { + if (spawnedItemOnSelected != null) + { + Entity.Spawner.AddEntityToRemoveQueue(spawnedItemOnSelected); + spawnedItemOnSelected = null; + } + } + if (character == null || character.Removed) { return; } + // Wake character's colliders so they don't just float in the air when taken out of the controller + character.AnimController.BodyInRest = false; + foreach (LimbPos lb in limbPositions) { Limb limb = character.AnimController.GetLimb(lb.LimbType); @@ -591,31 +794,80 @@ namespace Barotrauma.Items.Components return false; } - //someone already using the item - if (user != null && !user.Removed) + // Someone already using the item + if (User != null && !User.Removed) { - if (user == activator) + // Let the server handle the logic here + if (GameMain.NetworkMember is { IsClient: true }) { - IsActive = false; - CancelUsing(user); - user = null; return false; } - else if (user.IsBot && !activator.IsBot) + + // Prevent user from kicking character out if they are holding another character + if (AllowPuttingInOtherCharacters && CanPutSelectedCharacter(activator.SelectedCharacter)) + { + return false; + } + + if (User == activator || SelectingKicksCharacterOut) + { +#if SERVER + if (User != activator) + { + GameServer.Log($"{GameServer.CharacterLogName(activator)} removed {GameServer.CharacterLogName(User)} from {item.Name}", + ServerLog.MessageType.Attack); + } +#endif + + IsActive = false; + CancelUsing(User); + User = null; + return false; + } + else if (User.IsBot && !activator.IsBot) { if (AllowSelectingWhenSelectedByBot) { - CancelUsing(user); - user = activator; + CancelUsing(User); + User = activator; IsActive = true; return true; } } return AllowSelectingWhenSelectedByOther; } - else + else if (AllowPuttingInOtherCharacters && CanPutSelectedCharacter(activator.SelectedCharacter)) { - user = activator; + // Stun pets longer so they don't immediately get out of the controller + if (activator.SelectedCharacter.IsPet) + { + activator.SelectedCharacter.SetStun(MathF.Max(activator.SelectedCharacter.Stun, 4f), isNetworkMessage: true); + } + else + { + // Small amount of stun since non-ragdolled characters behave weirdly when syncing the periodic teleportation in multiplayer + activator.SelectedCharacter.SetStun(MathF.Max(activator.SelectedCharacter.Stun, 1f), isNetworkMessage: true); + } + +#if SERVER + GameServer.Log($"{GameServer.CharacterLogName(activator)} forced {GameServer.CharacterLogName(activator.SelectedCharacter)} into {item.Name}", + ServerLog.MessageType.Attack); +#endif + + User = activator.SelectedCharacter; + User.SelectedItem = this.Item; + IsActive = true; + if (ForceUserToStayAttached && item.Container != null) + { + forceSelectNextFrame = true; + } + return false; + } + else if (CanBeSelectedByCharacters) + { + activator.DeselectCharacter(); + + User = activator; IsActive = true; if (ForceUserToStayAttached && item.Container != null) { @@ -624,15 +876,12 @@ namespace Barotrauma.Items.Components } } - //allow the selection logic above to run when out of power, but allow sending signals + //allow the selection logic above to run when out of power, but disallow sending signals if (IsOutOfPower()) { return false; } - -#if SERVER - item.CreateServerEvent(this); -#endif + if (!string.IsNullOrEmpty(output)) { - item.SendSignal(new Signal(output, sender: user), "signal_out"); + item.SendSignal(new Signal(output, sender: User), "signal_out"); } return true; } @@ -642,7 +891,7 @@ namespace Barotrauma.Items.Components /// public bool IsAttachedUser(Character character) { - return character != null && character == user && ForceUserToStayAttached; + return character != null && character == User && ForceUserToStayAttached; } public override void FlipX(bool relativeToSub) @@ -671,12 +920,87 @@ namespace Barotrauma.Items.Components } } + public override bool HasRequiredItems(Character character, bool addMessage, LocalizedString msg = null) + { +#if CLIENT + UpdateMsg(); +#endif + + bool canPutCharacter = AllowPuttingInOtherCharacters && CanPutSelectedCharacter(character.SelectedCharacter, addMessage); + bool canKickCharacter = SelectingKicksCharacterOut && User != null && !User.Removed; + bool canUseController = CanBeSelectedByCharacters; + + // Prevent kicking a character out when the user is holding another character to put into the controller. + // This avoids accidentally taking out a character (e.g. from a deconstructor). + if (canPutCharacter && canKickCharacter) + { +#if CLIENT + if (addMessage) + { + GUI.AddMessage(TextManager.Get("ItemMsgAlreadyHasCharacterFail"), Color.Red, playSound: false); + SoundPlayer.PlayUISound(GUISoundType.PickItemFail); + } +#endif + + return false; + } + + if (!canKickCharacter && !canPutCharacter && !canUseController) + { + return false; + } + + if (IsSpawnContainerFull()) + { +#if CLIENT + if (addMessage) + { + GUI.AddMessage(TextManager.Get("ItemMsgNotEnoughSpaceCharacterFail"), Color.Red, playSound: false); + SoundPlayer.PlayUISound(GUISoundType.PickItemFail); + } +#endif + + return false; + } + + return base.HasRequiredItems(character, addMessage, msg); + } + public override bool HasAccess(Character character) { if (!item.IsInteractable(character)) { return false; } return base.HasAccess(character); } + private bool CanPutSelectedCharacter(Character character, bool showMessage = false) + { + if (character == null) + { + return false; + } + + if (!character.IsContainable) + { +#if CLIENT + if (showMessage) + { + GUI.AddMessage(TextManager.Get("ItemMsgPutCharacterFail"), Color.Red); + } +#endif + + return false; + } + + if (character.IsKnockedDownOrRagdolled) { return true; } + if (character.LockHands) { return true; } + if (character.IsPet) + { + return true; + } + + return false; + } + partial void HideHUDs(bool value); public override XElement Save(XElement parentElement) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs index cd08a541a..c9ab5d0ad 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs @@ -1,11 +1,13 @@ using Barotrauma.Abilities; using Barotrauma.Extensions; +using Barotrauma.LuaCs.Events; using Barotrauma.Networking; using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using System.Xml.Linq; +using static OneOf.Types.TrueFalseOrNull; namespace Barotrauma.Items.Components { @@ -114,7 +116,19 @@ namespace Barotrauma.Items.Components float deconstructTime = 0.0f; foreach (Item targetItem in inputContainer.Inventory.AllItems) { - deconstructTime += targetItem.Prefab.DeconstructTime / (deconstructionSpeed * deconstructionSpeedModifier); + float itemDeconstructTime = item.Submarine is { Info.Type: SubmarineType.Outpost } + ? targetItem.Prefab.DeconstructTimeInOutposts : targetItem.Prefab.DeconstructTime; + float targetDeconstructTime = itemDeconstructTime / (deconstructionSpeed * deconstructionSpeedModifier); + + var linkedCharacter = targetItem.GetComponent(); + if (linkedCharacter != null) + { + targetDeconstructTime *= linkedCharacter.DeconstructTimeMultiplier; + } + + deconstructTime += targetDeconstructTime; + + ApplyDeconstructionStatusEffects(targetItem, ActionType.OnDeconstructing, deltaTime); } progressState = Math.Min(progressTimer / deconstructTime, 1.0f); @@ -143,8 +157,21 @@ namespace Barotrauma.Items.Components var targetItem = inputContainer.Inventory.LastOrDefault(); if (targetItem == null) { return; } + ApplyDeconstructionStatusEffects(targetItem, ActionType.OnDeconstructing, deltaTime); + var validDeconstructItems = targetItem.Prefab.DeconstructItems.Where(it => it.IsValidDeconstructor(item)).ToList(); - float deconstructTime = validDeconstructItems.Any() ? targetItem.Prefab.DeconstructTime / (deconstructionSpeed * deconstructionSpeedModifier) : 1.0f; + + float itemDeconstructTime = item.Submarine is { Info.Type: SubmarineType.Outpost } + ? targetItem.Prefab.DeconstructTimeInOutposts : targetItem.Prefab.DeconstructTime; + + float deconstructTime = !targetItem.Prefab.DeconstructItems.Any() || validDeconstructItems.Any() + ? itemDeconstructTime / (deconstructionSpeed * deconstructionSpeedModifier) : 1.0f; + + var linkedCharacter = targetItem.GetComponent(); + if (linkedCharacter != null) + { + deconstructTime *= linkedCharacter.DeconstructTimeMultiplier; + } progressState = Math.Min(progressTimer / deconstructTime, 1.0f); if (progressTimer > deconstructTime) @@ -183,6 +210,8 @@ namespace Barotrauma.Items.Components amountMultiplier = (int)itemCreationMultiplier.Value; } + ApplyDeconstructionStatusEffects(targetItem, ActionType.OnDeconstructed, 1f); + if (targetItem.Prefab.RandomDeconstructionOutput) { int amount = targetItem.Prefab.RandomDeconstructionOutputAmount; @@ -297,30 +326,8 @@ namespace Barotrauma.Items.Components { humanAi.HandleRelocation(spawnedItem); } - for (int i = 0; i < outputContainer.Capacity; i++) - { - var containedItem = outputContainer.Inventory.GetItemAt(i); - bool combined = false; - if (containedItem?.OwnInventory != null) - { - foreach (Item subItem in containedItem.ContainedItems.ToList()) - { - if (subItem.Combine(spawnedItem, null)) - { - combined = true; - break; - } - } - } - if (!combined) - { - if (containedItem?.Combine(spawnedItem, null) ?? false) - { - break; - } - } - } - PutItemsToLinkedContainer(); + + TryMoveItemToOutputContainers(spawnedItem); }); } } @@ -332,8 +339,9 @@ namespace Barotrauma.Items.Components GameAnalyticsManager.AddDesignEvent("ItemDeconstructed:" + (GameMain.GameSession?.GameMode?.Preset.Identifier.Value ?? "none") + ":" + targetItem.Prefab.Identifier); } - bool? result = GameMain.LuaCs.Hook.Call("item.deconstructed", targetItem, this, user, allowRemove); - if (result == true) { return; } + bool? should = null; + LuaCsSetup.Instance.EventService.PublishEvent(x => should = x.OnItemDeconstructed(targetItem, this, user, allowRemove) ?? should); + if (should == true) { return; } if (targetItem.AllowDeconstruct && allowRemove) { @@ -350,6 +358,7 @@ namespace Barotrauma.Items.Components } } } + inputContainer.Inventory.RemoveItem(targetItem); Entity.Spawner.AddItemToRemoveQueue(targetItem); MoveInputQueue(); @@ -384,6 +393,34 @@ namespace Barotrauma.Items.Components } } + private void TryMoveItemToOutputContainers(Item spawnedItem) + { + for (int i = 0; i < outputContainer.Capacity; i++) + { + var containedItem = outputContainer.Inventory.GetItemAt(i); + bool combined = false; + if (containedItem?.OwnInventory != null) + { + foreach (Item subItem in containedItem.ContainedItems.ToList()) + { + if (subItem.Combine(spawnedItem, null)) + { + combined = true; + break; + } + } + } + if (!combined) + { + if (containedItem?.Combine(spawnedItem, null) ?? false) + { + break; + } + } + } + PutItemsToLinkedContainer(); + } + private void PutItemsToLinkedContainer() { if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; } @@ -402,6 +439,72 @@ namespace Barotrauma.Items.Components } } + private void ApplyDeconstructionStatusEffects(Item targetItem, ActionType type, float deltaTime) + { + var linkedCharacterComponent = targetItem.GetComponent(); + Character character = null; + if (linkedCharacterComponent is { Character.Removed: false }) + { + character = linkedCharacterComponent.Character; + } + + Limb limb = character?.AnimController.Limbs.GetRandomUnsynced(); + + if (user != null) + { + item.GetStatusEffectsOfType(type).ForEach(statusEffect => statusEffect.SetUser(user)); + targetItem.GetStatusEffectsOfType(type).ForEach(statusEffect => statusEffect.SetUser(user)); + } + + // Apply OnDeconstruct/OnDeconstructing to the Deconstructor/item being deconstructed + item.ApplyStatusEffects(type, deltaTime, character, limb, useTarget: targetItem); + targetItem.ApplyStatusEffects(type, deltaTime, character, limb); + + if (character != null) + { + if (type == ActionType.OnDeconstructed) + { + // Move whatever was on the character inventory to free up space for status effects that spawn items + MoveItemsFromCharacterToOutput(); + } + + character.ApplyStatusEffects(type, deltaTime); + + if (type == ActionType.OnDeconstructed) + { + // This needs to run next frame because the status effect might enqueue items to be spawned next frame + CoroutineManager.Invoke(() => + { + if (character.Removed) { return; } + + // Move items again since the status effect could have spawned additional items in the character inventory + MoveItemsFromCharacterToOutput(); + + Entity.Spawner?.AddEntityToRemoveQueue(character); + }, 0.1f); + } + + void MoveItemsFromCharacterToOutput() + { + if (character.Inventory != null) + { + foreach (var item in character.Inventory.AllItemsMod) + { + if (item.Removed) { continue; } + if (!item.IsPlayerTeamInteractable) { continue; } + + if (!outputContainer.Inventory.TryPutItem(item, user: null)) + { + item.Drop(dropper: null); + } + + TryMoveItemToOutputContainers(item); + } + } + } + } + } + /// /// Move items towards the last slot in the inventory if there's free slots /// diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs index 9c2423f0b..142f4176a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs @@ -389,6 +389,13 @@ namespace Barotrauma.Items.Components { Attack.DamageMultiplier = damageMultiplier; } + foreach (var statusEffect in Item.GetStatusEffectsOfType(ActionType.OnImpact)) + { + foreach (var explosion in statusEffect.Explosions) + { + explosion.Attack.DamageMultiplier = damageMultiplier; + } + } // Set user for hitscan projectiles to work properly. User = user; // Need to set null for non-characterusable items. @@ -461,6 +468,7 @@ namespace Barotrauma.Items.Components { initialRotation -= MathHelper.Pi; } + Submarine initialSubmarine = item.Submarine; for (int i = 0; i < HitScanCount; i++) { float launchAngle; @@ -477,6 +485,8 @@ namespace Barotrauma.Items.Components Vector2 launchDir = new Vector2((float)Math.Cos(launchAngle), (float)Math.Sin(launchAngle)); Vector2 prevSimpos = item.SimPosition; item.body.SetTransformIgnoreContacts(item.body.SimPosition, launchAngle); + //when launching multiple projectiles, ensure each raycast starts from the same sub + item.Submarine = initialSubmarine; if (Hitscan) { DoHitscan(launchDir); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Quality.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Quality.cs index 03b727bc3..758b29585 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Quality.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Quality.cs @@ -25,7 +25,7 @@ namespace Barotrauma.Items.Components FiringRateMultiplier } - private readonly Dictionary statValues = new Dictionary(); + public readonly Dictionary statValues = new Dictionary(); private int qualityLevel; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs index d2c7d4525..8d65850f5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs @@ -448,9 +448,10 @@ namespace Barotrauma.Items.Components UpdateProjSpecific(deltaTime); IsTinkering = false; - if (prevSentConditionValue != (int)item.ConditionPercentage || conditionSignal == null) + int condition = (int)(item.Condition / (item.MaxCondition / item.MaxRepairConditionMultiplier) * 100f); + if (prevSentConditionValue != condition || conditionSignal == null) { - prevSentConditionValue = (int)item.ConditionPercentage; + prevSentConditionValue = condition; conditionSignal = prevSentConditionValue.ToString(); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Connection.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Connection.cs index 37cb33d9c..a2b2bd577 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Connection.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Connection.cs @@ -27,6 +27,11 @@ namespace Barotrauma.Items.Components private init => _displayName = value; } + /// + /// Display name ignoring + /// + public LocalizedString DefaultDisplayName => _displayName; + public LocalizedString DisplayNameOverride; private readonly HashSet wires; @@ -353,15 +358,11 @@ namespace Barotrauma.Items.Components wire.RegisterSignal(signal, source: this); #endif SendSignalIntoConnection(signal, recipient); - GameMain.LuaCs.Hook.Call("signalReceived", signal, recipient); - GameMain.LuaCs.Hook.Call("signalReceived." + recipient.item.Prefab.Identifier, signal, recipient); } foreach (CircuitBoxConnection connection in CircuitBoxConnections.ToArray()) { connection.ReceiveSignal(signal); - GameMain.LuaCs.Hook.Call("signalReceived", signal, connection.Connection); - GameMain.LuaCs.Hook.Call("signalReceived." + connection.Connection.Item.Prefab.Identifier, signal, connection); } enumeratingWires = false; foreach (var removedWire in removedWires.ToArray()) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs index e0104a213..bccb65f42 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs @@ -296,6 +296,7 @@ namespace Barotrauma.Items.Components } #endif CheckIfNeedsUpdate(); + SetLightSourceTransformProjSpecific(); } public void CheckIfNeedsUpdate() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WifiComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WifiComponent.cs index 68a978626..94752c4f4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WifiComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WifiComponent.cs @@ -1,4 +1,5 @@ -using Barotrauma.Networking; +using Barotrauma.LuaCs.Events; +using Barotrauma.Networking; using Microsoft.Xna.Framework; using System; using System.Collections.Concurrent; @@ -6,6 +7,8 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Xml.Linq; +using static Barotrauma.CharacterHealth; +using static Barotrauma.MedicalClinic; namespace Barotrauma.Items.Components { @@ -230,8 +233,8 @@ namespace Barotrauma.Items.Components public void TransmitSignal(Signal signal, bool sentFromChat) { - var should = GameMain.LuaCs.Hook.Call("wifiSignalTransmitted", this, signal, sentFromChat); - + bool? should = null; + LuaCsSetup.Instance.EventService.PublishEvent(x => should = x.OnWifiSignalTransmitted(this, signal, sentFromChat) ?? should); if (should != null && should.Value) { return; } bool chatMsgSent = false; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs index d5d7678c5..9da0bcfba 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs @@ -42,6 +42,10 @@ namespace Barotrauma.Items.Components private float currentChargeTime; private bool tryingToCharge; + private const float LineOfSightCheckInterval = 0.5f; + private (Body WorldTarget, Body TransformedTarget, double Time) lastLineOfSightCheck; + private (Character Target, bool CanSee, double Time) lastCanSeeTargetCheck; + private enum ChargingState { Inactive, @@ -1088,6 +1092,7 @@ namespace Barotrauma.Items.Components foreach (Submarine sub in Submarine.Loaded) { if (sub == Item.Submarine) { continue; } + if (sub.IsRespawnShuttle) { continue; } if (item.Submarine != null) { if (Character.IsOnFriendlyTeam(item.Submarine.TeamID, sub.TeamID)) { continue; } @@ -1164,15 +1169,28 @@ namespace Barotrauma.Items.Components } Vector2 start = ConvertUnits.ToSimUnits(item.WorldPosition); Vector2 end = ConvertUnits.ToSimUnits(target.WorldPosition); + + bool doLineOfSightCheck = lastLineOfSightCheck.Time < Timing.TotalTimeUnpaused - LineOfSightCheckInterval; + if (doLineOfSightCheck) + { + lastLineOfSightCheck.WorldTarget = CheckLineOfSight(start, end); + lastLineOfSightCheck.Time = Timing.TotalTime; + } + // Check that there's not other entities that shouldn't be targeted (like a friendly sub) between us and the target. - Body worldTarget = CheckLineOfSight(start, end); + Body worldTarget = lastLineOfSightCheck.WorldTarget; bool shoot; if (target.Submarine != null) { - start -= target.Submarine.SimPosition; - end -= target.Submarine.SimPosition; - Body transformedTarget = CheckLineOfSight(start, end); - shoot = CanShoot(transformedTarget, user: null, friendlyTag, TargetSubmarines) && (worldTarget == null || CanShoot(worldTarget, user: null, friendlyTag, TargetSubmarines)); + if (doLineOfSightCheck) + { + start -= target.Submarine.SimPosition; + end -= target.Submarine.SimPosition; + lastLineOfSightCheck.TransformedTarget = CheckLineOfSight(start, end); + } + shoot = + (worldTarget == null || CanShoot(worldTarget, user: null, friendlyTag, TargetSubmarines)) && + CanShoot(lastLineOfSightCheck.TransformedTarget, user: null, friendlyTag, TargetSubmarines); } else { @@ -1437,8 +1455,20 @@ namespace Barotrauma.Items.Components // Adjust the target character position (limb or submarine) if (currentTarget is Character targetCharacter) { + bool enemyInAnotherSub = targetCharacter.Submarine != null && targetCharacter.CurrentHull != null && targetCharacter.Submarine != item.Submarine; + bool canSeeTarget = true; + if (enemyInAnotherSub) + { + if (lastCanSeeTargetCheck.Time < Timing.TotalTime - LineOfSightCheckInterval || + targetCharacter != lastCanSeeTargetCheck.Target) + { + canSeeTarget = targetCharacter.CanSeeTarget(Item); + lastCanSeeTargetCheck = (targetCharacter, canSeeTarget, Timing.TotalTime); + } + } + //if the enemy is inside another sub, aim at the room they're in to make it less obvious that the enemy "knows" exactly where the target is - if (targetCharacter.Submarine != null && targetCharacter.CurrentHull != null && targetCharacter.Submarine != item.Submarine && !targetCharacter.CanSeeTarget(Item)) + if (enemyInAnotherSub && !canSeeTarget) { targetPos = targetCharacter.CurrentHull.WorldPosition; if (closestDistance > maxDistance * maxDistance) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs index 68c35aab3..f391ec9ca 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs @@ -570,16 +570,16 @@ namespace Barotrauma /// /// If there is room, puts the item in the inventory and returns true, otherwise returns false /// - public virtual bool TryPutItem(Item item, Character user, IEnumerable allowedSlots = null, bool createNetworkEvent = true, bool ignoreCondition = false) + public virtual bool TryPutItem(Item item, Character user, IEnumerable allowedSlots = null, bool createNetworkEvent = true, bool ignoreCondition = false, bool triggerOnInsertedEffects = true) { int slot = FindAllowedSlot(item, ignoreCondition); if (slot < 0) { return false; } - PutItem(item, slot, user, true, createNetworkEvent); + PutItem(item, slot, user, true, createNetworkEvent, triggerOnInsertedEffects); return true; } - public virtual bool TryPutItem(Item item, int i, bool allowSwapping, bool allowCombine, Character user, bool createNetworkEvent = true, bool ignoreCondition = false) + public virtual bool TryPutItem(Item item, int i, bool allowSwapping, bool allowCombine, Character user, bool createNetworkEvent = true, bool ignoreCondition = false, bool triggerOnInsertedEffects = true) { if (!IsIndexInRange(i)) { @@ -610,14 +610,14 @@ namespace Barotrauma //item in the slot removed as a result of combining -> put this item in the now free slot if (!slots[i].Any()) { - return TryPutItem(item, i, allowSwapping, allowCombine, user, createNetworkEvent, ignoreCondition); + return TryPutItem(item, i, allowSwapping, allowCombine, user, createNetworkEvent, ignoreCondition, triggerOnInsertedEffects); } return true; } } if (CanBePutInSlot(item, i, ignoreCondition)) { - PutItem(item, i, user, true, createNetworkEvent); + PutItem(item, i, user, true, createNetworkEvent, triggerOnInsertedEffects); return true; } else if (slots[i].Any() && item.ParentInventory != null && allowSwapping) @@ -643,7 +643,7 @@ namespace Barotrauma } } - protected virtual void PutItem(Item item, int i, Character user, bool removeItem = true, bool createNetworkEvent = true) + protected virtual void PutItem(Item item, int i, Character user, bool removeItem = true, bool createNetworkEvent = true, bool triggerOnInsertedEffects = true) { if (!IsIndexInRange(i)) { @@ -652,11 +652,6 @@ namespace Barotrauma return; } - var should = GameMain.LuaCs.Hook.Call("inventoryPutItem", this, item, user, i, removeItem); - - if (should != null && should.Value) - return; - if (Owner == null) { return; } Inventory prevInventory = item.ParentInventory; @@ -766,11 +761,6 @@ namespace Barotrauma if (slots[index].Items.Any(it => !it.IsInteractable(user))) { return false; } if (!AllowSwappingContainedItems) { return false; } - var should = GameMain.LuaCs.Hook.Call("inventoryItemSwap", this, item, user, index, swapWholeStack); - - if (should != null) - return should.Value; - //swap to InvSlotType.Any if possible Inventory otherInventory = item.ParentInventory; bool otherIsEquipped = false; @@ -952,7 +942,8 @@ namespace Barotrauma { foreach (var item in items) { - if (!inventory.TryPutItem(item, slotIndex, false, false, user, createNetworkEvent, ignoreCondition: true) && + //don't trigger OnInserted effects: we're not really "inserting" but just putting it back where it was because swapping failed + if (!inventory.TryPutItem(item, slotIndex, false, false, user, createNetworkEvent, ignoreCondition: true, triggerOnInsertedEffects: false) && !inventory.GetItemsAt(slotIndex).Contains(item)) { inventory.ForceToSlot(item, slotIndex); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs index 275d18835..d455240e9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs @@ -1,15 +1,23 @@ -using Barotrauma.Items.Components; +using Barotrauma.Abilities; +using Barotrauma.Extensions; +using Barotrauma.Items.Components; +using Barotrauma.LuaCs.Events; +using Barotrauma.MapCreatures.Behavior; using Barotrauma.Networking; using FarseerPhysics; using FarseerPhysics.Dynamics; using FarseerPhysics.Dynamics.Contacts; using Microsoft.Xna.Framework; +using MoonSharp.Interpreter; using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Collections.Immutable; using System.Globalization; using System.Linq; using System.Xml.Linq; +using static Barotrauma.CharacterHealth; +using static Barotrauma.MedicalClinic; using Barotrauma.Extensions; using Barotrauma.MapCreatures.Behavior; using MoonSharp.Interpreter; @@ -385,6 +393,8 @@ namespace Barotrauma /// public Character Equipper; + public Inventory PreviousParentInventory { get; set; } + //the inventory in which the item is contained in public Inventory ParentInventory { @@ -400,9 +410,7 @@ namespace Barotrauma Container = parentInventory.Owner as Item; RemoveFromDroppedStack(allowClientExecute: false); } -#if SERVER PreviousParentInventory = value; -#endif } } @@ -683,6 +691,8 @@ namespace Barotrauma set; } = float.PositiveInfinity; + public Sprite OverrideInventorySprite { get; set; } + protected Color spriteColor; [Editable, Serialize("1.0,1.0,1.0,1.0", IsPropertySaveable.Yes)] public Color SpriteColor @@ -1225,7 +1235,7 @@ namespace Barotrauma public override string ToString() { - return Name + " (ID: " + ID + ")"; + return (Name.IsNullOrEmpty() ? Prefab.Identifier : Name) + " (ID: " + ID + ")"; } private readonly List allPropertyObjects = new List(); @@ -1534,8 +1544,6 @@ namespace Barotrauma if (Components.Any(ic => ic is Wire) && Components.All(ic => ic is Wire || ic is Holdable)) { isWire = true; } if (HasTag(Barotrauma.Tags.LogicItem)) { isLogic = true; } - GameMain.LuaCs.Hook.Call("item.created", this); - ApplyStatusEffects(ActionType.OnSpawn, 1.0f); // Set max condition multipliers from campaign settings for RecalculateConditionValues() @@ -1925,7 +1933,8 @@ namespace Barotrauma ic.Move(amount, ignoreContacts); } - if (body != null && (Submarine == null || !Submarine.Loading) || Screen.Selected is { IsEditor: true }) { FindHull(); } + // Refresh items without a body in editors so vents (or other static items that do something with hulls) know which hull they are in after being moved + if ((body != null || Screen.Selected is { IsEditor: true }) && Submarine is not { Loading: true }) { FindHull(); } } public Rectangle TransformTrigger(Rectangle trigger, bool world = false) @@ -2205,6 +2214,12 @@ namespace Barotrauma } } + public IEnumerable GetStatusEffectsOfType(ActionType type) + { + if (!hasStatusEffectsOfType[(int)type]) { return Enumerable.Empty(); } + return statusEffectLists[type]; + } + /// /// Executes all StatusEffects of the specified type. Note that condition checks are ignored here: that should be handled by the code calling the method. /// @@ -3427,7 +3442,9 @@ namespace Barotrauma bool showUiMsg = false; #if CLIENT if (!ic.HasRequiredSkills(user, out Skill tempRequiredSkill)) { hasRequiredSkills = false; skillMultiplier = ic.GetSkillMultiplier(); } - showUiMsg = user == Character.Controlled && Screen.Selected != GameMain.SubEditorScreen; + showUiMsg = user == Character.Controlled && Screen.Selected != GameMain.SubEditorScreen && + // Only show the UI message of the component that we actually want to interact with + (pickHit && ic.CanBePicked || selectHit && ic.CanBeSelected); #endif if (!ignoreRequiredItems && !ic.HasRequiredItems(user, showUiMsg)) { continue; } if ((ic.CanBePicked && pickHit && ic.Pick(user)) || @@ -3533,10 +3550,6 @@ namespace Barotrauma } if (condition <= 0.0f) { return; } - - var should = GameMain.LuaCs.Hook.Call("item.use", new object[] { this, user, targetLimb, useTarget }); - - if (should != null && should.Value) { return; } bool remove = false; @@ -3569,11 +3582,6 @@ namespace Barotrauma { if (condition <= 0.0f) { return; } - var should = GameMain.LuaCs.Hook.Call("item.secondaryUse", this, character); - - if (should != null && should.Value) - return; - bool remove = false; foreach (ItemComponent ic in components) @@ -4111,9 +4119,9 @@ namespace Barotrauma } } - var result = GameMain.LuaCs.Hook.Call("item.readPropertyChange", this, property, parentObject, allowEditing, sender); - if (result != null && result.Value) - return; + bool? should = null; + LuaCsSetup.Instance.EventService.PublishEvent(x => should = x.OnItemReadPropertyChange(this, property, parentObject, allowEditing, sender) ?? should); + if (should != null && should.Value) { return; } Type type = property.PropertyType; string logValue = ""; @@ -4579,6 +4587,9 @@ namespace Barotrauma Rotation = Rotation }; + if (FlippedX) { newItem.FlipX(relativeToSub: false); } + if (FlippedY) { newItem.FlipY(relativeToSub: false); } + float scaleRelativeToPrefab = Scale / Prefab.Scale; newItem.Scale *= scaleRelativeToPrefab; @@ -4830,8 +4841,6 @@ namespace Barotrauma body.Remove(); body = null; } - - GameMain.LuaCs.Hook.Call("item.removed", this); } public override void Remove() @@ -4915,8 +4924,6 @@ namespace Barotrauma } RemoveProjSpecific(); - - GameMain.LuaCs.Hook.Call("item.removed", this); } private void RemoveFromLists() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemInventory.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemInventory.cs index 1a8cf3cc1..29b914960 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemInventory.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemInventory.cs @@ -88,9 +88,9 @@ namespace Barotrauma return true; } - public override bool TryPutItem(Item item, Character user, IEnumerable allowedSlots = null, bool createNetworkEvent = true, bool ignoreCondition = false) + public override bool TryPutItem(Item item, Character user, IEnumerable allowedSlots = null, bool createNetworkEvent = true, bool ignoreCondition = false, bool triggerOnInsertedEffects = true) { - bool wasPut = base.TryPutItem(item, user, allowedSlots, createNetworkEvent, ignoreCondition); + bool wasPut = base.TryPutItem(item, user, allowedSlots, createNetworkEvent, ignoreCondition, triggerOnInsertedEffects); if (wasPut) { @@ -111,9 +111,9 @@ namespace Barotrauma return wasPut; } - public override bool TryPutItem(Item item, int i, bool allowSwapping, bool allowCombine, Character user, bool createNetworkEvent = true, bool ignoreCondition = false) + public override bool TryPutItem(Item item, int i, bool allowSwapping, bool allowCombine, Character user, bool createNetworkEvent = true, bool ignoreCondition = false, bool triggerOnInsertedEffects = true) { - bool wasPut = base.TryPutItem(item, i, allowSwapping, allowCombine, user, createNetworkEvent, ignoreCondition); + bool wasPut = base.TryPutItem(item, i, allowSwapping, allowCombine, user, createNetworkEvent, ignoreCondition, triggerOnInsertedEffects); if (wasPut && item.ParentInventory == this) { foreach (Character c in Character.CharacterList) @@ -124,7 +124,7 @@ namespace Barotrauma } container.IsActive = true; - container.OnItemContained(item); + container.OnItemContained(item, triggerOnInsertedEffects); #if SERVER GameMain.Server?.KarmaManager?.OnItemContained(item, container.Item, user); #endif diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs index c340efab6..a52d1d94c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs @@ -548,6 +548,8 @@ namespace Barotrauma public float DeconstructTime { get; private set; } + public float DeconstructTimeInOutposts { get; private set; } + public bool AllowDeconstruct { get; private set; } //Containers (by identifiers or tags) that this item should be placed in. These are preferences, which are not enforced. @@ -1074,6 +1076,7 @@ namespace Barotrauma var storePrices = new Dictionary(); var preferredContainers = new List(); DeconstructTime = 1.0f; + DeconstructTimeInOutposts = DeconstructTime; if (ConfigElement.GetAttribute("allowasextracargo") != null) { @@ -1174,6 +1177,7 @@ namespace Barotrauma break; case "deconstruct": DeconstructTime = subElement.GetAttributeFloat("time", 1.0f); + DeconstructTimeInOutposts = subElement.GetAttributeFloat("timeinoutposts", DeconstructTime); AllowDeconstruct = true; RandomDeconstructionOutput = subElement.GetAttributeBool("chooserandom", false); RandomDeconstructionOutputAmount = subElement.GetAttributeInt("amount", 1); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/RelatedItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/RelatedItem.cs index 2bb9cb587..cad423406 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/RelatedItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/RelatedItem.cs @@ -398,7 +398,7 @@ namespace Barotrauma } foreach (Item contained in container.Inventory.AllItems) { - if (TargetSlot > -1 && parentItem.OwnInventory.FindIndex(contained) != TargetSlot) { continue; } + if (TargetSlot > -1 && container.Inventory.FindIndex(contained) != TargetSlot) { continue; } if ((!ExcludeBroken || contained.Condition > 0.0f) && (!ExcludeFullCondition || !contained.IsFullCondition) && MatchesItem(contained)) { return true; } if (CheckContained(contained)) { return true; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/AsyncReaderWriterLock.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/AsyncReaderWriterLock.cs new file mode 100644 index 000000000..074b04c2e --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/AsyncReaderWriterLock.cs @@ -0,0 +1,76 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Barotrauma.LuaCs; + + +// taken from: +public sealed class AsyncReaderWriterLock : IDisposable +{ + readonly SemaphoreSlim _readSemaphore = new SemaphoreSlim(1, 1); + readonly SemaphoreSlim _writeSemaphore = new SemaphoreSlim(1, 1); + int _readerCount; + + public async Task AcquireWriterLock(CancellationToken token = default) + { + await _writeSemaphore.WaitAsync(token).ConfigureAwait(false); + try + { + await _readSemaphore.WaitAsync(token).ConfigureAwait(false); + } + catch + { + _writeSemaphore.Release(); + throw; + } + + return new LockToken(ReleaseWriterLock); + } + + private void ReleaseWriterLock() + { + _readSemaphore.Release(); + _writeSemaphore.Release(); + } + + public async Task AcquireReaderLock(CancellationToken token = default) + { + await _writeSemaphore.WaitAsync(token).ConfigureAwait(false); + if (Interlocked.Increment(ref _readerCount) == 1) + { + try + { + await _readSemaphore.WaitAsync(token).ConfigureAwait(false); + } + catch + { + Interlocked.Decrement(ref _readerCount); + _writeSemaphore.Release(); + throw; + } + } + + _writeSemaphore.Release(); + return new LockToken(ReleaseReaderLock); + } + + private void ReleaseReaderLock() + { + if (Interlocked.Decrement(ref _readerCount) == 0) + _readSemaphore.Release(); + } + + public void Dispose() + { + _writeSemaphore.Dispose(); + _readSemaphore.Dispose(); + } + + private sealed class LockToken : IDisposable + { + private readonly Action _action; + public LockToken(Action action) => _action = action; + public void Dispose() => _action?.Invoke(); + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/BarotraumaExtensions.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/BarotraumaExtensions.cs new file mode 100644 index 000000000..00349d44f --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/BarotraumaExtensions.cs @@ -0,0 +1,107 @@ +using Barotrauma; +using Barotrauma.Items.Components; +using Barotrauma.Networking; +using System; +using System.Reflection; +using static Barotrauma.Items.Components.Quality; + +namespace Barotrauma; + +static class MapEntityExtensions +{ + public static void AddLinked(this MapEntity entity, MapEntity other) + { + entity.linkedTo.Add(other); + } +} + + +static class ClientExtensions +{ +#if SERVER + public static void SetClientCharacter(this Client client, Character character) + { + GameMain.Server.SetClientCharacter(client, character); + } + + public static void Kick(this Client client, string reason = "") + { + GameMain.Server.KickClient(client.Connection, reason); + } + + public static void Ban(this Client client, string reason = "", float seconds = -1) + { + if (seconds == -1) + { + GameMain.Server.BanClient(client, reason, null); + } + else + { + GameMain.Server.BanClient(client, reason, TimeSpan.FromSeconds(seconds)); + } + } + + public static bool CheckPermission(this Client client, ClientPermissions permissions) + { + return client.Permissions.HasFlag(permissions); + } +#endif +} + +static class ItemExtensions +{ + public static object GetComponentString(this Item item, string component) + { + Type type = LuaCsSetup.Instance.PluginManagementService + .GetType("Barotrauma.Items.Components." + component); + + if (type == null) + { + return null; + } + + MethodInfo method = typeof(Item).GetMethod(nameof(Item.GetComponent)); + MethodInfo generic = method.MakeGenericMethod(type); + return generic.Invoke(item, null); + } + +#if SERVER + public static object CreateServerEventString(this Item item, string component) + { + var comp = item.GetComponentString(component); + + if (comp == null) + return null; + + MethodInfo method = typeof(Item).GetMethod( + nameof(Item.CreateServerEvent), + new Type[] { Type.MakeGenericMethodParameter(0) }); + + MethodInfo generic = method.MakeGenericMethod(comp.GetType()); + return generic.Invoke(item, new object[] { comp }); + } + + public static object CreateServerEventString(this Item item, string component, object[] extraData) + { + var comp = item.GetComponentString(component); + + if (comp == null) + return null; + + MethodInfo method = typeof(Item).GetMethod( + nameof(Item.CreateServerEvent), + new Type[] { Type.MakeGenericMethodParameter(0), typeof(object[]) }); + + MethodInfo generic = method.MakeGenericMethod(comp.GetType()); + return generic.Invoke(item, new object[] { comp, extraData }); + } +#endif +} + +static class QualityExtensions +{ + public static void SetValue(this Quality quality, StatType statType, float value) + { + quality.statValues[statType] = value; + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Compatibility/ILuaCsHook.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Compatibility/ILuaCsHook.cs new file mode 100644 index 000000000..dbe6c4916 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Compatibility/ILuaCsHook.cs @@ -0,0 +1,25 @@ +using System; +using System.Reflection; +using Barotrauma.LuaCs; + +namespace Barotrauma.LuaCs.Compatibility; + +public interface ILuaCsHook : ILuaPatcher, ILuaCsShim +{ + // Event Services + [Obsolete("ACsMod is deprecated. Use ILuaEventService.Add() instead.")] + 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); + T Call(string eventName, params object[] args); + + // Needs to be here instead of ILuaPatcher for compatiility purposes + public enum HookMethodType + { + Before, After + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Compatibility/ILuaCsLogger.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Compatibility/ILuaCsLogger.cs new file mode 100644 index 000000000..bcf5a3ebc --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Compatibility/ILuaCsLogger.cs @@ -0,0 +1,6 @@ +namespace Barotrauma.LuaCs.Compatibility; + +public interface ILuaCsLogger : ILuaCsShim +{ + +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Compatibility/ILuaCsNetworking.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Compatibility/ILuaCsNetworking.cs new file mode 100644 index 000000000..22b741488 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Compatibility/ILuaCsNetworking.cs @@ -0,0 +1,26 @@ +using Barotrauma.Networking; +using System.Collections.Generic; + +namespace Barotrauma.LuaCs.Compatibility; + +internal interface ILuaCsNetworking : ILuaCsShim +{ + void CreateEntityEvent(INetSerializable entity, NetEntityEvent.IData extraData); + ushort LastClientListUpdateID { get; set; } + void HttpRequest(string url, LuaCsAction callback, string data = null, string method = "POST", string contentType = "application/json", Dictionary headers = null, string savePath = null); + void HttpPost(string url, LuaCsAction callback, string data, string contentType = "application/json", Dictionary headers = null, string savePath = null); + void HttpGet(string url, LuaCsAction callback, Dictionary headers = null, string savePath = null); + void RequestGetHTTP(string url, LuaCsAction callback, Dictionary headers = null, string savePath = null); + void RequestPostHTTP(string url, LuaCsAction callback, string data, string contentType = "application/json", Dictionary headers = null, string savePath = null); + + void Receive(string netId, LuaCsAction action); +#if SERVER + int FileSenderMaxPacketsPerUpdate { get; set; } + void ClientWriteLobby(Client client); + void UpdateClientPermissions(Client client); + IWriteMessage Start(); + void Send(IWriteMessage mesage, NetworkConnection connection = null, DeliveryMethod deliveryMethod = DeliveryMethod.Reliable); +#elif CLIENT + void Send(IWriteMessage mesage, DeliveryMethod deliveryMethod = DeliveryMethod.Reliable); +#endif +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Compatibility/ILuaCsShim.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Compatibility/ILuaCsShim.cs new file mode 100644 index 000000000..8f119b106 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Compatibility/ILuaCsShim.cs @@ -0,0 +1,8 @@ +using Barotrauma.LuaCs; + +namespace Barotrauma.LuaCs.Compatibility; + +public interface ILuaCsShim : IService +{ + +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Compatibility/ILuaCsTimer.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Compatibility/ILuaCsTimer.cs new file mode 100644 index 000000000..9fe824d0e --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Compatibility/ILuaCsTimer.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; + +namespace Barotrauma.LuaCs.Compatibility; + +internal partial interface ILuaCsTimer : IReusableService, ILuaCsShim +{ + public static double Time => Timing.TotalTime; + public static double GetTime() => Time; + public static double AccumulatorMax { get; set; } + + public void Clear(); + public void Wait(LuaCsAction action, int millisecondDelay); + public void NextFrame(LuaCsAction action); +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Compatibility/ILuaCsUtility.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Compatibility/ILuaCsUtility.cs new file mode 100644 index 000000000..3ab6fafed --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Compatibility/ILuaCsUtility.cs @@ -0,0 +1,10 @@ +namespace Barotrauma.LuaCs.Compatibility; + +public interface ILuaCsUtility : ILuaCsShim +{ + public bool CanReadFromPath(string file); + public bool CanWriteToPath(string file); + internal bool IsPathAllowedException(string path, bool write = true, + LuaCsMessageOrigin origin = LuaCsMessageOrigin.Unknown); + +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Compatibility/LuaCsConfig.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Compatibility/LuaCsConfig.cs new file mode 100644 index 000000000..06598ab2d --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Compatibility/LuaCsConfig.cs @@ -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(); + 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 SaveDocTypes(IEnumerable 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 fields = tType.GetFields(BindingFlags.Instance | BindingFlags.Public) + .Concat(tType.GetFields(BindingFlags.Instance | BindingFlags.NonPublic)); + IEnumerable 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 types, Type type, XElement elem) + { + if (!types.Contains(type)) types.Add(type); + elem.SetAttributeValue("Type", types.IndexOf(type)); + } + + private static XElement ParseObject(List 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 fields = tType.GetFields(BindingFlags.Instance | BindingFlags.Public) + .Concat(tType.GetFields(BindingFlags.Instance | BindingFlags.NonPublic)); + IEnumerable 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(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(); + 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(string path) + { + using (var file = LuaCsFile.OpenRead(path)) return Load(file); + } + + public static void Save(string path, object obj) + { + using (var file = LuaCsFile.OpenWrite(path)) Save(file, obj); + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/LuaCsPerformanceCounter.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Compatibility/LuaCsPerformanceCounter.cs similarity index 88% rename from Barotrauma/BarotraumaShared/SharedSource/LuaCs/LuaCsPerformanceCounter.cs rename to Barotrauma/BarotraumaShared/SharedSource/LuaCs/Compatibility/LuaCsPerformanceCounter.cs index 77c18a887..807b30ecc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/LuaCsPerformanceCounter.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Compatibility/LuaCsPerformanceCounter.cs @@ -1,9 +1,14 @@ -using System; +using System; using System.Collections.Generic; using System.Diagnostics; + namespace Barotrauma { + /// + /// [Obsolete] Legacy compatibility only. + /// + [Obsolete("Deprecated.")] public class LuaCsPerformanceCounter { public bool EnablePerformanceCounter = false; @@ -33,4 +38,4 @@ namespace Barotrauma HookElapsedTime[eventName][hookName] = (double)ticks / Stopwatch.Frequency; } } -} \ No newline at end of file +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/DataInterfaceImplementations.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/DataInterfaceImplementations.cs new file mode 100644 index 000000000..b6186231d --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/DataInterfaceImplementations.cs @@ -0,0 +1,95 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Globalization; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using System.Xml.Linq; +using System.Xml.Serialization; +using Barotrauma.LuaCs; +using Barotrauma.Steam; +using OneOf; + +namespace Barotrauma.LuaCs.Data; + +#region ModConfigurationInfo + +public partial record ModConfigInfo : IModConfigInfo +{ + public ContentPackage Package { get; init; } + public ImmutableArray Assemblies { get; init; } + public ImmutableArray LuaScripts { get; init; } + public ImmutableArray Configs { get; init; } +} + +#endregion + +#region DataContracts_Resources + +public record BaseResourceInfo : IBaseResourceInfo +{ + public Platform SupportedPlatforms { get; init; } + public Target SupportedTargets { get; init; } + public int LoadPriority { get; init; } + public ImmutableArray FilePaths { get; init; } + public bool Optional { get; init; } + public string InternalName { get; init; } + public ContentPackage OwnerPackage { get; init; } + public ImmutableArray RequiredPackages { get; init; } + public ImmutableArray IncompatiblePackages { get; init; } +} + +public record AssemblyResourceInfo : BaseResourceInfo, IAssemblyResourceInfo +{ + public string FriendlyName { get; init; } + public bool IsScript { get; init; } + public bool UseInternalAccessName { get; init; } + public bool IsReferenceModeOnly { get; init; } +} + +/// +/// Note: Config settings and settings-profiles are stored in the same files. +/// +public record ConfigResourceInfo : BaseResourceInfo, IConfigResourceInfo {} + +public record LuaScriptsResourceInfo : BaseResourceInfo, ILuaScriptResourceInfo +{ + public bool IsAutorun { get; init; } + public bool RunUnrestricted { get; init; } +} + +#endregion + +#region DataContracts_ParsedInfo + +public record ConfigInfo : IConfigInfo +{ + public string InternalName { get; init; } + public ContentPackage OwnerPackage { get; init; } + public string DataType { get; init; } + public XElement Element { get; init; } + public RunState EditableStates { get; init; } + public NetSync NetSync { get; init; } + +#if CLIENT // IConfigDisplayInfo + public string DisplayName { get; init; } + public string Description { get; init; } + public string DisplayCategory { get; init; } + public bool ShowInMenus { get; init; } + public string Tooltip { get; init; } + public ContentPath ImageIconPath { get; init; } +#endif +} + +public record ConfigProfileInfo : IConfigProfileInfo +{ + /// + /// Profile name. + /// + public string InternalName { get; init; } + public ContentPackage OwnerPackage { get; init; } + public IReadOnlyList<(string SettingName, XElement Element)> ProfileValues { get; init; } +} + +#endregion diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/EPlatformsTargets.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/EPlatformsTargets.cs new file mode 100644 index 000000000..56d192dd4 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/EPlatformsTargets.cs @@ -0,0 +1,21 @@ +using System; +// ReSharper disable InconsistentNaming + +namespace Barotrauma.LuaCs.Data; + +[Flags] +public enum Platform +{ + Linux = 0x1, + OSX = 0x2, + Windows = 0x4, + Any = Linux | OSX | Windows +} + +[Flags] +public enum Target +{ + Client = 0x1, + Server = 0x2, + Any = Client | Server +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/IBaseInfoDefinitions.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/IBaseInfoDefinitions.cs new file mode 100644 index 000000000..2326ecce2 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/IBaseInfoDefinitions.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.ComponentModel.DataAnnotations; +using System.Globalization; +using System.Xml.Serialization; + +namespace Barotrauma.LuaCs.Data; + +public interface IDependencyInfo +{ + /// + /// List of dependency packages required by this resource. + /// + ImmutableArray RequiredPackages { get; } + /// + /// List of packages incompatible with this resource. + /// + ImmutableArray IncompatiblePackages { get; } +} + +public interface IPlatformInfo +{ + /// + /// Platforms that these localization files should be loaded for. + /// + [Required] + [XmlAttribute("Platform")] + Platform SupportedPlatforms { get; } + + /// + /// Targets that these localization files should be loaded for. + /// + [Required] + [XmlAttribute("Target")] + Target SupportedTargets { get; } +} + + +/// +/// ResourceInfos contain metadata about a resource. +/// +public interface IResourceInfo : IPlatformInfo +{ + /// + /// [Optional] + /// Specifies the loading order for all assets of the same type (ie. styles, assemblies, etc.) from + /// the same . Lower number is higher priority, see + /// + [XmlAttribute("LoadPriority")] + int LoadPriority { get; } + + /// + /// Resource absolute file paths. + /// + [Required] + ImmutableArray FilePaths { get; } + + /// + /// Marks this resource as optional (ie. Cross-CP content). Setting this to true will allow the dependency system to + /// try and order the loading but not fail if it runs into circular dependency issues. + /// + [XmlAttribute("Optional")] + bool Optional { get; } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/IConfigInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/IConfigInfo.cs new file mode 100644 index 000000000..696683af9 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/IConfigInfo.cs @@ -0,0 +1,35 @@ +using System; +using System.Xml.Linq; +using Barotrauma.LuaCs; +using Barotrauma.Networking; + +namespace Barotrauma.LuaCs.Data; + +/// +/// Parsed data from a configuration xml. +/// +public partial interface IConfigInfo : IDataInfo +{ + /// + /// Specifies the type initializer that will be used to instantiate the config var. + /// + string DataType { get; } + /// + /// The 'Setting' XML element. + /// + XElement Element { get; } + /// + /// In what (s) is this config editable. Will be editable in the selected state, and lower value states. + ///

+ /// [Important]
Setting this to value lower than 'Configuration` will render this config read-only. + ///

Expected Behaviour: + ///
[|]: Read-Only. + ///
[]: Can only be changed at the Main Menu (not in a lobby). + ///
[]: Can be changed at the Main Menu and while a lobby is active. + ///
+ RunState EditableStates { get; } + /// + /// Network synchronization rules for this config. + /// + NetSync NetSync { get; } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/IConfigProfileInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/IConfigProfileInfo.cs new file mode 100644 index 000000000..28e9a6e1a --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/IConfigProfileInfo.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; +using System.Xml.Linq; + +namespace Barotrauma.LuaCs.Data; + +public interface IConfigProfileInfo : IDataInfo +{ + IReadOnlyList<(string SettingName, XElement Element)> ProfileValues { get; } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/IDataInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/IDataInfo.cs new file mode 100644 index 000000000..652ddd967 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/IDataInfo.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using System.Xml.Serialization; + +namespace Barotrauma.LuaCs.Data; + +/// +/// Serves as a compound-key to refer to all resources and information that comes from a specific source. +/// +public interface IDataInfo : IEqualityComparer, IEquatable +{ + /// + /// Internal name unique within the resources inside a package. + /// + [XmlAttribute("Name")] + string InternalName { get; } + /// + /// The package this information belongs to. + /// + ContentPackage OwnerPackage { get; } + + bool IEqualityComparer.Equals(IDataInfo x, IDataInfo y) + { + if (x is null || y is null) + return false; + if (x.OwnerPackage is null) + throw new NullReferenceException($"ContentPackage not set for resource {x}!"); + if (y.OwnerPackage is null) + throw new NullReferenceException($"ContentPackage not set for resource {y}!"); + if (x.InternalName.IsNullOrWhiteSpace()) + throw new NullReferenceException($"InternalName not set for resource {x}!"); + if (y.InternalName.IsNullOrWhiteSpace()) + throw new NullReferenceException($"InternalName not set for resource {y}!"); + return x.OwnerPackage == y.OwnerPackage && x.InternalName == y.InternalName; + } + + bool IEquatable.Equals(IDataInfo other) + { + return Equals(this, other); + } + + int IEqualityComparer.GetHashCode(IDataInfo obj) + { + if (obj.OwnerPackage is null) + throw new NullReferenceException($"ContentPackage not set for resource {obj}!"); + if (obj.InternalName.IsNullOrWhiteSpace()) + throw new NullReferenceException($"InternalName is null for object {obj}!"); + return obj.InternalName.GetHashCode() + obj.OwnerPackage.GetHashCode(); + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/IModConfigInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/IModConfigInfo.cs new file mode 100644 index 000000000..c8740e51d --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/IModConfigInfo.cs @@ -0,0 +1,18 @@ +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; +using System.Xml.Linq; + +namespace Barotrauma.LuaCs.Data; + +public partial interface IModConfigInfo : IAssembliesResourcesInfo, + ILuaScriptsResourcesInfo, IConfigsResourcesInfo +{ + // package info + ContentPackage Package { get; } +} + +public record ResourceParserInfo( + [NotNull] ContentPackage Owner, + [NotNull] XElement Element, + ImmutableArray Required, + ImmutableArray Incompatible); diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/IResourceInfoDeclarations.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/IResourceInfoDeclarations.cs new file mode 100644 index 000000000..ba315464f --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/IResourceInfoDeclarations.cs @@ -0,0 +1,79 @@ +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Globalization; +using System.Runtime.CompilerServices; +using System.Xml.Serialization; + +namespace Barotrauma.LuaCs.Data; + + +public interface IBaseResourceInfo : IResourceInfo, IDataInfo, IDependencyInfo {} + +public interface IConfigResourceInfo : IBaseResourceInfo {} + +/// +/// Represents loadable Lua files. +/// +public interface ILuaScriptResourceInfo : IBaseResourceInfo +{ + /// + /// Should this script be run automatically. + /// + [XmlAttribute("IsAutorun")] + public bool IsAutorun { get; } + + /// + /// Indicates that this lua resources needs to run outside sandbox/requires unrestricted access. + /// + [XmlAttribute("RunUnrestricted")] + public bool RunUnrestricted { get; } +} + +public interface IAssemblyResourceInfo : IBaseResourceInfo +{ + /// + /// The friendly name of the assembly. Script files belonging to the same assembly should all have the same name. + /// Legacy scripts will all be given the sanitized name of the Content Package they belong to. + /// + [XmlAttribute("FriendlyName")] + public string FriendlyName { get; } + /// + /// Is this entry referring to a script file collection. + /// + [XmlAttribute("IsScript")] + public bool IsScript { get; } + + /// + /// [Required(IsScript: true)] Whether the internal compiled assembly name should be named to enabled use of the + /// attribute. + /// + [XmlAttribute("UseInternalAccessName")] + public bool UseInternalAccessName { get; } + + /// + /// Should the following resources only be used for Compilation MetadataReference. + /// NOTE: Affects the entire package's assembly resources, meant for internal use only. + /// + [XmlAttribute("IsReferenceModeOnly")] + public bool IsReferenceModeOnly { get; } +} + + +#region Collections + +public interface IAssembliesResourcesInfo +{ + ImmutableArray Assemblies { get; } +} + +public interface ILuaScriptsResourcesInfo +{ + ImmutableArray LuaScripts { get; } +} + +public interface IConfigsResourcesInfo +{ + ImmutableArray Configs { get; } +} + +#endregion diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/IRunConfig.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/IRunConfig.cs new file mode 100644 index 000000000..a0da6263b --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/IRunConfig.cs @@ -0,0 +1,18 @@ +namespace Barotrauma.LuaCs.Data; + +/// +/// Legacy data contract for the old run configuration system. Should be deprecated +/// once no longer needed. +/// +public interface IRunConfig +{ + bool UseNonPublicizedAssemblies { get; } + bool AutoGenerated { get; } + bool UseInternalAssemblyName { get; } + string Client { get; } + string Server { get; } + + bool IsForced(); + bool IsStandard(); + bool IsForcedOrStandard(); +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/ISettingTypeDef.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/ISettingTypeDef.cs new file mode 100644 index 000000000..d82349eea --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/ISettingTypeDef.cs @@ -0,0 +1,95 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Xml.Linq; +using Barotrauma.LuaCs.Data; +using Barotrauma.LuaCs; +using Barotrauma.Networking; +using OneOf; + +namespace Barotrauma.LuaCs.Data; + +public partial interface ISettingBase : IDataInfo, IEquatable, IDisposable +{ + /// + /// Settings production factory. Should be implemented by all types and registered with the Dependency Injector. + /// + /// An interface type derived from . + public interface IFactory where T : ISettingBase + { + /// + /// Creates an instance of the given type. + /// + /// Configuration information. + /// Called before a new value is assigned. Returns a boolean whether to allow + /// the value to be changed to the one given. + /// + T CreateInstance([NotNull]IConfigInfo configInfo, Func, bool> valueChangePredicate); + } + + IConfigInfo GetConfigInfo(); + #if CLIENT + IConfigDisplayInfo GetDisplayInfo(); + #endif + bool IsDisposed { get; } + Type GetValueType(); + string GetStringValue(); + string GetDefaultStringValue(); + bool TrySetSerializedValue(OneOf value); + event Action OnValueChanged; + OneOf.OneOf GetSerializableValue(); +} + +/// +/// Creates a setting representing a value of the given . Must be a compatible listed type.
+///
+/// +/// Compatible Types:
+/// Any primitive type:
+/// -
+/// -
+/// -
+/// -
+/// -
+/// -
+/// -
+/// -
+/// -
+/// -
+/// Extension types and Enums:
+/// -
+/// -
+///
+public interface ISettingBase : ISettingBase where T : IEquatable, IConvertible +{ + [NotNull] + T Value { get; } + [NotNull] + T DefaultValue { get; } + bool TrySetValue(T value); +} + +/// +/// Creates a setting representing a value of the given with a minimum and maximum value. +/// Can only be either an or a . +/// +/// The type selection is limited by the Undertow implementation of the GUI Slider. +/// The value type, either or +public interface ISettingRangeBase : ISettingBase where T : IEquatable, IConvertible +{ + T MinValue { get; } + T MaxValue { get; } + int IncrementalSteps { get; } +} + +/// +/// Creates a setting representing a value of the given with a distinct list of selectable values. +/// Must be a type compatible with . +/// +/// The value type. See +public interface ISettingList : ISettingBase where T : IEquatable, IConvertible +{ + bool TrySetValueByIndex(int index); + IReadOnlyList Options { get; } + IReadOnlyList StringOptions { get; } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/ServicesConfigData.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/ServicesConfigData.cs new file mode 100644 index 000000000..2601e1826 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/ServicesConfigData.cs @@ -0,0 +1,113 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Collections.ObjectModel; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Security.AccessControl; +using Barotrauma.LuaCs; +using Barotrauma.Networking; +using FluentResults; +using OneOf.Types; + +namespace Barotrauma.LuaCs.Data; + + +// --- Storage Service +// TODO: Configs should not be services, add new registration path for them. +public interface IStorageServiceConfig : IService +{ + string LocalModsDirectory { get; } + string WorkshopModsDirectory { get; } + string GameSettingsConfigPath { get; } +#if CLIENT + string TempDownloadsDirectory { get; } +#endif + string LocalDataSavePath { get; } + string LocalDataPathRegex { get; } + string LocalPackageDataPath { get; } +} + +public record StorageServiceConfig : IStorageServiceConfig +{ + private static readonly string ExecutionLocation = Directory.GetCurrentDirectory().CleanUpPathCrossPlatform(); + + public string LocalModsDirectory { get; init; } = System.IO.Path.GetFullPath(ContentPackage.LocalModsDir).CleanUpPath(); + public string WorkshopModsDirectory { get; init; } = System.IO.Path.GetFullPath(ContentPackage.WorkshopModsDir).CleanUpPath(); + public string GameSettingsConfigPath { get; init; } = System.IO.Path.GetFullPath( + string.IsNullOrEmpty(GameSettings.CurrentConfig.SavePath) + ? SaveUtil.DefaultSaveFolder + : GameSettings.CurrentConfig.SavePath).CleanUpPath(); +#if CLIENT + public string TempDownloadsDirectory { get; init; } = System.IO.Path.GetFullPath(ModReceiver.DownloadFolder).CleanUpPath(); +#endif + public string LocalDataSavePath => Path.Combine(ExecutionLocation, "Data/Mods").CleanUpPathCrossPlatform(); + public string LocalDataPathRegex => "%ModDir%"; + public string RunLocation => ExecutionLocation; + + public string LocalPackageDataPath => Path.Combine(LocalDataSavePath, LocalDataPathRegex); + + public void Dispose() + { + // cannot be disposed. + } + + public bool IsDisposed => false; +} + +// --- Config Service +public interface IConfigServiceConfig : IService +{ + string LocalConfigPathPartial { get; } + string FileNamePattern { get; } +} + +public record ConfigServiceConfig : IConfigServiceConfig +{ + public string LocalConfigPathPartial => $"/Config/{FileNamePattern}.xml"; + public string FileNamePattern => ""; + public void Dispose() + { + // ignored + } + public bool IsDisposed => false; +} + + +// --- Lua Scripts Service +public interface ILuaScriptServicesConfig : IService +{ + bool SafeLuaIOEnabled { get; } + bool UseCaching { get; } +} + +public record LuaScriptServicesConfig : ILuaScriptServicesConfig +{ + public bool SafeLuaIOEnabled => true; + public bool UseCaching => true; + public void Dispose() + { + // ignored + } + + public bool IsDisposed => false; +} + +// --- Package Management Service +public interface IPackageManagementServiceConfig : IService +{ + bool IsCsEnabled { get; } +} + +public class PackageManagementServiceConfig : IPackageManagementServiceConfig +{ + public void Dispose() + { + // ignored + } + + public bool IsDisposed => false; + public bool IsCsEnabled => true; +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/SettingBase.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/SettingBase.cs new file mode 100644 index 000000000..f9f9826de --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/SettingBase.cs @@ -0,0 +1,81 @@ +using System; +using System.Collections.Concurrent; +using System.Xml.Linq; +using Barotrauma.LuaCs.Data; +using Microsoft.Toolkit.Diagnostics; +using Microsoft.Xna.Framework; +using OneOf; + +namespace Barotrauma.LuaCs.Data; + +public abstract class SettingBase : ISettingBase +{ + protected SettingBase(IConfigInfo configInfo) + { + Guard.IsNotNull(configInfo, nameof(configInfo)); + ConfigInfo = configInfo; + } + + protected IConfigInfo ConfigInfo { get; private set; } + + public string InternalName => ConfigInfo.InternalName; + public ContentPackage OwnerPackage => ConfigInfo.OwnerPackage; + + public IConfigInfo GetConfigInfo() => ConfigInfo; + #if CLIENT + public IConfigDisplayInfo GetDisplayInfo() => ConfigInfo; + #endif + + public virtual bool Equals(ISettingBase other) + { + return other is not null && ( + ReferenceEquals(this, other) || !IsDisposed && + OwnerPackage == other.OwnerPackage && + InternalName.Equals(other.InternalName)); + } + + private int _isDisposed = 0; + public virtual bool IsDisposed + { + get => ModUtils.Threading.GetBool(ref _isDisposed); + private set => ModUtils.Threading.SetBool(ref _isDisposed, value); + } + + protected abstract void OnDispose(); + + public virtual void Dispose() + { + if (!ModUtils.Threading.CheckIfClearAndSetBool(ref _isDisposed)) + { + return; + } + + OnDispose(); + ConfigInfo = null; + GC.SuppressFinalize(this); + } + + // -- Must be implemented + + public abstract Type GetValueType(); + public abstract string GetStringValue(); + public abstract string GetDefaultStringValue(); + public abstract bool TrySetSerializedValue(OneOf value); + + public abstract event Action OnValueChanged; + public abstract OneOf GetSerializableValue(); +#if CLIENT + public virtual void AddDisplayComponent(GUILayoutGroup layoutGroup, Vector2 relativeSize, Action onSerializedValue) + { + new GUITextBox(new RectTransform(relativeSize, layoutGroup.RectTransform), font: GUIStyle.SmallFont) + { + Text = GetStringValue(), + OnTextChangedDelegate = (box, txt) => + { + onSerializedValue?.Invoke(txt); + return true; + } + }; + } +#endif +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/SettingEntry.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/SettingEntry.cs new file mode 100644 index 000000000..5b1cacd23 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/SettingEntry.cs @@ -0,0 +1,395 @@ +using System; +using System.Runtime.CompilerServices; +using System.Xml.Linq; +using Barotrauma.LuaCs.Data; +using Barotrauma.LuaCs; +using Barotrauma.Networking; +using Microsoft.Toolkit.Diagnostics; +using Microsoft.Xna.Framework; +using OneOf; + +namespace Barotrauma.LuaCs.Data; + +public partial class SettingEntry : SettingBase, ISettingBase, INetworkSyncVar where T : IEquatable, IConvertible +{ + public class Factory : ISettingBase.IFactory> + { + public ISettingBase CreateInstance(IConfigInfo configInfo, Func, bool> valueChangePredicate) + { + Guard.IsNotNull(configInfo, nameof(configInfo)); + return new SettingEntry(configInfo, valueChangePredicate); + } + } + + public SettingEntry(IConfigInfo configInfo, + Func, bool> valueChangePredicate) + : base(configInfo) + { + if (!( + typeof(T).IsEnum || + typeof(T).IsPrimitive || + typeof(T) == typeof(string))) + { + ThrowHelper.ThrowArgumentException($"{nameof(ISettingBase)}: The type of {nameof(T)} is not an allowed type."); + } + ValueChangePredicate = valueChangePredicate; + + try + { + Value = (T)Convert.ChangeType(ConfigInfo.Element.GetAttributeString("Value", null), typeof(T)); + DefaultValue = Value; + } + catch (Exception e) when (e is InvalidCastException or ArgumentNullException) + { + Value = default(T); + DefaultValue = default(T); + } + } + + protected Func, bool> ValueChangePredicate; + public T Value { get; protected set; } + + public T DefaultValue { get; protected set; } + + public virtual bool TrySetValue(T value) + { + if (value is null || value.Equals(Value)) + { + return false; + } +#if CLIENT + if (SyncType is NetSync.ServerAuthority && NetworkingService is not null + && GameMain.IsMultiplayer + && GameMain.Client is not null + && !GameMain.Client.HasPermission(this.WritePermissions)) + { + return false; + } +#endif + + if (!TrySetValueInternal(value)) + { + return false; + } + OnValueChanged?.Invoke(this); +#if CLIENT + if (GameMain.IsMultiplayer && SyncType is NetSync.ClientOneWay or NetSync.TwoWay) + { + NetworkingService?.SendNetVar(this); + } +#elif SERVER + if (SyncType is NetSync.TwoWay or NetSync.ServerAuthority) + { + NetworkingService?.SendNetVar(this); + } +#endif + return true; + } + + private bool TrySetValueInternal(T value) + { + if (value is null) + { + return false; + } + + if (ValueChangePredicate != null && !ValueChangePredicate(value)) + { + return false; + } + + Value = value; + return true; + } + + /// + /// handles internal networking rules after reading the net message (to avoid synchro issues). + /// + /// + /// + private bool TrySetValueNetwork(T value) + { + if (NetworkingService is null) + { + return false; + } +#if CLIENT + if (SyncType is NetSync.None or NetSync.ClientOneWay) + { + return false; + } +#else + if (SyncType is NetSync.None or NetSync.ServerAuthority) + { + return false; + } +#endif + if (!TrySetValueInternal(value)) + { + return false; + } + +#if SERVER + if (SyncType is NetSync.TwoWay) + { + NetworkingService?.SendNetVar(this); + } +#endif + + OnValueChanged?.Invoke(this); + return true; + } + + protected override void OnDispose() + { + ValueChangePredicate = null; + NetworkingService?.DeregisterNetVar(this); + } + + public override Type GetValueType() => typeof(T); + public override string GetStringValue() => Value?.ToString() ?? string.Empty; + public override string GetDefaultStringValue() => DefaultValue?.ToString() ?? string.Empty; + + public override bool TrySetSerializedValue(OneOf value) + { + bool isFailed = false; + var typeConvertedValue = value.Match( + (string val) => + { + try + { + return (T)Convert.ChangeType(val, typeof(T)); + } + catch (Exception e) + { + // ignored + isFailed = true; + return default(T); + } + }, + (XElement val) => + { + try + { + return (T)Convert.ChangeType(val.GetAttributeString("Value", null), typeof(T)); + } + catch (Exception e) + { + isFailed = true; + return default(T); + } + }); + return !isFailed && TrySetValue(typeConvertedValue); + } + + public override event Action OnValueChanged; + + public override OneOf GetSerializableValue() => Value.ToString(); + + // -- Networking + protected IEntityNetworkingService NetworkingService; + public Guid InstanceId => NetworkingService?.GetNetworkIdForInstance(this) ?? Guid.Empty; + public void SetNetworkOwner(IEntityNetworkingService networkingService) + { + NetworkingService = networkingService; + } + + public NetSync SyncType => ConfigInfo?.NetSync ?? NetSync.None; + // needs to be added IConfigInfo + public ClientPermissions WritePermissions => ClientPermissions.ManageSettings; + + public void ReadNetMessage(IReadMessage message) + { + if (SyncType == NetSync.None || NetworkingService is null) + { + return; + } + + try + { + if (typeof(T).IsEnum) + { + TrySetValueInternal((T)(object)message.ReadInt32()); + } + + // No...there's no better way to do this... + var typeCode = Type.GetTypeCode(typeof(T)); + switch (typeCode) + { + case TypeCode.Boolean: + TrySetValueNetwork((T)Convert.ChangeType(message.ReadBoolean(), typeCode)); + return; + case TypeCode.Byte: + TrySetValueNetwork((T)Convert.ChangeType(message.ReadByte(), typeCode)); + return; + // SByte not supported by interface + case TypeCode.SByte: + TrySetValueNetwork((T)Convert.ChangeType(message.ReadInt16(), typeCode)); + return; + case TypeCode.Int16: + TrySetValueNetwork((T)Convert.ChangeType(message.ReadInt16(), typeCode)); + return; + case TypeCode.Char: + case TypeCode.UInt16: + TrySetValueNetwork((T)Convert.ChangeType(message.ReadUInt16(), typeCode)); + return; + case TypeCode.Int32: + TrySetValueNetwork((T)Convert.ChangeType(message.ReadInt32(), typeCode)); + return; + case TypeCode.UInt32: + TrySetValueNetwork((T)Convert.ChangeType(message.ReadUInt32(), typeCode)); + return; + case TypeCode.Int64: + TrySetValueNetwork((T)Convert.ChangeType(message.ReadInt64(), typeCode)); + return; + case TypeCode.UInt64: + TrySetValueNetwork((T)Convert.ChangeType(message.ReadUInt64(), typeCode)); + return; + case TypeCode.Single: + TrySetValueNetwork((T)Convert.ChangeType(message.ReadSingle(), typeCode)); + return; + case TypeCode.Double: + TrySetValueNetwork((T)Convert.ChangeType(message.ReadDouble(), typeCode)); + return; + case TypeCode.String: + TrySetValueNetwork((T)Convert.ChangeType(message.ReadString(), typeCode)); + return; + case TypeCode.Decimal: + default: + ThrowHelper.ThrowNotSupportedException($"{nameof(SettingEntry)}: The type {typeof(T).Name} is not supported."); + break; + } + } + catch (Exception e) + { + // Suppress unless we're testing. +#if DEBUG + throw; +#endif + } + } + + public void WriteNetMessage(IWriteMessage message) + { + if (SyncType == NetSync.None || NetworkingService is null) + { + return; + } + + try + { + if (typeof(T).IsEnum) + { + message.WriteInt32((int)((IConvertible)Value)); + } + + // No...there's no better way to do this... + var typeCode = Type.GetTypeCode(typeof(T)); + switch (typeCode) + { + case TypeCode.Boolean: + message.WriteBoolean((bool)Convert.ChangeType(Value, typeCode)!); + return; + case TypeCode.Byte: + message.WriteByte((byte)Convert.ChangeType(Value, typeCode)!); + return; + // SByte not supported by interface + case TypeCode.SByte: + message.WriteInt16((short)Convert.ChangeType(Value, typeCode)!); + return; + case TypeCode.Int16: + message.WriteInt16((short)Convert.ChangeType(Value, typeCode)!); + return; + case TypeCode.Char: + case TypeCode.UInt16: + message.WriteUInt16((ushort)Convert.ChangeType(Value, typeCode)!); + return; + case TypeCode.Int32: + message.WriteInt32((int)Convert.ChangeType(Value, typeCode)!); + return; + case TypeCode.UInt32: + message.WriteUInt32((uint)Convert.ChangeType(Value, typeCode)!); + return; + case TypeCode.Int64: + message.WriteInt64((long)Convert.ChangeType(Value, typeCode)!); + return; + case TypeCode.UInt64: + message.WriteUInt64((ulong)Convert.ChangeType(Value, typeCode)!); + return; + case TypeCode.Single: + message.WriteSingle((float)Convert.ChangeType(Value, typeCode)!); + return; + case TypeCode.Double: + message.WriteDouble((double)Convert.ChangeType(Value, typeCode)!); + return; + case TypeCode.String: + message.WriteString((string)Convert.ChangeType(Value, typeCode)!); + return; + case TypeCode.Decimal: + default: + ThrowHelper.ThrowNotSupportedException($"{nameof(SettingEntry)}: The type {typeof(T).Name} is not supported."); + break; + } + } + catch (Exception e) + { + // Suppress unless we're testing. +#if DEBUG + throw; +#endif + } + } + +#if CLIENT + public override void AddDisplayComponent(GUILayoutGroup layoutGroup, Vector2 relativeSize, Action onSerializedValue) + { + switch (Type.GetTypeCode(typeof(T))) + { + case TypeCode.Boolean: + new GUITickBox(new RectTransform(relativeSize, layoutGroup.RectTransform), "") + { + Selected = (bool)Convert.ChangeType(this.Value, TypeCode.Boolean), + OnSelected = (box) => + { + onSerializedValue?.Invoke(box.Selected.ToString()); + return true; + } + }; + break; + case TypeCode.Byte: + case TypeCode.SByte: + case TypeCode.Int16: + case TypeCode.Char: + case TypeCode.UInt16: + case TypeCode.Int32: + case TypeCode.UInt32: + case TypeCode.Int64: + case TypeCode.UInt64: + new GUINumberInput(new RectTransform(relativeSize, layoutGroup.RectTransform), NumberType.Int) + { + IntValue = (int)Convert.ChangeType(this.Value, TypeCode.Int32)!, + OnValueChanged = (num) => + { + onSerializedValue?.Invoke(num.IntValue.ToString()); + } + }; + break; + case TypeCode.Single: + case TypeCode.Double: + new GUINumberInput(new RectTransform(relativeSize, layoutGroup.RectTransform), NumberType.Float) + { + FloatValue = (float)Convert.ChangeType(this.Value, TypeCode.Single)!, + OnValueChanged = (num) => + { + onSerializedValue?.Invoke(num.FloatValue.ToString()); + } + }; + break; + case TypeCode.String: + default: + base.AddDisplayComponent(layoutGroup, relativeSize, onSerializedValue); + break; + } + } +#endif +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/SettingList.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/SettingList.cs new file mode 100644 index 000000000..5c080f5ba --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/SettingList.cs @@ -0,0 +1,121 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Xml; +using System.Xml.Linq; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.Toolkit.Diagnostics; +using Microsoft.Xna.Framework; + +namespace Barotrauma.LuaCs.Data; + +public class SettingList : SettingEntry, ISettingList where T : IEquatable, IConvertible +{ + public class LFactory : ISettingBase.IFactory> + { + public ISettingList CreateInstance(IConfigInfo configInfo, Func, bool> valueChangePredicate) + { + Guard.IsNotNull(configInfo, nameof(configInfo)); + return new SettingList(configInfo, valueChangePredicate); + } + } + + public SettingList(IConfigInfo configInfo, Func, bool> valueChangePredicate) : base(configInfo, valueChangePredicate) + { + if (!( + typeof(T).IsEnum || + typeof(T).IsPrimitive || + typeof(T) == typeof(string))) + { + ThrowHelper.ThrowArgumentException($"{nameof(ISettingBase)}: The type of {nameof(T)} is not an allowed type."); + } + ValueChangePredicate = valueChangePredicate; + + var valuesElements = ConfigInfo.Element.GetChildElement("Values")?.GetChildElements("Value")?.ToImmutableArray(); + + Guard.IsNotNull(valuesElements, this.InternalName); + if (valuesElements.Value.IsEmpty) + { + ThrowHelper.ThrowArgumentNullException($"{this.InternalName}: Could not find any values in list!"); + } + + foreach (var element in valuesElements.Value) + { + if (!TryConvert(element, out var v1)) + { + ThrowHelper.ThrowArgumentException($"{this.InternalName}: Error while parsing list values"); + } + _valuesList.Add(v1); + } + + if (TryConvert(ConfigInfo.Element, out var v) && _valuesList.Contains(v)) + { + Value = v; + DefaultValue = v; + } + else + { + Value = _valuesList[0]; + DefaultValue = _valuesList[0]; + } + + + bool TryConvert(XElement element, out T value) + { + try + { + value = (T)Convert.ChangeType(element.GetAttributeString("Value", null), typeof(T)); + return true; + } + catch (Exception e) when (e is InvalidCastException or ArgumentNullException) + { + value = default(T); + return false; + } + } + } + + private readonly List _valuesList = new(); + + public override bool TrySetValue(T value) + { + if (!_valuesList.Contains(value)) + { + return false; + } + + return base.TrySetValue(value); + } + + public bool TrySetValueByIndex(int index) + { + if (_valuesList.Count <= index) + { + return false; + } + return base.TrySetValue(_valuesList[index]); + } + + public IReadOnlyList Options => _valuesList.AsReadOnly(); + + public IReadOnlyList StringOptions => _valuesList.Select(e => e.ToString()).ToImmutableArray(); + +#if CLIENT + public override void AddDisplayComponent(GUILayoutGroup layoutGroup, Vector2 relativeSize, Action onSerializedValue) + { + GUIUtil.Dropdown(layoutGroup, (T val) => GetLocalizedString(val.ToString(), val.ToString()), null, Options, Value, (T val) => + { + onSerializedValue?.Invoke(val.ToString()); + }, new Vector2(relativeSize.X, 1f)); + + string GetLocalizedString(string identifier, string defaultValue) + { + var lstr = TextManager.Get($"{XmlConvert.EncodeLocalName(OwnerPackage.Name)}.{InternalName}.{identifier}.DisplayName"); + return lstr.IsNullOrWhiteSpace() ? defaultValue : lstr.Value; + } + } +#endif + +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/SettingRangeEntry.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/SettingRangeEntry.cs new file mode 100644 index 000000000..f2c1909b9 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/SettingRangeEntry.cs @@ -0,0 +1,104 @@ +using System; +using System.Globalization; +using System.Xml.Linq; +using Barotrauma.LuaCs.Data; +using Microsoft.Toolkit.Diagnostics; +using Microsoft.Xna.Framework; +using OneOf; + +namespace Barotrauma.LuaCs.Data; + +public abstract class SettingRangeBase : SettingEntry, ISettingRangeBase where T : IEquatable, IConvertible +{ + public SettingRangeBase(IConfigInfo configInfo, Func, bool> valueChangePredicate) : base(configInfo, valueChangePredicate) + { + } + + public T MinValue { get; protected init; } + public T MaxValue { get; protected init; } + public int IncrementalSteps { get; protected init; } +} + +public class SettingRangeFloat : SettingRangeBase +{ + public class RangeFactory : ISettingBase.IFactory + { + public SettingRangeFloat CreateInstance(IConfigInfo configInfo, Func, bool> valueChangePredicate) + { + Guard.IsNotNull(configInfo, nameof(configInfo)); + return new SettingRangeFloat(configInfo, valueChangePredicate); + } + } + + public SettingRangeFloat(IConfigInfo configInfo, Func, bool> valueChangePredicate) : base(configInfo, valueChangePredicate) + { + // funny values in case they forget to set them in the config. + MinValue = configInfo.Element.GetAttributeFloat("Min", float.MinValue); + MaxValue = configInfo.Element.GetAttributeFloat("Max", float.MaxValue); + IncrementalSteps = configInfo.Element.GetAttributeInt("Steps", 3); + } + + public override bool TrySetValue(float value) + { + if (value > MaxValue || value < MinValue) + { + return false; + } + return base.TrySetValue(value); + } + +#if CLIENT + public override void AddDisplayComponent(GUILayoutGroup layoutGroup, Vector2 relativeSize, Action onSerializedValue) + { + GUIUtil.Slider(layoutGroup, new Vector2(MinValue, MaxValue), IncrementalSteps, labelFunc: val => + { + return val.ToString("G4", CultureInfo.InvariantCulture); + }, Value, setter: val => + { + onSerializedValue?.Invoke(val.ToString()); + }, TextManager.Get(this.GetDisplayInfo().Tooltip), relativeSize); + } +#endif +} + +public class SettingRangeInt : SettingRangeBase +{ + public class RangeFactory : ISettingBase.IFactory + { + public SettingRangeInt CreateInstance(IConfigInfo configInfo, Func, bool> valueChangePredicate) + { + Guard.IsNotNull(configInfo, nameof(configInfo)); + return new SettingRangeInt(configInfo, valueChangePredicate); + } + } + + public SettingRangeInt(IConfigInfo configInfo, Func, bool> valueChangePredicate) : base(configInfo, valueChangePredicate) + { + // funny values in case they forget to set them in the config. + MinValue = configInfo.Element.GetAttributeInt("Min", int.MinValue); + MaxValue = configInfo.Element.GetAttributeInt("Max", int.MaxValue); + IncrementalSteps = configInfo.Element.GetAttributeInt("Steps", 3); + } + + public override bool TrySetValue(int value) + { + if (value > MaxValue || value < MinValue) + { + return false; + } + return base.TrySetValue(value); + } + +#if CLIENT + public override void AddDisplayComponent(GUILayoutGroup layoutGroup, Vector2 relativeSize, Action onSerializedValue) + { + GUIUtil.Slider(layoutGroup, new Vector2(MinValue, MaxValue), IncrementalSteps, labelFunc: val => + { + return ((int)val).ToString(); + }, Value, setter: val => + { + onSerializedValue?.Invoke(((int)val).ToString()); + }, TextManager.Get(this.GetDisplayInfo().Tooltip), relativeSize); + } +#endif +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/SettingsFactoryRegistrationProvider.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/SettingsFactoryRegistrationProvider.cs new file mode 100644 index 000000000..8e705c4d6 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/SettingsFactoryRegistrationProvider.cs @@ -0,0 +1,123 @@ +using System; +using System.Xml.Linq; +using Barotrauma.LuaCs.Data; +using Barotrauma.LuaCs; +using OneOf; + +namespace Barotrauma.LuaCs.Data; + +public interface ISettingsRegistrationProvider : IService +{ + void RegisterTypeProviders(IConfigService configService, Func, bool> valueChangePredicate); +} + +public class SettingsEntryRegistrar : ISettingsRegistrationProvider +{ + private ILuaCsInfoProvider _infoProvider; + + public SettingsEntryRegistrar(ILuaCsInfoProvider infoProvider) + { + _infoProvider = infoProvider; + } + + public void RegisterTypeProviders(IConfigService configService, Func, bool> valueChangePredicate) + { + RegisterSettingEntry(configService, "bool", valueChangePredicate); + RegisterSettingEntry(configService, "byte", valueChangePredicate); + RegisterSettingEntry(configService, "sbyte", valueChangePredicate); + RegisterSettingEntry(configService, "short", valueChangePredicate); + RegisterSettingEntry(configService, "ushort", valueChangePredicate); + RegisterSettingEntry(configService, "int", valueChangePredicate); + RegisterSettingEntry(configService, "uint", valueChangePredicate); + RegisterSettingEntry(configService, "long", valueChangePredicate); + RegisterSettingEntry(configService, "ulong", valueChangePredicate); + RegisterSettingEntry(configService, "string", valueChangePredicate); + RegisterSettingEntry(configService, "float", valueChangePredicate); + RegisterSettingEntry(configService, "single", valueChangePredicate); + RegisterSettingEntry(configService, "double", valueChangePredicate); + + // ISettingRangeBase + configService.RegisterSettingTypeInitializer("rangeInt", cfgInfo => + { + return new SettingRangeInt.RangeFactory().CreateInstance(cfgInfo.Info, (val) => + IsValueChangeAllowed(cfgInfo.Info, val, valueChangePredicate)); + }); + + configService.RegisterSettingTypeInitializer("rangeFloat", cfgInfo => + { + return new SettingRangeFloat.RangeFactory().CreateInstance(cfgInfo.Info, (val) => + IsValueChangeAllowed(cfgInfo.Info, val, valueChangePredicate)); + }); + +#if CLIENT + configService.RegisterSettingTypeInitializer("control" , cfgInfo => + { + return new SettingControl.Factory().CreateInstance(cfgInfo.Info, val => + IsValueChangeAllowed(cfgInfo.Info, val, valueChangePredicate)); + }); +#endif + + RegisterSettingList(configService, "listBool", valueChangePredicate); + RegisterSettingList(configService, "listByte", valueChangePredicate); + RegisterSettingList(configService, "listSbyte", valueChangePredicate); + RegisterSettingList(configService, "listShort", valueChangePredicate); + RegisterSettingList(configService, "listUshort", valueChangePredicate); + RegisterSettingList(configService, "listInt", valueChangePredicate); + RegisterSettingList(configService, "listUint", valueChangePredicate); + RegisterSettingList(configService, "listLong", valueChangePredicate); + RegisterSettingList(configService, "listUlong", valueChangePredicate); + RegisterSettingList(configService, "listString", valueChangePredicate); + RegisterSettingList(configService, "listFloat", valueChangePredicate); + RegisterSettingList(configService, "listSingle", valueChangePredicate); + RegisterSettingList(configService, "listDouble", valueChangePredicate); + } + + private void RegisterSettingList(IConfigService configService, string typeName, Func, bool> valueChangePredicate) where T : IEquatable, IConvertible + { + configService.RegisterSettingTypeInitializer(typeName, cfgInfo => + { + return new SettingList.LFactory().CreateInstance(cfgInfo.Info, (val) => + IsValueChangeAllowed(cfgInfo.Info, val, valueChangePredicate)); + }); + } + + private void RegisterSettingEntry(IConfigService configService, string typeName, Func, bool> valueChangePredicate) where T : IEquatable, IConvertible + { + configService.RegisterSettingTypeInitializer(typeName, cfgInfo => + { + return new SettingEntry.Factory().CreateInstance(cfgInfo.Info, (val) => + IsValueChangeAllowed(cfgInfo.Info, val, valueChangePredicate)); + }); + } + + private bool IsValueChangeAllowed(IConfigInfo info, OneOf newValue, + Func, bool> valueChangePredicate) + { +#if CLIENT + return !info.Element.GetAttributeBool("ReadOnly", false) + || info.EditableStates < _infoProvider.CurrentRunState + || valueChangePredicate is null + || valueChangePredicate.Invoke(newValue); +#else + // Server has absolute authority. + return !info.Element.GetAttributeBool("ReadOnly", false); +#endif + } + + public void Dispose() + { + if (!ModUtils.Threading.CheckIfClearAndSetBool(ref _isDisposed)) + { + return; + } + _infoProvider.Dispose(); + _infoProvider = null; + } + + private int _isDisposed; + public bool IsDisposed + { + get => ModUtils.Threading.GetBool(ref _isDisposed); + private set => ModUtils.Threading.SetBool(ref _isDisposed, value); + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Lua/LuaDocs.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/DocsInternals.cs similarity index 100% rename from Barotrauma/BarotraumaShared/SharedSource/LuaCs/Lua/LuaDocs.cs rename to Barotrauma/BarotraumaShared/SharedSource/LuaCs/DocsInternals.cs diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/IEvents.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/IEvents.cs new file mode 100644 index 000000000..ca52dab77 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/IEvents.cs @@ -0,0 +1,1186 @@ +using Barotrauma.Items.Components; +using Barotrauma.LuaCs.Data; +using Barotrauma.Networking; +using FarseerPhysics.Dynamics; +using Microsoft.Xna.Framework; +using MoonSharp.Interpreter; +using Steamworks.Ugc; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace Barotrauma.LuaCs.Events; + +/* + * The following is a collection of interfaces that types can implement to be registered events. + * Note: Internally-marked interfaces should be consumed using a publicizer. This is due to the Barotrauma source + * types being internal by default. +*/ + +public interface IEvent +{ + bool IsLuaRunner() => false; + + public abstract class LuaWrapperBase : IEvent + { + protected readonly IDictionary LuaFuncs; + protected LuaWrapperBase(IDictionary luaFuncs) => LuaFuncs = luaFuncs; + public bool IsLuaRunner() => true; + } +} + +public interface IEvent : IEvent where T : class, IEvent +{ + static virtual T GetLuaRunner(IDictionary luaFunc) + { + throw new InvalidOperationException($"Lua runners forbidden for {typeof(T).Name}"); + } +} + +#region RuntimeServiceEvents + +/// +/// Called when the current (game state) changes. Upstream Type 'Screen' is internal. +/// +internal interface IEventScreenSelected : IEvent +{ + void OnScreenSelected(Screen screen); +} + +/// +/// Called whenever the list of all (enabled and disabled) on disk has changed. +/// +internal interface IEventAllPackageListChanged : IEvent +{ + void OnAllPackageListChanged(IEnumerable corePackages, IEnumerable regularPackages); +} + +/// +/// Called whenever the list of enabled has changed. +/// +internal interface IEventEnabledPackageListChanged : IEvent +{ + void OnEnabledPackageListChanged(CorePackage package, IEnumerable regularPackages); +} + +internal interface IEventReloadAllPackages : IEvent +{ + void OnReloadAllPackages(); +} + +internal interface IEventSettingInstanceLifetime : IEvent +{ + void OnSettingInstanceCreated(T configInstance) where T : ISettingBase; + void OnSettingInstanceDisposed(T configInstance) where T : ISettingBase; +} + +#endregion + +#region GameEvents + +#if SERVER +/// +/// Allows the user to modify a chat message on the server before it is sent to clients, or reject the message altogether. +/// +/// Legacy Lua Event Name: "modifyChatMessage" +internal interface IEventModifyChatMessage : IEvent +{ + bool? OnModifyMessagePredicate(ChatMessage message, WifiComponent senderRadio); + + static IEventModifyChatMessage IEvent.GetLuaRunner(IDictionary luaFunc) => + new LuaWrapper(luaFunc); + + public sealed class LuaWrapper : LuaWrapperBase, IEventModifyChatMessage + { + public LuaWrapper(IDictionary luaFuncs) : base(luaFuncs) + { + } + + /// + /// Called before a chat message is sent to clients. + /// + /// Message to be sent. + /// [CanBeNull] The source , if any. + /// Whether to reject the message. + public bool? OnModifyMessagePredicate(ChatMessage message, WifiComponent senderRadio) + { + object result = LuaFuncs[nameof(OnModifyMessagePredicate)](message, senderRadio); + if (result is DynValue dynValue && dynValue.Type == DataType.Boolean) + { + return dynValue.Boolean; + } + + return null; + } + } +} + +#endif + +internal interface IEventAfflictionUpdate : IEvent +{ + void OnAfflictionUpdate(Affliction affliction, CharacterHealth characterHealth, Limb targetLimb, float deltaTime); + + static IEventAfflictionUpdate IEvent.GetLuaRunner(IDictionary luaFunc) => + new LuaWrapper(luaFunc); + + public sealed class LuaWrapper : LuaWrapperBase, IEventAfflictionUpdate + { + public LuaWrapper(IDictionary luaFuncs) : base(luaFuncs) + { + } + + public void OnAfflictionUpdate(Affliction affliction, CharacterHealth characterHealth, Limb targetLimb, float deltaTime) + { + LuaFuncs[nameof(OnAfflictionUpdate)](affliction, characterHealth, targetLimb, deltaTime); + } + } +} + +internal interface IEventGiveCharacterJobItems : IEvent +{ + void OnGiveCharacterJobItems(Character character, WayPoint spawnPoint, bool isPvPMode); + + static IEventGiveCharacterJobItems IEvent.GetLuaRunner( + IDictionary luaFunc) => new LuaWrapper(luaFunc); + + public sealed class LuaWrapper : LuaWrapperBase, IEventGiveCharacterJobItems + { + public LuaWrapper(IDictionary luaFuncs) : base(luaFuncs) + { + } + + public void OnGiveCharacterJobItems(Character character, WayPoint spawnPoint, bool isPvPMode) + { + LuaFuncs[nameof(OnGiveCharacterJobItems)](character, spawnPoint, isPvPMode); + } + } +} + +internal interface IEventCharacterCreated : IEvent +{ + void OnCharacterCreated(Character character); + + static IEventCharacterCreated IEvent.GetLuaRunner(IDictionary luaFunc) + => new LuaWrapper(luaFunc); + + public sealed class LuaWrapper : LuaWrapperBase, IEventCharacterCreated + { + public LuaWrapper(IDictionary luaFuncs) : base(luaFuncs) + { + } + + public void OnCharacterCreated(Character character) + { + LuaFuncs[nameof(OnCharacterCreated)](character); + } + } +} + +// TODO: harmony-fy +internal interface IEventHumanCPRSuccess : IEvent +{ + void OnCharacterCPRSuccess(HumanoidAnimController animController); + + static IEventHumanCPRSuccess IEvent.GetLuaRunner(IDictionary luaFunc) + => new LuaWrapper(luaFunc); + + public sealed class LuaWrapper : LuaWrapperBase, IEventHumanCPRSuccess + { + public LuaWrapper(IDictionary luaFuncs) : base(luaFuncs) + { + } + + public void OnCharacterCPRSuccess(HumanoidAnimController animController) + { + LuaFuncs[nameof(OnCharacterCPRSuccess)](animController); + } + } +} + +// TODO: harmony-fy +internal interface IEventHumanCPRFailed : IEvent +{ + void OnCharacterCPRFailed(HumanoidAnimController animController); + + static IEventHumanCPRFailed IEvent.GetLuaRunner(IDictionary luaFunc) + => new LuaWrapper(luaFunc); + + public sealed class LuaWrapper : LuaWrapperBase, IEventHumanCPRFailed + { + public LuaWrapper(IDictionary luaFuncs) : base(luaFuncs) + { + } + + public void OnCharacterCPRFailed(HumanoidAnimController animController) + { + LuaFuncs[nameof(OnCharacterCPRFailed)](animController); + } + } +} + +// TODO: harmony-fy +internal interface IEventClientControlHusk : IEvent +{ + void OnClientControlHusk(Client client, Character husk); + + static IEventClientControlHusk IEvent.GetLuaRunner(IDictionary luaFunc) + => new LuaWrapper(luaFunc); + + public sealed class LuaWrapper : LuaWrapperBase, IEventClientControlHusk + { + public LuaWrapper(IDictionary luaFuncs) : base(luaFuncs) + { + } + + public void OnClientControlHusk(Client client, Character husk) + { + LuaFuncs[nameof(OnClientControlHusk)](client, husk); + } + } +} + +// TODO: harmony-fy +internal interface IEventMeleeWeaponHandleImpact : IEvent +{ + void OnMeleeWeaponHandleImpact(MeleeWeapon meleeWeapon, Body target); + + static IEventMeleeWeaponHandleImpact IEvent.GetLuaRunner(IDictionary luaFunc) + => new LuaWrapper(luaFunc); + + public sealed class LuaWrapper : LuaWrapperBase, IEventMeleeWeaponHandleImpact + { + public LuaWrapper(IDictionary luaFuncs) : base(luaFuncs) + { + } + + public void OnMeleeWeaponHandleImpact(MeleeWeapon meleeWeapon, Body target) + { + LuaFuncs[nameof(OnMeleeWeaponHandleImpact)](meleeWeapon, target); + } + } +} + +// TODO: harmony-fy +internal interface IEventServerLog : IEvent +{ + void OnServerLog(string line, ServerLog.MessageType messageType); + + static IEventServerLog IEvent.GetLuaRunner(IDictionary luaFunc) + => new LuaWrapper(luaFunc); + + public sealed class LuaWrapper : LuaWrapperBase, IEventServerLog + { + public LuaWrapper(IDictionary luaFuncs) : base(luaFuncs) + { + } + + public void OnServerLog(string line, ServerLog.MessageType messageType) + { + LuaFuncs[nameof(OnServerLog)](line, messageType); + } + } +} + +// TODO: harmony-fy +internal interface IEventChatMessage : IEvent +{ + bool? OnChatMessage(string messageText, Client sender, ChatMessageType type, ChatMessage message); + + static IEventChatMessage IEvent.GetLuaRunner(IDictionary luaFunc) + => new LuaWrapper(luaFunc); + + public sealed class LuaWrapper : LuaWrapperBase, IEventChatMessage + { + public LuaWrapper(IDictionary luaFuncs) : base(luaFuncs) + { + } + + public bool? OnChatMessage(string messageText, Client sender, ChatMessageType type, ChatMessage message) + { + object result = LuaFuncs[nameof(OnChatMessage)](messageText, sender, type, message); + if (result is DynValue dynValue && dynValue.Type == DataType.Boolean) + { + return dynValue.Boolean; + } + + return null; + } + } +} + +// TODO: harmony-fy +internal interface IEventTryClientChangeName : IEvent +{ + bool? OnTryClienChangeName(Client client, string newName, Identifier newJob, CharacterTeamType newTeam); + + static IEventTryClientChangeName IEvent.GetLuaRunner(IDictionary luaFunc) + => new LuaWrapper(luaFunc); + + public sealed class LuaWrapper : LuaWrapperBase, IEventTryClientChangeName + { + public LuaWrapper(IDictionary luaFuncs) : base(luaFuncs) + { + } + + public bool? OnTryClienChangeName(Client client, string newName, Identifier newJob, CharacterTeamType newTeam) + { + var result = LuaFuncs[nameof(OnTryClienChangeName)](client, newName, newJob, newTeam); + if (result is DynValue dynValue && dynValue.Type == DataType.Boolean) + { + return dynValue.Boolean; + } + + return null; + } + } +} + +// TODO: harmony-fy +internal interface IEventChangeFallDamage : IEvent +{ + float? OnChangeFallDamage(float impactDamage, Character character, Vector2 impactPos, Vector2 velocity); + + static IEventChangeFallDamage IEvent.GetLuaRunner(IDictionary luaFunc) + => new LuaWrapper(luaFunc); + + public sealed class LuaWrapper : LuaWrapperBase, IEventChangeFallDamage + { + public LuaWrapper(IDictionary luaFuncs) : base(luaFuncs) + { + } + + public float? OnChangeFallDamage(float impactDamage, Character character, Vector2 impactPos, Vector2 velocity) + { + var result = LuaFuncs[nameof(OnChangeFallDamage)](impactDamage, character, impactPos, velocity); + if (result is DynValue dynValue && dynValue.Type == DataType.Number) + { + return (float)dynValue.Number; + } + + return null; + } + } +} + +// TODO: harmony-fy +internal interface IEventGapOxygenUpdate : IEvent +{ + bool? OnGapOxygenUpdate(Gap gap, Hull hull1, Hull hull2); + + static IEventGapOxygenUpdate IEvent.GetLuaRunner(IDictionary luaFunc) + => new LuaWrapper(luaFunc); + + public sealed class LuaWrapper : LuaWrapperBase, IEventGapOxygenUpdate + { + public LuaWrapper(IDictionary luaFuncs) : base(luaFuncs) + { + } + + public bool? OnGapOxygenUpdate(Gap gap, Hull hull1, Hull hull2) + { + var result = LuaFuncs[nameof(OnGapOxygenUpdate)](gap, hull1, hull2); + if (result is DynValue dynValue && dynValue.Type == DataType.Boolean) + { + return dynValue.Boolean; + } + + return null; + } + } +} + +// TODO: harmony-fy +internal interface IEventCharacterApplyDamage : IEvent +{ + bool? OnCharacterApplyDamage(CharacterHealth characterHealth, AttackResult attackResult, Limb hitLimb, bool allowStacking); + + static IEventCharacterApplyDamage IEvent.GetLuaRunner(IDictionary luaFunc) + => new LuaWrapper(luaFunc); + + public sealed class LuaWrapper : LuaWrapperBase, IEventCharacterApplyDamage + { + public LuaWrapper(IDictionary luaFuncs) : base(luaFuncs) + { + } + + public bool? OnCharacterApplyDamage(CharacterHealth characterHealth, AttackResult attackResult, Limb hitLimb, bool allowStacking) + { + var result = LuaFuncs[nameof(OnCharacterApplyDamage)](characterHealth, attackResult, hitLimb, allowStacking); + if (result is DynValue dynValue && dynValue.Type == DataType.Boolean) + { + return dynValue.Boolean; + } + + return null; + } + } +} + +// TODO: harmony-fy +internal interface IEventCharacterApplyAffliction : IEvent +{ + bool? OnCharacterApplyAffliction(CharacterHealth characterHealth, CharacterHealth.LimbHealth limbHealth, Affliction newAffliction, bool allowStacking); + + static IEventCharacterApplyAffliction IEvent.GetLuaRunner(IDictionary luaFunc) + => new LuaWrapper(luaFunc); + + public sealed class LuaWrapper : LuaWrapperBase, IEventCharacterApplyAffliction + { + public LuaWrapper(IDictionary luaFuncs) : base(luaFuncs) + { + } + + public bool? OnCharacterApplyAffliction(CharacterHealth characterHealth, CharacterHealth.LimbHealth limbHealth, Affliction newAffliction, bool allowStacking) + { + var result = LuaFuncs[nameof(OnCharacterApplyAffliction)](characterHealth, limbHealth, newAffliction, allowStacking); + if (result is DynValue dynValue && dynValue.Type == DataType.Boolean) + { + return dynValue.Boolean; + } + + return null; + } + } +} + +// TODO: harmony-fy +internal interface IEventItemReadPropertyChange : IEvent +{ + bool? OnItemReadPropertyChange(Item item, SerializableProperty property, object parentObject, bool allowEditing, Client sender); + + static IEventItemReadPropertyChange IEvent.GetLuaRunner(IDictionary luaFunc) + => new LuaWrapper(luaFunc); + + public sealed class LuaWrapper : LuaWrapperBase, IEventItemReadPropertyChange + { + public LuaWrapper(IDictionary luaFuncs) : base(luaFuncs) + { + } + + public bool? OnItemReadPropertyChange(Item item, SerializableProperty property, object parentObject, bool allowEditing, Client sender) + { + var result = LuaFuncs[nameof(OnItemReadPropertyChange)](item, property, parentObject, allowEditing, sender); + if (result is DynValue dynValue && dynValue.Type == DataType.Boolean) + { + return dynValue.Boolean; + } + + return null; + } + } +} + +// TODO: harmony-fy +internal interface IEventCanUseVoiceRadio : IEvent +{ + bool? OnCanUseVoiceRadio(Client sender, Client recipient); + + static IEventCanUseVoiceRadio IEvent.GetLuaRunner(IDictionary luaFunc) + => new LuaWrapper(luaFunc); + + public sealed class LuaWrapper : LuaWrapperBase, IEventCanUseVoiceRadio + { + public LuaWrapper(IDictionary luaFuncs) : base(luaFuncs) + { + } + + public bool? OnCanUseVoiceRadio(Client sender, Client recipient) + { + var result = LuaFuncs[nameof(OnCanUseVoiceRadio)](sender, recipient); + if (result is DynValue dynValue && dynValue.Type == DataType.Boolean) + { + return dynValue.Boolean; + } + + return null; + } + } +} + +// TODO: harmony-fy +internal interface IEventChangeLocalVoiceRange : IEvent +{ + float? OnChangeLocalVoiceRange(Client sender, Client recipient); + + static IEventChangeLocalVoiceRange IEvent.GetLuaRunner(IDictionary luaFunc) + => new LuaWrapper(luaFunc); + + public sealed class LuaWrapper : LuaWrapperBase, IEventChangeLocalVoiceRange + { + public LuaWrapper(IDictionary luaFuncs) : base(luaFuncs) + { + } + + public float? OnChangeLocalVoiceRange(Client sender, Client recipient) + { + var result = LuaFuncs[nameof(OnChangeLocalVoiceRange)](sender, recipient); + if (result is DynValue dynValue && dynValue.Type == DataType.Number) + { + return (float)dynValue.Number; + } + + return null; + } + } +} + +// TODO: harmony-fy +internal interface IEventItemDeconstructed : IEvent +{ + bool? OnItemDeconstructed(Item item, Deconstructor deconstructor, Character user, bool allowRemove); + + static IEventItemDeconstructed IEvent.GetLuaRunner(IDictionary luaFunc) + => new LuaWrapper(luaFunc); + + public sealed class LuaWrapper : LuaWrapperBase, IEventItemDeconstructed + { + public LuaWrapper(IDictionary luaFuncs) : base(luaFuncs) + { + } + + public bool? OnItemDeconstructed(Item item, Deconstructor deconstructor, Character user, bool allowRemove) + { + var result = LuaFuncs[nameof(OnItemDeconstructed)](item, deconstructor, user, allowRemove); + if (result is DynValue dynValue && dynValue.Type == DataType.Boolean) + { + return dynValue.Boolean; + } + + return null; + } + } +} + +// TODO: harmony-fy +internal interface IEventWifiSignalTransmitted : IEvent +{ + bool? OnWifiSignalTransmitted(WifiComponent wifiComponent, Signal signal, bool sentFromChat); + + static IEventWifiSignalTransmitted IEvent.GetLuaRunner(IDictionary luaFunc) + => new LuaWrapper(luaFunc); + + public sealed class LuaWrapper : LuaWrapperBase, IEventWifiSignalTransmitted + { + public LuaWrapper(IDictionary luaFuncs) : base(luaFuncs) + { + } + + public bool? OnWifiSignalTransmitted(WifiComponent wifiComponent, Signal signal, bool sentFromChat) + { + var result = LuaFuncs[nameof(OnWifiSignalTransmitted)](wifiComponent, signal, sentFromChat); + if (result is DynValue dynValue && dynValue.Type == DataType.Boolean) + { + return dynValue.Boolean; + } + + return null; + } + } +} + +internal interface IEventCharacterDeath : IEvent +{ + void OnCharacterDeath(Character character, Affliction causeOfDeathAffliction, CauseOfDeathType causeOfDeathType); + + static IEventCharacterDeath IEvent.GetLuaRunner(IDictionary luaFunc) + => new LuaWrapper(luaFunc); + + public sealed class LuaWrapper : LuaWrapperBase, IEventCharacterDeath + { + public LuaWrapper(IDictionary luaFuncs) : base(luaFuncs) + { + } + + public void OnCharacterDeath(Character character, Affliction causeOfDeathAffliction, CauseOfDeathType causeOfDeathType) + { + LuaFuncs[nameof(OnCharacterDeath)](character, causeOfDeathAffliction, causeOfDeathType); + } + } +} + +public interface IEventKeyUpdate : IEvent +{ + void OnKeyUpdate(double deltaTime); + + static IEventKeyUpdate IEvent.GetLuaRunner(IDictionary luaFunc) + => new LuaWrapper(luaFunc); + + public sealed class LuaWrapper : LuaWrapperBase, IEventKeyUpdate + { + public LuaWrapper(IDictionary luaFuncs) : base(luaFuncs) + { + } + + public void OnKeyUpdate(double deltaTime) + { + LuaFuncs[nameof(OnKeyUpdate)](deltaTime); + } + } +} + +/// +/// Called as soon as round begins to load before any loading takes place. +/// +public interface IEventRoundStarting : IEvent +{ + void OnRoundStarting(); + + static IEventRoundStarting IEvent.GetLuaRunner(IDictionary luaFunc) + => new LuaWrapper(luaFunc); + + public sealed class LuaWrapper : LuaWrapperBase, IEventRoundStarting + { + public LuaWrapper(IDictionary luaFuncs) : base(luaFuncs) + { + } + + public void OnRoundStarting() + { + LuaFuncs[nameof(OnRoundStarting)](); + } + } +} + +/// +/// Called when a round has started and fully loaded. +/// +public interface IEventRoundStarted : IEvent +{ + void OnRoundStart(); + + static IEventRoundStarted IEvent.GetLuaRunner(IDictionary luaFunc) + => new LuaWrapper(luaFunc); + + public sealed class LuaWrapper : LuaWrapperBase, IEventRoundStarted + { + public LuaWrapper(IDictionary luaFuncs) : base(luaFuncs) + { + } + + public void OnRoundStart() + { + LuaFuncs[nameof(OnRoundStart)](); + } + } +} + +/// +/// Called when a round has ended. +/// +public interface IEventRoundEnded : IEvent +{ + void OnRoundEnd(); + + static IEventRoundEnded IEvent.GetLuaRunner(IDictionary luaFunc) + => new LuaWrapper(luaFunc); + + public sealed class LuaWrapper : LuaWrapperBase, IEventRoundEnded + { + public LuaWrapper(IDictionary luaFuncs) : base(luaFuncs) + { + } + + public void OnRoundEnd() + { + LuaFuncs[nameof(OnRoundEnd)](); + } + } +} + +internal interface IEventMissionsEnded : IEvent +{ + void OnMissionsEnded(IReadOnlyList missions); + + static IEventMissionsEnded IEvent.GetLuaRunner(IDictionary luaFunc) + => new LuaWrapper(luaFunc); + + public sealed class LuaWrapper : LuaWrapperBase, IEventMissionsEnded + { + public LuaWrapper(IDictionary luaFuncs) : base(luaFuncs) + { + } + + public void OnMissionsEnded(IReadOnlyList missions) + { + LuaFuncs[nameof(OnMissionsEnded)](missions); + } + } +} + +/// +/// Called on game loop normal update. +/// +public interface IEventUpdate : IEvent +{ + void OnUpdate(double fixedDeltaTime); + static IEventUpdate IEvent.GetLuaRunner(IDictionary luaFunc) + => new LuaWrapper(luaFunc); + + public sealed class LuaWrapper : LuaWrapperBase, IEventUpdate + { + public LuaWrapper(IDictionary luaFuncs) : base(luaFuncs) + { + } + + public void OnUpdate(double deltaTime) + { + LuaFuncs[nameof(OnUpdate)](deltaTime); + } + } +} + +/// +/// Called on game loop draw update. +/// +public interface IEventDrawUpdate : IEvent +{ + void OnDrawUpdate(double deltaTime); + + static IEventDrawUpdate IEvent.GetLuaRunner(IDictionary luaFunc) + => new LuaWrapper(luaFunc); + + public sealed class LuaWrapper : LuaWrapperBase, IEventDrawUpdate + { + public LuaWrapper(IDictionary luaFuncs) : base(luaFuncs) + { + } + + public void OnDrawUpdate(double deltaTime) + { + LuaFuncs[nameof(OnDrawUpdate)](deltaTime); + } + } +} + +interface IEventSignalReceived : IEvent +{ + void OnSignalReceived(Signal signal, Connection connection); + + static IEventSignalReceived IEvent.GetLuaRunner(IDictionary luaFunc) + => new LuaWrapper(luaFunc); + + public sealed class LuaWrapper : LuaWrapperBase, IEventSignalReceived + { + public LuaWrapper(IDictionary luaFuncs) : base(luaFuncs) + { + } + + public void OnSignalReceived(Signal signal, Connection connection) + { + LuaFuncs[nameof(OnSignalReceived)](signal, connection); + } + } +} + +interface IEventItemCreated : IEvent +{ + void OnItemCreated(Item item); + + static IEventItemCreated IEvent.GetLuaRunner(IDictionary luaFunc) + => new LuaWrapper(luaFunc); + + public sealed class LuaWrapper : LuaWrapperBase, IEventItemCreated + { + public LuaWrapper(IDictionary luaFuncs) : base(luaFuncs) + { + } + + public void OnItemCreated(Item item) + { + LuaFuncs[nameof(OnItemCreated)](item); + } + } +} + +interface IEventItemRemoved : IEvent +{ + void OnItemRemoved(Item item); + + static IEventItemRemoved IEvent.GetLuaRunner(IDictionary luaFunc) + => new LuaWrapper(luaFunc); + + public sealed class LuaWrapper : LuaWrapperBase, IEventItemRemoved + { + public LuaWrapper(IDictionary luaFuncs) : base(luaFuncs) + { + } + + public void OnItemRemoved(Item item) + { + LuaFuncs[nameof(OnItemRemoved)](item); + } + } +} + +interface IEventItemUse : IEvent +{ + bool? OnItemUsed(Item item, Character user, Limb targetLimb, Entity useTarget); + + static IEventItemUse IEvent.GetLuaRunner(IDictionary luaFunc) + => new LuaWrapper(luaFunc); + + public sealed class LuaWrapper : LuaWrapperBase, IEventItemUse + { + public LuaWrapper(IDictionary luaFuncs) : base(luaFuncs) + { + } + + public bool? OnItemUsed(Item item, Character user, Limb targetLimb, Entity useTarget) + { + var result = LuaFuncs[nameof(OnItemUsed)](item, user, targetLimb, useTarget); + if (result is DynValue dynValue && dynValue.Type == DataType.Boolean) + { + return dynValue.Boolean; + } + + return null; + } + } +} + +interface IEventItemSecondaryUse : IEvent +{ + bool? OnItemSecondaryUsed(Item item, Character user); + + static IEventItemSecondaryUse IEvent.GetLuaRunner(IDictionary luaFunc) + => new LuaWrapper(luaFunc); + + public sealed class LuaWrapper : LuaWrapperBase, IEventItemSecondaryUse + { + public LuaWrapper(IDictionary luaFuncs) : base(luaFuncs) + { + } + + public bool? OnItemSecondaryUsed(Item item, Character user) + { + var result = LuaFuncs[nameof(OnItemSecondaryUsed)](item, user); + if (result is DynValue dynValue && dynValue.Type == DataType.Boolean) + { + return dynValue.Boolean; + } + + return null; + } + } +} + +interface IEventCharacterDamageLimb : IEvent +{ + AttackResult? OnCharacterDamageLimb(Character character, Vector2 worldPosition, Limb hitLimb, IEnumerable afflictions, float stun, bool playSound, Vector2 attackImpulse, Character attacker = null, float damageMultiplier = 1, bool allowStacking = true, float penetration = 0f, bool shouldImplode = false); + + static IEventCharacterDamageLimb IEvent.GetLuaRunner(IDictionary luaFunc) + => new LuaWrapper(luaFunc); + + public sealed class LuaWrapper : LuaWrapperBase, IEventCharacterDamageLimb + { + public LuaWrapper(IDictionary luaFuncs) : base(luaFuncs) + { + } + + public AttackResult? OnCharacterDamageLimb(Character character, Vector2 worldPosition, Limb hitLimb, IEnumerable afflictions, float stun, bool playSound, Vector2 attackImpulse, Character attacker = null, float damageMultiplier = 1, bool allowStacking = true, float penetration = 0f, bool shouldImplode = false) + { + object result = LuaFuncs[nameof(OnCharacterDamageLimb)](character, worldPosition, hitLimb, afflictions, stun, playSound, attackImpulse, attacker, damageMultiplier, allowStacking, penetration, shouldImplode); + if (result is DynValue dynValue) + { + result = dynValue.ToObject(); + } + + if (result is AttackResult attackResult) + { + return attackResult; + } + + return null; + } + } +} + +interface IEventInventoryPutItem : IEvent +{ + bool? OnInventoryPutItem(Inventory inventory, Item item, Character user, int i, bool removeItem); + + static IEventInventoryPutItem IEvent.GetLuaRunner(IDictionary luaFunc) + => new LuaWrapper(luaFunc); + + public sealed class LuaWrapper : LuaWrapperBase, IEventInventoryPutItem + { + public LuaWrapper(IDictionary luaFuncs) : base(luaFuncs) + { + } + + public bool? OnInventoryPutItem(Inventory inventory, Item item, Character user, int i, bool removeItem) + { + var result = LuaFuncs[nameof(OnInventoryPutItem)](inventory, item, user, i, removeItem); + if (result is DynValue dynValue && dynValue.Type == DataType.Boolean) + { + return dynValue.Boolean; + } + + return null; + } + } +} + +interface IEventInventoryItemSwap : IEvent +{ + bool? OnInventoryItemSwap(Inventory inventory, Item item, Character user, int i, bool swapWholeStack); + + static IEventInventoryItemSwap IEvent.GetLuaRunner(IDictionary luaFunc) + => new LuaWrapper(luaFunc); + + public sealed class LuaWrapper : LuaWrapperBase, IEventInventoryItemSwap + { + public LuaWrapper(IDictionary luaFuncs) : base(luaFuncs) + { + } + + public bool? OnInventoryItemSwap(Inventory inventory, Item item, Character user, int i, bool swapWholeStack) + { + var result = LuaFuncs[nameof(OnInventoryItemSwap)](inventory, item, user, i, swapWholeStack); + if (result is DynValue dynValue && dynValue.Type == DataType.Boolean) + { + return dynValue.Boolean; + } + + return null; + } + } +} + +#endregion + +#region Networking + + + +#region Networking-Server +#if SERVER +public interface IEventClientRawNetMessageReceived : IEvent +{ + bool? OnReceivedClientNetMessage(IReadMessage netMessage, ClientPacketHeader clientPacketHeader, NetworkConnection sender); + + static IEventClientRawNetMessageReceived IEvent.GetLuaRunner(IDictionary luaFunc) + => new LuaWrapper(luaFunc); + + public sealed class LuaWrapper : LuaWrapperBase, IEventClientRawNetMessageReceived + { + public LuaWrapper(IDictionary luaFuncs) : base(luaFuncs) + { + } + + public bool? OnReceivedClientNetMessage(IReadMessage netMessage, ClientPacketHeader clientPacketHeader, NetworkConnection sender) + { + if (GameMain.Server == null) { return null; } + + Client client = GameMain.Server.ConnectedClients.FirstOrDefault(c => c.Connection == sender); + + if (client == null) { return null; } + + var result = LuaFuncs[nameof(OnReceivedClientNetMessage)](netMessage, clientPacketHeader, client); + + if (result is DynValue dynValue && dynValue.Type == DataType.Boolean) + { + return dynValue.Boolean; + } + + return null; + } + } +} + +/// +/// Called when a client connects to the server. +/// +interface IEventClientConnected : IEvent +{ + /// + /// Called when a client connects to the server. + /// + /// The connecting client. + void OnClientConnected(Client client); + + static IEventClientConnected IEvent.GetLuaRunner(IDictionary luaFunc) + => new LuaWrapper(luaFunc); + + public sealed class LuaWrapper : LuaWrapperBase, IEventClientConnected + { + public LuaWrapper(IDictionary luaFuncs) : base(luaFuncs) + { + } + + public void OnClientConnected(Client client) + { + LuaFuncs[nameof(OnClientConnected)](client); + } + } +} + +/// +/// Called when a client disconnects from the server. +/// +interface IEventClientDisconnected : IEvent +{ + /// + /// Called when a client connects to the server. + /// + /// The connecting client. + void OnClientDisconnected(Client client); + + static IEventClientDisconnected IEvent.GetLuaRunner(IDictionary luaFunc) + => new LuaWrapper(luaFunc); + + public sealed class LuaWrapper : LuaWrapperBase, IEventClientDisconnected + { + public LuaWrapper(IDictionary luaFuncs) : base(luaFuncs) + { + } + + public void OnClientDisconnected(Client client) + { + LuaFuncs[nameof(OnClientDisconnected)](client); + } + } +} + +interface IEventJobsAssigned : IEvent +{ + /// + /// Called when a client connects to the server. + /// + /// The connecting client. + void OnJobsAssigned(IReadOnlyList unassignedClients); + + static IEventJobsAssigned IEvent.GetLuaRunner(IDictionary luaFunc) + => new LuaWrapper(luaFunc); + + public sealed class LuaWrapper : LuaWrapperBase, IEventJobsAssigned + { + public LuaWrapper(IDictionary luaFuncs) : base(luaFuncs) + { + } + + public void OnJobsAssigned(IReadOnlyList unassignedClients) + { + LuaFuncs[nameof(OnJobsAssigned)](unassignedClients); + } + } +} +#endif + +#endregion + +#region Networking-Client +#if CLIENT + +public interface IEventServerRawNetMessageReceived : IEvent +{ + bool? OnReceivedServerNetMessage(IReadMessage netMessage, ServerPacketHeader serverPacketHeader); + + static IEventServerRawNetMessageReceived IEvent.GetLuaRunner(IDictionary luaFunc) + => new LuaWrapper(luaFunc); + + public sealed class LuaWrapper : LuaWrapperBase, IEventServerRawNetMessageReceived + { + public LuaWrapper(IDictionary luaFuncs) : base(luaFuncs) + { + } + + public bool? OnReceivedServerNetMessage(IReadMessage netMessage, ServerPacketHeader serverPacketHeader) + { + var result = LuaFuncs[nameof(OnReceivedServerNetMessage)](netMessage, serverPacketHeader); + if (result is DynValue dynValue && dynValue.Type == DataType.Boolean) + { + return dynValue.Boolean; + } + + return null; + } + } +} + +/// +/// Called when the client has connected to the server and loaded to the lobby. +/// +public interface IEventServerConnected : IEvent +{ + void OnServerConnected(); + + static IEventServerConnected IEvent.GetLuaRunner(IDictionary luaFunc) + => new LuaWrapper(luaFunc); + + public sealed class LuaWrapper : LuaWrapperBase, IEventServerConnected + { + public LuaWrapper(IDictionary luaFuncs) : base(luaFuncs) + { + } + + public void OnServerConnected() + { + LuaFuncs[nameof(OnServerConnected)](); + } + } +} +#endif +#endregion + +#endregion + +#region Assembly_PluginEvents + +/// +/// Called on plugin normal, use this for basic/core loading that does not rely on any other modded content. +/// +public interface IEventPluginInitialize : IEvent +{ + void Initialize(); +} + +/// +/// Called once all plugins have been loaded. if you have integrations with any other mod, put that code here. +/// +public interface IEventPluginLoadCompleted : IEvent +{ + void OnLoadCompleted(); +} + +/// +/// Called before Barotrauma initializes plugins. Use if you want to patch another plugin's behaviour 'unofficially'. +/// WARNING: This method is called before Initialize()! +/// +public interface IEventPluginPreInitialize : IEvent +{ + void PreInitPatching(); +} + +/// +/// Called whenever a new assembly is loaded. +/// +public interface IEventAssemblyLoaded : IEvent +{ + void OnAssemblyLoaded(Assembly assembly); +} + +/// +/// Called whenever an is instanced. +/// +public interface IEventAssemblyContextCreated : IEvent +{ + void OnAssemblyCreated(IAssemblyLoaderService loaderService); +} + +/// +/// Called whenever an begins unloading. +/// +public interface IEventAssemblyContextUnloading : IEvent +{ + void OnAssemblyUnloading(WeakReference loaderService); +} + +public interface IEventAssemblyUnloading : IEvent +{ + void OnAssemblyUnloading(Assembly assembly); +} + +#endregion diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Lua/LuaBarotraumaAdditions.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Lua/LuaBarotraumaAdditions.cs deleted file mode 100644 index 357c02bb2..000000000 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Lua/LuaBarotraumaAdditions.cs +++ /dev/null @@ -1,110 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; -using MoonSharp.Interpreter; -using Microsoft.Xna.Framework; -using Barotrauma.Networking; - -namespace Barotrauma.Networking -{ - partial class Client - { - public static IReadOnlyList ClientList - { - get - { - if (GameMain.IsSingleplayer) { return new List(); } - -#if SERVER - return GameMain.Server.ConnectedClients; -#else - return GameMain.Client.ConnectedClients; -#endif - } - } - - public ulong SteamID - { - get - { - if (AccountId.TryUnwrap(out AccountId outValue) && outValue is SteamId steamId) - { - return steamId.Value; - } - else - { - return 0; - } - } - } - - } - -} - -namespace Barotrauma -{ - using Barotrauma.Networking; - using System.Linq; - using System.Reflection; - - - - partial class Character - { - - } - - partial class Item - { - public object GetComponentString(string component) - { - Type type = LuaUserData.GetType("Barotrauma.Items.Components." + component); - - if (type == null) - { - return null; - } - - MethodInfo method = typeof(Item).GetMethod(nameof(Item.GetComponent)); - MethodInfo generic = method.MakeGenericMethod(type); - return generic.Invoke(this, null); - } - - } - - partial class ItemPrefab - { - - public static ItemPrefab GetItemPrefab(string itemNameOrId) - { - ItemPrefab itemPrefab = - (MapEntityPrefab.Find(itemNameOrId, identifier: null, showErrorMessages: false) ?? - MapEntityPrefab.Find(null, identifier: itemNameOrId, showErrorMessages: false)) as ItemPrefab; - - return itemPrefab; - } - } - - abstract partial class MapEntity - { - public void AddLinked(MapEntity entity) - { - linkedTo.Add(entity); - } - } - -} - -namespace Barotrauma.Items.Components -{ - using Barotrauma.Networking; - - partial class CustomInterface - { - } - - partial struct Signal - { - } -} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Lua/LuaClasses/LuaUserData.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Lua/LuaClasses/LuaUserData.cs deleted file mode 100644 index a1e53d8eb..000000000 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Lua/LuaClasses/LuaUserData.cs +++ /dev/null @@ -1,363 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using MoonSharp.Interpreter; -using MoonSharp.Interpreter.Interop; - -namespace Barotrauma -{ - partial class LuaUserData - { - public static Type GetType(string typeName) => LuaCsSetup.GetType(typeName); - - public static IUserDataDescriptor RegisterType(string typeName) - { - Type type = GetType(typeName); - - if (type == null) - { - throw new ScriptRuntimeException($"tried to register a type that doesn't exist: {typeName}."); - } - - return UserData.RegisterType(type); - } - - public static void RegisterExtensionType(string typeName) - { - Type type = GetType(typeName); - - if (type == null) - { - throw new ScriptRuntimeException($"tried to register a type that doesn't exist: {typeName}."); - } - - UserData.RegisterExtensionType(type); - } - - public static bool IsRegistered(string typeName) - { - Type type = GetType(typeName); - - if (type == null) - { - return false; - } - - return UserData.GetDescriptorForType(type, true) != null; - } - - public static void UnregisterType(string typeName, bool deleteHistory = false) - { - Type type = GetType(typeName); - - if (type == null) - { - throw new ScriptRuntimeException($"tried to unregister a type that doesn't exist: {typeName}."); - } - - UserData.UnregisterType(type, deleteHistory); - } - public static IUserDataDescriptor RegisterGenericType(string typeName, params string[] typeNameArguements) - { - Type type = GetType(typeName); - Type[] typeArguements = typeNameArguements.Select(x => GetType(x)).ToArray(); - Type genericType = type.MakeGenericType(typeArguements); - return UserData.RegisterType(genericType); - } - - public static void UnregisterGenericType(string typeName, params string[] typeNameArguements) - { - Type type = GetType(typeName); - Type[] typeArguements = typeNameArguements.Select(x => GetType(x)).ToArray(); - Type genericType = type.MakeGenericType(typeArguements); - UserData.UnregisterType(genericType); - } - - public static bool IsTargetType(object obj, string typeName) - { - if (obj == null) { throw new ScriptRuntimeException("userdata is nil"); } - Type targetType = GetType(typeName); - if (targetType == null) { throw new ScriptRuntimeException("target type not found"); } - - Type type = obj is Type ? (Type)obj : obj.GetType(); - return targetType.IsAssignableFrom(type); - } - - public static string TypeOf(object obj) - { - if (obj == null) { throw new ScriptRuntimeException("userdata is nil"); } - - return obj.GetType().FullName; - } - - public static object CreateStatic(string typeName) - { - Type type = GetType(typeName); - - if (type == null) - { - throw new ScriptRuntimeException($"tried to create a static userdata of a type that doesn't exist: {typeName}."); - } - - MethodInfo method = typeof(UserData).GetMethod(nameof(UserData.CreateStatic), 1, new Type[0]); - MethodInfo generic = method.MakeGenericMethod(type); - return generic.Invoke(null, null); - } - - public static object CreateEnumTable(string typeName) - { - Type type = GetType(typeName); - - if (type == null) - { - throw new ScriptRuntimeException($"tried to create an enum table with a type that doesn't exist:: {typeName}."); - } - - Dictionary result = new Dictionary(); - - foreach (var value in Enum.GetValues(type)) - { - string name = Enum.GetName(type, value); - - result[name] = value; - } - - return result; - } - - private static FieldInfo FindFieldRecursively(Type type, string fieldName) - { - var field = type.GetField(fieldName, BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static); - - if (field == null && type.BaseType != null) - { - return FindFieldRecursively(type.BaseType, fieldName); - } - - return field; - } - - public static void MakeFieldAccessible(IUserDataDescriptor IUUD, string fieldName) - { - if (IUUD == null) - { - throw new ScriptRuntimeException($"tried to use a UserDataDescriptor that is null to make {fieldName} accessible."); - } - - var descriptor = (StandardUserDataDescriptor)IUUD; - FieldInfo field = FindFieldRecursively(IUUD.Type, fieldName); - - if (field == null) - { - throw new ScriptRuntimeException($"tried to make field '{fieldName}' accessible, but the field doesn't exist."); - } - - descriptor.RemoveMember(fieldName); - descriptor.AddMember(fieldName, new FieldMemberDescriptor(field, InteropAccessMode.Default)); - } - - private static MethodInfo FindMethodRecursively(Type type, string methodName, Type[] types = null) - { - MethodInfo method; - - if (types == null) - { - method = type.GetMethod(methodName, BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static); - } - else - { - method = type.GetMethod(methodName, BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static, types); - } - - if (method == null && type.BaseType != null) - { - return FindMethodRecursively(type.BaseType, methodName, types); - } - - return method; - } - - public static void MakeMethodAccessible(IUserDataDescriptor IUUD, string methodName, string[] parameters = null) - { - if (IUUD == null) - { - throw new ScriptRuntimeException($"tried to use a UserDataDescriptor that is null to make {methodName} accessible."); - } - - Type[] parameterTypes = null; - - - if (parameters != null) - { - parameterTypes = new Type[parameters.Length]; - - for (int i = 0; i < parameters.Length; i++) - { - Type type = LuaUserData.GetType(parameters[i]); - if (type == null) - { - throw new ScriptRuntimeException($"invalid parameter type '{parameters[i]}'"); - } - parameterTypes[i] = type; - } - } - - var descriptor = (StandardUserDataDescriptor)IUUD; - - MethodBase method; - - try - { - method = FindMethodRecursively(IUUD.Type, methodName, parameterTypes); - } - catch (AmbiguousMatchException ex) - { - throw new ScriptRuntimeException("ambiguous method signature."); - } - - if (method == null) - { - throw new ScriptRuntimeException($"tried to make method '{methodName}' accessible, but the method doesn't exist."); - } - - descriptor.AddMember(methodName, new MethodMemberDescriptor(method, InteropAccessMode.Default)); - } - - private static PropertyInfo FindPropertyRecursively(Type type, string propertyName) - { - var property = type.GetProperty(propertyName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static); - - if (property == null && type.BaseType != null) - { - return FindPropertyRecursively(type.BaseType, propertyName); - } - - return property; - } - - public static void MakePropertyAccessible(IUserDataDescriptor IUUD, string propertyName) - { - if (IUUD == null) - { - throw new ScriptRuntimeException($"tried to use a UserDataDescriptor that is null to make {propertyName} accessible."); - } - - var descriptor = (StandardUserDataDescriptor)IUUD; - PropertyInfo property = FindPropertyRecursively(IUUD.Type, propertyName); - - if (property == null) - { - throw new ScriptRuntimeException($"tried to make property '{propertyName}' accessible, but the property doesn't exist."); - } - - descriptor.RemoveMember(propertyName); - descriptor.AddMember(propertyName, new PropertyMemberDescriptor(property, InteropAccessMode.Default, property.GetGetMethod(true), property.GetSetMethod(true))); - } - - public static void AddMethod(IUserDataDescriptor IUUD, string methodName, object function) - { - if (IUUD == null) - { - throw new ScriptRuntimeException($"tried to use a UserDataDescriptor that is null to add method {methodName}."); - } - - var descriptor = (StandardUserDataDescriptor)IUUD; - - descriptor.RemoveMember(methodName); - descriptor.AddMember(methodName, new ObjectCallbackMemberDescriptor(methodName, (object arg1, ScriptExecutionContext arg2, CallbackArguments arg3) => - { - if (GameMain.LuaCs != null) - return GameMain.LuaCs.CallLuaFunction(function, arg3.GetArray()); - return null; - })); - } - - public static void AddField(IUserDataDescriptor IUUD, string fieldName, DynValue value) - { - if (IUUD == null) - { - throw new ScriptRuntimeException($"tried to use a UserDataDescriptor that is null to add field {fieldName}."); - } - - var descriptor = (StandardUserDataDescriptor)IUUD; - descriptor.RemoveMember(fieldName); - descriptor.AddMember(fieldName, new DynValueMemberDescriptor(fieldName, value)); - } - - public static void RemoveMember(IUserDataDescriptor IUUD, string memberName) - { - if (IUUD == null) - { - throw new ScriptRuntimeException($"tried to use a UserDataDescriptor that is null to remove the member {memberName}."); - } - - var descriptor = (StandardUserDataDescriptor)IUUD; - descriptor.RemoveMember(memberName); - } - - public static bool HasMember(object obj, string memberName) - { - if (obj == null) { throw new ScriptRuntimeException("object is nil"); } - - Type type; - if (obj is Type) - { - type = (Type)obj; - } - else if(obj is IUserDataDescriptor descriptor) - { - type = descriptor.Type; - - if (((StandardUserDataDescriptor)descriptor).HasMember(memberName)) - { - return true; - } - } - else - { - type = obj.GetType(); - } - - if (type.GetMember(memberName).Length == 0) - { - return false; - } - - return true; - } - - - /// - /// See . - /// - /// Lua value to convert and wrap in a userdata. - /// Descriptor of the type of the object to convert the Lua value to. Uses MoonSharp ScriptToClr converters. - /// A userdata that wraps the Lua value converted to an object of the desired type as described by . - public static DynValue CreateUserDataFromDescriptor(DynValue scriptObject, IUserDataDescriptor desiredTypeDescriptor) - { - return UserData.Create(scriptObject.ToObject(desiredTypeDescriptor.Type), desiredTypeDescriptor); - } - - /// - /// Converts a Lua value to a CLR object of a desired type and wraps it in a userdata. - /// If the type is not registered, then a new will be created and used. - /// The goal of this method is to allow Lua scripts to create userdata to wrap certain data without having to register types. - /// Wrapping the value in a userdata preserves the original type during script-to-CLR conversions. - /// A Lua script needs to pass a List`1 to a CLR method expecting System.Object, MoonSharp gets - /// in the way by converting the List`1 to a MoonSharp.Interpreter.Table and breaking everything. - /// Registering the List`1 type can break other scripts relying on default converters, so instead - /// it is better to manually wrap the List`1 object into a userdata. - /// - /// - /// Lua value to convert and wrap in a userdata. - /// Type describing the CLR type of the object to convert the Lua value to. - /// A userdata that wraps the Lua value converted to an object of the desired type. - public static DynValue CreateUserDataFromType(DynValue scriptObject, Type desiredType) - { - IUserDataDescriptor descriptor = UserData.GetDescriptorForType(desiredType, true); - descriptor ??= new StandardUserDataDescriptor(desiredType, InteropAccessMode.Default); - return CreateUserDataFromDescriptor(scriptObject, descriptor); - } - } -} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Lua/LuaScriptLoader.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Lua/LuaScriptLoader.cs deleted file mode 100644 index 212c70ae5..000000000 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Lua/LuaScriptLoader.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; -using System.IO; -using MoonSharp.Interpreter; -using MoonSharp.Interpreter.Loaders; -using System.Linq; - -namespace Barotrauma -{ - class LuaScriptLoader : ScriptLoaderBase - { - - public override object LoadFile(string file, Table globalContext) - { - if (!LuaCsFile.IsPathAllowedLuaException(file, false)) return null; - - return File.ReadAllText(file); - } - - public override bool ScriptFileExists(string file) - { - if (!LuaCsFile.IsPathAllowedLuaException(file, false)) return false; - - return File.Exists(file); - } - } -} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/LuaCsInstaller.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/LuaCsInstaller.cs index ee5c43e83..002b059b3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/LuaCsInstaller.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/LuaCsInstaller.cs @@ -9,10 +9,19 @@ namespace Barotrauma { private static string[] trackingFiles = new string[] { - "Barotrauma.dll", "Barotrauma.deps.json", "Barotrauma.pdb", "BarotraumaCore.dll", "BarotraumaCore.pdb", - "0Harmony.dll", "Mono.Cecil.dll", + /* Barotrauma */ + "Barotrauma.dll", + "Barotrauma.deps.json", + "Barotrauma.pdb", + "BarotraumaCore.dll", + "BarotraumaCore.pdb", + + /* HarmonyX Package */ + "0Harmony.dll", + "Mono.Cecil.dll", "Sigil.dll", - "Mono.Cecil.Mdb.dll", "Mono.Cecil.Pdb.dll", + "Mono.Cecil.Mdb.dll", + "Mono.Cecil.Pdb.dll", "Mono.Cecil.Rocks.dll", "MonoMod.Backports.dll", "MonoMod.Core.dll", @@ -20,15 +29,32 @@ namespace Barotrauma "MonoMod.RuntimeDetour.dll", "MonoMod.Utils.dll", "MonoMod.Iced.dll", - "MoonSharp.Interpreter.dll", "MoonSharp.VsCodeDebugger.dll", + + /* MoonSharp */ + "MoonSharp.Interpreter.dll", + "MoonSharp.VsCodeDebugger.dll", - "Microsoft.CodeAnalysis.dll", "Microsoft.CodeAnalysis.CSharp.dll", - "Microsoft.CodeAnalysis.CSharp.Scripting.dll", "Microsoft.CodeAnalysis.Scripting.dll", - - "System.Reflection.Metadata.dll", "System.Collections.Immutable.dll", + /* Microsoft SDKs */ + "Microsoft.CodeAnalysis.dll", + "Microsoft.CodeAnalysis.CSharp.dll", + "Microsoft.CodeAnalysis.CSharp.Scripting.dll", + "Microsoft.CodeAnalysis.Scripting.dll", + "Microsoft.Toolkit.Diagnostics.dll", + "Microsoft.Extensions.Logging.Abstractions.dll", + "System.Reflection.Metadata.dll", + "System.Collections.Immutable.dll", "System.Runtime.CompilerServices.Unsafe.dll", - "Publicized/DedicatedServer.dll", "Publicized/Barotrauma.dll" + /* Assembly Script Dependencies */ + "Publicized/DedicatedServer.dll", + "Publicized/Barotrauma.dll", + "Publicized/BarotraumaCore.dll", + + /* Other NuGet Packages */ + "Basic.Reference.Assemblies.Net80.dll", + "FluentResults.dll", + "LightInject.dll", + "OneOf.dll" }; private static void CreateMissingDirectory() diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/LuaCsLogger.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/LuaCsLogger.cs deleted file mode 100644 index a8816c595..000000000 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/LuaCsLogger.cs +++ /dev/null @@ -1,181 +0,0 @@ -using System; -using Barotrauma.Networking; -using Microsoft.Xna.Framework; -using MoonSharp.Interpreter; - -namespace Barotrauma -{ - internal enum LuaCsMessageOrigin - { - LuaCs, - Unknown, - LuaMod, - CSharpMod, - } - - partial class LuaCsLogger - { - public static bool HideUserNames = true; - -#if SERVER - private const string LogPrefix = "SV"; - private const int NetMaxLength = 1024; - private const int NetMaxMessages = 60; - - // This is used so its possible to call logging functions inside the serverLog - // hook without creating an infinite loop - private static bool lockLog = false; -#else - private const string LogPrefix = "CL"; -#endif - - public static LuaCsMessageLogger MessageLogger; - public static LuaCsExceptionHandler ExceptionHandler; - - public static void HandleException(Exception ex, LuaCsMessageOrigin origin) - { - string errorString = ""; - switch (ex) - { - case NetRuntimeException netRuntimeException: - if (netRuntimeException.DecoratedMessage == null) - { - errorString = netRuntimeException.ToString(); - } - else - { - // FIXME: netRuntimeException.ToString() doesn't print the InnerException's stack trace... - errorString = $"{netRuntimeException.DecoratedMessage}: {netRuntimeException}"; - } - break; - case InterpreterException interpreterException: - if (interpreterException.DecoratedMessage == null) - { - errorString = interpreterException.ToString(); - } - else - { - errorString = interpreterException.DecoratedMessage; - } - break; - default: - errorString = ex.StackTrace != null - ? ex.ToString() - : $"{ex}\n{Environment.StackTrace}"; - break; - } - - LogError(Environment.UserName + " " + errorString, origin); - } - - public static void LogError(string message, LuaCsMessageOrigin origin) - { - if (HideUserNames && !Environment.UserName.IsNullOrEmpty()) - { - message = message.Replace(Environment.UserName, "USERNAME"); - } - - switch (origin) - { - case LuaCsMessageOrigin.LuaCs: - case LuaCsMessageOrigin.Unknown: - LogError($"[{LogPrefix} ERROR] {message}"); - break; - case LuaCsMessageOrigin.LuaMod: - LogError($"[{LogPrefix} LUA ERROR] {message}"); - break; - case LuaCsMessageOrigin.CSharpMod: - LogError($"[{LogPrefix} CS ERROR] {message}"); - break; - } - } - - public static void LogError(string message) - { - Log($"{message}", Color.Red, ServerLog.MessageType.Error); - } - - public static void LogMessage(string message, Color? serverColor = null, Color? clientColor = null) - { - if (serverColor == null) { serverColor = Color.MediumPurple; } - if (clientColor == null) { clientColor = Color.Purple; } - -#if SERVER - Log(message, serverColor); -#else - Log(message, clientColor); -#endif - } - - public static void Log(string message, Color? color = null, ServerLog.MessageType messageType = ServerLog.MessageType.ServerMessage) - { - MessageLogger?.Invoke(message); - - DebugConsole.NewMessage(message, color); - -#if SERVER - void broadcastMessage(string m) - { - foreach (var client in GameMain.Server.ConnectedClients) - { - //if (client.ChatMsgQueue.Count > NetMaxMessages) - //{ - // If there's an error or message happening many times per second (inside Update loop for example) - // we will need to discart some messages so the client doesn't get overloaded by all - // those net messages. - // continue; - //} - - ChatMessage consoleMessage = ChatMessage.Create("", m, ChatMessageType.Console, null, textColor: color); - GameMain.Server.SendDirectChatMessage(consoleMessage, client); - - if (!GameMain.Server.ServerSettings.SaveServerLogs || !client.HasPermission(ClientPermissions.ServerLog)) - { - continue; - } - - ChatMessage logMessage = ChatMessage.Create(messageType.ToString(), "[LuaCs] " + m, ChatMessageType.ServerLog, null); - GameMain.Server.SendDirectChatMessage(logMessage, client); - } - } - - if (GameMain.Server != null) - { - if (GameMain.Server.ServerSettings.SaveServerLogs) - { - string logMessage = "[LuaCs] " + message; - GameMain.Server.ServerSettings.ServerLog.WriteLine(logMessage, messageType, false); - - if (!lockLog) - { - lockLog = true; - GameMain.LuaCs?.Hook?.Call("serverLog", logMessage, messageType); - lockLog = false; - } - } - - for (int i = 0; i < message.Length; i += NetMaxLength) - { - string subStr = message.Substring(i, Math.Min(1024, message.Length - i)); - - broadcastMessage(subStr); - } - } -#endif - } - } - - partial class LuaCsSetup - { - // Compatibility with cs mods that use this method. - public static void PrintLuaError(object message) => LuaCsLogger.LogError($"{message}", LuaCsMessageOrigin.LuaMod); - public static void PrintCsError(object message) => LuaCsLogger.LogError($"{message}", LuaCsMessageOrigin.CSharpMod); - public static void PrintGenericError(object message) => LuaCsLogger.LogError($"{message}", LuaCsMessageOrigin.LuaCs); - - internal void PrintMessage(object message) => LuaCsLogger.LogMessage($"{message}"); - - public static void PrintCsMessage(object message) => LuaCsLogger.LogMessage($"{message}"); - - internal void HandleException(Exception ex, LuaCsMessageOrigin origin) => LuaCsLogger.HandleException(ex, origin); - } -} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/LuaCsModStore.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/LuaCsModStore.cs deleted file mode 100644 index cdd77e17d..000000000 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/LuaCsModStore.cs +++ /dev/null @@ -1,114 +0,0 @@ -using MoonSharp.Interpreter; -using MoonSharp.Interpreter.Interop; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; - -namespace Barotrauma -{ - partial class LuaCsSetup - { - public class LuaCsModStore - { - public abstract class ModStore - { - protected Dictionary store; - - public TStore Set(string name, TStore value) => store[name] = value; - public TStore Get(string name) => store[name]; - - public ModStore(Dictionary store) => this.store = store; - - public abstract bool Equals(T value); - } - public class LuaModStore : ModStore - { - public string Name; - - public LuaModStore(Dictionary store) : base(store) { } - public override bool Equals(string value) => Name == value; - } - public class CsModStore : ModStore - { - public ACsMod Mod; - - public CsModStore(Dictionary store) : base(store) { } - public override bool Equals(ACsMod value) => Mod == value; - } - - private HashSet luaModInterface; - private HashSet csModInterface; - - public LuaCsModStore() - { - luaModInterface = new HashSet(); - csModInterface = new HashSet(); - } - - public void Initialize() - { - UserData.RegisterType(); - UserData.RegisterType(); - var msType = UserData.RegisterType(); - var msDesc = (StandardUserDataDescriptor)msType; - - typeof(StandardUserDataDescriptor).GetMethods(BindingFlags.NonPublic | BindingFlags.Instance).ToList().ForEach(m => - { - if ( - m.Name.Contains("Register") - ) - { - msDesc.AddMember(m.Name, new MethodMemberDescriptor(m, InteropAccessMode.Default)); - } - }); - } - public void Clear() - { - luaModInterface.Clear(); - csModInterface.Clear(); - } - - protected LuaModStore Register(string modName) - { - if (luaModInterface.Any(i => i.Equals(modName))) - { - LuaCsLogger.HandleException(new ArgumentException($"'{modName}' entry already registered"), LuaCsMessageOrigin.LuaMod); - return null; - } - - var newHandle = new LuaModStore(new Dictionary()); - if (luaModInterface.Add(newHandle)) return newHandle; - else return null; - } - [MoonSharpHidden] - public CsModStore Register(ACsMod mod) - { - if (csModInterface.Any(i => i.Equals(mod))) - { - LuaCsLogger.HandleException(new ArgumentException($"'{mod.GetType().FullName}' entry already registered"), LuaCsMessageOrigin.CSharpMod); - return null; - } - - var newHandle = new CsModStore(new Dictionary()); - if (csModInterface.Add(newHandle)) return newHandle; - else return null; - } - - public CsModStore GetCsStore(string modName) { - var result = csModInterface.Where(i => i.Mod.GetType().FullName == modName).FirstOrDefault(); - if (result != null) - { - if (!result.Mod.IsDisposed) return result; - else - { - csModInterface.Remove(result); - return null; - } - } - else return null; - } - protected LuaModStore GetLuaStore(string modName) => luaModInterface.Where(i => i.Name == modName).FirstOrDefault(); - } - } -} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/LuaCsNetworking.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/LuaCsNetworking.cs deleted file mode 100644 index 845f90559..000000000 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/LuaCsNetworking.cs +++ /dev/null @@ -1,173 +0,0 @@ -using System; -using System.IO; -using System.Collections.Generic; -using System.Net.Http; -using System.Text; -using Barotrauma.Networking; - -namespace Barotrauma -{ - partial class LuaCsNetworking - { - private static readonly HttpClient client = new HttpClient(); - - private enum LuaCsClientToServer - { - NetMessageId, - NetMessageString, - RequestSingleId, - RequestAllIds, - } - - private enum LuaCsServerToClient - { - NetMessageId, - NetMessageString, - ReceiveIds - } - - public bool RestrictMessageSize = true; - - private Dictionary netReceives = new Dictionary(); - private Dictionary idToString = new Dictionary(); - private Dictionary stringToId = new Dictionary(); - - public void Initialize() - { -#if CLIENT - SendSyncMessage(); -#endif - } - - public void Remove(string netMessageName) - { - netReceives.Remove(netMessageName); - } - - public IWriteMessage Start() - { - return new WriteOnlyMessage(); - } - - public string IdToString(ushort id) - { - if (idToString.ContainsKey(id)) { return idToString[id]; } - - return null; - } - - public ushort StringToId(string name) - { - if (stringToId.ContainsKey(name)) { return stringToId[name]; } - - return 0; - } - - private void HandleNetMessage(IReadMessage netMessage, string name, Client client = null) - { - if (netReceives.ContainsKey(name)) - { - try - { - netReceives[name](netMessage, client); - } - catch (Exception e) - { - LuaCsLogger.LogError($"Exception thrown inside NetMessageReceive({name})", LuaCsMessageOrigin.CSharpMod); - LuaCsLogger.HandleException(e, LuaCsMessageOrigin.CSharpMod); - } - } - else - { - if (GameSettings.CurrentConfig.VerboseLogging) - { -#if SERVER - LuaCsLogger.LogError($"Received NetMessage for unknown name {name} from {GameServer.ClientLogName(client)}."); -#else - LuaCsLogger.LogError($"Received NetMessage for unknown name {name} from server."); -#endif - } - } - } - - private void HandleNetMessageString(IReadMessage netMessage, Client client = null) - { - string name = netMessage.ReadString(); - - HandleNetMessage(netMessage, name, client); - } - - public async void HttpRequest(string url, LuaCsAction callback, string data = null, string method = "POST", string contentType = "application/json", Dictionary headers = null, string savePath = null) - { - try - { - HttpRequestMessage request = new HttpRequestMessage(new HttpMethod(method), url); - - if (headers != null) - { - foreach (var header in headers) - { - request.Headers.Add(header.Key, header.Value); - } - } - - if (data != null) - { - request.Content = new StringContent(data, Encoding.UTF8, contentType); - } - - HttpResponseMessage response = await client.SendAsync(request); - - if (savePath != null) - { - if (LuaCsFile.IsPathAllowedException(savePath)) - { - byte[] responseData = await response.Content.ReadAsByteArrayAsync(); - - using (var fileStream = new FileStream(savePath, FileMode.Create, FileAccess.Write)) - { - fileStream.Write(responseData, 0, responseData.Length); - } - } - } - - string responseBody = await response.Content.ReadAsStringAsync(); - - GameMain.LuaCs.Timer.Wait((object[] par) => - { - callback(responseBody, (int)response.StatusCode, response.Headers); - }, 0); - } - catch (HttpRequestException e) - { - GameMain.LuaCs.Timer.Wait((object[] par) => { callback(e.Message, e.StatusCode, null); }, 0); - } - catch (Exception e) - { - GameMain.LuaCs.Timer.Wait((object[] par) => { callback(e.Message, null, null); }, 0); - } - } - - public void HttpPost(string url, LuaCsAction callback, string data, string contentType = "application/json", Dictionary headers = null, string savePath = null) - { - HttpRequest(url, callback, data, "POST", contentType, headers, savePath); - } - - - public void HttpGet(string url, LuaCsAction callback, Dictionary headers = null, string savePath = null) - { - HttpRequest(url, callback, null, "GET", null, headers, savePath); - } - - public void CreateEntityEvent(INetSerializable entity, NetEntityEvent.IData extraData) - { - GameMain.NetworkMember.CreateEntityEvent(entity, extraData); - } - - public ushort LastClientListUpdateID - { - get { return GameMain.NetworkMember.LastClientListUpdateID; } - set { GameMain.NetworkMember.LastClientListUpdateID = value; } - } - } -} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/LuaCsSetup.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/LuaCsSetup.cs index d51193a50..03c2501b9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/LuaCsSetup.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/LuaCsSetup.cs @@ -1,546 +1,535 @@ -using System; -using System.IO; +using Barotrauma.LuaCs; +using Barotrauma.LuaCs.Compatibility; +using Barotrauma.LuaCs.Data; +using Barotrauma.LuaCs.Events; +using LightInject; using MoonSharp.Interpreter; -using MoonSharp.Interpreter.Interop; -using System.Runtime.CompilerServices; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.IO; using System.Linq; +using System.Runtime.CompilerServices; using System.Threading; -using LuaCsCompatPatchFunc = Barotrauma.LuaCsPatch; -using System.Diagnostics; -using MoonSharp.VsCodeDebugger; -using System.Reflection; -using System.Runtime.Loader; -using System.Xml.Linq; -using Barotrauma.Networking; +using AssemblyLoader = Barotrauma.LuaCs.AssemblyLoader; +[assembly: InternalsVisibleTo("ImpromptuInterfaceDynamicAssembly")] +[assembly: InternalsVisibleTo("Dynamitey")] namespace Barotrauma { - class LuaCsSetupConfig - { - public bool EnableCsScripting = false; - public bool TreatForcedModsAsNormal = true; - public bool PreferToUseWorkshopLuaSetup = false; - public bool DisableErrorGUIOverlay = false; - public bool HideUserNames - { - get { return LuaCsLogger.HideUserNames; } - set { LuaCsLogger.HideUserNames = value; } - } - - public LuaCsSetupConfig() { } - public LuaCsSetupConfig(LuaCsSetupConfig config) - { - EnableCsScripting = config.EnableCsScripting; - TreatForcedModsAsNormal = config.TreatForcedModsAsNormal; - PreferToUseWorkshopLuaSetup = config.PreferToUseWorkshopLuaSetup; - DisableErrorGUIOverlay = config.DisableErrorGUIOverlay; - } - } - internal delegate void LuaCsMessageLogger(string message); internal delegate void LuaCsErrorHandler(Exception ex, LuaCsMessageOrigin origin); internal delegate void LuaCsExceptionHandler(Exception ex, LuaCsMessageOrigin origin); + - partial class LuaCsSetup + partial class LuaCsSetup : IDisposable, IEventScreenSelected, IEventEnabledPackageListChanged, + IEventReloadAllPackages { - public const string LuaSetupFile = "Lua/LuaSetup.lua"; - public const string VersionFile = "luacsversion.txt"; -#if WINDOWS - public static ContentPackageId LuaForBarotraumaId = new SteamWorkshopId(2559634234); -#elif LINUX - public static ContentPackageId LuaForBarotraumaId = new SteamWorkshopId(2970628943); -#elif OSX - public static ContentPackageId LuaForBarotraumaId = new SteamWorkshopId(2970890020); -#endif + public const string PackageName = "LuaCsForBarotrauma"; - public static ContentPackageId CsForBarotraumaId = new SteamWorkshopId(2795927223); + private static LuaCsSetup _luaCsSetup; + public static LuaCsSetup Instance => _luaCsSetup ??= new LuaCsSetup(); + /// + /// The index of the last Vanilla command. + /// + public static int DebugConsoleCommandVanillaIndex { get; private set; } - private const string configFileName = "LuaCsSetupConfig.xml"; + private LuaCsSetup() + { + if (_luaCsSetup != null) + { + throw new Exception("Tried to create another LuaCsSetup instance"); + } + DebugConsoleCommandVanillaIndex = DebugConsole.Commands.Count; + + // == startup + _servicesProvider = SetupServicesProvider(); + _runStateMachine = SetupStateMachine(); + SubscribeToLuaCsEvents(); + } + + private void SubscribeToLuaCsEvents() + { + EventService.Subscribe(this); // game state hook in + EventService.Subscribe(this); + EventService.Subscribe(this); + } + + #region CONST_DEF + #if SERVER public const bool IsServer = true; - public const bool IsClient = false; #else public const bool IsServer = false; - public const bool IsClient = true; #endif + public const bool IsClient = !IsServer; - public static bool IsRunningInsideWorkshop - { - get - { -#if SERVER - return Path.GetDirectoryName(System.Reflection.Assembly.GetEntryAssembly().Location) != Directory.GetCurrentDirectory(); -#else - return false; // unnecessary but just keeps things clear that this is NOT for client stuff -#endif - } - } - - private static int executionNumber = 0; - - - public Script Lua { get; private set; } - public LuaScriptLoader LuaScriptLoader { get; private set; } - - public LuaGame Game { get; private set; } - public LuaCsHook Hook { get; private set; } - public LuaCsTimer Timer { get; private set; } - public LuaCsNetworking Networking { get; private set; } - public LuaCsSteam Steam { get; private set; } - public LuaCsPerformanceCounter PerformanceCounter { get; private set; } - - // must be available at anytime - private static AssemblyManager _assemblyManager; - public static AssemblyManager AssemblyManager => _assemblyManager ??= new AssemblyManager(); + #endregion - private CsPackageManager _pluginPackageManager; - public CsPackageManager PluginPackageManager => _pluginPackageManager ??= new CsPackageManager(AssemblyManager, this); - - public LuaCsModStore ModStore { get; private set; } - private LuaRequire require { get; set; } - public LuaCsSetupConfig Config { get; private set; } - public MoonSharpVsCodeDebugServer DebugServer { get; private set; } - public bool IsInitialized { get; private set; } - - private bool ShouldRunCs - { - get - { -#if SERVER - if (GetPackage(CsForBarotraumaId, false, false) != null && GameMain.Server.ServerPeer is LidgrenServerPeer) { return true; } -#endif - - return Config.EnableCsScripting; - } - } - - public LuaCsSetup() - { - Script.GlobalOptions.Platform = new LuaPlatformAccessor(); - - Hook = new LuaCsHook(this); - ModStore = new LuaCsModStore(); - - Game = new LuaGame(); - Networking = new LuaCsNetworking(); - DebugServer = new MoonSharpVsCodeDebugServer(); - - ReadSettings(); - } + #region Services_CVars - [Obsolete("Use AssemblyManager::GetTypesByName()")] - public static Type GetType(string typeName, bool throwOnError = false, bool ignoreCase = false) + /* + * === Singleton Services + */ + + private readonly IServicesProvider _servicesProvider; + + private PerformanceCounterService _performanceCounterService; + public PerformanceCounterService PerformanceCounterService => _performanceCounterService ??= _servicesProvider.GetService(); + public ILoggerService Logger => _servicesProvider.GetService(); + public IConfigService ConfigService => _servicesProvider.GetService(); + public IPackageManagementService PackageManagementService => _servicesProvider.GetService(); + public IPluginManagementService PluginManagementService => _servicesProvider.GetService(); + public ILuaScriptManagementService LuaScriptManagementService => _servicesProvider.GetService(); + public INetworkingService NetworkingService => _servicesProvider.GetService(); + // hotpath performance ref cache + private IEventService _eventService = null; + public IEventService EventService => _eventService ??= _servicesProvider.GetService(); + // hotpath performance ref cache + private LuaGame _game; + public LuaGame Game => _game ??= _servicesProvider.GetService(); + public Script Lua => LuaScriptManagementService.InternalScript; + + private ISettingBase _isCsEnabledForSession; + public bool IsCsEnabledForSession { - return AssemblyManager.GetTypesByName(typeName).FirstOrDefault((Type)null); - } - - public void ToggleDebugger(int port = 41912) - { - if (!GameMain.LuaCs.DebugServer.IsStarted) + get => _isCsEnabledForSession?.Value ?? false; + internal set { - DebugServer.Start(); - AttachDebugger(); - - LuaCsLogger.Log($"Lua Debug Server started on port {port}."); - } - else - { - DetachDebugger(); - DebugServer.Stop(); - - LuaCsLogger.Log($"Lua Debug Server stopped."); - } - } - - public void AttachDebugger() - { - DebugServer.AttachToScript(Lua, "Script", s => - { - if (s.Name.StartsWith("LocalMods") || s.Name.StartsWith("Lua")) + _isCsEnabledForSession?.TrySetValue(value); + if (_isCsEnabledForSession != null) { - return Environment.CurrentDirectory + "/" + s.Name; + if (_isCsEnabledForSession.GetConfigInfo() == null) + { + Logger.LogError($"Config info was nil while trying to save {IsCsEnabledForSession}"); + return; + } + ConfigService.SaveConfigValue(_isCsEnabledForSession); } - return s.Name; + } + } + + /// + /// Whether C# plugin code is enabled. + /// + public bool IsCsEnabled + { +#if CLIENT + get => _csRunPolicy?.Value == "Enabled" || IsCsEnabledForSession; +#elif SERVER + // cs settings cannot be changed on the server after launch + get => _csRunPolicy?.Value is "Enabled" or "Prompt"; +#endif + } + + private ISettingList _csRunPolicy; + + public string CsRunPolicyValue => _csRunPolicy?.Value ?? "Prompt"; + + /// + /// Whether usernames are anonymized or show in logs. + /// + public bool HideUserNamesInLogs + { + get => _hideUserNamesInLogs?.Value ?? false; + internal set => _hideUserNamesInLogs?.TrySetValue(value); + } + private ISettingBase _hideUserNamesInLogs; + + public bool UseCaching + { + get => _useCaching?.Value ?? true; + } + private ISettingBase _useCaching; + + public static ContentPackage GetLuaCsPackage() + { + return ContentPackageManager.EnabledPackages.Regular.FirstOrDefault(cp => cp.NameMatches(PackageName), null) + ?? ContentPackageManager.LocalPackages.FirstOrDefault(cp => cp.NameMatches(PackageName)) + ?? ContentPackageManager.WorkshopPackages.FirstOrDefault(cp => cp.NameMatches(PackageName)); + } + + void LoadLuaCsConfig() + { + var luaCsPackage = GetLuaCsPackage(); + + _csRunPolicy = + ConfigService.TryGetConfig>(luaCsPackage, "CsRunPolicy", out var val1) + ? val1 + : null; + _hideUserNamesInLogs = + ConfigService.TryGetConfig>(luaCsPackage, "HideUserNamesInLogs", out var val4) + ? val4 + : null; + _useCaching = + ConfigService.TryGetConfig>(luaCsPackage, "UseCaching", out var val5) + ? val5 + : null; + _isCsEnabledForSession = + ConfigService.TryGetConfig>(luaCsPackage, "IsCsEnabledForSession", out var val6) + ? val6 + : null; + + if (!ContentPackageManager.EnabledPackages.All.Contains(luaCsPackage)) + { + // sorry perfidius (not sorry) + luaCsPackage.UnloadFilesOfType(); + luaCsPackage.LoadFilesOfType(); + } + } + + private IServicesProvider SetupServicesProvider() + { + var servicesProvider = new ServicesProvider(); + + // Base Service + servicesProvider.RegisterServiceType(ServiceLifetime.Singleton); + servicesProvider.RegisterServiceType(ServiceLifetime.Singleton); + servicesProvider.RegisterServiceType(ServiceLifetime.Transient); + servicesProvider.RegisterServiceType(ServiceLifetime.Transient); + servicesProvider.RegisterServiceType(ServiceLifetime.Singleton); + servicesProvider.RegisterServiceResolver(factory => factory.GetInstance() as ILuaCsHook); + servicesProvider.RegisterServiceType(ServiceLifetime.Singleton); + servicesProvider.RegisterServiceType(ServiceLifetime.Singleton); + servicesProvider.RegisterServiceResolver(factory => factory.GetInstance()); + servicesProvider.RegisterServiceType(ServiceLifetime.Singleton); + servicesProvider.RegisterServiceType(ServiceLifetime.Singleton); + servicesProvider.RegisterServiceType(ServiceLifetime.Singleton); + servicesProvider.RegisterServiceType(ServiceLifetime.Transient); + servicesProvider.RegisterServiceType(ServiceLifetime.Singleton); + servicesProvider.RegisterServiceType(ServiceLifetime.Transient); + servicesProvider.RegisterServiceType(ServiceLifetime.Singleton); + servicesProvider.RegisterServiceResolver(factory => factory.GetInstance() as ILuaConfigService); + + // Extension/Sub Services + servicesProvider.RegisterServiceType(ServiceLifetime.Transient); + servicesProvider.RegisterServiceType(ServiceLifetime.Transient); + servicesProvider.RegisterServiceType(ServiceLifetime.Transient); + servicesProvider.RegisterServiceType, ModConfigFileParserService>(ServiceLifetime.Transient); + servicesProvider.RegisterServiceType, ModConfigFileParserService>(ServiceLifetime.Transient); + servicesProvider.RegisterServiceType, ModConfigFileParserService>(ServiceLifetime.Transient); + servicesProvider.RegisterServiceType, SettingsFileParserService>(ServiceLifetime.Transient); + servicesProvider.RegisterServiceType, SettingsFileParserService>(ServiceLifetime.Transient); + servicesProvider.RegisterServiceType(ServiceLifetime.Transient); + + // All Lua Extras + servicesProvider.RegisterServiceType(ServiceLifetime.Singleton); + servicesProvider.RegisterServiceType(ServiceLifetime.Singleton); + servicesProvider.RegisterServiceType(ServiceLifetime.Singleton); + servicesProvider.RegisterServiceType(ServiceLifetime.Singleton); + servicesProvider.RegisterServiceType(ServiceLifetime.Transient); + servicesProvider.RegisterServiceType(ServiceLifetime.Transient); + servicesProvider.RegisterServiceType(ServiceLifetime.Singleton); + servicesProvider.RegisterServiceType(ServiceLifetime.Singleton); + + // service config data + servicesProvider.RegisterServiceType(ServiceLifetime.Singleton); + servicesProvider.RegisterServiceType(ServiceLifetime.Singleton); + servicesProvider.RegisterServiceType(ServiceLifetime.Singleton); + servicesProvider.RegisterServiceType(ServiceLifetime.Singleton); + +#if CLIENT + SetupServicesProviderClient(servicesProvider); +#endif + + // gen IL + servicesProvider.CompileAndRun(); + return servicesProvider; + } + + #endregion + + #region StateMachine + + private RunState _runState; + /// + /// The current run state of all services managed by LuaCs. + /// + public RunState CurrentRunState + { + get => _runState; + private set => _runState = value; + } + + private readonly StateMachine _runStateMachine; + + public void OnEnabledPackageListChanged(CorePackage package, IEnumerable regularPackages) + { + ProcessEnabledPackageChanges(new []{ package }.Concat(regularPackages).ToImmutableArray()); + } + + public void OnReloadAllPackages() + { + CoroutineManager.Invoke(() => + { + SetRunState(RunState.Unloaded); + CoroutineManager.Invoke(() => + { + SetRunState(RunState.Running); + },0.25f); }); } - public void DetachDebugger() => DebugServer.Detach(Lua); - - public void ReadSettings() + private void ProcessEnabledPackageChanges(ImmutableArray packages) { - Config = new LuaCsSetupConfig(); - - if (File.Exists(configFileName)) + if (CurrentRunState < RunState.LoadedNoExec) { - try - { - using (var file = File.Open(configFileName, FileMode.Open, FileAccess.Read)) - { - XDocument document = XDocument.Load(file); - Config.EnableCsScripting = document.Root.GetAttributeBool("EnableCsScripting", Config.EnableCsScripting); - Config.TreatForcedModsAsNormal = document.Root.GetAttributeBool("TreatForcedModsAsNormal", Config.TreatForcedModsAsNormal); - Config.PreferToUseWorkshopLuaSetup = document.Root.GetAttributeBool("PreferToUseWorkshopLuaSetup", Config.PreferToUseWorkshopLuaSetup); - Config.DisableErrorGUIOverlay = document.Root.GetAttributeBool("DisableErrorGUIOverlay", Config.DisableErrorGUIOverlay); - Config.HideUserNames = document.Root.GetAttributeBool("HideUserNames", Config.HideUserNames); - } - } - catch (Exception e) - { - LuaCsLogger.HandleException(e, LuaCsMessageOrigin.LuaCs); - } + return; } - } - - public void WriteSettings() - { - XDocument document = new XDocument(); - document.Add(new XElement("LuaCsSetupConfig")); - document.Root.SetAttributeValue("EnableCsScripting", Config.EnableCsScripting); - document.Root.SetAttributeValue("EnableCsScripting", Config.EnableCsScripting); - document.Root.SetAttributeValue("TreatForcedModsAsNormal", Config.TreatForcedModsAsNormal); - document.Root.SetAttributeValue("PreferToUseWorkshopLuaSetup", Config.PreferToUseWorkshopLuaSetup); - document.Root.SetAttributeValue("DisableErrorGUIOverlay", Config.DisableErrorGUIOverlay); - document.Root.SetAttributeValue("HideUserNames", Config.HideUserNames); - document.Save(configFileName); - } - - public static ContentPackage GetPackage(ContentPackageId id, bool fallbackToAll = true, bool useBackup = false) - { - foreach (ContentPackage package in ContentPackageManager.EnabledPackages.All) - { - if (package.UgcId.ValueEquals(id)) - { - return package; - } - } - - if (fallbackToAll) - { - foreach (ContentPackage package in ContentPackageManager.LocalPackages) - { - if (package.UgcId.ValueEquals(id)) - { - return package; - } - } - - foreach (ContentPackage package in ContentPackageManager.AllPackages) - { - if (package.UgcId.ValueEquals(id)) - { - return package; - } - } - } - - if (useBackup && ContentPackageManager.EnabledPackages.BackupPackages.Regular != null) - { - foreach (ContentPackage package in ContentPackageManager.EnabledPackages.BackupPackages.Regular.Value) - { - if (package.UgcId.ValueEquals(id)) - { - return package; - } - } - } - - return null; - } - - private DynValue DoFile(string file, Table globalContext = null, string codeStringFriendly = null) - { - if (!LuaCsFile.CanReadFromPath(file)) - { - throw new ScriptRuntimeException($"dofile: File access to {file} not allowed."); - } - - if (!LuaCsFile.Exists(file)) - { - throw new ScriptRuntimeException($"dofile: File {file} not found."); - } - - return Lua.DoFile(file, globalContext, codeStringFriendly); - } - - private DynValue LoadFile(string file, Table globalContext = null, string codeStringFriendly = null) - { - if (!LuaCsFile.CanReadFromPath(file)) - { - throw new ScriptRuntimeException($"loadfile: File access to {file} not allowed."); - } - - if (!LuaCsFile.Exists(file)) - { - throw new ScriptRuntimeException($"loadfile: File {file} not found."); - } - - return Lua.LoadFile(file, globalContext, codeStringFriendly); - } - - public DynValue CallLuaFunction(object function, params object[] args) - { - // XXX: `lua` might be null if `LuaCsSetup.Stop()` is called while - // a patched function is still running. - if (Lua == null) { return null; } - - lock (Lua) - { - try - { - return Lua.Call(function, args); - } - catch (Exception e) - { - LuaCsLogger.HandleException(e, LuaCsMessageOrigin.LuaMod); - } - return null; - } - } - - private void SetModulePaths(string[] str) - { - LuaScriptLoader.ModulePaths = str; - } - - public void Update() - { - Timer?.Update(); - Steam?.Update(); - -#if CLIENT - Stopwatch luaSw = new Stopwatch(); - luaSw.Start(); -#endif - Hook?.Call("think"); -#if CLIENT - luaSw.Stop(); - GameMain.PerformanceCounter.AddElapsedTicks("Think Hook", luaSw.ElapsedTicks); -#endif - } - - public void Stop() - { - PluginPackageManager.UnloadPlugins(); - // unregister types - foreach (Type type in AssemblyManager.GetAllLoadedACLs().SelectMany( - acl => acl.AssembliesTypes.Select(kvp => kvp.Value))) + var state = CurrentRunState; + if (CurrentRunState > RunState.LoadedNoExec) { - UserData.UnregisterType(type, true); + SetRunState(RunState.LoadedNoExec); } - - if (Lua?.Globals is not null) - { - Lua.Globals.Remove("CsPackageManager"); - Lua.Globals.Remove("AssemblyManager"); - } - - if (Thread.CurrentThread == GameMain.MainThread) - { - Hook?.Call("stop"); - } - - if (Lua != null && DebugServer.IsStarted) - { - DebugServer.Detach(Lua); - } - - Game?.Stop(); - - Hook?.Clear(); - ModStore.Clear(); - LuaScriptLoader = null; - Lua = null; - // we can only unload assemblies after clearing ModStore/references. - PluginPackageManager.Dispose(); -#pragma warning disable CS0618 - ACsMod.LoadedMods.Clear(); -#pragma warning restore CS0618 - - Game = new LuaGame(); - Networking = new LuaCsNetworking(); - Timer = new LuaCsTimer(); - Steam = new LuaCsSteam(); - PerformanceCounter = new LuaCsPerformanceCounter(); - - IsInitialized = false; + this.Logger.LogResults(PackageManagementService.SyncLoadedPackagesList(GetLuaCsEnabledPackagesList(packages))); + ConfigService.LoadSavedConfigsValues(); + SetRunState(state); // restore + } + + public void SetRunState(RunState targetRunState) + { + if (CurrentRunState == targetRunState) + { + return; + } + _runStateMachine.GotoState(targetRunState); } - public void Initialize(bool forceEnableCs = false) + private ImmutableArray GetEnabledPackagesList() + => GetLuaCsEnabledPackagesList(ContentPackageManager.EnabledPackages.Regular + .ToImmutableArray()); + + private ImmutableArray GetLuaCsEnabledPackagesList(ImmutableArray enabledRegular) { - if (IsInitialized) + if (!enabledRegular.Any(p => p.Name.Equals(PackageName, StringComparison.InvariantCultureIgnoreCase))) { - Stop(); + var luaCs = ContentPackageManager.AllPackages.FirstOrDefault(p => p.Name.Equals(PackageName, StringComparison.InvariantCultureIgnoreCase)); + if (luaCs is null) + { + DebugConsole.ThrowError($"The '{PackageName}' mod could not be found. Please subscribe to it and add it to the EnabledPackages List!", + new NullReferenceException($"The '{PackageName}' mod could not be found. Please subscribe to it and add it to the EnabledPackages List!"), + createMessageBox: true); + return enabledRegular; + } + + enabledRegular = new[] { luaCs }.Concat(enabledRegular).ToImmutableArray(); + } + + return enabledRegular; + } + + private StateMachine SetupStateMachine() + { + return new StateMachine(false, RunState.Unloaded, onEnter: RunStateUnloaded_OnEnter, null) + .AddState(RunState.LoadedNoExec, onEnter: RunStateLoadedNoExec_OnEnter, null) + .AddState(RunState.Running, onEnter: RunStateRunning_OnEnter, RunStateRunning_OnExit); + + // ReSharper disable InconsistentNaming + void RunStateUnloaded_OnEnter(State currentState) + { + Logger.LogMessage("LuaCs unloaded state entered"); + + if (PackageManagementService.IsAnyPackageRunning()) + { + Logger.LogResults(PackageManagementService.StopRunningPackages()); + } + + if (PackageManagementService.IsAnyPackageLoaded()) + { + DisposeLuaCsConfig(); + Logger.LogResults(PackageManagementService.UnloadAllPackages()); + } + + EventService.Reset(); + ConfigService.Reset(); + LuaScriptManagementService.Reset(); + PackageManagementService.Reset(); + NetworkingService.Reset(); + Game.Reset(); + _servicesProvider.GetService().Reset(); + + Logger.LogMessage("Services have been reset"); + + SubscribeToLuaCsEvents(); + + CurrentRunState = RunState.Unloaded; } - IsInitialized = true; - - LuaCsLogger.LogMessage("Lua! Version " + AssemblyInfo.GitRevision); - - bool csActive = ShouldRunCs || forceEnableCs; - - LuaScriptLoader = new LuaScriptLoader(); - LuaScriptLoader.ModulePaths = new string[] { }; - - RegisterLuaConverters(); - - Lua = new Script(CoreModules.Preset_SoftSandbox | CoreModules.Debug | CoreModules.IO | CoreModules.OS_System); - Lua.Options.DebugPrint = (o) => { LuaCsLogger.LogMessage(o); }; - Lua.Options.ScriptLoader = LuaScriptLoader; - Lua.Options.CheckThreadAccess = false; - Script.GlobalOptions.ShouldPCallCatchException = (Exception ex) => { return true; }; - - require = new LuaRequire(Lua); - - Game = new LuaGame(); - Networking = new LuaCsNetworking(); - Timer = new LuaCsTimer(); - Steam = new LuaCsSteam(); - PerformanceCounter = new LuaCsPerformanceCounter(); - Hook.Initialize(); - ModStore.Initialize(); - Networking.Initialize(); - - UserData.RegisterType(); - UserData.RegisterType(); - UserData.RegisterType(); - UserData.RegisterType(); - UserData.RegisterType(); - UserData.RegisterType(); - UserData.RegisterType(); - UserData.RegisterType(); - UserData.RegisterType(); - UserData.RegisterType(); - UserData.RegisterType(); - UserData.RegisterType(); - UserData.RegisterType(); - UserData.RegisterType(); - UserData.RegisterType(); - - Lua.Globals["printerror"] = (DynValue o) => { LuaCsLogger.LogError(o.ToString(), LuaCsMessageOrigin.LuaMod); }; - - Lua.Globals["setmodulepaths"] = (Action)SetModulePaths; - - Lua.Globals["dofile"] = (Func)DoFile; - Lua.Globals["loadfile"] = (Func)LoadFile; - Lua.Globals["require"] = (Func)require.Require; - - Lua.Globals["dostring"] = (Func)Lua.DoString; - Lua.Globals["load"] = (Func)Lua.LoadString; - - Lua.Globals["Logger"] = UserData.CreateStatic(); - Lua.Globals["LuaUserData"] = UserData.CreateStatic(); - Lua.Globals["Game"] = Game; - Lua.Globals["Hook"] = Hook; - Lua.Globals["ModStore"] = ModStore; - Lua.Globals["Timer"] = Timer; - Lua.Globals["File"] = UserData.CreateStatic(); - Lua.Globals["Networking"] = Networking; - Lua.Globals["Steam"] = Steam; - Lua.Globals["PerformanceCounter"] = PerformanceCounter; - Lua.Globals["LuaCsConfig"] = new LuaCsSetupConfig(Config); - - Lua.Globals["ExecutionNumber"] = executionNumber; - Lua.Globals["CSActive"] = csActive; - - Lua.Globals["SERVER"] = IsServer; - Lua.Globals["CLIENT"] = IsClient; - - if (DebugServer.IsStarted) + void RunStateLoadedNoExec_OnEnter(State currentState) { - AttachDebugger(); + Logger.LogMessage("LuaCs no execution state entered"); + + if (PackageManagementService.IsAnyPackageRunning()) + { + Logger.LogResults(PackageManagementService.StopRunningPackages()); + } + + if (!PackageManagementService.IsAnyPackageLoaded()) + { + foreach (var registrationProvider in _servicesProvider.GetAllServices()) + { + registrationProvider.RegisterTypeProviders(ConfigService, null); + } + Logger.LogResults(PackageManagementService.LoadPackagesInfo(GetEnabledPackagesList())); + Logger.LogResults(ConfigService.LoadSavedConfigsValues()); + LoadLuaCsConfig(); + } + + CurrentRunState = RunState.LoadedNoExec; } - - if (csActive) - { - LuaCsLogger.LogMessage("Cs! Version " + AssemblyInfo.GitRevision); - - UserData.RegisterType(); - UserData.RegisterType(); - UserData.RegisterType(); - - Lua.Globals["PluginPackageManager"] = PluginPackageManager; - Lua.Globals["AssemblyManager"] = AssemblyManager; - try + void RunStateRunning_OnEnter(State currentState) + { + if (!PackageManagementService.IsAnyPackageLoaded()) { - Stopwatch taskTimer = new(); - taskTimer.Start(); - ModStore.Clear(); - - var state = PluginPackageManager.LoadAssemblyPackages(); - if (state is AssemblyLoadingSuccessState.Success or AssemblyLoadingSuccessState.AlreadyLoaded) + foreach (var registrationProvider in _servicesProvider.GetAllServices()) { - if(!PluginPackageManager.PluginsInitialized) - PluginPackageManager.InstantiatePlugins(true); - if(!PluginPackageManager.PluginsPreInit) - PluginPackageManager.RunPluginsPreInit(); // this is intended to be called at startup in the future - if(!PluginPackageManager.PluginsLoaded) - PluginPackageManager.RunPluginsInit(); - state = AssemblyLoadingSuccessState.Success; - taskTimer.Stop(); - ModUtils.Logging.PrintMessage($"{nameof(LuaCsSetup)}: Completed assembly loading. Total time {taskTimer.ElapsedMilliseconds}ms."); - } - else - { - PluginPackageManager.Dispose(); // cleanup if there's an error - } - - if(state is not AssemblyLoadingSuccessState.Success) - { - ModUtils.Logging.PrintError($"{nameof(LuaCsSetup)}: Error while loading Cs-Assembly Mods | Err: {state}"); - taskTimer.Stop(); + registrationProvider.RegisterTypeProviders(ConfigService, null); } + Logger.LogResults(PackageManagementService.LoadPackagesInfo(GetEnabledPackagesList())); + Logger.LogResults(ConfigService.LoadSavedConfigsValues()); + LoadLuaCsConfig(); } - catch (Exception e) + + string csEnabled = IsCsEnabled ? "enabled" : "disabled"; + Logger.LogMessage($"LuaCs running state entered. Running under commit {AssemblyInfo.GitRevision}, CSharp is {csEnabled}"); + + if (!PackageManagementService.IsAnyPackageRunning()) { - ModUtils.Logging.PrintError($"{nameof(LuaCsSetup)}::{nameof(Initialize)}() | Error while loading assemblies! Details: {e.Message} | {e.StackTrace}"); + Logger.LogResults(PackageManagementService.ExecuteLoadedPackages(GetEnabledPackagesList(), IsCsEnabled)); } - } - - ContentPackage luaPackage = GetPackage(LuaForBarotraumaId); - - void RunLocal() - { - LuaCsLogger.LogMessage("Using LuaSetup.lua from the Barotrauma Lua/ folder."); - string luaPath = LuaSetupFile; - CallLuaFunction(Lua.LoadFile(luaPath), Path.GetDirectoryName(Path.GetFullPath(luaPath))); - } - - void RunWorkshop() - { - LuaCsLogger.LogMessage("Using LuaSetup.lua from the content package."); - string luaPath = Path.Combine(Path.GetDirectoryName(luaPackage.Path), "Binary/Lua/LuaSetup.lua"); - CallLuaFunction(Lua.LoadFile(luaPath), Path.GetDirectoryName(Path.GetFullPath(luaPath))); - } - - void RunNone() - { - LuaCsLogger.LogError("LuaSetup.lua not found! Lua/LuaSetup.lua, no Lua scripts will be executed or work.", LuaCsMessageOrigin.LuaMod); - } - - if (Config.PreferToUseWorkshopLuaSetup) - { - if (luaPackage != null) { RunWorkshop(); } - else if (File.Exists(LuaSetupFile)) { RunLocal(); } - else { RunNone(); } - } - else - { - if (File.Exists(LuaSetupFile)) { RunLocal(); } - else if (luaPackage != null) { RunWorkshop(); } - else { RunNone(); } - } +#if CLIENT + // Technically not very accurate, but we want to call after we run mods anyway + if (GameMain.Client != null) + { + EventService.PublishEvent(static p => p.OnServerConnected()); + } +#endif #if SERVER - GameMain.Server.ServerSettings.LoadClientPermissions(); + GameMain.Server.ServerSettings.LoadClientPermissions(); #endif - executionNumber++; + CurrentRunState = RunState.Running; + } + + + void RunStateRunning_OnExit(State currentState) + { + EventService.Call("stop"); + Logger.LogResults(PackageManagementService.StopRunningPackages()); + Logger.LogMessage("LuaCs running state exited"); + } + // ReSharper restore InconsistentNaming + } + + + + + + #endregion + + /// + /// Checks for Cs Execution Policy (ie. prompting the user) and then calls the delegate once completed. + /// + /// + partial void CheckReadyToRun(Action onReadyToRun); + + #region LegacyRedirects + + // --- Compatibility + /// + /// [Obsolete] Legacy support only. + /// + [Obsolete] + public LuaCsPerformanceCounter PerformanceCounter { get; private set; } = new LuaCsPerformanceCounter(); + /// + /// [Obsolete] Use instead. + /// + [Obsolete($"Use {nameof(PluginManagementService)} instead.")] + public IPluginManagementService PluginPackageManager => this.PluginManagementService; + public ILuaCsHook Hook => this.EventService; + public INetworkingService Networking => this.NetworkingService; + public ILuaCsTimer Timer => _servicesProvider.GetService(); + public DynValue CallLuaFunction(object function, params object[] args) => LuaScriptManagementService.CallFunctionSafe(function, args); + + #endregion + + public void Dispose() + { + try + { + SetRunState(RunState.Unloaded); + } + catch (Exception e) + { + Logger.LogError(e.Message); + } + + try + { + DisposeLuaCsConfig(); + + PluginManagementService.Dispose(); + LuaScriptManagementService.Dispose(); + ConfigService.Dispose(); + PackageManagementService.Dispose(); + // TODO: Add all missing services. + //NetworkingService.Dispose(); + EventService.Dispose(); + + _eventService = null; + _game = null; + PerformanceCounter = null; + _servicesProvider.DisposeAndReset(); + } + catch (Exception e) + { + Console.WriteLine(e); + throw; + } + + _luaCsSetup = null; + + GC.SuppressFinalize(this); + } + + /// + /// Handles changes in game states tracked by screen changes. + /// + /// The new game screen. + public partial void OnScreenSelected(Screen screen); + + void DisposeLuaCsConfig() + { + _csRunPolicy = null; + _hideUserNamesInLogs = null; } } + + /// + /// Specifies the current run state of the LuaCs Modding System. + /// [Important]Enum State values ordering must be in the form of (lower state) === (higher state) + /// + public enum RunState : byte + { + /// + /// No assets are loaded, code execution suspended. + /// + Unloaded = 0, + /// + /// Loaded mod configs, settings and assets. No code execution. + /// + LoadedNoExec = 1, + /// + /// All assets loaded, code execution is active. + /// + Running = 2 + } } + diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/LuaCsUtility.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/LuaCsUtility.cs deleted file mode 100644 index 3354449e3..000000000 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/LuaCsUtility.cs +++ /dev/null @@ -1,538 +0,0 @@ -using Barotrauma.Items.Components; -using Barotrauma.Networking; -using MoonSharp.Interpreter; -using System; -using System.Collections; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Net; -using System.Reflection; -using System.Xml.Linq; - -namespace Barotrauma -{ - partial class LuaCsFile - { - public static bool CanReadFromPath(string path) - { - string getFullPath(string p) => System.IO.Path.GetFullPath(p).CleanUpPath(); - - path = getFullPath(path); - - bool pathStartsWith(string prefix) => path.StartsWith(prefix, StringComparison.OrdinalIgnoreCase); - - string localModsDir = getFullPath(ContentPackage.LocalModsDir); - string workshopModsDir = getFullPath(ContentPackage.WorkshopModsDir); -#if CLIENT - string tempDownloadDir = getFullPath(ModReceiver.DownloadFolder); -#endif - if (pathStartsWith(getFullPath(string.IsNullOrEmpty(GameSettings.CurrentConfig.SavePath) ? SaveUtil.DefaultSaveFolder : GameSettings.CurrentConfig.SavePath))) - return true; - - if (pathStartsWith(localModsDir)) - return true; - - if (pathStartsWith(workshopModsDir)) - return true; - -#if CLIENT - if (pathStartsWith(tempDownloadDir)) - return true; -#endif - - if (pathStartsWith(getFullPath("."))) - return true; - - return false; - } - - public static bool CanWriteToPath(string path) - { - string getFullPath(string p) => System.IO.Path.GetFullPath(p).CleanUpPath(); - - path = getFullPath(path); - - bool pathStartsWith(string prefix) => path.StartsWith(prefix, StringComparison.OrdinalIgnoreCase); - - foreach (var package in ContentPackageManager.AllPackages) - { - if (package.UgcId.ValueEquals(LuaCsSetup.LuaForBarotraumaId) && pathStartsWith(getFullPath(package.Path))) - { - return false; - } - } - - if (pathStartsWith(getFullPath(string.IsNullOrEmpty(GameSettings.CurrentConfig.SavePath) ? SaveUtil.DefaultSaveFolder : GameSettings.CurrentConfig.SavePath))) - return true; - - if (pathStartsWith(getFullPath(ContentPackage.LocalModsDir))) - return true; - - if (pathStartsWith(getFullPath(ContentPackage.WorkshopModsDir))) - return true; -#if CLIENT - if (pathStartsWith(getFullPath(ModReceiver.DownloadFolder))) - return true; -#endif - - return false; - } - - public static bool IsPathAllowedException(string path, bool write = true, LuaCsMessageOrigin origin = LuaCsMessageOrigin.Unknown) - { - if (write) - { - if (CanWriteToPath(path)) - { - return true; - } - else - { - throw new Exception("File access to \"" + path + "\" not allowed."); - } - } - else - { - if (CanReadFromPath(path)) - { - return true; - } - else - { - throw new Exception("File access to \"" + path + "\" not allowed."); - } - } - } - - public static bool IsPathAllowedLuaException(string path, bool write = true) => - IsPathAllowedException(path, write, LuaCsMessageOrigin.LuaMod); - public static bool IsPathAllowedCsException(string path, bool write = true) => - IsPathAllowedException(path, write, LuaCsMessageOrigin.CSharpMod); - - public static string Read(string path) - { - if (!IsPathAllowedException(path, false)) - return ""; - - return File.ReadAllText(path); - } - - public static void Write(string path, string text) - { - if (!IsPathAllowedException(path)) - return; - - File.WriteAllText(path, text); - } - - public static void Delete(string path) - { - if (!IsPathAllowedException(path)) - return; - - File.Delete(path); - } - - public static void DeleteDirectory(string path) - { - if (!IsPathAllowedException(path)) - return; - - Directory.Delete(path, true); - } - - public static void Move(string path, string destination) - { - if (!IsPathAllowedException(path)) - return; - - if (!IsPathAllowedException(destination)) - return; - - File.Move(path, destination, true); - } - - public static FileStream OpenRead(string path) - { - if (!IsPathAllowedException(path)) - return null; - - return File.Open(path, FileMode.Open, FileAccess.Read); - } - public static FileStream OpenWrite(string path) - { - if (!IsPathAllowedException(path)) - return null; - - if (File.Exists(path)) return File.Open(path, FileMode.Truncate, FileAccess.Write); - else return File.Open(path, FileMode.Create, FileAccess.Write); - } - - public static bool Exists(string path) - { - if (!IsPathAllowedException(path, false)) - return false; - - return File.Exists(path); - } - - public static bool CreateDirectory(string path) - { - if (!IsPathAllowedException(path)) - return false; - - Directory.CreateDirectory(path); - - return true; - } - - public static bool DirectoryExists(string path) - { - if (!IsPathAllowedException(path, false)) - return false; - - return Directory.Exists(path); - } - - public static string[] GetFiles(string path) - { - if (!IsPathAllowedException(path, false)) - return null; - - return Directory.GetFiles(path); - } - - public static string[] GetDirectories(string path) - { - if (!IsPathAllowedException(path, false)) - return new string[] { }; - - return Directory.GetDirectories(path); - } - - public static string[] DirSearch(string sDir) - { - if (!IsPathAllowedException(sDir, false)) - return new string[] { }; - - List files = new List(); - - try - { - foreach (string f in Directory.GetFiles(sDir)) - { - files.Add(f); - } - - foreach (string d in Directory.GetDirectories(sDir)) - { - foreach (string f in Directory.GetFiles(d)) - { - files.Add(f); - } - DirSearch(d); - } - } - catch (System.Exception excpt) - { - Console.WriteLine(excpt.Message); - } - - return files.ToArray(); - } - } - - - class LuaCsConfig - { - private enum ValueType - { - None, - Text, - Integer, - Decimal, - Boolean, - Collection, - Object, - Enum - } - - private static Type[] LoadDocTypes(XElement typesElem) - { - var result = new List(); - var loadedTypes = LuaCsSetup.AssemblyManager - .GetAllTypesInLoadedAssemblies() - .ToImmutableHashSet(); - - 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 SaveDocTypes(IEnumerable 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 fields = tType.GetFields(BindingFlags.Instance | BindingFlags.Public) - .Concat(tType.GetFields(BindingFlags.Instance | BindingFlags.NonPublic)); - IEnumerable 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 types, Type type, XElement elem) - { - if (!types.Contains(type)) types.Add(type); - elem.SetAttributeValue("Type", types.IndexOf(type)); - } - - private static XElement ParseObject(List 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 fields = tType.GetFields(BindingFlags.Instance | BindingFlags.Public) - .Concat(tType.GetFields(BindingFlags.Instance | BindingFlags.NonPublic)); - IEnumerable 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(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(); - 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(string path) - { - using (var file = LuaCsFile.OpenRead(path)) return Load(file); - } - - public static void Save(string path, object obj) - { - using (var file = LuaCsFile.OpenWrite(path)) Save(file, obj); - } - } -} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/ModUtils.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/ModUtils.cs index 089cea715..9e667cf09 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/ModUtils.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/ModUtils.cs @@ -1,341 +1,646 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; using System.Text; +using System.Threading; +using System.Threading.Tasks; using System.Xml.Serialization; using Barotrauma; using Barotrauma.Items.Components; +using Barotrauma.LuaCs.Data; using Barotrauma.Networking; using Microsoft.CodeAnalysis; using Microsoft.Xna.Framework; +using OneOf; +using Platform = Barotrauma.LuaCs.Data.Platform; +// ReSharper disable ConvertClosureToMethodGroup -namespace Barotrauma; - -public static class ModUtils +// This file is cursed, we put everything in it, and I'm not sorry about it. +namespace Barotrauma { - #region LOGGING - public static class Logging + public static class ModUtils { - public static void PrintMessage(string s) + public static class ItemPrefab { -#if SERVER - LuaCsLogger.LogMessage($"[Server] {s}"); -#else - LuaCsLogger.LogMessage($"[Client] {s}"); -#endif + internal static Barotrauma.ItemPrefab GetItemPrefab(string itemNameOrId) + { + Barotrauma.ItemPrefab itemPrefab = + (Barotrauma.MapEntityPrefab.Find(itemNameOrId, identifier: null, showErrorMessages: false) ?? + Barotrauma.MapEntityPrefab.Find(null, identifier: itemNameOrId, showErrorMessages: false)) as Barotrauma.ItemPrefab; + + return itemPrefab; + } } - public static void PrintWarning(string s) + public static class Client { + internal static ulong GetSteamId(Barotrauma.Networking.Client client) + { + if (client.AccountId.TryUnwrap(out AccountId outValue) && outValue is SteamId steamId) + { + return steamId.Value; + } + else + { + return 0; + } + } + #if SERVER - LuaCsLogger.Log($"[Server] {s}", Color.Yellow); -#else - LuaCsLogger.Log($"[Client] {s}", Color.Yellow); + internal static void UnbanPlayer(string playerName) + { + GameMain.Server.UnbanPlayer(playerName); + } + + internal static void BanPlayer(string player, string reason, bool range = false, float seconds = -1) + { + if (seconds == -1) + { + GameMain.Server.BanPlayer(player, reason, null); + } + else + { + GameMain.Server.BanPlayer(player, reason, TimeSpan.FromSeconds(seconds)); + } + } #endif + + internal static IReadOnlyList ClientList + { + get + { + if (GameMain.IsSingleplayer) { return new List(); } + +#if SERVER + return GameMain.Server.ConnectedClients; +#else + return GameMain.Client.ConnectedClients; +#endif + } + } + } + + public static class Definitions + { + public const string LuaCsForBarotrauma = nameof(LuaCsForBarotrauma); } - public static void PrintError(string s) + public static class Environment { -#if SERVER - LuaCsLogger.LogError($"[Server] {s}"); + internal static void SetCurrentThreadAsMain() => MainThreadId = Thread.CurrentThread.ManagedThreadId; + public static int MainThreadId { get; private set; } = Int32.MinValue; + public static bool IsMainThread + { + get + { + if (MainThreadId == Int32.MinValue) + throw new ArgumentNullException("MainThread ID not set."); + return Thread.CurrentThread.ManagedThreadId == MainThreadId; + } + } + + public static readonly Platform CurrentPlatform = +#if WINDOWS + Platform.Windows; +#elif MACOS + Platform.MacOS; +#elif LINUX + Platform.Linux; #else - LuaCsLogger.LogError($"[Client] {s}"); + Platform.Linux; #endif + + public static readonly Target CurrentTarget = +#if CLIENT + Target.Client; +#elif SERVER + Target.Server; +#else + Target.Server; +#endif + + } + + #region LOGGING + + public static class Logging + { + public static void PrintMessage(string s) + { +#if SERVER + LuaCsSetup.Instance.Logger.LogMessage($"{s}"); +#else + LuaCsSetup.Instance.Logger.LogMessage($"{s}"); +#endif + } + + public static void PrintWarning(string s) + { +#if SERVER + LuaCsSetup.Instance.Logger.Log($"{s}", Color.Yellow); +#else + LuaCsSetup.Instance.Logger.Log($"{s}", Color.Yellow); +#endif + } + + public static void PrintError(string s) + { +#if SERVER + LuaCsSetup.Instance.Logger.LogError($"{s}"); +#else + LuaCsSetup.Instance.Logger.LogError($"{s}"); +#endif + } + } + + #endregion + + #region FILE_IO + + // ReSharper disable once InconsistentNaming + public static class IO + { + public static IEnumerable FindAllFilesInDirectory(string folder, string pattern, + SearchOption option) + { + try + { + return Directory.GetFiles(folder, pattern, option); + } + catch (DirectoryNotFoundException e) + { + return new string[] { }; + } + } + + public static string PrepareFilePathString(string filePath) => + PrepareFilePathString(Path.GetDirectoryName(filePath)!, Path.GetFileName(filePath)); + + public static string PrepareFilePathString(string path, string fileName) => + Path.Combine(SanitizePath(path), SanitizeFileName(fileName)); + + public static string SanitizeFileName(string fileName) + { + foreach (char c in Barotrauma.IO.Path.GetInvalidFileNameCharsCrossPlatform()) + fileName = fileName.Replace(c, '_'); + return fileName; + } + + /// + /// Gets the sanitized path for the top-level directory for a given content package. + /// + /// + /// + public static string GetContentPackageDir(ContentPackage package) + { + return SanitizePath(Path.GetFullPath(package.Dir)); + } + + public static string SanitizePath(string path) + { + foreach (char c in Path.GetInvalidPathChars()) + path = path.Replace(c.ToString(), "_"); + return path.CleanUpPath(); + } + + public static IOActionResultState GetOrCreateFileText(string filePath, out string fileText, + Func fileDataFactory = null, bool createFile = true) + { + fileText = null; + string fp = Path.GetFullPath(SanitizePath(filePath)); + + IOActionResultState ioActionResultState = IOActionResultState.Success; + if (createFile) + { + ioActionResultState = CreateFilePath(SanitizePath(filePath), out fp, fileDataFactory); + } + else if (!File.Exists(fp)) + { + return IOActionResultState.FileNotFound; + } + + if (ioActionResultState == IOActionResultState.Success) + { + try + { + fileText = File.ReadAllText(fp!); + return IOActionResultState.Success; + } + catch (ArgumentNullException ane) + { + ModUtils.Logging.PrintError( + $"ModUtils::CreateFilePath() | Exception: An argument is null. path: {fp ?? "null"} | Exception Details: {ane.Message}"); + return IOActionResultState.FilePathNull; + } + catch (ArgumentException ae) + { + ModUtils.Logging.PrintError( + $"ModUtils::CreateFilePath() | Exception: An argument is invalid. path: {fp ?? "null"} | Exception Details: {ae.Message}"); + return IOActionResultState.FilePathInvalid; + } + catch (DirectoryNotFoundException dnfe) + { + ModUtils.Logging.PrintError( + $"ModUtils::CreateFilePath() | Exception: Cannot find directory. path: {fp ?? "null"} | Exception Details: {dnfe.Message}"); + return IOActionResultState.DirectoryMissing; + } + catch (PathTooLongException ptle) + { + ModUtils.Logging.PrintError( + $"ModUtils::CreateFilePath() | Exception: path length is over 200 characters. path: {fp ?? "null"} | Exception Details: {ptle.Message}"); + return IOActionResultState.PathTooLong; + } + catch (NotSupportedException nse) + { + ModUtils.Logging.PrintError( + $"ModUtils::CreateFilePath() | Exception: Operation not supported on your platform/environment (permissions?). path: {fp ?? "null"} | Exception Details: {nse.Message}"); + return IOActionResultState.InvalidOperation; + } + catch (IOException ioe) + { + ModUtils.Logging.PrintError( + $"ModUtils::CreateFilePath() | Exception: IO tasks failed (Operation not supported). path: {fp ?? "null"} | Exception Details: {ioe.Message}"); + return IOActionResultState.IOFailure; + } + catch (Exception e) + { + ModUtils.Logging.PrintError( + $"ModUtils::CreateFilePath() | Exception: Unknown/Other Exception. path: {fp ?? "null"} | ExceptionMessage: {e.Message}"); + return IOActionResultState.UnknownError; + } + } + + return ioActionResultState; + } + + public static IOActionResultState CreateFilePath(string filePath, out string formattedFilePath, + Func fileDataFactory = null) + { + string file = Path.GetFileName(filePath); + string path = Path.GetDirectoryName(filePath)!; + + formattedFilePath = IO.PrepareFilePathString(path, file); + try + { + if (!Directory.Exists(path)) + Directory.CreateDirectory(path); + if (!File.Exists(formattedFilePath)) + File.WriteAllText(formattedFilePath, fileDataFactory is null ? "" : fileDataFactory.Invoke()); + return IOActionResultState.Success; + } + catch (ArgumentNullException ane) + { + ModUtils.Logging.PrintError( + $"ModUtils::CreateFilePath() | Exception: An argument is null. path: {formattedFilePath ?? "null"} | Exception Details: {ane.Message}"); + return IOActionResultState.FilePathNull; + } + catch (ArgumentException ae) + { + ModUtils.Logging.PrintError( + $"ModUtils::CreateFilePath() | Exception: An argument is invalid. path: {formattedFilePath ?? "null"} | Exception Details: {ae.Message}"); + return IOActionResultState.FilePathInvalid; + } + catch (DirectoryNotFoundException dnfe) + { + ModUtils.Logging.PrintError( + $"ModUtils::CreateFilePath() | Exception: Cannot find directory. path: {path ?? "null"} | Exception Details: {dnfe.Message}"); + return IOActionResultState.DirectoryMissing; + } + catch (PathTooLongException ptle) + { + ModUtils.Logging.PrintError( + $"ModUtils::CreateFilePath() | Exception: path length is over 200 characters. path: {formattedFilePath ?? "null"} | Exception Details: {ptle.Message}"); + return IOActionResultState.PathTooLong; + } + catch (NotSupportedException nse) + { + ModUtils.Logging.PrintError( + $"ModUtils::CreateFilePath() | Exception: Operation not supported on your platform/environment (permissions?). path: {formattedFilePath ?? "null"} | Exception Details: {nse.Message}"); + return IOActionResultState.InvalidOperation; + } + catch (IOException ioe) + { + ModUtils.Logging.PrintError( + $"ModUtils::CreateFilePath() | Exception: IO tasks failed (Operation not supported). path: {formattedFilePath ?? "null"} | Exception Details: {ioe.Message}"); + return IOActionResultState.IOFailure; + } + catch (Exception e) + { + ModUtils.Logging.PrintError( + $"ModUtils::CreateFilePath() | Exception: Unknown/Other Exception. path: {path ?? "null"} | Exception Details: {e.Message}"); + return IOActionResultState.UnknownError; + } + } + + public static IOActionResultState WriteFileText(string filePath, string fileText) + { + IOActionResultState ioActionResultState = CreateFilePath(filePath, out var fp); + if (ioActionResultState == IOActionResultState.Success) + { + try + { + File.WriteAllText(fp!, fileText); + return IOActionResultState.Success; + } + catch (ArgumentNullException ane) + { + ModUtils.Logging.PrintError( + $"ModUtils::WriteFileText() | Exception: An argument is null. path: {fp ?? "null"} | Exception Details: {ane.Message}"); + return IOActionResultState.FilePathNull; + } + catch (ArgumentException ae) + { + ModUtils.Logging.PrintError( + $"ModUtils::WriteFileText() | Exception: An argument is invalid. path: {fp ?? "null"} | Exception Details: {ae.Message}"); + return IOActionResultState.FilePathInvalid; + } + catch (DirectoryNotFoundException dnfe) + { + ModUtils.Logging.PrintError( + $"ModUtils::WriteFileText() | Exception: Cannot find directory. path: {fp ?? "null"} | Exception Details: {dnfe.Message}"); + return IOActionResultState.DirectoryMissing; + } + catch (PathTooLongException ptle) + { + ModUtils.Logging.PrintError( + $"ModUtils::WriteFileText() | Exception: path length is over 200 characters. path: {fp ?? "null"} | Exception Details: {ptle.Message}"); + return IOActionResultState.PathTooLong; + } + catch (NotSupportedException nse) + { + ModUtils.Logging.PrintError( + $"ModUtils::WriteFileText() | Exception: Operation not supported on your platform/environment (permissions?). path: {fp ?? "null"} | Exception Details: {nse.Message}"); + return IOActionResultState.InvalidOperation; + } + catch (IOException ioe) + { + ModUtils.Logging.PrintError( + $"ModUtils::WriteFileText() | Exception: IO tasks failed (Operation not supported). path: {fp ?? "null"} | Exception Details: {ioe.Message}"); + return IOActionResultState.IOFailure; + } + catch (Exception e) + { + ModUtils.Logging.PrintError( + $"ModUtils::WriteFileText() | Exception: Unknown/Other Exception. path: {fp ?? "null"} | ExceptionMessage: {e.Message}"); + return IOActionResultState.UnknownError; + } + } + + return ioActionResultState; + } + + /// + /// + /// + /// + /// + /// + /// + /// + /// + public static bool LoadOrCreateTypeXml(out T instance, + string filepath, Func typeFactory = null, bool createFile = true) where T : class, new() + { + instance = null; + filepath = filepath.CleanUpPath(); + if (IOActionResultState.Success == GetOrCreateFileText( + filepath, out string fileText, typeFactory is not null + ? () => + { + using StringWriter sw = new StringWriter(); + T t = typeFactory?.Invoke(); + if (t is not null) + { + XmlSerializer s = new XmlSerializer(typeof(T)); + s.Serialize(sw, t); + return sw.ToString(); + } + + return ""; + } + : null, createFile)) + { + XmlSerializer s = new XmlSerializer(typeof(T)); + try + { + using TextReader tr = new StringReader(fileText); + instance = (T)s.Deserialize(tr); + return true; + } + catch (InvalidOperationException ioe) + { + ModUtils.Logging.PrintError($"Error while parsing type data for {typeof(T)}."); +#if DEBUG + ModUtils.Logging.PrintError( + $"Exception: {ioe.Message}. Details: {ioe.InnerException?.Message}"); +#endif + instance = null; + return false; + } + } + + return false; + } + + public enum IOActionResultState + { + Success, + FileNotFound, + FilePathNull, + FilePathInvalid, + DirectoryMissing, + PathTooLong, + InvalidOperation, + IOFailure, + UnknownError + } + } + + #endregion + + #region GAME + + public static class Game + { + /// + /// Returns whether or not there is a round running. + /// + /// + public static bool IsRoundInProgress() + { +#if CLIENT + if (Screen.Selected is not null + && Screen.Selected.IsEditor) + return false; +#endif + return GameMain.GameSession is not null && Level.Loaded is not null; + } + + } + + #endregion + + #region THREADING + + public static class Threading + { + /// + /// Gets the boolean value of an integer with thread-safety via Interlocked. + /// + /// + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool GetBool(ref int var) => Interlocked.CompareExchange(ref var, 1, 1) > 0; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void SetBool(ref int var, bool value) + { + if (value) + { + Interlocked.CompareExchange(ref var, 1, 0); + } + else + { + Interlocked.CompareExchange(ref var, 0, 1); + } + } + + /// + /// Gets if the integer is under 1 (is zero/false) and, if so, sets the value to one/true. + /// + /// + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool CheckIfClearAndSetBool(ref int var) + { + return Interlocked.CompareExchange(ref var, 1, 0) < 1; + } + + /// + /// Gets if the integer is over 0 (is one/true) and, if so, sets the value to zero/false. + /// + /// + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool CheckIfSetAndClearBool(ref int var) + { + return Interlocked.CompareExchange(ref var, 0, 1) > 0; + } + } + + #endregion + + #region UTILITIES_CORE + + public static V TryGetOrSet(this IDictionary dict, K key, Func valueFactory) where K : IEquatable + { + if (dict.TryGetValue(key, out var dictValue)) return dictValue; + if (valueFactory is not null) + dict.Add(key, valueFactory()); + else + return default; + return dict[key]; + } + + #endregion + } + + public static class AssemblyExtensions + { + /// + /// Gets all types in the given assembly. Handles invalid type scenarios. + /// + /// The assembly to scan + /// An enumerable collection of types. + public static IEnumerable GetSafeTypes(this Assembly assembly) + { + // Based on https://github.com/Qkrisi/ktanemodkit/blob/master/Assets/Scripts/ReflectionHelper.cs#L53-L67 + + try + { + return assembly.GetTypes(); + } + catch (ReflectionTypeLoadException re) + { + try + { + return re.Types.Where(x => x != null)!; + } + catch (InvalidOperationException) + { + return new List(); + } + } + catch (Exception) + { + return new List(); + } } } - #endregion - - #region FILE_IO - - // ReSharper disable once InconsistentNaming - public static class IO + public static class CollectionExtensions { - public static IEnumerable FindAllFilesInDirectory(string folder, string pattern, - SearchOption option) - { - try - { - return Directory.GetFiles(folder, pattern, option); - } - catch (DirectoryNotFoundException e) - { - return new string[] { }; - } - } - - public static string PrepareFilePathString(string filePath) => - PrepareFilePathString(Path.GetDirectoryName(filePath)!, Path.GetFileName(filePath)); - - public static string PrepareFilePathString(string path, string fileName) => - Path.Combine(SanitizePath(path), SanitizeFileName(fileName)); - - public static string SanitizeFileName(string fileName) - { - foreach (char c in Barotrauma.IO.Path.GetInvalidFileNameCharsCrossPlatform()) - fileName = fileName.Replace(c, '_'); - return fileName; - } - /// - /// Gets the sanitized path for the top-level directory for a given content package. + /// Executes a series of asynchronous tasks with limited parallelism to maintain execution efficiency. /// - /// - /// - public static string GetContentPackageDir(ContentPackage package) - { - return SanitizePath(Path.GetFullPath(package.Dir)); - } - - public static string SanitizePath(string path) - { - foreach (char c in Path.GetInvalidPathChars()) - path = path.Replace(c.ToString(), "_"); - return path.CleanUpPath(); - } - - public static IOActionResultState GetOrCreateFileText(string filePath, out string fileText, Func fileDataFactory = null, bool createFile = true) - { - fileText = null; - string fp = Path.GetFullPath(SanitizePath(filePath)); - - IOActionResultState ioActionResultState = IOActionResultState.Success; - if (createFile) - { - ioActionResultState = CreateFilePath(SanitizePath(filePath), out fp, fileDataFactory); - } - else if (!File.Exists(fp)) - { - return IOActionResultState.FileNotFound; - } - - if (ioActionResultState == IOActionResultState.Success) - { - try - { - fileText = File.ReadAllText(fp!); - return IOActionResultState.Success; - } - catch (ArgumentNullException ane) - { - ModUtils.Logging.PrintError($"ModUtils::CreateFilePath() | Exception: An argument is null. path: {fp ?? "null"} | Exception Details: {ane.Message}"); - return IOActionResultState.FilePathNull; - } - catch (ArgumentException ae) - { - ModUtils.Logging.PrintError($"ModUtils::CreateFilePath() | Exception: An argument is invalid. path: {fp ?? "null"} | Exception Details: {ae.Message}"); - return IOActionResultState.FilePathInvalid; - } - catch (DirectoryNotFoundException dnfe) - { - ModUtils.Logging.PrintError($"ModUtils::CreateFilePath() | Exception: Cannot find directory. path: {fp ?? "null"} | Exception Details: {dnfe.Message}"); - return IOActionResultState.DirectoryMissing; - } - catch (PathTooLongException ptle) - { - ModUtils.Logging.PrintError($"ModUtils::CreateFilePath() | Exception: path length is over 200 characters. path: {fp ?? "null"} | Exception Details: {ptle.Message}"); - return IOActionResultState.PathTooLong; - } - catch (NotSupportedException nse) - { - ModUtils.Logging.PrintError($"ModUtils::CreateFilePath() | Exception: Operation not supported on your platform/environment (permissions?). path: {fp ?? "null"} | Exception Details: {nse.Message}"); - return IOActionResultState.InvalidOperation; - } - catch (IOException ioe) - { - ModUtils.Logging.PrintError($"ModUtils::CreateFilePath() | Exception: IO tasks failed (Operation not supported). path: {fp ?? "null"} | Exception Details: {ioe.Message}"); - return IOActionResultState.IOFailure; - } - catch (Exception e) - { - ModUtils.Logging.PrintError($"ModUtils::CreateFilePath() | Exception: Unknown/Other Exception. path: {fp ?? "null"} | ExceptionMessage: {e.Message}"); - return IOActionResultState.UnknownError; - } - } - - return ioActionResultState; - } - - public static IOActionResultState CreateFilePath(string filePath, out string formattedFilePath, Func fileDataFactory = null) - { - string file = Path.GetFileName(filePath); - string path = Path.GetDirectoryName(filePath)!; - - formattedFilePath = IO.PrepareFilePathString(path, file); - try - { - if (!Directory.Exists(path)) - Directory.CreateDirectory(path); - if (!File.Exists(formattedFilePath)) - File.WriteAllText(formattedFilePath, fileDataFactory is null ? "" : fileDataFactory.Invoke()); - return IOActionResultState.Success; - } - catch (ArgumentNullException ane) - { - ModUtils.Logging.PrintError($"ModUtils::CreateFilePath() | Exception: An argument is null. path: {formattedFilePath ?? "null"} | Exception Details: {ane.Message}"); - return IOActionResultState.FilePathNull; - } - catch (ArgumentException ae) - { - ModUtils.Logging.PrintError($"ModUtils::CreateFilePath() | Exception: An argument is invalid. path: {formattedFilePath ?? "null"} | Exception Details: {ae.Message}"); - return IOActionResultState.FilePathInvalid; - } - catch (DirectoryNotFoundException dnfe) - { - ModUtils.Logging.PrintError($"ModUtils::CreateFilePath() | Exception: Cannot find directory. path: {path ?? "null"} | Exception Details: {dnfe.Message}"); - return IOActionResultState.DirectoryMissing; - } - catch (PathTooLongException ptle) - { - ModUtils.Logging.PrintError($"ModUtils::CreateFilePath() | Exception: path length is over 200 characters. path: {formattedFilePath ?? "null"} | Exception Details: {ptle.Message}"); - return IOActionResultState.PathTooLong; - } - catch (NotSupportedException nse) - { - ModUtils.Logging.PrintError($"ModUtils::CreateFilePath() | Exception: Operation not supported on your platform/environment (permissions?). path: {formattedFilePath ?? "null"} | Exception Details: {nse.Message}"); - return IOActionResultState.InvalidOperation; - } - catch (IOException ioe) - { - ModUtils.Logging.PrintError($"ModUtils::CreateFilePath() | Exception: IO tasks failed (Operation not supported). path: {formattedFilePath ?? "null"} | Exception Details: {ioe.Message}"); - return IOActionResultState.IOFailure; - } - catch (Exception e) - { - ModUtils.Logging.PrintError($"ModUtils::CreateFilePath() | Exception: Unknown/Other Exception. path: {path ?? "null"} | Exception Details: {e.Message}"); - return IOActionResultState.UnknownError; - } - } - - public static IOActionResultState WriteFileText(string filePath, string fileText) - { - IOActionResultState ioActionResultState = CreateFilePath(filePath, out var fp); - if (ioActionResultState == IOActionResultState.Success) - { - try - { - File.WriteAllText(fp!, fileText); - return IOActionResultState.Success; - } - catch (ArgumentNullException ane) - { - ModUtils.Logging.PrintError($"ModUtils::WriteFileText() | Exception: An argument is null. path: {fp ?? "null"} | Exception Details: {ane.Message}"); - return IOActionResultState.FilePathNull; - } - catch (ArgumentException ae) - { - ModUtils.Logging.PrintError($"ModUtils::WriteFileText() | Exception: An argument is invalid. path: {fp ?? "null"} | Exception Details: {ae.Message}"); - return IOActionResultState.FilePathInvalid; - } - catch (DirectoryNotFoundException dnfe) - { - ModUtils.Logging.PrintError($"ModUtils::WriteFileText() | Exception: Cannot find directory. path: {fp ?? "null"} | Exception Details: {dnfe.Message}"); - return IOActionResultState.DirectoryMissing; - } - catch (PathTooLongException ptle) - { - ModUtils.Logging.PrintError($"ModUtils::WriteFileText() | Exception: path length is over 200 characters. path: {fp ?? "null"} | Exception Details: {ptle.Message}"); - return IOActionResultState.PathTooLong; - } - catch (NotSupportedException nse) - { - ModUtils.Logging.PrintError($"ModUtils::WriteFileText() | Exception: Operation not supported on your platform/environment (permissions?). path: {fp ?? "null"} | Exception Details: {nse.Message}"); - return IOActionResultState.InvalidOperation; - } - catch (IOException ioe) - { - ModUtils.Logging.PrintError($"ModUtils::WriteFileText() | Exception: IO tasks failed (Operation not supported). path: {fp ?? "null"} | Exception Details: {ioe.Message}"); - return IOActionResultState.IOFailure; - } - catch (Exception e) - { - ModUtils.Logging.PrintError($"ModUtils::WriteFileText() | Exception: Unknown/Other Exception. path: {fp ?? "null"} | ExceptionMessage: {e.Message}"); - return IOActionResultState.UnknownError; - } - } - - return ioActionResultState; - } - - /// - /// - /// - /// - /// - /// - /// + /// + /// + /// /// /// - public static bool LoadOrCreateTypeXml(out T instance, - string filepath, Func typeFactory = null, bool createFile = true) where T : class, new() + public static Task ParallelForEachAsync(this IEnumerable source, Func funcBody, int maxDegreeOfParallelism = 4) { - instance = null; - filepath = filepath.CleanUpPath(); - if (IOActionResultState.Success == GetOrCreateFileText( - filepath, out string fileText, typeFactory is not null ? () => - { - using StringWriter sw = new StringWriter(); - T t = typeFactory?.Invoke(); - if (t is not null) - { - XmlSerializer s = new XmlSerializer(typeof(T)); - s.Serialize(sw, t); - return sw.ToString(); - } - return ""; - } : null, createFile)) + async Task AwaitParallelLimit(IEnumerator partition) { - XmlSerializer s = new XmlSerializer(typeof(T)); - try + using (partition) { - using TextReader tr = new StringReader(fileText); - instance = (T)s.Deserialize(tr); - return true; - } - catch(InvalidOperationException ioe) - { - ModUtils.Logging.PrintError($"Error while parsing type data for {typeof(T)}."); - #if DEBUG - ModUtils.Logging.PrintError($"Exception: {ioe.Message}. Details: {ioe.InnerException?.Message}"); - #endif - instance = null; - return false; + while (partition.MoveNext()) + { + await Task.Yield(); // prevents a sync/hot thread hangup + await funcBody(partition.Current); + } } } - return false; - } - - public enum IOActionResultState - { - Success, FileNotFound, FilePathNull, FilePathInvalid, DirectoryMissing, PathTooLong, InvalidOperation, IOFailure, UnknownError + return Task.WhenAll( + Partitioner + .Create(source) + .GetPartitions(maxDegreeOfParallelism) + .AsParallel() + .Select(p => AwaitParallelLimit(p))); } } - - #endregion - - #region GAME - - public static class Game - { - /// - /// Returns whether or not there is a round running. - /// - /// - public static bool IsRoundInProgress() - { -#if CLIENT - if (Screen.Selected is not null - && Screen.Selected.IsEditor) - return false; -#endif - return GameMain.GameSession is not null && Level.Loaded is not null; - } - - } - - #endregion } + + + +#region ExceptionData + +namespace FluentResults.LuaCs +{ + public static class MetadataType + { + public static string ExceptionDetails = nameof(ExceptionDetails); + /// + /// The object that threw the exception. + /// + public static string ExceptionObject = nameof(ExceptionObject); + /// + /// The parameter-object responsible for the exception thrown (not the exception thrower). + /// + public static string RootObject = nameof(RootObject); + /// + /// Additional exception sources. + /// + public static string Sources = nameof(Sources); + public static string StackTrace = nameof(StackTrace); + } +} + +#endregion diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Plugins/ApplicationMode.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Plugins/ApplicationMode.cs deleted file mode 100644 index 6e60184bb..000000000 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Plugins/ApplicationMode.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Barotrauma; - -public enum ApplicationMode -{ - Client, Server -} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Plugins/AssemblyLoadingSuccessState.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Plugins/AssemblyLoadingSuccessState.cs deleted file mode 100644 index e55821eb3..000000000 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Plugins/AssemblyLoadingSuccessState.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace Barotrauma; - -public enum AssemblyLoadingSuccessState -{ - ACLLoadFailure, - AlreadyLoaded, - BadFilePath, - CannotLoadFile, - InvalidAssembly, - NoAssemblyFound, - PluginInstanceFailure, - BadName, - CannotLoadFromStream, - Success -} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Plugins/AssemblyManager.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Plugins/AssemblyManager.cs deleted file mode 100644 index c7f582395..000000000 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Plugins/AssemblyManager.cs +++ /dev/null @@ -1,901 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.Loader; -using System.Threading; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; - -// ReSharper disable EventNeverSubscribedTo.Global -// ReSharper disable InconsistentNaming - -namespace Barotrauma; - -/*** - * Note: This class was written to be thread-safe in order to allow parallelization in loading in the future if the need - * becomes necessary as there is almost no serial performance overhead for adding threading protection. - */ - -/// -/// Provides functionality for the loading, unloading and management of plugins implementing IAssemblyPlugin. -/// All plugins are loaded into their own AssemblyLoadContext along with their dependencies. -/// -public class AssemblyManager -{ - #region ExternalAPI - - /// - /// Called when an assembly is loaded. - /// - public event Action OnAssemblyLoaded; - - /// - /// Called when an assembly is marked for unloading, before unloading begins. You should use this to cleanup - /// any references that you have to this assembly. - /// - public event Action OnAssemblyUnloading; - - /// - /// Called whenever an exception is thrown. First arg is a formatted message, Second arg is the Exception. - /// - public event Action OnException; - - /// - /// For unloading issue debugging. Called whenever MemoryFileAssemblyContextLoader [load context] is unloaded. - /// - public event Action OnACLUnload; - - - /// - /// [DEBUG ONLY] - /// Returns a list of the current unloading ACLs. - /// - public ImmutableList> StillUnloadingACLs - { - get - { - OpsLockUnloaded.EnterReadLock(); - try - { - return UnloadingACLs.ToImmutableList(); - } - finally - { - OpsLockUnloaded.ExitReadLock(); - } - } - } - - - // ReSharper disable once MemberCanBePrivate.Global - /// - /// Checks if there are any AssemblyLoadContexts still in the process of unloading. - /// - public bool IsCurrentlyUnloading - { - get - { - OpsLockUnloaded.EnterReadLock(); - try - { - return UnloadingACLs.Any(); - } - catch (Exception) - { - return false; - } - finally - { - OpsLockUnloaded.ExitReadLock(); - } - } - } - - // Old API compatibility - public IEnumerable GetSubTypesInLoadedAssemblies() - { - return GetSubTypesInLoadedAssemblies(false); - } - - - /// - /// Allows iteration over all non-interface types in all loaded assemblies in the AsmMgr that are assignable to the given type (IsAssignableFrom). - /// Warning: care should be used when using this method in hot paths as performance may be affected. - /// - /// The type to compare against - /// Forces caches to clear and for the lists of types to be rebuilt. - /// An Enumerator for matching types. - public IEnumerable GetSubTypesInLoadedAssemblies(bool rebuildList) - { - Type targetType = typeof(T); - string typeName = targetType.FullName ?? targetType.Name; - - // rebuild - if (rebuildList) - RebuildTypesList(); - - // check cache - if (_subTypesLookupCache.TryGetValue(typeName, out var subTypeList)) - { - return subTypeList; - } - - // build from scratch - OpsLockLoaded.EnterReadLock(); - try - { - // build list - var list1 = _defaultContextTypes - .Where(kvp1 => targetType.IsAssignableFrom(kvp1.Value) && !kvp1.Value.IsInterface) - .Concat(LoadedACLs - .SelectMany(kvp => kvp.Value.AssembliesTypes) - .Where(kvp2 => targetType.IsAssignableFrom(kvp2.Value) && !kvp2.Value.IsInterface)) - .Select(kvp3 => kvp3.Value) - .ToImmutableList(); - - // only add if we find something - if (list1.Count > 0) - { - if (!_subTypesLookupCache.TryAdd(typeName, list1)) - { - ModUtils.Logging.PrintError( - $"{nameof(AssemblyManager)}: Unable to add subtypes to cache of type {typeName}!"); - } - } - else - { - ModUtils.Logging.PrintMessage( - $"{nameof(AssemblyManager)}: Warning: No types found during search for subtypes of {typeName}"); - } - - return list1; - } - catch (Exception e) - { - this.OnException?.Invoke($"{nameof(AssemblyManager)}::{nameof(GetSubTypesInLoadedAssemblies)}() | Error: {e.Message}", e); - return ImmutableList.Empty; - } - finally - { - OpsLockLoaded.ExitReadLock(); - } - } - - /// - /// Tries to get types assignable to type from the ACL given the Guid. - /// - /// - /// - /// - /// - public bool TryGetSubTypesFromACL(Guid id, out IEnumerable types) - { - Type targetType = typeof(T); - - if (TryGetACL(id, out var acl)) - { - types = acl.AssembliesTypes - .Where(kvp => targetType.IsAssignableFrom(kvp.Value) && !kvp.Value.IsInterface) - .Select(kvp => kvp.Value); - return true; - } - - types = null; - return false; - } - - /// - /// Tries to get types from the ACL given the Guid. - /// - /// - /// - /// - public bool TryGetSubTypesFromACL(Guid id, out IEnumerable types) - { - if (TryGetACL(id, out var acl)) - { - types = acl.AssembliesTypes.Select(kvp => kvp.Value); - return true; - } - - types = null; - return false; - } - - - /// - /// Allows iteration over all types, including interfaces, in all loaded assemblies in the AsmMgr who's names match the string. - /// Note: Will return the by-reference equivalent type if the type name is prefixed with "out " or "ref ". - /// - /// The string name of the type to search for. - /// An Enumerator for matching types. List will be empty if bad params are supplied. - public IEnumerable GetTypesByName(string typeName) - { - List types = new(); - if (typeName.IsNullOrWhiteSpace()) - return types; - - bool byRef = false; - if (typeName.StartsWith("out ") || typeName.StartsWith("ref ")) - { - typeName = typeName.Remove(0, 4); - byRef = true; - } - - - TypesListHelper(); - if (types.Count > 0) - return types; - - // we couldn't find it, rebuild and try one more time - RebuildTypesList(); - TypesListHelper(); - - if (types.Count > 0) - return types; - - OpsLockLoaded.EnterReadLock(); - try - { - // fallback to Type.GetType - Type t = Type.GetType(typeName, false, false); - if (t is not null) - { - types.Add(byRef ? t.MakeByRefType() : t); - return types; - } - - foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies()) - { - try - { - t = assembly.GetType(typeName, false, false); - if (t is not null) - types.Add(byRef ? t.MakeByRefType() : t); - } - catch (Exception e) - { - this.OnException?.Invoke( - $"{nameof(AssemblyManager)}::{nameof(GetTypesByName)}() | Error: {e.Message}", e); - } - } - } - finally - { - OpsLockLoaded.ExitReadLock(); - } - - return types; - - void TypesListHelper() - { - if (_defaultContextTypes.TryGetValue(typeName, out var type1)) - { - if (type1 is not null) - types.Add(byRef ? type1.MakeByRefType() : type1); - } - - OpsLockLoaded.EnterReadLock(); - try - { - foreach (KeyValuePair loadedAcl in LoadedACLs) - { - var at = loadedAcl.Value.AssembliesTypes; - if (at.TryGetValue(typeName, out var type2)) - { - if (type2 is not null) - types.Add(byRef ? type2.MakeByRefType() : type2); - } - } - } - finally - { - OpsLockLoaded.ExitReadLock(); - } - } - } - - /// - /// Allows iteration over all types (including interfaces) in all loaded assemblies managed by the AsmMgr. - /// Warning: High usage may result in performance issues. - /// - /// An Enumerator for iteration. - public IEnumerable GetAllTypesInLoadedAssemblies() - { - OpsLockLoaded.EnterReadLock(); - try - { - return _defaultContextTypes - .Select(kvp => kvp.Value) - .Concat(LoadedACLs - .SelectMany(kvp => kvp.Value?.AssembliesTypes.Select(kv => kv.Value))) - .ToImmutableList(); - } - catch - { - return ImmutableList.Empty; - } - finally - { - OpsLockLoaded.ExitReadLock(); - } - } - - /// - /// Returns a list of all loaded ACLs. - /// WARNING: References to these ACLs outside of the AssemblyManager should be kept in a WeakReference in order - /// to avoid causing issues with unloading/disposal. - /// - /// - public IEnumerable GetAllLoadedACLs() - { - OpsLockLoaded.EnterReadLock(); - try - { - if (!LoadedACLs.Any()) - { - return ImmutableList.Empty; - } - - return LoadedACLs.Select(kvp => kvp.Value).ToImmutableList(); - } - catch - { - return ImmutableList.Empty; - } - finally - { - OpsLockLoaded.ExitReadLock(); - } - } - - #endregion - - #region InternalAPI - - /// - /// [Unsafe] Warning: only for use in nested threading functions. Requires care to manage access. - /// Does not make any guarantees about the state of the ACL after the list has been returned. - /// - /// - [MethodImpl(MethodImplOptions.Synchronized | MethodImplOptions.NoInlining)] - internal ImmutableList UnsafeGetAllLoadedACLs() - { - if (LoadedACLs.IsEmpty) - return ImmutableList.Empty; - return LoadedACLs.Select(kvp => kvp.Value).ToImmutableList(); - } - - /// - /// Used by content package and plugin management to stop unloading of a given ACL until all plugins have gracefully closed. - /// - public event System.Func IsReadyToUnloadACL; - - /// - /// Compiles an assembly from supplied references and syntax trees into the specified AssemblyContextLoader. - /// A new ACL will be created if the Guid supplied is Guid.Empty. - /// - /// - /// - /// - /// - /// A non-unique name for later reference. Optional, set to null if unused. - /// The guid of the assembly - /// - /// - public AssemblyLoadingSuccessState LoadAssemblyFromMemory([NotNull] string compiledAssemblyName, - [NotNull] IEnumerable syntaxTree, - IEnumerable externalMetadataReferences, - [NotNull] CSharpCompilationOptions compilationOptions, - string friendlyName, - ref Guid id, - IEnumerable externFileAssemblyRefs = null) - { - // validation - if (compiledAssemblyName.IsNullOrWhiteSpace()) - return AssemblyLoadingSuccessState.BadName; - - if (syntaxTree is null) - return AssemblyLoadingSuccessState.InvalidAssembly; - - if (!GetOrCreateACL(id, friendlyName, out var acl)) - return AssemblyLoadingSuccessState.ACLLoadFailure; - - id = acl.Id; // pass on true id returned - - // this acl is already hosting an in-memory assembly - if (acl.Acl.CompiledAssembly is not null) - return AssemblyLoadingSuccessState.AlreadyLoaded; - - // compile - AssemblyLoadingSuccessState state; - string messages; - try - { - state = acl.Acl.CompileAndLoadScriptAssembly(compiledAssemblyName, syntaxTree, externalMetadataReferences, - compilationOptions, out messages, externFileAssemblyRefs); - } - catch (Exception e) - { - ModUtils.Logging.PrintError($"{nameof(AssemblyManager)}::{nameof(LoadAssemblyFromMemory)}() | Failed to compile and load assemblies for [ {compiledAssemblyName} / {friendlyName} ]! Details: {e.Message} | {e.StackTrace}"); - return AssemblyLoadingSuccessState.InvalidAssembly; - } - - // get types - if (state is AssemblyLoadingSuccessState.Success) - { - _subTypesLookupCache.Clear(); - acl.RebuildTypesList(); - OnAssemblyLoaded?.Invoke(acl.Acl.CompiledAssembly); - } - else - { - ModUtils.Logging.PrintError($"Unable to compile assembly '{compiledAssemblyName}' due to errors: {messages}"); - } - - return state; - } - - /// - /// Switches the ACL with the given Guid to Template Mode, which disables assembly name resolution for any assemblies loaded in it. - /// These ACLs are intended to be used to host Assemblies for information only and not for code execution. - /// WARNING: This process is irreversible. - /// - /// Guid of the ACL. - /// Whether or not an ACL was found with the given ID. - public bool SetACLToTemplateMode(Guid guid) - { - if (!TryGetACL(guid, out var acl)) - return false; - acl.Acl.IsTemplateMode = true; - return true; - } - - /// - /// Tries to load all assemblies at the supplied file paths list into the ACl with the given Guid. - /// If the supplied Guid is Empty, then a new ACl will be created and the Guid will be assigned to it. - /// - /// List of assemblies to try and load. - /// A non-unique name for later reference. Optional. - /// Guid of the ACL or Empty if none specified. Guid of ACL will be assigned to this var. - /// Operation success messages. - /// - public AssemblyLoadingSuccessState LoadAssembliesFromLocations([NotNull] IEnumerable filePaths, - string friendlyName, ref Guid id) - { - - if (filePaths is null) - { - var exception = new ArgumentNullException( - $"{nameof(AssemblyManager)}::{nameof(LoadAssembliesFromLocations)}() | file paths supplied is null!"); - this.OnException?.Invoke($"Error: {exception.Message}", exception); - throw exception; - } - - ImmutableList assemblyFilePaths = filePaths.ToImmutableList(); // copy the list before loading - - if (!assemblyFilePaths.Any()) - { - return AssemblyLoadingSuccessState.NoAssemblyFound; - } - - if (GetOrCreateACL(id, friendlyName, out var loadedAcl)) - { - var state = loadedAcl.Acl.LoadFromFiles(assemblyFilePaths); - // if failure, we dispose of the acl - if (state != AssemblyLoadingSuccessState.Success) - { - DisposeACL(loadedAcl.Id); - ModUtils.Logging.PrintError($"ACL {friendlyName} failed, unloading..."); - return state; - } - // build types list - _subTypesLookupCache.Clear(); - loadedAcl.RebuildTypesList(); - id = loadedAcl.Id; - foreach (Assembly assembly in loadedAcl.Acl.Assemblies) - { - OnAssemblyLoaded?.Invoke(assembly); - } - return state; - } - - return AssemblyLoadingSuccessState.ACLLoadFailure; - } - - - [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.Synchronized)] - public bool TryBeginDispose() - { - OpsLockLoaded.EnterWriteLock(); - OpsLockUnloaded.EnterWriteLock(); - try - { - _subTypesLookupCache.Clear(); - _defaultContextTypes = _defaultContextTypes.Clear(); - - foreach (KeyValuePair loadedAcl in LoadedACLs) - { - if (loadedAcl.Value.Acl is not null) - { - if (IsReadyToUnloadACL is not null) - { - foreach (Delegate del in IsReadyToUnloadACL.GetInvocationList()) - { - if (del is System.Func { } func) - { - if (!func.Invoke(loadedAcl.Value)) - return false; // Not ready, exit - } - } - } - - foreach (Assembly assembly in loadedAcl.Value.Acl.Assemblies) - { - OnAssemblyUnloading?.Invoke(assembly); - } - - UnloadingACLs.Add(new WeakReference(loadedAcl.Value.Acl, true)); - loadedAcl.Value.ClearTypesList(); - loadedAcl.Value.Acl.Unload(); - loadedAcl.Value.ClearACLRef(); - OnACLUnload?.Invoke(loadedAcl.Value.Id); - } - } - - LoadedACLs.Clear(); - return true; - } - catch(Exception e) - { - // should never happen - this.OnException?.Invoke($"{nameof(TryBeginDispose)}() | Error: {e.Message}", e); - return false; - } - finally - { - OpsLockUnloaded.ExitWriteLock(); - OpsLockLoaded.ExitWriteLock(); - } - } - - - [MethodImpl(MethodImplOptions.NoInlining)] - public bool FinalizeDispose() - { - bool isUnloaded; - OpsLockUnloaded.EnterUpgradeableReadLock(); - try - { - GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced); // force the gc to collect unloaded acls. - List> toRemove = new(); - foreach (WeakReference weakReference in UnloadingACLs) - { - if (!weakReference.TryGetTarget(out _)) - { - toRemove.Add(weakReference); - } - } - - if (toRemove.Any()) - { - OpsLockUnloaded.EnterWriteLock(); - try - { - foreach (WeakReference reference in toRemove) - { - UnloadingACLs.Remove(reference); - } - } - finally - { - OpsLockUnloaded.ExitWriteLock(); - } - } - isUnloaded = !UnloadingACLs.Any(); - } - finally - { - OpsLockUnloaded.ExitUpgradeableReadLock(); - } - - return isUnloaded; - } - - /// - /// Tries to retrieve the LoadedACL with the given ID or null if none is found. - /// WARNING: External references to this ACL with long lifespans should be kept in a WeakReference - /// to avoid causing unloading/disposal issues. - /// - /// GUID of the ACL. - /// The found ACL or null if none was found. - /// Whether or not an ACL was found. - [MethodImpl(MethodImplOptions.NoInlining)] - public bool TryGetACL(Guid id, out LoadedACL acl) - { - acl = null; - OpsLockLoaded.EnterReadLock(); - try - { - if (id.Equals(Guid.Empty) || !LoadedACLs.ContainsKey(id)) - return false; - acl = LoadedACLs[id]; - return true; - } - finally - { - OpsLockLoaded.ExitReadLock(); - } - } - - - /// - /// Gets or creates an AssemblyCtxLoader for the given ID. Creates if the ID is empty or no ACL can be found. - /// [IMPORTANT] After calling this method, the id you use should be taken from the acl container (acl.Id). - /// - /// - /// A non-unique name for later reference. Optional. - /// - /// Should only return false if an error occurs. - [MethodImpl(MethodImplOptions.NoInlining)] - private bool GetOrCreateACL(Guid id, string friendlyName, out LoadedACL acl) - { - OpsLockLoaded.EnterUpgradeableReadLock(); - try - { - if (id.Equals(Guid.Empty) || !LoadedACLs.ContainsKey(id) || LoadedACLs[id] is null) - { - OpsLockLoaded.EnterWriteLock(); - try - { - id = Guid.NewGuid(); - acl = new LoadedACL(id, this, friendlyName); - LoadedACLs[id] = acl; - return true; - } - finally - { - OpsLockLoaded.ExitWriteLock(); - } - } - else - { - acl = LoadedACLs[id]; - return true; - } - - } - catch(Exception e) - { - this.OnException?.Invoke($"{nameof(GetOrCreateACL)}Error: {e.Message}", e); - acl = null; - return false; - } - finally - { - OpsLockLoaded.ExitUpgradeableReadLock(); - } - } - - - [MethodImpl(MethodImplOptions.NoInlining)] - private bool DisposeACL(Guid id) - { - OpsLockLoaded.EnterWriteLock(); - OpsLockUnloaded.EnterWriteLock(); - try - { - if (LoadedACLs.ContainsKey(id) && LoadedACLs[id] == null) - { - if (!LoadedACLs.TryRemove(id, out _)) - { - ModUtils.Logging.PrintWarning($"An ACL with the GUID {id.ToString()} was found as null. Unable to remove null ACL entry."); - } - } - - if (id.Equals(Guid.Empty) || !LoadedACLs.ContainsKey(id)) - { - return false; // nothing to dispose of - } - - var acl = LoadedACLs[id]; - - foreach (Assembly assembly in acl.Acl.Assemblies) - { - OnAssemblyUnloading?.Invoke(assembly); - } - - _subTypesLookupCache.Clear(); - UnloadingACLs.Add(new WeakReference(acl.Acl, true)); - acl.Acl.Unload(); - acl.ClearACLRef(); - OnACLUnload?.Invoke(acl.Id); - - return true; - } - catch (Exception e) - { - this.OnException?.Invoke($"{nameof(DisposeACL)}() | Error: {e.Message}", e); - return false; - } - finally - { - OpsLockLoaded.ExitWriteLock(); - OpsLockUnloaded.ExitWriteLock(); - } - } - - internal AssemblyManager() - { - RebuildTypesList(); - } - - /// - /// Rebuilds the list of types in the default assembly load context. - /// - private void RebuildTypesList() - { - try - { - _defaultContextTypes = AssemblyLoadContext.Default.Assemblies - .SelectMany(a => a.GetSafeTypes()) - .ToImmutableDictionary(t => t.FullName ?? t.Name, t => t); - _subTypesLookupCache.Clear(); - } - catch(ArgumentException ae) - { - this.OnException?.Invoke($"{nameof(RebuildTypesList)}() | Error: {ae.Message}", ae); - try - { - // some types must've had duplicate type names, build the list while filtering - Dictionary types = new(); - foreach (var type in AssemblyLoadContext.Default.Assemblies.SelectMany(a => a.GetSafeTypes())) - { - try - { - types.TryAdd(type.FullName ?? type.Name, type); - } - catch - { - // ignore, null key exception - } - } - - _defaultContextTypes = types.ToImmutableDictionary(); - } - catch (Exception e) - { - this.OnException?.Invoke($"{nameof(RebuildTypesList)}() | Error: {e.Message}", e); - ModUtils.Logging.PrintError($"{nameof(AssemblyManager)}: Unable to create list of default assembly types! Default AssemblyLoadContext types searching not available."); -#if DEBUG - ModUtils.Logging.PrintError($"{nameof(AssemblyManager)}: Exception Details :{e.Message} | {e.InnerException}"); -#endif - _defaultContextTypes = ImmutableDictionary.Empty; - } - } - } - - #endregion - - #region Data - - private readonly ConcurrentDictionary> _subTypesLookupCache = new(); - private ImmutableDictionary _defaultContextTypes; - private readonly ConcurrentDictionary LoadedACLs = new(); - private readonly List> UnloadingACLs= new(); - private readonly ReaderWriterLockSlim OpsLockLoaded = new (); - private readonly ReaderWriterLockSlim OpsLockUnloaded = new (); - - #endregion - - #region TypeDefs - - - public sealed class LoadedACL - { - public readonly Guid Id; - private ImmutableDictionary _assembliesTypes = ImmutableDictionary.Empty; - public MemoryFileAssemblyContextLoader Acl { get; private set; } - - internal LoadedACL(Guid id, AssemblyManager manager, string friendlyName) - { - this.Id = id; - this.Acl = new(manager) - { - FriendlyName = friendlyName - }; - } - public ref readonly ImmutableDictionary AssembliesTypes => ref _assembliesTypes; - - /// - /// Warning: For use by the Assembly Manager only! Do not call this method otherwise. - /// - internal void ClearACLRef() - { - Acl = null; - } - - /// - /// Rebuild the list of types from assemblies loaded in the AsmCtxLoader. - /// - internal void RebuildTypesList() - { - if (this.Acl is null) - { - ModUtils.Logging.PrintWarning($"{nameof(RebuildTypesList)}() | ACL with GUID {Id.ToString()} is null, cannot rebuild."); - return; - } - - ClearTypesList(); - try - { - _assembliesTypes = this.Acl.Assemblies - .SelectMany(a => a.GetSafeTypes()) - .ToImmutableDictionary(t => t.FullName ?? t.Name, t => t); - } - catch(ArgumentException) - { - // some types must've had duplicate type names, build the list while filtering - Dictionary types = new(); - foreach (var type in this.Acl.Assemblies.SelectMany(a => a.GetSafeTypes())) - { - try - { - types.TryAdd(type.FullName ?? type.Name, type); - } - catch - { - // ignore, null key exception - } - } - - _assembliesTypes = types.ToImmutableDictionary(); - } - } - - internal void ClearTypesList() - { - _assembliesTypes = ImmutableDictionary.Empty; - } - } - - #endregion -} - -public static class AssemblyExtensions -{ - /// - /// Gets all types in the given assembly. Handles invalid type scenarios. - /// - /// The assembly to scan - /// An enumerable collection of types. - public static IEnumerable GetSafeTypes(this Assembly assembly) - { - // Based on https://github.com/Qkrisi/ktanemodkit/blob/master/Assets/Scripts/ReflectionHelper.cs#L53-L67 - - try - { - return assembly.GetTypes(); - } - catch (ReflectionTypeLoadException re) - { - try - { - return re.Types.Where(x => x != null)!; - } - catch (InvalidOperationException) - { - return new List(); - } - } - catch (Exception) - { - return new List(); - } - } -} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Plugins/CsPackageManager.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Plugins/CsPackageManager.cs deleted file mode 100644 index 8ba1f8921..000000000 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Plugins/CsPackageManager.cs +++ /dev/null @@ -1,1097 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Diagnostics.CodeAnalysis; -using System.IO; -using System.Linq; -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Text; -using System.Threading; -using Barotrauma.Steam; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using MonoMod.Utils; - -// ReSharper disable InconsistentNaming - -namespace Barotrauma; - -public sealed class CsPackageManager : IDisposable -{ - #region PRIVATE_FUNCDATA - - private static readonly CSharpParseOptions ScriptParseOptions = CSharpParseOptions.Default - .WithPreprocessorSymbols(new[] - { -#if SERVER - "SERVER" -#elif CLIENT - "CLIENT" -#else - "UNDEFINED" -#endif -#if DEBUG - ,"DEBUG" -#endif - }); - -#if WINDOWS - private const string PLATFORM_TARGET = "Windows"; -#elif OSX - private const string PLATFORM_TARGET = "OSX"; -#elif LINUX - private const string PLATFORM_TARGET = "Linux"; -#endif - -#if CLIENT - private const string ARCHITECTURE_TARGET = "Client"; -#elif SERVER - private const string ARCHITECTURE_TARGET = "Server"; -#endif - - private static readonly CSharpCompilationOptions CompilationOptions = new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary) - .WithMetadataImportOptions(MetadataImportOptions.All) -#if DEBUG - .WithOptimizationLevel(OptimizationLevel.Debug) -#else - .WithOptimizationLevel(OptimizationLevel.Release) -#endif - .WithAllowUnsafe(true); - - private static readonly SyntaxTree BaseAssemblyImports = CSharpSyntaxTree.ParseText( - new StringBuilder() - .AppendLine("using System.Reflection;") - .AppendLine("using Barotrauma;") - .AppendLine("using System.Runtime.CompilerServices;") - .AppendLine("[assembly: IgnoresAccessChecksTo(\"BarotraumaCore\")]") -#if CLIENT - .AppendLine("[assembly: IgnoresAccessChecksTo(\"Barotrauma\")]") -#elif SERVER - .AppendLine("[assembly: IgnoresAccessChecksTo(\"DedicatedServer\")]") -#endif - .ToString(), - ScriptParseOptions); - - private readonly string[] _publicizedAssembliesToLoad = - { - "BarotraumaCore.dll", -#if CLIENT - "Barotrauma.dll" -#elif SERVER - "DedicatedServer.dll" -#endif - }; - - - private const string SCRIPT_FILE_REGEX = "*.cs"; - private const string ASSEMBLY_FILE_REGEX = "*.dll"; - - private readonly float _assemblyUnloadTimeoutSeconds = 6f; - private Guid _publicizedAssemblyLoader; - private readonly List _currentPackagesByLoadOrder = new(); - private readonly Dictionary> _packagesDependencies = new(); - private readonly Dictionary _loadedCompiledPackageAssemblies = new(); - private readonly Dictionary _reverseLookupGuidList = new(); - private readonly Dictionary> _loadedPlugins = new (); - private readonly Dictionary> _pluginTypes = new(); // where Type : IAssemblyPlugin - private readonly Dictionary _packageRunConfigs = new(); - private readonly Dictionary> _luaRegisteredTypes = new(); - private readonly AssemblyManager _assemblyManager; - private readonly LuaCsSetup _luaCsSetup; - private DateTime _assemblyUnloadStartTime; - - - #endregion - - #region PUBLIC_API - - #region LUA_EXTENSIONS - - /// - /// Searches for all types in all loaded assemblies from content packages who's names contain the name string and registers them with the Lua Interpreter. - /// - /// - /// - /// - public bool LuaTryRegisterPackageTypes(string name, bool caseSensitive = false) - { - if (!AssembliesLoaded) - return false; - var matchingPacks = _loadedCompiledPackageAssemblies - .Where(kvp => kvp.Key.Name.ToLowerInvariant().Contains(name.ToLowerInvariant())) - .Select(kvp => kvp.Value) - .ToImmutableList(); - if (!matchingPacks.Any()) - return false; - var types = matchingPacks - .Where(guid => !_luaRegisteredTypes.ContainsKey(guid)) - .Select(guid => new KeyValuePair>( - guid, - _assemblyManager.TryGetSubTypesFromACL(guid, out var types) - ? types.ToImmutableList() - : ImmutableList.Empty)) - .ToImmutableList(); - if (!types.Any()) - return false; - foreach (var kvp in types) - { - _luaRegisteredTypes[kvp.Key] = kvp.Value; - foreach (Type type in kvp.Value) - { - MoonSharp.Interpreter.UserData.RegisterType(type); - } - } - - return true; - } - - #endregion - - /// - /// Whether or not assemblies have been loaded. - /// - public bool AssembliesLoaded { get; private set; } - - - /// - /// Whether or not loaded plugins had their preloader run. - /// - public bool PluginsPreInit { get; private set; } - - /// - /// Whether or not plugins' types have been instantiated. - /// - public bool PluginsInitialized { get; private set; } - - /// - /// Whether or not plugins are fully loaded. - /// - public bool PluginsLoaded { get; private set; } - - public IEnumerable GetCurrentPackagesByLoadOrder() => _currentPackagesByLoadOrder; - - /// - /// Tries to find the content package that a given plugin belongs to. - /// - /// Package if found, null otherwise. - /// The IAssemblyPlugin type to find. - /// - public bool TryGetPackageForPlugin(out ContentPackage package) where T : IAssemblyPlugin - { - package = null; - - var t = typeof(T); - var guid = _pluginTypes - .Where(kvp => kvp.Value.Contains(t)) - .Select(kvp => kvp.Key) - .FirstOrDefault(Guid.Empty); - - if (guid.Equals(Guid.Empty) || !_reverseLookupGuidList.ContainsKey(guid) || _reverseLookupGuidList[guid] is null) - return false; - package = _reverseLookupGuidList[guid]; - return true; - } - - - /// - /// Tries to get the loaded plugins for a given package. - /// - /// Package to find. - /// The collection of loaded plugins. - /// - public bool TryGetLoadedPluginsForPackage(ContentPackage package, out IEnumerable loadedPlugins) - { - loadedPlugins = null; - if (package is null || !_loadedCompiledPackageAssemblies.ContainsKey(package)) - return false; - var guid = _loadedCompiledPackageAssemblies[package]; - if (guid.Equals(Guid.Empty) || !_loadedPlugins.ContainsKey(guid)) - return false; - loadedPlugins = _loadedPlugins[guid]; - return true; - } - - /// - /// Called when clean up is being performed. Use when relying on or making use of references from this manager. - /// - public event Action OnDispose; - - [MethodImpl(MethodImplOptions.Synchronized)] - public void Dispose() - { - // send events for cleanup - try - { - OnDispose?.Invoke(); - } - catch (Exception e) - { - ModUtils.Logging.PrintError($"Error while executing Dispose event: {e.Message}"); - } - - // cleanup events - if (OnDispose is not null) - { - foreach (Delegate del in OnDispose.GetInvocationList()) - { - OnDispose -= (del as System.Action); - } - } - - // cleanup plugins and assemblies - ReflectionUtils.ResetCache(); - UnloadPlugins(); - // try cleaning up the assemblies - _pluginTypes.Clear(); // remove assembly references - _loadedPlugins.Clear(); - _publicizedAssemblyLoader = Guid.Empty; - _packagesDependencies.Clear(); - _loadedCompiledPackageAssemblies.Clear(); - _reverseLookupGuidList.Clear(); - _packageRunConfigs.Clear(); - _currentPackagesByLoadOrder.Clear(); - - // lua cleanup - foreach (var kvp in _luaRegisteredTypes) - { - foreach (Type type in kvp.Value) - { - MoonSharp.Interpreter.UserData.UnregisterType(type); - } - } - _luaRegisteredTypes.Clear(); - - _assemblyUnloadStartTime = DateTime.Now; - _publicizedAssemblyLoader = Guid.Empty; - - // we can't wait forever or app dies but we can try to be graceful - while (!_assemblyManager.TryBeginDispose()) - { - Thread.Sleep(20); // give the assembly context unloader time to run (async) - if (_assemblyUnloadStartTime.AddSeconds(_assemblyUnloadTimeoutSeconds) > DateTime.Now) - { - break; - } - } - - _assemblyUnloadStartTime = DateTime.Now; - Thread.Sleep(100); // give the garbage collector time to finalize the disposed assemblies. - while (!_assemblyManager.FinalizeDispose()) - { - Thread.Sleep(100); // give the garbage collector time to finalize the disposed assemblies. - if (_assemblyUnloadStartTime.AddSeconds(_assemblyUnloadTimeoutSeconds) > DateTime.Now) - { - break; - } - } - - _assemblyManager.OnAssemblyLoaded -= AssemblyManagerOnAssemblyLoaded; - _assemblyManager.OnAssemblyUnloading -= AssemblyManagerOnAssemblyUnloading; - - AssembliesLoaded = false; - GC.SuppressFinalize(this); - } - - /// - /// Begins the loading process of scanning packages for scripts and binary assemblies, compiling and executing them. - /// - /// - public AssemblyLoadingSuccessState LoadAssemblyPackages() - { - if (AssembliesLoaded) - { - return AssemblyLoadingSuccessState.AlreadyLoaded; - } - - _assemblyManager.OnAssemblyLoaded += AssemblyManagerOnAssemblyLoaded; - _assemblyManager.OnAssemblyUnloading += AssemblyManagerOnAssemblyUnloading; - - // log error if some ACLs are still unloading (some assembly is still in use) - _assemblyManager.FinalizeDispose(); //Update lists - if (_assemblyManager.IsCurrentlyUnloading) - { - ModUtils.Logging.PrintMessage($"The below ACLs are still unloading:"); - foreach (var wkref in _assemblyManager.StillUnloadingACLs) - { - if (wkref.TryGetTarget(out var tgt)) - { - ModUtils.Logging.PrintMessage($"ACL Name: {tgt.FriendlyName}"); - foreach (Assembly assembly in tgt.Assemblies) - { - ModUtils.Logging.PrintMessage($"-- Assembly: {assembly.GetName()}"); - } - } - } - } - - ImmutableList publicizedAssemblies = ImmutableList.Empty; - List publicizedAssembliesLocList = new(); - - foreach (string dllName in _publicizedAssembliesToLoad) - { - GetFiles(publicizedAssembliesLocList, dllName); - } - - void GetFiles(List list, string searchQuery) - { - bool workshopFirst = _luaCsSetup.Config.PreferToUseWorkshopLuaSetup || LuaCsSetup.IsRunningInsideWorkshop; - - var publicizedDir = Path.Combine(Environment.CurrentDirectory, "Publicized"); - - // if using workshop lua setup is checked, try to use the publicized assemblies in the content package there instead. - if (workshopFirst) - { - var pck = LuaCsSetup.GetPackage(LuaCsSetup.LuaForBarotraumaId); - if (pck is not null) - { - publicizedDir = Path.Combine(pck.Dir, "Binary", "Publicized"); - } - } - - try - { - list.AddRange(Directory.GetFiles(publicizedDir, searchQuery)); - } - // no directory found, use the other one - catch (DirectoryNotFoundException) - { - if (workshopFirst) - { - ModUtils.Logging.PrintError($"Unable to find /Binary/Publicized/ . Using Game folder instead."); - publicizedDir = Path.Combine(Environment.CurrentDirectory, "Publicized"); - } - else - { - ModUtils.Logging.PrintError($"Unable to find /Publicized/ . Using LuaCsPackage folder instead."); - var pck = LuaCsSetup.GetPackage(LuaCsSetup.LuaForBarotraumaId); - if (pck is not null) - { - publicizedDir = Path.Combine(pck.Dir, "Binary", "Publicized"); - } - } - - // search for assemblies - list.AddRange(Directory.GetFiles(publicizedDir, searchQuery)); - } - } - - // try load them into an acl - var loadState = _assemblyManager.LoadAssembliesFromLocations(publicizedAssembliesLocList, "luacs_publicized_assemblies", ref _publicizedAssemblyLoader); - - // loaded - if (loadState is AssemblyLoadingSuccessState.Success) - { - if (_assemblyManager.TryGetACL(_publicizedAssemblyLoader, out var acl)) - { - publicizedAssemblies = acl.Acl.Assemblies.ToImmutableList(); - _assemblyManager.SetACLToTemplateMode(_publicizedAssemblyLoader); - } - } - - - // get packages - IEnumerable packages = BuildPackagesList(); - - // check and load config - _packageRunConfigs.AddRange(packages - .Select(p => new KeyValuePair(p, GetRunConfigForPackage(p))) - .ToDictionary(p => p.Key, p=> p.Value)); - - // filter not to be loaded - var cpToRunA = _packageRunConfigs - .Where(kvp => ShouldRunPackage(kvp.Key, kvp.Value)) - .Select(kvp => kvp.Key) - .ToHashSet(); - - //-- filter and remove duplicate mods, prioritize /LocalMods/ - HashSet cpNames = new(); - HashSet duplicateNames = new(); - - // search - foreach (ContentPackage package in cpToRunA) - { - if (cpNames.Contains(package.Name)) - { - if (!duplicateNames.Contains(package.Name)) - { - duplicateNames.Add(package.Name); - } - } - else - { - cpNames.Add(package.Name); - } - } - - // remove - foreach (string name in duplicateNames) - { - var duplCpList = cpToRunA - .Where(p => p.Name.Equals(name)) - .ToHashSet(); - - if (duplCpList.Count < 2) // one or less found - continue; - - ContentPackage toKeep = null; - foreach (ContentPackage package in duplCpList) - { - if (package.Dir.Contains("LocalMods")) - { - toKeep = package; - break; - } - } - - toKeep ??= duplCpList.First(); - - duplCpList.Remove(toKeep); // remove all but this one - cpToRunA.RemoveWhere(p => duplCpList.Contains(p)); - } - - var cpToRun = cpToRunA.ToImmutableList(); - - // build dependencies map - bool reliableMap = TryBuildDependenciesMap(cpToRun, out var packDeps); - if (!reliableMap) - { - ModUtils.Logging.PrintMessage($"{nameof(CsPackageManager)}: Unable to create reliable dependencies map."); - } - - _packagesDependencies.AddRange(packDeps.ToDictionary( - kvp => kvp.Key, - kvp => kvp.Value.ToImmutableList()) - ); - - List packagesToLoadInOrder = new(); - - // build load order - if (reliableMap && OrderAndFilterPackagesByDependencies( - _packagesDependencies, - out var readyToLoad, - out var cannotLoadPackages)) - { - packagesToLoadInOrder.AddRange(readyToLoad); - if (cannotLoadPackages is not null) - { - ModUtils.Logging.PrintError($"{nameof(CsPackageManager)}: Unable to load the following mods due to dependency errors:"); - foreach (var pair in cannotLoadPackages) - { - ModUtils.Logging.PrintError($"Package: {pair.Key.Name} | Reason: {pair.Value}"); - } - } - } - else - { - // use unsorted list on failure and send error message. - packagesToLoadInOrder.AddRange(_packagesDependencies.Select( p=> p.Key)); - ModUtils.Logging.PrintError($"{nameof(CsPackageManager)}: Unable to create a reliable load order. Defaulting to unordered loading!"); - } - - // get assemblies and scripts' filepaths from packages - var toLoad = packagesToLoadInOrder - .Select(cp => new KeyValuePair( - cp, - new LoadableData( - TryScanPackagesForAssemblies(cp, out var list1) ? list1 : null, - TryScanPackageForScripts(cp, out var list2) ? list2 : null, - GetRunConfigForPackage(cp)))) - .ToImmutableDictionary(); - - HashSet badPackages = new(); - foreach (var pair in toLoad) - { - // check if unloadable - if (badPackages.Contains(pair.Key)) - continue; - - // try load binary assemblies - var id = Guid.Empty; // id for the ACL for this package defined by AssemblyManager. - AssemblyLoadingSuccessState successState; - if (pair.Value.AssembliesFilePaths is not null && pair.Value.AssembliesFilePaths.Any()) - { - ModUtils.Logging.PrintMessage($"Loading assemblies for CPackage {pair.Key.Name}"); -#if DEBUG - foreach (string assembliesFilePath in pair.Value.AssembliesFilePaths) - { - ModUtils.Logging.PrintMessage($"Found assemblies located at {Path.GetFullPath(ModUtils.IO.SanitizePath(assembliesFilePath))}"); - } -#endif - - successState = _assemblyManager.LoadAssembliesFromLocations(pair.Value.AssembliesFilePaths, pair.Key.Name, ref id); - - // error handling - if (successState is not AssemblyLoadingSuccessState.Success) - { - ModUtils.Logging.PrintError($"{nameof(CsPackageManager)}: Unable to load the binary assemblies for package {pair.Key.Name}. Error: {successState.ToString()}"); - UpdatePackagesToDisable(ref badPackages, pair.Key, _packagesDependencies); - continue; - } - } - - // try compile scripts to assemblies - if (pair.Value.ScriptsFilePaths is not null && pair.Value.ScriptsFilePaths.Any()) - { - ModUtils.Logging.PrintMessage($"Loading scripts for CPackage {pair.Key.Name}"); - List syntaxTrees = new(); - - syntaxTrees.Add(GetPackageScriptImports()); - bool abortPackage = false; - // load scripts data from files - foreach (string scriptPath in pair.Value.ScriptsFilePaths) - { - var state = ModUtils.IO.GetOrCreateFileText(scriptPath, out string fileText, null, false); - // could not load file data - if (state is not ModUtils.IO.IOActionResultState.Success) - { - ModUtils.Logging.PrintError($"{nameof(CsPackageManager)}: Unable to load the script files for package {pair.Key.Name}. Error: {state.ToString()}"); - UpdatePackagesToDisable(ref badPackages, pair.Key, _packagesDependencies); - abortPackage = true; - break; - } - - try - { - CancellationToken token = new(); - syntaxTrees.Add(SyntaxFactory.ParseSyntaxTree(fileText, ScriptParseOptions, scriptPath, Encoding.Default, token)); - // cancel if parsing failed - if (token.IsCancellationRequested) - { - ModUtils.Logging.PrintError($"{nameof(CsPackageManager)}: Unable to load the script files for package {pair.Key.Name}. Error: Syntax Parse Error."); - UpdatePackagesToDisable(ref badPackages, pair.Key, _packagesDependencies); - abortPackage = true; - break; - } - } - catch (Exception e) - { - // unknown error - ModUtils.Logging.PrintError($"{nameof(CsPackageManager)}: Unable to load the script files for package {pair.Key.Name}. Error: {e.Message}"); - UpdatePackagesToDisable(ref badPackages, pair.Key, _packagesDependencies); - abortPackage = true; - break; - } - - } - - if (abortPackage) - continue; - - // try compile - successState = _assemblyManager.LoadAssemblyFromMemory( - pair.Value.config.UseInternalAssemblyName ? "CompiledAssembly" : pair.Key.Name.Replace(" ",""), - syntaxTrees, - null, - CompilationOptions, - pair.Key.Name, - ref id, - pair.Value.config.UseNonPublicizedAssemblies ? null : publicizedAssemblies); - - if (successState is not AssemblyLoadingSuccessState.Success) - { - ModUtils.Logging.PrintError($"{nameof(CsPackageManager)}: Unable to compile script assembly for package {pair.Key.Name}. Error: {successState.ToString()}"); - UpdatePackagesToDisable(ref badPackages, pair.Key, _packagesDependencies); - continue; - } - } - - // something was loaded, add to index - if (id != Guid.Empty) - { - ModUtils.Logging.PrintMessage($"Assemblies from CPackage {pair.Key.Name} loaded with Guid {id}."); - _loadedCompiledPackageAssemblies.Add(pair.Key, id); - _reverseLookupGuidList.Add(id, pair.Key); - } - } - - // update loaded packages to exclude bad packages - _currentPackagesByLoadOrder.AddRange(toLoad - .Where(p => !badPackages.Contains(p.Key)) - .Select(p => p.Key)); - - // build list of plugins - foreach (var pair in _loadedCompiledPackageAssemblies) - { - if (_assemblyManager.TryGetSubTypesFromACL(pair.Value, out var types)) - { - _pluginTypes[pair.Value] = types.ToImmutableHashSet(); - foreach (var type in _pluginTypes[pair.Value]) - { - ModUtils.Logging.PrintMessage($"Loading type: {type.Name}"); - } - } - } - - this.AssembliesLoaded = true; - return AssemblyLoadingSuccessState.Success; - - - bool ShouldRunPackage(ContentPackage package, RunConfig config) - { - return (!_luaCsSetup.Config.TreatForcedModsAsNormal && config.IsForced()) - || (ContentPackageManager.EnabledPackages.All.Contains(package) && config.IsForcedOrStandard()); - } - - void UpdatePackagesToDisable(ref HashSet set, - ContentPackage newDisabledPackage, - IEnumerable>> dependenciesMap) - { - set.Add(newDisabledPackage); - foreach (var package in dependenciesMap) - { - if (package.Value.Contains(newDisabledPackage)) - set.Add(newDisabledPackage); - } - } - } - - /// - /// Executes instantiated plugins' Initialize() and OnLoadCompleted() methods. - /// - public void RunPluginsInit() - { - if (!AssembliesLoaded) - { - ModUtils.Logging.PrintError($"{nameof(CsPackageManager)}: Attempted to call plugins' Initialize() without any loaded assemblies!"); - return; - } - - if (!PluginsInitialized) - { - ModUtils.Logging.PrintError($"{nameof(CsPackageManager)}: Attempted to call plugins' Initialize() without type instantiation!"); - return; - } - - if (PluginsLoaded) - return; - - foreach (var contentPlugins in _loadedPlugins) - { - // init - foreach (var plugin in contentPlugins.Value) - { - TryRun(() => plugin.Initialize(), $"{nameof(IAssemblyPlugin.Initialize)}", $"CP: {_reverseLookupGuidList[contentPlugins.Key].Name} Plugin: {plugin.GetType().Name}"); - } - } - - foreach (var contentPlugins in _loadedPlugins) - { - // load complete - foreach (var plugin in contentPlugins.Value) - { - TryRun(() => plugin.OnLoadCompleted(), $"{nameof(IAssemblyPlugin.OnLoadCompleted)}", $"CP: {_reverseLookupGuidList[contentPlugins.Key].Name} Plugin: {plugin.GetType().Name}"); - } - } - - PluginsLoaded = true; - } - - /// - /// Executes instantiated plugins' PreInitPatching() method. - /// - public void RunPluginsPreInit() - { - if (!AssembliesLoaded) - { - ModUtils.Logging.PrintError($"{nameof(CsPackageManager)}: Attempted to call plugins' PreInitPatching() without any loaded assemblies!"); - return; - } - - if (!PluginsInitialized) - { - ModUtils.Logging.PrintError($"{nameof(CsPackageManager)}: Attempted to call plugins' PreInitPatching() without type initialization!"); - return; - } - - if (PluginsPreInit) - { - return; - } - - foreach (var contentPlugins in _loadedPlugins) - { - // init - foreach (var plugin in contentPlugins.Value) - { - TryRun(() => plugin.PreInitPatching(), $"{nameof(IAssemblyPlugin.PreInitPatching)}", $"CP: {_reverseLookupGuidList[contentPlugins.Key].Name} Plugin: {plugin.GetType().Name}"); - } - } - - PluginsPreInit = true; - } - - /// - /// Initializes plugin types that are registered. - /// - /// - public void InstantiatePlugins(bool force = false) - { - if (!AssembliesLoaded) - { - ModUtils.Logging.PrintError($"{nameof(CsPackageManager)}: Attempted to instantiate plugins without any loaded assemblies!"); - return; - } - - if (PluginsInitialized) - { - if (force) - UnloadPlugins(); - else - { - ModUtils.Logging.PrintError($"{nameof(CsPackageManager)}: Attempted to load plugins when they were already loaded!"); - return; - } - } - - foreach (var pair in _pluginTypes) - { - // instantiate - foreach (Type type in pair.Value) - { - if (!_loadedPlugins.ContainsKey(pair.Key)) - _loadedPlugins.Add(pair.Key, new()); - else if (_loadedPlugins[pair.Key] is null) - _loadedPlugins[pair.Key] = new(); - IAssemblyPlugin plugin = null; - try - { - plugin = (IAssemblyPlugin)Activator.CreateInstance(type); - _loadedPlugins[pair.Key].Add(plugin); - } - catch (Exception e) - { - ModUtils.Logging.PrintError($"{nameof(CsPackageManager)}: Error while instantiating plugin of type {type}. Now disposing..."); - ModUtils.Logging.PrintError($"{nameof(CsPackageManager)}: Details: {e.Message} | {e.InnerException}"); - - if (plugin is not null) - { - // ReSharper disable once AccessToModifiedClosure - TryRun(() => plugin?.Dispose(), nameof(IAssemblyPlugin.Dispose), type.FullName ?? type.Name); - plugin = null; - } - } - } - } - - PluginsInitialized = true; - } - - /// - /// Unloads all plugins by calling Dispose() on them. Note: This does not remove their external references nor - /// unregister their types. - /// - public void UnloadPlugins() - { - foreach (var contentPlugins in _loadedPlugins) - { - foreach (var plugin in contentPlugins.Value) - { - TryRun(() => plugin.Dispose(), $"{nameof(IAssemblyPlugin.Dispose)}", $"CP: {_reverseLookupGuidList[contentPlugins.Key].Name} Plugin: {plugin.GetType().Name}"); - } - contentPlugins.Value.Clear(); - } - - _loadedPlugins.Clear(); - - PluginsInitialized = false; - PluginsPreInit = false; - PluginsLoaded = false; - } - - - /// - /// Gets the RunConfig.xml for the given package located at [cp_root]/CSharp/RunConfig.xml. - /// Generates a default config if one is not found. - /// - /// The package to search for. - /// RunConfig data. - /// True if a config is loaded, false if one was created. - public static bool GetOrCreateRunConfig(ContentPackage package, out RunConfig config) - { - var path = System.IO.Path.Combine(Path.GetFullPath(package.Dir), "CSharp", "RunConfig.xml"); - if (!File.Exists(path)) - { - config = new RunConfig(true).Sanitize(); - return false; - } - return ModUtils.IO.LoadOrCreateTypeXml(out config, path, () => new RunConfig(true).Sanitize(), false); - } - - #endregion - - #region INTERNALS - - private void TryRun(Action action, string messageMethodName, string messageTypeName) - { - try - { - action?.Invoke(); - } - catch (Exception e) - { - ModUtils.Logging.PrintError($"{nameof(CsPackageManager)}: Error while running {messageMethodName}() on plugin of type {messageTypeName}"); - ModUtils.Logging.PrintError($"{nameof(CsPackageManager)}: Details: {e.Message} | {e.InnerException}"); - } - } - - private void AssemblyManagerOnAssemblyUnloading(Assembly assembly) - { - ReflectionUtils.RemoveAssemblyFromCache(assembly); - } - - private void AssemblyManagerOnAssemblyLoaded(Assembly assembly) - { - //ReflectionUtils.AddNonAbstractAssemblyTypes(assembly); - // As ReflectionUtils.GetDerivedNonAbstract is only used for Prefabs & Barotrauma-specific implementing types, - // we can safely not register System/Core assemblies. - if (assembly.FullName is not null && assembly.FullName.StartsWith("System.")) - return; - ReflectionUtils.AddNonAbstractAssemblyTypes(assembly, true); - } - - internal CsPackageManager([NotNull] AssemblyManager assemblyManager, [NotNull] LuaCsSetup luaCsSetup) - { - this._assemblyManager = assemblyManager; - this._luaCsSetup = luaCsSetup; - } - - ~CsPackageManager() - { - this.Dispose(); - } - - private static bool TryScanPackageForScripts(ContentPackage package, out ImmutableList scriptFilePaths) - { - string pathShared = Path.Combine(ModUtils.IO.GetContentPackageDir(package), "CSharp", "Shared"); - string pathArch = Path.Combine(ModUtils.IO.GetContentPackageDir(package), "CSharp", ARCHITECTURE_TARGET); - - List files = new(); - - if (Directory.Exists(pathShared)) - files.AddRange(Directory.GetFiles(pathShared, SCRIPT_FILE_REGEX, SearchOption.AllDirectories)); - if (Directory.Exists(pathArch)) - files.AddRange(Directory.GetFiles(pathArch, SCRIPT_FILE_REGEX, SearchOption.AllDirectories)); - - if (files.Count > 0) - { - scriptFilePaths = files.ToImmutableList(); - return true; - } - scriptFilePaths = ImmutableList.Empty; - return false; - } - - private static bool TryScanPackagesForAssemblies(ContentPackage package, out ImmutableList assemblyFilePaths) - { - string path = Path.Combine(ModUtils.IO.GetContentPackageDir(package), "bin", ARCHITECTURE_TARGET, PLATFORM_TARGET); - - if (!Directory.Exists(path)) - { - assemblyFilePaths = ImmutableList.Empty; - return false; - } - - assemblyFilePaths = System.IO.Directory.GetFiles(path, ASSEMBLY_FILE_REGEX, SearchOption.AllDirectories) - .ToImmutableList(); - return assemblyFilePaths.Count > 0; - } - - private static RunConfig GetRunConfigForPackage(ContentPackage package) - { - if (!GetOrCreateRunConfig(package, out var config)) - config.AutoGenerated = true; - return config; - } - - private IEnumerable BuildPackagesList() - { - // get unique list of content packages. - // Note: there is an old issue where the AllPackages group - // would sometimes not contain packages downloaded from the host, so we union enabled. - return ContentPackageManager.AllPackages.Union(ContentPackageManager.EnabledPackages.All).Where(pack => !pack.Name.ToLowerInvariant().Equals("vanilla")); - } - - - private static SyntaxTree GetPackageScriptImports() => BaseAssemblyImports; - - - /// - /// Builds a list of ContentPackage dependencies for each of the packages in the list. Note: All dependencies must be included in the provided list of packages. - /// - /// List of packages to check - /// Dependencies by package - /// True if all dependencies were found. - private static bool TryBuildDependenciesMap(ImmutableList packages, out Dictionary> dependenciesMap) - { - bool reliableMap = true; // remains true if all deps were found. - dependenciesMap = new(); - foreach (var package in packages) - { - dependenciesMap.Add(package, new()); - if (GetOrCreateRunConfig(package, out var config)) - { - if (config.Dependencies is null || !config.Dependencies.Any()) - continue; - - foreach (RunConfig.Dependency dependency in config.Dependencies) - { - ContentPackage dep = packages.FirstOrDefault(p => - (dependency.SteamWorkshopId != 0 && p.TryExtractSteamWorkshopId(out var steamWorkshopId) - && steamWorkshopId.Value == dependency.SteamWorkshopId) - || (!dependency.PackageName.IsNullOrWhiteSpace() && p.Name.ToLowerInvariant().Contains(dependency.PackageName.ToLowerInvariant())), null); - - if (dep is not null) - { - dependenciesMap[package].Add(dep); - } - else - { - ModUtils.Logging.PrintWarning($"Warning: The ContentPackage {package.Name} lists a dependency of (STEAMID: {dependency.SteamWorkshopId}, PackageName: {dependency.PackageName}) but it could not be found in the to-be-loaded CSharp packages list!"); - reliableMap = false; - } - } - } - } - - return reliableMap; - } - - /// - /// Given a table of packages and dependent packages, will sort them by dependency loading order along with packages - /// that cannot be loaded due to errors or failing the predicate checks. - /// - /// A dictionary/map with key as the package and the elements as it's dependencies. - /// List of packages that are ready to load and in the correct order. - /// Packages with errors or cyclic dependencies. Element is error message. Null if empty. - /// Optional: Allows for a custom checks to be performed on each package. - /// Returns a bool indicating if the package is ready to load. - /// Whether or not the process produces a usable list. - private static bool OrderAndFilterPackagesByDependencies( - Dictionary> packages, - out IEnumerable readyToLoad, - out IEnumerable> cannotLoadPackages, - Func packageChecksPredicate = null) - { - HashSet completedPackages = new(); - List readyPackages = new(); - Dictionary unableToLoad = new(); - HashSet currentNodeChain = new(); - - readyToLoad = readyPackages; - - try - { - foreach (var toProcessPack in packages) - { - ProcessPackage(toProcessPack.Key, toProcessPack.Value); - } - - PackageProcRet ProcessPackage(ContentPackage packageToProcess, IEnumerable dependencies) - { - //cyclic handling - if (unableToLoad.ContainsKey(packageToProcess)) - { - return PackageProcRet.BadPackage; - } - - // already processed - if (completedPackages.Contains(packageToProcess)) - { - return PackageProcRet.AlreadyCompleted; - } - - // cyclic check - if (currentNodeChain.Contains(packageToProcess)) - { - StringBuilder sb = new(); - sb.AppendLine("Error: Cyclic Dependency. ") - .Append( - "The following ContentPackages rely on eachother in a way that makes it impossible to know which to load first! ") - .Append( - "Note: the package listed twice shows where the cycle starts/ends and is not necessarily the problematic package."); - int i = 0; - foreach (var package in currentNodeChain) - { - i++; - sb.AppendLine($"{i}. {package.Name}"); - } - - sb.AppendLine($"{i}. {packageToProcess.Name}"); - unableToLoad.Add(packageToProcess, sb.ToString()); - completedPackages.Add(packageToProcess); - return PackageProcRet.BadPackage; - } - - if (packageChecksPredicate is not null && !packageChecksPredicate.Invoke(packageToProcess)) - { - unableToLoad.Add(packageToProcess, $"Unable to load package {packageToProcess.Name} due to failing checks."); - completedPackages.Add(packageToProcess); - return PackageProcRet.BadPackage; - } - - currentNodeChain.Add(packageToProcess); - - foreach (ContentPackage dependency in dependencies) - { - // The mod lists a dependent that was not found during the discovery phase. - if (!packages.ContainsKey(dependency)) - { - // search to see if it's enabled - if (!ContentPackageManager.EnabledPackages.All.Contains(dependency)) - { - // present warning but allow loading anyways, better to let the user just disable the package if it's really an issue. - ModUtils.Logging.PrintWarning( - $"Warning: the ContentPackage of {packageToProcess.Name} requires the Dependency {dependency.Name} but this package wasn't found in the enabled mods list!"); - } - - continue; - } - - var ret = ProcessPackage(dependency, packages[dependency]); - - if (ret is PackageProcRet.BadPackage) - { - if (!unableToLoad.ContainsKey(packageToProcess)) - { - unableToLoad.Add(packageToProcess, $"Error: Dependency failure. Failed to load {dependency.Name}"); - } - currentNodeChain.Remove(packageToProcess); - if (!completedPackages.Contains(packageToProcess)) - { - completedPackages.Add(packageToProcess); - } - return PackageProcRet.BadPackage; - } - } - - currentNodeChain.Remove(packageToProcess); - completedPackages.Add(packageToProcess); - readyPackages.Add(packageToProcess); - return PackageProcRet.Completed; - } - } - catch (Exception e) - { - ModUtils.Logging.PrintError($"Error while generating dependency loading order! Exception: {e.Message}"); -#if DEBUG - ModUtils.Logging.PrintError($"Stack Trace: {e.StackTrace}"); -#endif - cannotLoadPackages = unableToLoad.Any() ? unableToLoad : null; - return false; - } - cannotLoadPackages = unableToLoad.Any() ? unableToLoad : null; - return true; - } - - private enum PackageProcRet : byte - { - AlreadyCompleted, - Completed, - BadPackage - } - - private record LoadableData(ImmutableList AssembliesFilePaths, ImmutableList ScriptsFilePaths, RunConfig config); - - #endregion -} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Plugins/IAssemblyPlugin.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Plugins/IAssemblyPlugin.cs deleted file mode 100644 index 5a450ba74..000000000 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Plugins/IAssemblyPlugin.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System; - -namespace Barotrauma; - -public interface IAssemblyPlugin : IDisposable -{ - /// - /// Called on plugin normal, use this for basic/core loading that does not rely on any other modded content. - /// - void Initialize(); - - /// - /// Called once all plugins have been loaded. if you have integrations with any other mod, put that code here. - /// - void OnLoadCompleted(); - - - /// - /// Called before Barotrauma initializes vanilla content. WARNING: This method may be called before Initialize()! - /// - void PreInitPatching(); -} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Plugins/MemoryFileAssemblyContextLoader.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Plugins/MemoryFileAssemblyContextLoader.cs deleted file mode 100644 index dd61c0108..000000000 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Plugins/MemoryFileAssemblyContextLoader.cs +++ /dev/null @@ -1,340 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Diagnostics.CodeAnalysis; -using System.IO; -using System.Linq; -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.Loader; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -// ReSharper disable ConditionIsAlwaysTrueOrFalse - -[assembly: InternalsVisibleTo("CompiledAssembly")] - -namespace Barotrauma; - -/// -/// AssemblyLoadContext to compile from syntax trees in memory and to load from disk/file. Provides dependency resolution. -/// [IMPORTANT] Only supports 1 in-memory compiled assembly at a time. Use more instances if you need more. -/// [IMPORTANT] All file assemblies required for the compilation of syntax trees should be loaded first. -/// -public class MemoryFileAssemblyContextLoader : AssemblyLoadContext -{ - // public - public string FriendlyName { get; set; } - // ReSharper disable MemberCanBePrivate.Global - public Assembly CompiledAssembly { get; private set; } - public byte[] CompiledAssemblyImage { get; private set; } - // ReSharper restore MemberCanBePrivate.Global - // internal - private readonly Dictionary _dependencyResolvers = new(); // path-folder, resolver - protected bool IsResolving; //this is to avoid circular dependency lookup. - private AssemblyManager _assemblyManager; - public bool IsTemplateMode { get; set; } - public bool IsDisposed { get; private set; } - - public MemoryFileAssemblyContextLoader(AssemblyManager assemblyManager) : base(isCollectible: true) - { - this._assemblyManager = assemblyManager; - this.IsDisposed = false; - base.Unloading += OnUnload; - } - - - /// - /// Try to load the list of disk-file assemblies. - /// - /// Operation success or failure reason. - public AssemblyLoadingSuccessState LoadFromFiles([NotNull] IEnumerable assemblyFilePaths) - { - if (assemblyFilePaths is null) - throw new ArgumentNullException( - $"{nameof(MemoryFileAssemblyContextLoader)}::{nameof(LoadFromFiles)}() | The supplied filepath list is null."); - - foreach (string filepath in assemblyFilePaths) - { - // path verification - if (filepath.IsNullOrWhiteSpace()) - continue; - string sanitizedFilePath = System.IO.Path.GetFullPath(filepath.CleanUpPath()); - string directoryKey = System.IO.Path.GetDirectoryName(sanitizedFilePath); - - if (directoryKey is null) - return AssemblyLoadingSuccessState.BadFilePath; - - // setup dep resolver if not available - if (!_dependencyResolvers.ContainsKey(directoryKey) || _dependencyResolvers[directoryKey] is null) - { - _dependencyResolvers[directoryKey] = new AssemblyDependencyResolver(sanitizedFilePath); // supply the first assembly to be loaded - } - - // try loading the assemblies - try - { - LoadFromAssemblyPath(sanitizedFilePath); - } - // on fail of any we're done because we assume that loaded files are related. This ACL needs to be unloaded and collected. - catch (ArgumentNullException ane) - { - ModUtils.Logging.PrintError($"MemFileACL::{nameof(LoadFromFiles)}() | Error loading file path {sanitizedFilePath}. Details: {ane.Message} | {ane.StackTrace}"); - return AssemblyLoadingSuccessState.BadFilePath; - } - catch (ArgumentException ae) - { - ModUtils.Logging.PrintError($"MemFileACL::{nameof(LoadFromFiles)}() | Error loading file path {sanitizedFilePath}. Details: {ae.Message} | {ae.StackTrace}"); - return AssemblyLoadingSuccessState.BadFilePath; - } - catch (FileLoadException fle) - { - ModUtils.Logging.PrintError($"MemFileACL::{nameof(LoadFromFiles)}() | Error loading file path {sanitizedFilePath}. Details: {fle.Message} | {fle.StackTrace}"); - return AssemblyLoadingSuccessState.CannotLoadFile; - } - catch (FileNotFoundException fnfe) - { - ModUtils.Logging.PrintError($"MemFileACL::{nameof(LoadFromFiles)}() | Error loading file path {sanitizedFilePath}. Details: {fnfe.Message} | {fnfe.StackTrace}"); - return AssemblyLoadingSuccessState.NoAssemblyFound; - } - catch (BadImageFormatException bife) - { - ModUtils.Logging.PrintError($"MemFileACL::{nameof(LoadFromFiles)}() | Error loading file path {sanitizedFilePath}. Details: {bife.Message} | {bife.StackTrace}"); - return AssemblyLoadingSuccessState.InvalidAssembly; - } - catch (Exception e) - { -#if SERVER - LuaCsLogger.LogError($"Unable to load dependency assembly file at {filepath.CleanUpPath()} for the assembly named {CompiledAssembly?.FullName}. | Data: {e.Message} | InnerException: {e.InnerException}"); -#elif CLIENT - LuaCsLogger.ShowErrorOverlay($"Unable to load dependency assembly file at {filepath} for the assembly named {CompiledAssembly?.FullName}. | Data: {e.Message} | InnerException: {e.InnerException}"); -#endif - return AssemblyLoadingSuccessState.ACLLoadFailure; - } - } - - return AssemblyLoadingSuccessState.Success; - } - - - /// - /// Compiles the supplied syntaxtrees and options into an in-memory assembly image. - /// Builds metadata from loaded assemblies, only supply your own if you have in-memory images not managed by the - /// AssemblyManager class. - /// - /// Name of the assembly. Must be supplied for in-memory assemblies. - /// Syntax trees to compile into the assembly. - /// Metadata to be used for compilation. - /// [IMPORTANT] This method builds metadata from loaded assemblies, only supply your own if you have in-memory - /// images not managed by the AssemblyManager class. - /// CSharp compilation options. This method automatically adds the 'IgnoreAccessChecks' property for compilation. - /// Will contain any diagnostic messages for compilation failure. - /// Additional assemblies located in the FileSystem to build metadata references from. - /// Assemblies here will have duplicates by the same name that are currently loaded filtered out. - /// Success state of the operation. - /// Throws exception if any of the required arguments are null. - public AssemblyLoadingSuccessState CompileAndLoadScriptAssembly( - [NotNull] string assemblyName, - [NotNull] IEnumerable syntaxTrees, - IEnumerable externMetadataReferences, - [NotNull] CSharpCompilationOptions compilationOptions, - out string compilationMessages, - IEnumerable externFileAssemblyReferences = null) - { - compilationMessages = ""; - - if (this.CompiledAssembly is not null) - { - return AssemblyLoadingSuccessState.AlreadyLoaded; - } - - var externAssemblyRefs = externFileAssemblyReferences is not null ? externFileAssemblyReferences.ToImmutableList() : ImmutableList.Empty; - var externAssemblyNames = externAssemblyRefs.Any() ? externAssemblyRefs - .Where(a => a.FullName is not null) - .Select(a => a.FullName).ToImmutableHashSet() - : ImmutableHashSet.Empty; - - // verifications - if (assemblyName.IsNullOrWhiteSpace()) - throw new ArgumentNullException( - $"{nameof(MemoryFileAssemblyContextLoader)}::{nameof(CompileAndLoadScriptAssembly)}() | The supplied assembly name is null!"); - - if (syntaxTrees is null) - throw new ArgumentNullException( - $"{nameof(MemoryFileAssemblyContextLoader)}::{nameof(CompileAndLoadScriptAssembly)}() | The supplied syntax tree is null!"); - - // add external references - List metadataReferences = new(); - if (externMetadataReferences is not null) - metadataReferences.AddRange(externMetadataReferences); - - // build metadata refs from default where not an in-memory compiled assembly and not the same assembly as supplied. - metadataReferences.AddRange(AssemblyLoadContext.Default.Assemblies - .Where(a => - { - if (a.IsDynamic || string.IsNullOrWhiteSpace(a.Location) || a.Location.Contains("xunit")) - return false; - if (a.FullName is null) - return true; - return !externAssemblyNames.Contains(a.FullName); // exclude duplicates - }) - .Select(a => MetadataReference.CreateFromFile(a.Location) as MetadataReference) - .Union(externAssemblyRefs // add custom supplied assemblies - .Where(a => !(a.IsDynamic || string.IsNullOrEmpty(a.Location) || a.Location.Contains("xunit"))) - .Select(a => MetadataReference.CreateFromFile(a.Location) as MetadataReference) - ).ToList()); - - ImmutableList loadedAcls = _assemblyManager.GetAllLoadedACLs().ToImmutableList(); - if (loadedAcls.Any()) - { - // build metadata refs from ACL assemblies from files/disk. - foreach (AssemblyManager.LoadedACL loadedAcl in loadedAcls) - { - if(loadedAcl?.Acl is null || loadedAcl.Acl.IsTemplateMode || loadedAcl.Acl.IsDisposed) - continue; - metadataReferences.AddRange(loadedAcl.Acl.Assemblies - .Where(a => - { - if (a.IsDynamic || string.IsNullOrWhiteSpace(a.Location) || a.Location.Contains("xunit")) - return false; - if (a.FullName is null) - return true; - return !externAssemblyNames.Contains(a.FullName); // exclude duplicates - }) - .Select(a => MetadataReference.CreateFromFile(a.Location) as MetadataReference) - .Union(externAssemblyRefs // add custom supplied assemblies - .Where(a => !(a.IsDynamic || string.IsNullOrEmpty(a.Location) || a.Location.Contains("xunit"))) - .Select(a => MetadataReference.CreateFromFile(a.Location) as MetadataReference) - ).ToList()); - } - - // build metadata refs from in-memory images - foreach (var loadedAcl in loadedAcls) - { - if (loadedAcl?.Acl?.CompiledAssemblyImage is null || loadedAcl.Acl.CompiledAssemblyImage.Length == 0) - continue; - metadataReferences.Add(MetadataReference.CreateFromImage(loadedAcl.Acl.CompiledAssemblyImage)); - } - } - - // Change inaccessible options to allow public access to restricted members - var topLevelBinderFlagsProperty = typeof(CSharpCompilationOptions).GetProperty("TopLevelBinderFlags", BindingFlags.Instance | BindingFlags.NonPublic); - topLevelBinderFlagsProperty?.SetValue(compilationOptions, (uint)1 << 22); - - // begin compilation - using var memoryCompilation = new MemoryStream(); - // compile, emit - var result = CSharpCompilation.Create(assemblyName, syntaxTrees, metadataReferences, compilationOptions).Emit(memoryCompilation); - // check for errors - if (!result.Success) - { - IEnumerable failures = result.Diagnostics.Where(d => d.IsWarningAsError || d.Severity == DiagnosticSeverity.Error); - foreach (Diagnostic diagnostic in failures) - { - compilationMessages += $"\n{diagnostic}"; - } - - return AssemblyLoadingSuccessState.InvalidAssembly; - } - - // read compiled assembly from memory stream into an in-memory assembly & image - memoryCompilation.Seek(0, SeekOrigin.Begin); // reset - try - { - CompiledAssembly = LoadFromStream(memoryCompilation); - CompiledAssemblyImage = memoryCompilation.ToArray(); - } - catch (Exception e) - { -#if SERVER - LuaCsLogger.LogError($"Unable to load memory assembly from stream. | Data: {e.Message} | InnerException: {e.InnerException}"); -#elif CLIENT - LuaCsLogger.ShowErrorOverlay($"Unable to load memory assembly from stream. | Data: {e.Message} | InnerException: {e.InnerException}"); -#endif - return AssemblyLoadingSuccessState.CannotLoadFromStream; - } - - return AssemblyLoadingSuccessState.Success; - } - - [SuppressMessage("ReSharper", "ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract")] - protected override Assembly Load(AssemblyName assemblyName) - { - if (IsResolving) - return null; //circular resolution fast exit. - - try - { - IsResolving = true; - - // resolve self collection - Assembly ass = this.Assemblies.FirstOrDefault(a => - a.FullName is not null && a.FullName.Equals(assemblyName.FullName), null); - if (ass is not null) - return ass; - - // resolve to local folders - foreach (KeyValuePair pair in _dependencyResolvers) - { - var asspath = pair.Value.ResolveAssemblyToPath(assemblyName); - if (asspath is null) - continue; - ass = LoadFromAssemblyPath(asspath); - // ReSharper disable once ConditionIsAlwaysTrueOrFalse - if (ass is not null) - return ass; - } - - //try resolve against other loaded alcs - ImmutableList list; - try - { - list = _assemblyManager.UnsafeGetAllLoadedACLs(); - } - catch - { - list = ImmutableList.Empty; - } - - if (!list.IsEmpty) - { - foreach (var loadedAcL in list) - { - if (loadedAcL.Acl is null || loadedAcL.Acl.IsTemplateMode || loadedAcL.Acl.IsDisposed) - continue; - - try - { - ass = loadedAcL.Acl.LoadFromAssemblyName(assemblyName); - if (ass is not null) - return ass; - } - catch - { - // LoadFromAssemblyName throws, no need to propagate - } - } - } - - ass = AssemblyLoadContext.Default.LoadFromAssemblyName(assemblyName); - if (ass is not null) - return ass; - } - finally - { - IsResolving = false; - } - - return null; - } - - - private void OnUnload(AssemblyLoadContext alc) - { - CompiledAssembly = null; - CompiledAssemblyImage = null; - _dependencyResolvers.Clear(); - _assemblyManager = null; - base.Unloading -= OnUnload; - this.IsDisposed = true; - } -} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/SigilExtensions.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/SigilExtensions.cs new file mode 100644 index 000000000..81143ffce --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/SigilExtensions.cs @@ -0,0 +1,399 @@ +using Microsoft.Xna.Framework; +using Sigil; +using Sigil.NonGeneric; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq.Expressions; +using System.Reflection; + +namespace Barotrauma.LuaCs; + +internal static class SigilExtensions +{ + /// + /// Puts a type on the stack, as a object instead of a + /// runtime type token. + /// + /// The IL emitter. + /// The type to put on the stack. + public static void LoadType(this Emit il, Type type) + { + if (type == null) throw new ArgumentNullException(nameof(type)); + il.LoadConstant(type); // ldtoken + // This converts the type token into a Type object + il.Call(typeof(Type).GetMethod( + name: nameof(Type.GetTypeFromHandle), + bindingAttr: BindingFlags.Public | BindingFlags.Static, + binder: null, + types: new Type[] { typeof(RuntimeTypeHandle) }, + modifiers: null)); + } + + /// + /// Converts the value on the stack to . + /// + /// The IL emitter. + /// The type of the value on the stack. + public static void ToObject(this Emit il, Type type) + { + if (type == null) throw new ArgumentNullException(nameof(type)); + il.DerefIfByRef(ref type); + if (type.IsValueType) + { + il.Box(type); + } + else if (type != typeof(object)) + { + il.CastClass(); + } + } + + /// + /// Deferences the value on stack if the provided type is ByRef. + /// + /// The IL emitter. + /// The type to check if ByRef. + public static void DerefIfByRef(this Emit il, Type type) => il.DerefIfByRef(ref type); + + /// + /// Deferences the value on stack if the provided type is ByRef. + /// + /// The IL emitter. + /// The type to check if ByRef. + public static void DerefIfByRef(this Emit il, ref Type type) + { + if (type == null) throw new ArgumentNullException(nameof(type)); + if (type.IsByRef) + { + type = type.GetElementType(); + if (type.IsValueType) + { + il.LoadObject(type); + } + else + { + il.LoadIndirect(type); + } + } + } + + // Copied from https://github.com/evilfactory/moonsharp/blob/5264656c6442e783f3c75082cce69a93d66d4cc0/src/MoonSharp.Interpreter/Interop/Converters/ScriptToClrConversions.cs#L79-L99 + private static MethodInfo GetImplicitOperatorMethod(Type baseType, Type targetType) + { + try + { + return Expression.Convert(Expression.Parameter(baseType, null), targetType).Method; + } + catch + { + if (baseType.BaseType != null) + { + return GetImplicitOperatorMethod(baseType.BaseType, targetType); + } + + if (targetType.BaseType != null) + { + return GetImplicitOperatorMethod(baseType, targetType.BaseType); + } + + return null; + } + } + + /// + /// Loads a local variable and casts it to the target type. + /// + /// The IL emitter. + /// The value to cast. Must be of type . + /// The type to cast into. + public static void LoadLocalAndCast(this Emit il, Local value, Type targetType) + { + if (value == null) throw new ArgumentNullException(nameof(value)); + if (targetType == null) throw new ArgumentNullException(nameof(targetType)); + if (value.LocalType != typeof(object)) + { + throw new ArgumentException($"Expected local type {typeof(object)}; got {value.LocalType}.", nameof(value)); + } + + var guid = Guid.NewGuid().ToString("N"); + + if (targetType.IsByRef) + { + targetType = targetType.GetElementType(); + } + + // IL: var baseType = value.GetType(); + var baseType = il.DeclareLocal(typeof(Type), $"cast_baseType_{guid}"); + il.LoadLocal(value); + il.Call(typeof(object).GetMethod("GetType")); + il.StoreLocal(baseType); + + // IL: var implicitOperatorMethod = SigilExtensions.GetImplicitOperatorMethod(baseType, ); + var implicitOperatorMethod = il.DeclareLocal(typeof(MethodInfo), $"cast_implicitOperatorMethod_{guid}"); + il.LoadLocal(baseType); + il.LoadType(targetType); + il.Call(typeof(SigilExtensions).GetMethod(nameof(GetImplicitOperatorMethod), BindingFlags.NonPublic | BindingFlags.Static)); + il.StoreLocal(implicitOperatorMethod); + + // IL: castValue; + var castValue = il.DeclareLocal(targetType, $"cast_castValue_{guid}"); + + // IL: if (implicitConversionMethod != null) + il.LoadLocal(implicitOperatorMethod); + il.Branch((il) => + { + // IL: var methodInvokeParams = new object[1]; + var methodInvokeParams = il.DeclareLocal(typeof(object[]), $"cast_methodInvokeParams_{guid}"); + il.LoadConstant(1); + il.NewArray(typeof(object)); + il.StoreLocal(methodInvokeParams); + + // IL: methodInvokeParams[0] = value; + il.LoadLocal(methodInvokeParams); + il.LoadConstant(0); + il.LoadLocal(value); + il.StoreElement(); + + // IL: castValue = ()implicitConversionMethod.Invoke(null, methodInvokeParams); + il.LoadLocal(implicitOperatorMethod); + il.LoadNull(); // first parameter is null because implicit cast operators are static + il.LoadLocal(methodInvokeParams); + il.Call(typeof(MethodInfo).GetMethod("Invoke", new[] { typeof(object), typeof(object[]) })); + if (targetType.IsValueType) + { + il.UnboxAny(targetType); + } + else + { + il.CastClass(targetType); + } + il.StoreLocal(castValue); + }, + (il) => + { + // IL: castValue = ()value; + il.LoadLocal(value); + if (targetType.IsValueType) + { + il.UnboxAny(targetType); + } + else + { + il.CastClass(targetType); + } + il.StoreLocal(castValue); + }); + + il.LoadLocal(castValue); + } + + /// + /// Emits a call to . + /// + /// The IL emitter. + /// The string format. + /// The local variables passed to string.Format. + public static void FormatString(this Emit il, string format, params Local[] args) + { + if (format == null) throw new ArgumentNullException(nameof(format)); + if (args == null) throw new ArgumentNullException(nameof(args)); + + var guid = Guid.NewGuid().ToString("N"); + + var listType = typeof(List<>).MakeGenericType(typeof(object)); + var list = il.DeclareLocal(listType, $"formatString_list_{guid}"); + il.NewObject(listType); + il.StoreLocal(list); + + foreach (var arg in args) + { + il.LoadLocal(list); + il.LoadLocal(arg); + il.ToObject(arg.LocalType); + il.CallVirtual(listType.GetMethod("Add", new[] { typeof(object) })); + } + + var arr = il.DeclareLocal($"formatString_arr_{guid}"); + il.LoadLocal(list); + il.CallVirtual(listType.GetMethod("ToArray", new Type[0])); + il.StoreLocal(arr); + + il.LoadConstant(format); + il.LoadLocal(arr); + il.Call(typeof(string).GetMethod("Format", new[] { typeof(string), typeof(object[]) })); + } + + /// + /// Emits a call to . + /// + /// The IL emitter. + /// The message to print. + public static void NewMessage(this Emit il, string message) + { + var newMessage = typeof(DebugConsole).GetMethod( + name: nameof(DebugConsole.NewMessage), + bindingAttr: BindingFlags.Public | BindingFlags.Static, + binder: null, + types: new Type[] { typeof(string), typeof(Color?), typeof(bool) }, + modifiers: null); + il.LoadConstant(message); + il.Call(typeof(Color).GetProperty(nameof(Color.LightBlue), BindingFlags.Public | BindingFlags.Static).GetGetMethod()); + il.LoadConstant(false); + il.Call(newMessage); + } + + /// + /// Emits a call to , + /// using the string on the stack. + /// + /// The IL emitter. + public static void NewMessage(this Emit il) + { + var newMessage = typeof(DebugConsole).GetMethod( + name: nameof(DebugConsole.NewMessage), + bindingAttr: BindingFlags.Public | BindingFlags.Static, + binder: null, + types: new Type[] { typeof(string), typeof(Color?), typeof(bool) }, + modifiers: null); + il.Call(typeof(Color).GetProperty(nameof(Color.LightBlue), BindingFlags.Public | BindingFlags.Static).GetGetMethod()); + il.LoadConstant(false); + il.Call(newMessage); + } + + /// + /// Emits a foreach loop that iterates over an local variable. + /// + /// The type of elements in the enumerable. + /// The IL emitter. + /// The enumerable. + /// The body of code to run on each iteration. + public static void ForEachEnumerable(this Emit il, Local enumerable, Action action) + { + if (enumerable == null) throw new ArgumentNullException(nameof(enumerable)); + if (action == null) throw new ArgumentNullException(nameof(action)); + if (!typeof(IEnumerable).IsAssignableFrom(enumerable.LocalType)) + { + throw new ArgumentException($"Expected local type {typeof(IEnumerator)}; got {enumerable.LocalType}.", nameof(enumerable)); + } + + var guid = Guid.NewGuid().ToString("N"); + + var enumerator = il.DeclareLocal>($"forEachEnumerable_enumerator_{guid}"); + il.LoadLocal(enumerable); + il.CallVirtual(typeof(IEnumerable).GetMethod("GetEnumerator")); + il.StoreLocal(enumerator); + ForEachEnumerator(il, enumerator, action); + } + + /// + /// Emits a foreach loop that iterates over an local variable. + /// + /// The type of elements in the enumerable. + /// The IL emitter. + /// The enumerator. + /// The body of code to run on each iteration. + public static void ForEachEnumerator(this Emit il, Local enumerator, Action action) + { + if (enumerator == null) throw new ArgumentNullException(nameof(enumerator)); + if (action == null) throw new ArgumentNullException(nameof(action)); + if (!typeof(IEnumerator).IsAssignableFrom(enumerator.LocalType)) + { + throw new ArgumentException($"Expected local type {typeof(IEnumerator)}; got {enumerator.LocalType}.", nameof(enumerator)); + } + + var guid = Guid.NewGuid().ToString("N"); + var labelLoopStart = il.DefineLabel($"forEach_loopStart_{guid}"); + var labelMoveNext = il.DefineLabel($"forEach_moveNext_{guid}"); + var labelLeave = il.DefineLabel($"forEach_leave_{guid}"); + + il.BeginExceptionBlock(out var exceptionBlock); + il.Branch(labelMoveNext); // MoveNext() needs to be called at least once before iterating + il.MarkLabel(labelLoopStart); + + // IL: var current = enumerator.Current; + var current = il.DeclareLocal($"forEachEnumerator_current_{guid}"); + il.LoadLocal(enumerator); + il.CallVirtual(enumerator.LocalType.GetProperty("Current").GetGetMethod()); + il.StoreLocal(current); + + action(il, current, labelLeave); + + il.MarkLabel(labelMoveNext); + il.LoadLocal(enumerator); + il.CallVirtual(typeof(IEnumerator).GetMethod("MoveNext")); + il.BranchIfTrue(labelLoopStart); // loop if MoveNext() returns true + + // IL: finally { enumerator.Dispose(); } + il.BeginFinallyBlock(exceptionBlock, out var finallyBlock); + il.LoadLocal(enumerator); + il.CallVirtual(typeof(IDisposable).GetMethod("Dispose")); + il.EndFinallyBlock(finallyBlock); + + il.EndExceptionBlock(exceptionBlock); + + il.MarkLabel(labelLeave); + } + + /// + /// Emits a branch that only executes if the last value on the stack + /// is truthy (e.g. non-null references, 1, etc). + /// + /// The IL emitter. + /// The body of code to run if the value is truthy. + public static void If(this Emit il, Action action) + { + if (action == null) throw new ArgumentNullException(nameof(action)); + il.Branch(@if: action); + } + + /// + /// Emits a branch that only executes if the last value on the stack + /// is falsy (e.g. null references, 0, etc). + /// + /// The IL emitter. + /// The body of code to run if the value is falsy. + public static void IfNot(this Emit il, Action action) + { + if (action == null) throw new ArgumentNullException(nameof(action)); + il.Branch(@else: action); + } + + /// + /// Emits two branches that diverge based on a condition -- analogous + /// to an if-else statement. If either + /// or are omitted, it behaves the same as + /// + /// and . + /// + /// The IL emitter. + /// The body of code to run if the value is truthy. + /// The body of code to run if the value is falsy. + public static void Branch(this Emit il, Action @if = null, Action @else = null) + { + if (@if == null && @else == null) throw new ArgumentException("At least one of the two branches must be defined."); + + var guid = Guid.NewGuid().ToString("N"); + var labelEnd = il.DefineLabel($"branch_end_{guid}"); + if (@if != null && @else != null) + { + var labelElse = il.DefineLabel($"branch_else_{guid}"); + il.BranchIfFalse(labelElse); + @if(il); + il.Branch(labelEnd); + il.MarkLabel(labelElse); + @else(il); + } + else if (@if != null) + { + il.BranchIfFalse(labelEnd); + @if(il); + } + else + { + il.BranchIfTrue(labelEnd); + @else(il); + } + il.MarkLabel(labelEnd); + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/StateMachine.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/StateMachine.cs new file mode 100644 index 000000000..809a2f9c1 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/StateMachine.cs @@ -0,0 +1,107 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using Microsoft.Toolkit.Diagnostics; + +namespace Barotrauma.LuaCs; + +public class StateMachine where T : Enum +{ + private readonly ConcurrentDictionary> _states; + private State _currentState; + public T CurrentState => _currentState.StateId; + private bool _errorOnSameStateSelected; + private readonly AsyncReaderWriterLock _operationsLock = new(); + + public StateMachine(bool errorOnSameState, T defaultState, Action> onEnter, Action> onExit) + { + _errorOnSameStateSelected = errorOnSameState; + _states = new ConcurrentDictionary>(); + var defState = new State(defaultState, onEnter, onExit); + _currentState = defState; + _states[defaultState] = defState; + } + + public StateMachine AddState(T stateId, Action> onEnter, Action> onExit) + { + using var lck = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult(); + if (_states.TryGetValue(stateId, out _)) + { + ThrowHelper.ThrowArgumentException($"State with id {stateId} already exists."); + } + + _states[stateId] = new State(stateId, onEnterState: onEnter, onExitState: onExit); + return this; + } + + public StateMachine RemoveState(T stateId) + { + using var lck = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult(); + if (EqualityComparer.Default.Equals(stateId, CurrentState)) + { + ThrowHelper.ThrowInvalidOperationException($"State with id {CurrentState} is active. Cannot remove."); + } + + _states.TryRemove(stateId, out _); + return this; + } + + public StateMachine AddOrReplaceState(T oldStateId, T newStateId, Action> onEnter, Action> onExit) + { + using var lck = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult(); + if (EqualityComparer.Default.Equals(oldStateId, CurrentState)) + { + ThrowHelper.ThrowInvalidOperationException($"State with id {CurrentState} is active. Cannot replace."); + } + + _states[oldStateId] = new State(newStateId, onEnter, onExit); + return this; + } + + public StateMachine GotoState(T stateId) + { + using var lck = _operationsLock.AcquireWriterLock().ConfigureAwait(false).GetAwaiter().GetResult(); + if (EqualityComparer.Default.Equals(stateId, CurrentState)) + { + if (_errorOnSameStateSelected) + { + ThrowHelper.ThrowInvalidOperationException($"State with id {stateId} is already selected."); + } + + return this; + } + + if (!_states.TryGetValue(stateId, out var newState)) + { + ThrowHelper.ThrowArgumentNullException($"Target state with id {stateId} does not exist."); + } + + _currentState.OnExit(); + _currentState = newState; + _currentState.OnEnter(); + return this; + } +} + +public class State where T : Enum +{ + public T StateId; + private Action> _onEnter, _onExit; + public State(T stateId, Action> onEnterState, Action> onExitState) + { + StateId = stateId; + _onEnter = onEnterState; + _onExit = onExitState; + } + + public virtual void OnEnter() + { + _onEnter?.Invoke(this); + } + + public virtual void OnExit() + { + _onExit?.Invoke(this); + } +} + diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Networking/INetCallback.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Networking/INetCallback.cs new file mode 100644 index 000000000..62ce88f26 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Networking/INetCallback.cs @@ -0,0 +1,15 @@ +using System; + +namespace Barotrauma.LuaCs; + +public partial interface INetCallback +{ + public ushort CallbackId { get; } +} + +#if SERVER +public partial interface INetCallback +{ + +} +#endif diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Networking/INetworkIdProvider.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Networking/INetworkIdProvider.cs new file mode 100644 index 000000000..441f6e8bc --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Networking/INetworkIdProvider.cs @@ -0,0 +1,37 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using Barotrauma.Items.Components; +using Barotrauma.LuaCs.Data; + +namespace Barotrauma.LuaCs; + +/// +/// Provides a deterministic ID for a given instance under multiple circumstances, for use with +/// network synchronization. +/// +internal interface INetworkIdProvider : IService +{ + /// + /// Deterministically generates a GUID for the given parameters. + /// + /// The instance. + /// The GUID for the entity. + Guid GetNetworkIdForInstance([NotNull] IDataInfo instance); + + /// + /// Deterministically generates a GUID for the given parameters. + /// + /// The instance. + /// The that this instance is attached to, if any. + /// The entity type, if any. + /// The GUID for the entity. + Guid GetNetworkIdForInstance([NotNull] IDataInfo instance, TEntity attachedEntity) where TEntity : Entity; + + /// + /// Deterministically generates a GUID for the given parameters. + /// + /// The instance. + /// The that this instance is attached to, if any. + /// The GUID for the entity. + Guid GetNetworkIdForInstance([NotNull] IDataInfo instance, [MaybeNull] ItemComponent attachedItemComponent); +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Networking/INetworkSyncEntity.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Networking/INetworkSyncEntity.cs new file mode 100644 index 000000000..a13e0a190 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Networking/INetworkSyncEntity.cs @@ -0,0 +1,72 @@ +using System; +using Barotrauma.LuaCs.Data; +using Barotrauma.LuaCs; +using Barotrauma.Networking; + +namespace Barotrauma.LuaCs; + +public interface INetworkSyncVar : IDataInfo +{ + /// + /// Network-synchronized object ID. Used for networking send/receive message events. + /// + Guid InstanceId { get; } + + /// + /// Sets the that is currently managing this instance. The + /// is retrieved from here. + /// + /// The networking service managing this instance or null to deregister. + void SetNetworkOwner(IEntityNetworkingService networkingService); + + /// + /// Synchronization type. See for more information. + /// + NetSync SyncType { get; } + + /// + /// Permissions needed by clients to send net-events and/or receive net messages. + /// + ClientPermissions WritePermissions { get; } + + /// + /// Called when an incoming net message has data for this network object, typically from the same entity on another + /// machine. + /// + /// Wrapper for the internal type: + void ReadNetMessage(IReadMessage message); + + /// + /// Called when a network send-event involving this entity is triggered. Any data expected to be read by the recipient + /// network object on the other instance(s) should be written to the packet. + /// + /// Wrapper for the internal type: + void WriteNetMessage(IWriteMessage message); +} + +/// +/// Specifies the networking send/receive relationship for network object. Objects implementing this interface are +/// expected to adhere to the contract or de-sync may occur. +/// +public enum NetSync +{ + /// + /// No network synchronization. + /// + None, + /// + /// Both the client and the server have 'send' and 'receive' permissions (limited by ). Can also be used to allow two-way communication + /// with the server. + /// + TwoWay, + /// + /// Only the host/server has the authority to change this value. + /// + ServerAuthority, + /// + /// Only clients (with the required by ) may change the value and all value changes are communicated to the server/host. + ///

[Important] The host/server will not send the value to other connected clients.
+ /// Intended to allow clients to send one-way messages to the server. + ///
+ ClientOneWay +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Networking/NetworkingIdProvider.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Networking/NetworkingIdProvider.cs new file mode 100644 index 000000000..a3b2157c2 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Networking/NetworkingIdProvider.cs @@ -0,0 +1,41 @@ +using System; +using Barotrauma.Items.Components; +using Barotrauma.LuaCs.Data; +using System.Security.Cryptography; +using System.Text; + +namespace Barotrauma.LuaCs; + +internal class NetworkingIdProvider : INetworkIdProvider +{ + public void Dispose() + { + //stateless service + } + + public bool IsDisposed => false; + + private Guid GetNetworkIdFromStringMd5(string id) + { + return new Guid(MD5.Create().ComputeHash(Encoding.ASCII.GetBytes(id))); + } + + public Guid GetNetworkIdForInstance(IDataInfo instance) + { + var str = $"{instance.OwnerPackage.Name}.{instance.InternalName}"; + return GetNetworkIdFromStringMd5(str); + } + + public Guid GetNetworkIdForInstance(IDataInfo instance, TEntity attachedEntity) where TEntity : Entity + { + var str = $"{nameof(TEntity)}({attachedEntity.ID}).{instance.OwnerPackage.Name}.{instance.InternalName}"; + return GetNetworkIdFromStringMd5(str); + } + + public Guid GetNetworkIdForInstance(IDataInfo instance, ItemComponent attachedItemComponent) + { + var attachedEntity = attachedItemComponent.Item; + var str = $"{attachedEntity.GetType().Name}({attachedEntity.ID}).ComponentId({attachedEntity.Components.IndexOf(attachedItemComponent)}).{instance.OwnerPackage.Name}.{instance.InternalName}"; + return GetNetworkIdFromStringMd5(str); + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Plugins/ACsMod.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Plugins/ACsMod.cs similarity index 83% rename from Barotrauma/BarotraumaShared/SharedSource/LuaCs/Plugins/ACsMod.cs rename to Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Plugins/ACsMod.cs index 76dfac73f..1a189e31b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Plugins/ACsMod.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Plugins/ACsMod.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.IO; +using Barotrauma.LuaCs; namespace Barotrauma { @@ -8,9 +9,11 @@ namespace Barotrauma public abstract class ACsMod : IAssemblyPlugin { private static List mods = new List(); + [Obsolete("$This does nothing. Stop using it!")] public static List LoadedMods { get => mods; } private const string MOD_STORE = "LocalMods/.modstore"; + [Obsolete("$This does nothing. Stop using it!")] public static string GetStoreFolder() where T : ACsMod { if (!Directory.Exists(MOD_STORE)) Directory.CreateDirectory(MOD_STORE); @@ -19,14 +22,7 @@ namespace Barotrauma return modFolder; } - public bool IsDisposed { get; private set; } - - /// Mod initialization - public ACsMod() - { - IsDisposed = false; - LoadedMods.Add(this); - } + public bool IsDisposed { get; private set; } = false; /// /// Called as soon as plugin loading begins, use this for internal setup only. @@ -52,10 +48,8 @@ namespace Barotrauma } catch (Exception e) { - LuaCsLogger.HandleException(e, LuaCsMessageOrigin.CSharpMod); + LuaCsSetup.Instance.Logger.HandleException(e); } - - LoadedMods.Remove(this); IsDisposed = true; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Plugins/AssemblyLoader.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Plugins/AssemblyLoader.cs new file mode 100644 index 000000000..9b964cf65 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Plugins/AssemblyLoader.cs @@ -0,0 +1,721 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Loader; +using System.Text; +using System.Threading; +using Barotrauma.Extensions; +using Barotrauma.LuaCs; +using Microsoft.CodeAnalysis; +using FluentResults; +using FluentResults.LuaCs; +using Microsoft.CodeAnalysis.CSharp; +using OneOf; +using Path = System.IO.Path; + +[assembly: InternalsVisibleTo(IAssemblyLoaderService.InternalsAwareAssemblyName)] + +namespace Barotrauma.LuaCs; +public sealed class AssemblyLoader : AssemblyLoadContext, IAssemblyLoaderService +{ + public class Factory : IAssemblyLoaderService.IFactory + { + public IAssemblyLoaderService CreateInstance(IAssemblyLoaderService.LoaderInitData initData) + { + return new AssemblyLoader(initData); + } + + public void Dispose() + { + //stateless service + } + public bool IsDisposed => false; + } + + public Guid Id { get; init; } + public ContentPackage OwnerPackage { get; private set; } + public bool IsReferenceOnlyMode { get; init; } + public bool IsDisposed + { + get => ModUtils.Threading.GetBool(ref _isDisposed); + private set => ModUtils.Threading.SetBool(ref _isDisposed, value); + } + private int _isDisposed; + + /// + /// This bool-int wrapper increments/decrements when set as true/false respectively and return true if the value > 0. + /// + private bool AreOperationRunning + { + get => Interlocked.CompareExchange(ref _operationsRunning, 0, 0) > 0; + set // we use the set as our inc/decr + { + if (value) + { + Interlocked.Add(ref _operationsRunning, 1); + } + else + { + Interlocked.Add(ref _operationsRunning, -1); + } + } + } + private int _operationsRunning; + + //internal + private readonly Action _onUnload; + private readonly Func _onResolvingManaged; + private readonly Func _onResolvingUnmanagedDll; + private readonly ConcurrentDictionary _dependencyResolvers = new(); + private readonly ConcurrentDictionary _loadedAssemblyData = new(); + + private readonly ThreadLocal _isResolving = new(static()=>false); // cyclic resolution exit + private readonly ThreadLocal _isResolvingNative = new(static () => false); + + public AssemblyLoader(IAssemblyLoaderService.LoaderInitData initData) + : base(isCollectible: true, name: initData.Name) + { + Id = initData.InstanceId; + IsReferenceOnlyMode = initData.IsReferenceMode; + this._onUnload = initData.OnUnload; + this._onResolvingManaged = initData.OnResolvingManaged; + this._onResolvingUnmanagedDll = initData.OnResolvingUnmanagedDll; + this.OwnerPackage = initData.OwnerPackage; + base.Unloading += OnUnload; + base.Resolving += OnResolvingManagedAssembly; + base.ResolvingUnmanagedDll += OnResolvingUnmanagedDll; + } + + private IntPtr OnResolvingUnmanagedDll(Assembly invokingAssembly, string assemblyName) + { + if (IsDisposed) + return 0; + + if (_isResolvingNative.Value) + return 0; + + AreOperationRunning = true; + _isResolvingNative.Value = true; + try + { + if (!_dependencyResolvers.IsEmpty) + { + foreach (var resolver in _dependencyResolvers) + { + try + { + var path = resolver.Value.ResolveUnmanagedDllToPath(assemblyName); + if (path.IsNullOrWhiteSpace()) + continue; + return base.LoadUnmanagedDllFromPath(path); + } + catch + { + // ignored + continue; + } + } + } + + if (_onResolvingUnmanagedDll is not null) + { + try + { + return _onResolvingUnmanagedDll(invokingAssembly, assemblyName); + } + catch + { + // ignored + } + } + + return 0; + } + finally + { + AreOperationRunning = false; + _isResolvingNative.Value = false; + } + } + + private Assembly OnResolvingManagedAssembly(AssemblyLoadContext assemblyLoadContext, AssemblyName assemblyName) + { + if (IsDisposed) + return null; + + if (_isResolving.Value) + return null; + + if (assemblyLoadContext != this) + return null; + + AreOperationRunning = true; + _isResolving.Value = true; + try + { + if (!_dependencyResolvers.IsEmpty) + { + foreach (var resolver in _dependencyResolvers) + { + try + { + var path = resolver.Value.ResolveAssemblyToPath(assemblyName); + if (path.IsNullOrWhiteSpace()) + continue; + return assemblyLoadContext.LoadFromAssemblyPath(path); + } + catch + { + // ignored + continue; + } + } + } + + if (_onResolvingManaged is not null) + { + try + { + return _onResolvingManaged(this, assemblyName); + } + catch + { + // ignored + } + } + + return null; + } + finally + { + AreOperationRunning = false; + _isResolving.Value = false; + } + } + + public IEnumerable AssemblyReferences + { + get + { + if (IsDisposed || _loadedAssemblyData.IsEmpty) + yield return null; + AreOperationRunning = true; + foreach (var data in _loadedAssemblyData.Values) + { + if (data.AssemblyReference is null) + { + continue; + } + yield return data.AssemblyReference; + } + AreOperationRunning = false; + } + } + + public FluentResults.Result AddDependencyPaths(ImmutableArray paths) + { + if (IsDisposed) + return FluentResults.Result.Fail($"Loader is disposed!"); + AreOperationRunning = true; + try + { + if (paths.Length == 0) + return FluentResults.Result.Ok(); + var res = new FluentResults.Result(); + foreach (var path in paths) + { + try + { + var p = Path.GetFullPath(path.CleanUpPath()); + if (!_dependencyResolvers.ContainsKey(p)) + { + _dependencyResolvers[p] = new AssemblyDependencyResolver(p); + } + } + catch (Exception ex) + { + res = res.WithError(new ExceptionalError(ex) + .WithMetadata(MetadataType.Sources, path)); + } + } + + if (res.Errors.Any()) + return FluentResults.Result.Fail(res.Errors); + return FluentResults.Result.Ok(); + } + finally + { + AreOperationRunning = false; + } + } + + public Result CompileScriptAssembly([NotNull] string assemblyName, + bool compileWithInternalAccess, + ImmutableArray syntaxTrees, + ImmutableArray metadataReferences, + CSharpCompilationOptions compilationOptions = null) + { + if (IsDisposed) + return FluentResults.Result.Fail($"Loader is disposed!"); + AreOperationRunning = true; + try + { + if (assemblyName.IsNullOrWhiteSpace()) + { + return new Result().WithError(new Error($"The name provided is null!") + .WithMetadata(MetadataType.ExceptionObject, this) + .WithMetadata(MetadataType.RootObject, syntaxTrees)); + } + + if (_loadedAssemblyData.ContainsKey(assemblyName)) + { + return new Result().WithError( + new Error($"The name provided is already assigned to an assembly!") + .WithMetadata(MetadataType.ExceptionObject, this) + .WithMetadata(MetadataType.RootObject, syntaxTrees)); + } + + var compilationAssemblyName = compileWithInternalAccess + ? IAssemblyLoaderService.InternalsAwareAssemblyName + : assemblyName; + + compilationOptions ??= new CSharpCompilationOptions( + outputKind: OutputKind.DynamicallyLinkedLibrary, + optimizationLevel: OptimizationLevel.Release, + concurrentBuild: true, + reportSuppressedDiagnostics: false, + warningLevel: 0, + allowUnsafe: true); + + if (!compileWithInternalAccess) + { + typeof(CSharpCompilationOptions) + .GetProperty("TopLevelBinderFlags", BindingFlags.Instance | BindingFlags.NonPublic) + ?.SetValue(compilationOptions, + (uint)1 << 25 // CSharp.BinderFlags.AllowAwaitInUnsafeContext + | (uint)1 << 22 // CSharp.BinderFlags.IgnoreAccessibility + | (uint)1 << 1 // CSharp.BinderFlags.SuppressObsoleteChecks + ); + } + + using var asmMemoryStream = new MemoryStream(); + var result = CSharpCompilation + .Create(compilationAssemblyName, syntaxTrees, + metadataReferences, compilationOptions) + .Emit(asmMemoryStream); + if (!result.Success) + { + StringBuilder sb = new StringBuilder(); + foreach (var resultDiagnostic in result.Diagnostics) + { + if (resultDiagnostic.IsWarningAsError || resultDiagnostic.Severity == DiagnosticSeverity.Error) + { + //sb.AppendLine($">>> {resultDiagnostic.GetMessage()} | Location: {resultDiagnostic.Location.SourceTree?.GetLineSpan(resultDiagnostic.Location.SourceSpan)} "); + sb.AppendLine($"\n{resultDiagnostic}"); + } + } + var res = new FluentResults.Result().WithError( + new Error($"Package Error: {OwnerPackage.Name}: Compilation failed for assembly {assemblyName}!\n {sb.ToString()}\n") + .WithMetadata(MetadataType.ExceptionObject, this) + .WithMetadata(MetadataType.RootObject, syntaxTrees)); + + return res; + } + + asmMemoryStream.Seek(0, SeekOrigin.Begin); + var data = new AssemblyData(LoadFromStream(asmMemoryStream), asmMemoryStream.ToArray()); + _loadedAssemblyData[data.Assembly] = data; + return new Result().WithSuccess($"Compiled assembly {assemblyName} successful.") + .WithValue(data.Assembly); + } + catch (Exception ex) + { + return new FluentResults.Result().WithError(new ExceptionalError(ex) + .WithMetadata(MetadataType.ExceptionObject, this) + .WithMetadata(MetadataType.RootObject, assemblyName) + .WithMetadata(MetadataType.Sources, syntaxTrees)); + } + finally + { + AreOperationRunning = false; + } + } + + public FluentResults.Result LoadAssemblyFromFile(string assemblyFilePath, + ImmutableArray additionalDependencyPaths) + { + if (IsDisposed) + return FluentResults.Result.Fail($"Loader is disposed!"); + + AreOperationRunning = true; + try + { + if (assemblyFilePath.IsNullOrWhiteSpace()) + return new Result().WithError(new Error($"The path provided is empty.")); + + if (additionalDependencyPaths.Any()) + { + var r = AddDependencyPaths(additionalDependencyPaths); + if (r.IsFailed) + { + // we have errors, loading may not work. + return FluentResults.Result.Fail(new Error($"Failed to load dependency paths for '{assemblyFilePath}' with paths: {additionalDependencyPaths.Aggregate((s, ac) => $"{ac}| P={s}")}.") + .WithMetadata(MetadataType.ExceptionObject, this) + .WithMetadata(MetadataType.RootObject, assemblyFilePath)) + .WithErrors(r.Errors); + } + } + + string sanitizedFilePath = Path.GetFullPath(assemblyFilePath.CleanUpPath()); + string directoryKey = Path.GetDirectoryName(sanitizedFilePath); + + if (directoryKey is null) + { + return FluentResults.Result.Fail(new Error($"Unable to load assembly: bath file path: {assemblyFilePath}") + .WithMetadata(MetadataType.ExceptionObject, this) + .WithMetadata(MetadataType.RootObject, sanitizedFilePath)); + } + + try + { + var assembly = LoadFromAssemblyPath(sanitizedFilePath); + _loadedAssemblyData[assembly] = new AssemblyData(assembly, assembly.Location); + return new Result().WithSuccess($"Loaded assembly '{assembly.GetName()}'").WithValue(assembly); + } + catch (FileNotFoundException fnfe) + { + // last attempt + try + { + var assemblyName = new AssemblyName(System.IO.Path.GetFileName(sanitizedFilePath)); + foreach (var resolver in _dependencyResolvers) + { + try + { + var path = resolver.Value.ResolveAssemblyToPath(assemblyName); + return base.LoadFromAssemblyPath(path); + } + catch + { + continue; + } + } + return GenerateExceptionReturn(fnfe); + } + catch (Exception e) + { + return GenerateExceptionReturn(fnfe); + } + } + catch (Exception e) + { + return GenerateExceptionReturn(e); + } + } + finally + { + AreOperationRunning = false; + } + + FluentResults.Result GenerateExceptionReturn(T exception) where T : Exception + { + return FluentResults.Result.Fail(new ExceptionalError(exception) + .WithMetadata(MetadataType.ExceptionObject, this) + .WithMetadata(MetadataType.RootObject, assemblyFilePath) + .WithMetadata(MetadataType.ExceptionDetails, exception.Message) + .WithMetadata(MetadataType.StackTrace, exception.StackTrace)); + } + } + + public FluentResults.Result GetAssemblyByName(string assemblyName) + { + if (IsDisposed) + return FluentResults.Result.Fail(new Error($"Loader is disposed!")); + if (assemblyName.IsNullOrWhiteSpace()) + { + return FluentResults.Result.Fail(new Error($"Assembly name is empty.") + .WithMetadata(MetadataType.ExceptionObject, this)); + } + AreOperationRunning = true; + try + { + if (_loadedAssemblyData.TryGetValue(assemblyName, out var data)) + { + return new Result().WithSuccess(new Success($"Assembly found.")).WithValue(data.Assembly); + } + + // search any assemblies that were background loaded and we're unaware of. + foreach (var assembly1 in this.Assemblies.Where(a => !_loadedAssemblyData.ContainsKey(a))) + { + if (assembly1.GetName().FullName == assemblyName) + { + try + { + if (!assembly1.Location.IsNullOrWhiteSpace()) + { + _loadedAssemblyData[assembly1] = new AssemblyData(assembly1, assembly1.Location); + } + // we don't have the original byte array so we can't store it. + } + catch (NotSupportedException nse) // dynamic assembly or location property threw + { + // ignored + } + + return new Result().WithSuccess(new Success($"Assembly found.")).WithValue(assembly1); + } + } + + return FluentResults.Result.Fail(new Error($"Assembly named '{ assemblyName }' not found!")); + } + finally + { + AreOperationRunning = false; + } + } + + public FluentResults.Result> GetTypesInAssemblies() + { + if (IsDisposed) + return FluentResults.Result.Fail(new Error($"Loader is disposed!")); + AreOperationRunning = true; + try + { + return new FluentResults.Result>().WithValue(_loadedAssemblyData + .SelectMany(kvp => kvp.Value.Types).ToImmutableArray()); + } + catch (Exception e) + { + return FluentResults.Result.Fail(new ExceptionalError(e)); + } + finally + { + AreOperationRunning = false; + } + } + + public IEnumerable UnsafeGetTypesInAssemblies() + { + if (IsDisposed) + yield return null; + AreOperationRunning = true; + try + { + if (_loadedAssemblyData.None()) + { + yield return null; + } + else + { + foreach (var assemblyData in _loadedAssemblyData.Values) + { + foreach (var type in assemblyData.Types) + { + yield return type; + } + } + } + } + finally + { + AreOperationRunning = false; + } + } + + public Result GetTypeInAssemblies(string typeName) + { + if (IsDisposed) + return FluentResults.Result.Fail(new Error($"Loader is disposed!")); + AreOperationRunning = true; + try + { + if (_loadedAssemblyData.IsEmpty) + return FluentResults.Result.Fail(new Error($"No assemblies loaded!")); + foreach (var assemblyData in _loadedAssemblyData) + { + if (assemblyData.Value.TypesByName.TryGetValue(typeName, out var type)) + return new FluentResults.Result().WithSuccess($"Found type.").WithValue(type); + } + return FluentResults.Result.Fail(new Error($"No matching types found for { typeName }!")); + } + finally + { + AreOperationRunning = false; + } + } + + public void Dispose() + { + if (IsDisposed) + return; // we don't want to invoke events twice nor cause strong GC handles. + IsDisposed = true; + this.Unload(); + this.DisposeInternal(); + GC.SuppressFinalize(this); + } + + ~AssemblyLoader() + { + this.DisposeInternal(); + } + + private void OnUnload(AssemblyLoadContext context) + { + // Try to wait for loading ops on other threads if they happen to occur with a timeout. + // This should be an edge, should it even occur. + DateTime timeout = DateTime.Now.AddSeconds(2); + while (timeout > DateTime.Now) + { + if (!AreOperationRunning) + break; + Thread.Sleep(1000/Timing.FixedUpdateRate-1); + } + + var wf = new WeakReference(this); + _onUnload?.Invoke(this); + } + + private void DisposeInternal() + { + IsDisposed = true; + base.Resolving -= OnResolvingManagedAssembly; + base.ResolvingUnmanagedDll -= OnResolvingUnmanagedDll; + base.Unloading -= OnUnload; + this._dependencyResolvers.Clear(); + this._loadedAssemblyData.Clear(); + } + + protected override Assembly Load(AssemblyName assemblyName) + { + if (IsDisposed) + return null; + AreOperationRunning = true; + try + { + if (_loadedAssemblyData.TryGetValue(assemblyName.FullName, out var assembly)) + return assembly.Assembly; + return null; + } + catch + { + return null; + } + finally + { + AreOperationRunning = false; + } + } + + protected override IntPtr LoadUnmanagedDll(string unmanagedDllName) + { + if (IsDisposed) + return 0; + + GCHandle? handle = null; + AreOperationRunning = true; + try + { + if (_loadedAssemblyData.TryGetValue(unmanagedDllName, out var assemblyData)) + { + handle = GCHandle.Alloc(assemblyData.Assembly, GCHandleType.Pinned); + nint asmPtr = GCHandle.ToIntPtr(handle.Value); + return asmPtr; + } + } + catch + { + return 0; + } + finally + { + AreOperationRunning = false; + try + { + if (handle.HasValue) + handle.Value.Free(); + } + catch + { + // ignored. We just want to ensure that free is called. + } + } + + return 0; + } + + private readonly record struct AssemblyData + { + public readonly Assembly Assembly; + public readonly OneOf AssemblyImageOrPath; + public readonly MetadataReference AssemblyReference; + public readonly ImmutableArray Types; + public readonly ImmutableDictionary TypesByName; + + public AssemblyData(Assembly assembly, byte[] assemblyImage) + { + Assembly = assembly ?? throw new ArgumentNullException(nameof(assembly)); + AssemblyImageOrPath = assemblyImage ?? throw new ArgumentNullException(nameof(assemblyImage)); + AssemblyReference = MetadataReference.CreateFromImage(assemblyImage); + Types = assembly.GetSafeTypes().ToImmutableArray(); + TypesByName = Types.ToImmutableDictionary(type => type.FullName, type => type); + } + + public AssemblyData(Assembly assembly, string path) + { + Assembly = assembly ?? throw new ArgumentNullException(nameof(assembly)); + AssemblyImageOrPath = path ?? throw new ArgumentNullException(nameof(path)); + AssemblyReference = MetadataReference.CreateFromFile(path); + Types = assembly.GetSafeTypes().ToImmutableArray(); + TypesByName = Types.ToImmutableDictionary(type => type.FullName, type => type); + } + } + + private readonly record struct AssemblyOrStringKey : IEquatable, IEqualityComparer + { + public Assembly Assembly { get; init; } + public string AssemblyName { get; init; } + public readonly int HashCode; + + public AssemblyOrStringKey(Assembly assembly) + { + if(assembly == null) + throw new ArgumentNullException(nameof(assembly)); + Assembly = assembly; + AssemblyName = assembly.GetName().FullName; + if (AssemblyName == null) + throw new ArgumentNullException(nameof(AssemblyName)); + HashCode = AssemblyName.GetHashCode(); + } + + public AssemblyOrStringKey(string assemblyName) + { + if (assemblyName.IsNullOrWhiteSpace()) + throw new ArgumentNullException(nameof(assemblyName)); + Assembly = null; + AssemblyName = assemblyName; + HashCode = AssemblyName.GetHashCode(); + } + + public bool Equals(AssemblyOrStringKey x, AssemblyOrStringKey y) + { + if (x.Assembly is not null && y.Assembly is not null) + return x.Assembly == y.Assembly; + return x.AssemblyName == y.AssemblyName; + } + + public int GetHashCode(AssemblyOrStringKey obj) + { + return this.HashCode; + } + + public static implicit operator AssemblyOrStringKey(Assembly assembly) => new AssemblyOrStringKey(assembly); + public static implicit operator AssemblyOrStringKey(string name) => new AssemblyOrStringKey(name); + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Plugins/IAssemblyLoaderService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Plugins/IAssemblyLoaderService.cs new file mode 100644 index 000000000..f02f5c89b --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Plugins/IAssemblyLoaderService.cs @@ -0,0 +1,139 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.ComponentModel.DataAnnotations; +using System.Diagnostics.CodeAnalysis; +using System.Reflection; +using System.Runtime.CompilerServices; +using Barotrauma.LuaCs; +using FluentResults; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; + +namespace Barotrauma.LuaCs; + +public interface IAssemblyLoaderService : IService +{ + public interface IFactory : IService + { + IAssemblyLoaderService CreateInstance(LoaderInitData initData); + } + + /// + /// Constructor record for instancing. + /// + /// + /// + /// + /// Assemblies and Types in this context are for only. + /// Execution of assembly data is forbidden. + /// + /// + /// + /// + public record LoaderInitData( + [Required] Guid InstanceId, + [Required][NotNull] string Name, + [Required] bool IsReferenceMode, + ContentPackage OwnerPackage, + Action OnUnload, + Func OnResolvingManaged, + Func OnResolvingUnmanagedDll); + + /// + /// ID for this instance. + /// + Guid Id { get; } + /// + /// The owner content package. + /// + ContentPackage OwnerPackage { get; } + /// + /// Indicates that the assemblies in this load context are metadata references only and not + /// intended for execution. + /// + bool IsReferenceOnlyMode { get; } + /// + /// Runtime value of constant for extensibility use. + /// + public static readonly string InternalsAccessAssemblyName = InternalsAwareAssemblyName; + /// + /// Name for all runtime-compiled assemblies requiring access to internal assembly components. + /// + public const string InternalsAwareAssemblyName = "InternalsAwareAssembly"; + + /// + /// Add additional locations for dependency resolution to use. + /// + /// + /// + public FluentResults.Result AddDependencyPaths(ImmutableArray paths); + + /// + /// Compiles the supplied syntaxtrees and options into an in-memory assembly image. + /// Builds metadata from loaded assemblies, only supply your own if you have in-memory images not managed by the + /// AssemblyManager class. + /// + /// [NotNull]Name reference of the assembly. + /// [IMPORTANT] This is used to reference this assembly as the true name will be forced if + /// publicized assemblies are not used (InternalsVisibleTo Attrib). + /// Must be supplied for in-memory assemblies. + /// Must be unique to all other assemblies explicitly loaded using this context. + /// Forces the assembly name to and grants access to internal. + /// [NotNull]Syntax trees to compile into the assembly. + /// All MetadataReferences to be used for compilation. + /// [IMPORTANT] This method builds metadata from loaded assemblies, only supply your own if you have in-memory + /// images not managed by the AssemblyManager class. + /// [NotNull]CSharp compilation options. This method automatically adds the 'IgnoreAccessChecks' property for compilation. + /// [IMPORTANT]Cannot be null or empty if is false. + /// Success state of the operation. + public Result CompileScriptAssembly([NotNull] string assemblyName, + bool compileWithInternalAccess, + ImmutableArray syntaxTrees, + ImmutableArray metadataReferences, + CSharpCompilationOptions compilationOptions = null); + + /// + /// Loads the assembly from the provided location and registers all new paths provided with dependency resolution. + /// + /// Absolute path to the managed assembly. + /// Additional paths for dependency resolution. + /// Success and reference to the assembly if successful. + public FluentResults.Result LoadAssemblyFromFile(string assemblyFilePath, + ImmutableArray additionalDependencyPaths); + + /// + /// Returns the already loaded assembly with the same name. + /// + /// Name of the assembly. + /// Operation success on assembly found and assembly. + public FluentResults.Result GetAssemblyByName(string assemblyName); + + /// + /// Gets the list of Types from loaded assemblies. + /// + /// + public FluentResults.Result> GetTypesInAssemblies(); + + /// + /// Gets the list of Types from loaded assemblies. Does not create a defensive copy and blocks loading/unloading. + /// + /// + public IEnumerable UnsafeGetTypesInAssemblies(); + + /// + /// Returns the first found type given it's fully qualified name. + /// + /// + /// + public FluentResults.Result GetTypeInAssemblies(string typeName); + + /// + /// List of loaded assemblies. + /// + public IEnumerable Assemblies { get; } + + public IEnumerable AssemblyReferences { get; } +} + + diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Plugins/IAssemblyPlugin.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Plugins/IAssemblyPlugin.cs new file mode 100644 index 000000000..7667c86cd --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Plugins/IAssemblyPlugin.cs @@ -0,0 +1,6 @@ +using System; +using Barotrauma.LuaCs.Events; + +namespace Barotrauma.LuaCs; + +public interface IAssemblyPlugin : IDisposable, IEventPluginPreInitialize, IEventPluginInitialize, IEventPluginLoadCompleted { } diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Plugins/RunConfig.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Plugins/RunConfig.cs similarity index 93% rename from Barotrauma/BarotraumaShared/SharedSource/LuaCs/Plugins/RunConfig.cs rename to Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Plugins/RunConfig.cs index 64bc66006..d5283156a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Plugins/RunConfig.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Plugins/RunConfig.cs @@ -1,25 +1,27 @@ using System; using System.ComponentModel; using System.Xml.Serialization; +using Barotrauma.LuaCs.Data; namespace Barotrauma; [Serializable] -public sealed class RunConfig +[Obsolete($"Use {nameof(IModConfigInfo)} instead. This class exists for legacy compatibility only.")] +public sealed class RunConfig : IRunConfig { /// /// How should scripts be run on the server. /// [XmlElement(ElementName = "Server")] [DefaultValue("Standard")] - public string Server; + public string Server { get; set; } /// /// How should scripts be run on the client. /// [XmlElement(ElementName = "Client")] [DefaultValue("Standard")] - public string Client; + public string Client { get; set; } /// /// List of dependencies by either Steam Workshop ID or by Partial Inclusive Name (ie. "ModDep" will match a mod named "A ModDependency"). diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/ConfigService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/ConfigService.cs new file mode 100644 index 000000000..b6a4732ec --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/ConfigService.cs @@ -0,0 +1,682 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Frozen; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.IO; +using System.Linq; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; +using System.Xml; +using System.Xml.Linq; +using Barotrauma.LuaCs.Data; +using Barotrauma.LuaCs.Events; +using Barotrauma.LuaCs; +using FluentResults; +using Microsoft.Toolkit.Diagnostics; +using Microsoft.Xna.Framework; + +namespace Barotrauma.LuaCs; + +public sealed partial class ConfigService : IConfigService +{ + #region Disposal_Locks_Reset + + private readonly AsyncReaderWriterLock _operationLock = new (); + private readonly AsyncReaderWriterLock _settingsByPackageLock = new (); + private int _isDisposed = 0; + public bool IsDisposed + { + get => ModUtils.Threading.GetBool(ref _isDisposed); + private set => ModUtils.Threading.SetBool(ref _isDisposed, value); + } + + public void Dispose() + { + using var lck = _operationLock.AcquireWriterLock().ConfigureAwait(false).GetAwaiter().GetResult(); + using var settingsLck = _settingsByPackageLock.AcquireWriterLock().ConfigureAwait(false).GetAwaiter().GetResult(); + if (!ModUtils.Threading.CheckIfClearAndSetBool(ref _isDisposed)) + { + return; + } + + _logger.LogDebug($"{nameof(ConfigService)}: Disposing."); + + _configInfoParserService.Dispose(); + _configProfileInfoParserService.Dispose(); + + if (!_settingsInstances.IsEmpty) + { + foreach (var instance in _settingsInstances) + { + try + { + if (instance.Value is null) + { + continue; + } + + _eventService.PublishEvent(sub => + // ReSharper disable once AccessToDisposedClosure + sub.OnSettingInstanceDisposed(instance.Value)); + instance.Value.Dispose(); + } + catch + { + // ignored + continue; + } + } + } + + _settingsInstances.Clear(); + _instanceFactory.Clear(); + _settingsInstancesByPackage.Clear(); + _commandsService.Dispose(); + + _storageService = null; + _logger = null; + _eventService = null; + _configInfoParserService = null; + _configProfileInfoParserService = null; + _commandsService = null; + _infoProvider = null; + } + + public FluentResults.Result Reset() + { + using var lck = _operationLock.AcquireWriterLock().ConfigureAwait(false).GetAwaiter().GetResult(); + IService.CheckDisposed(this); + + var result = new FluentResults.Result(); + + if (!_settingsInstances.IsEmpty) + { + foreach (var instance in _settingsInstances) + { + try + { + if (instance.Value is null) + { + continue; + } + + _eventService.PublishEvent(sub => + // ReSharper disable once AccessToDisposedClosure + sub.OnSettingInstanceDisposed(instance.Value)); + instance.Value.Dispose(); + } + catch (Exception e) + { + result.WithError(new ExceptionalError(e)); + } + } + } + + _settingsInstances.Clear(); + _instanceFactory.Clear(); + _settingsInstancesByPackage.Clear(); + _storageService.PurgeCache(); + + return result; + } + + #endregion + + private const string SaveDataFileName = "SettingsData.xml"; + + // --- Settings + private readonly ConcurrentDictionary<(ContentPackage OwnerPackage, string InternalName), ISettingBase> + _settingsInstances = new(); + private readonly ConcurrentDictionary> + _instanceFactory = new(); + private readonly ConcurrentDictionary> + _settingsInstancesByPackage = new(); + + // --- Profiles + private readonly ConcurrentDictionary<(ContentPackage Package, string ProfileName), IConfigProfileInfo> + _settingsProfiles = new(); + + private IStorageService _storageService; + private ILoggerService _logger; + private IEventService _eventService; + private IConsoleCommandsService _commandsService; + private ILuaCsInfoProvider _infoProvider; + private IParserServiceOneToManyAsync _configInfoParserService; + private IParserServiceOneToManyAsync _configProfileInfoParserService; + + public ConfigService(ILoggerService logger, + IStorageService storageService, + IParserServiceOneToManyAsync configInfoParserService, + IParserServiceOneToManyAsync configProfileInfoParserService, + IEventService eventService, + IConsoleCommandsService commandsService, + ILuaCsInfoProvider infoProvider) + { + _logger = logger; + _storageService = storageService; + _configInfoParserService = configInfoParserService; + _configProfileInfoParserService = configProfileInfoParserService; + _eventService = eventService; + _commandsService = commandsService; + _infoProvider = infoProvider; + + _storageService.UseCaching = false; + InjectCommands(commandsService); + } + + private void InjectCommands(IConsoleCommandsService commandsService) + { + commandsService.RegisterCommand("cfg_getvalue", "cfg_getvalue [Content Package] [InternalName] [ValueString]: gets a config value.", (string[] args) => + { + if (args.Length < 1) + { + _logger.LogError("Please specify the name of the package to set the config."); + return; + } + + if (args.Length < 2) + { + _logger.LogError("Please specify the name of the config."); + return; + } + + var package = ContentPackageManager.RegularPackages.FirstOrDefault(p => p.Name == args[0]); + if (package == null) + { + _logger.LogError($"Could not find the package {args[0]}!"); + return; + } + + string internalName = args[1]; + + if (!TryGetConfig(package, internalName, out ISettingBase setting)) + { + _logger.LogError($"Could not get config with name {internalName}"); + return; + } + + _logger.LogMessage($"config {internalName} value is {setting.GetStringValue()}", Color.Green); + }, getValidArgs: () => new[] + { + ContentPackageManager.RegularPackages.Select(p => p.Name).ToArray() + }); + + commandsService.RegisterCommand("cfg_setvalue", "cfg_setvalue [Content Package] [InternalName] [ValueString]: sets a config.", (string[] args) => + { + if (args.Length < 1) + { + _logger.LogError("Please specify the name of the package to set the config."); + return; + } + + if (args.Length < 2) + { + _logger.LogError("Please specify the name of the config."); + return; + } + + if (args.Length < 3) + { + _logger.LogError("Please specify the value to set the config to."); + return; + } + + var package = ContentPackageManager.RegularPackages.FirstOrDefault(p => p.Name == args[0]); + if (package == null) + { + _logger.LogError($"Could not find the package {args[0]}!"); + return; + } + + string internalName = args[1]; + string valueString = args[2]; + + if (!TryGetConfig(package, internalName, out ISettingBase setting)) + { + _logger.LogError($"Could not get config with name {internalName}"); + return; + } + + if (setting.TrySetSerializedValue(valueString)) + { + _logger.LogMessage($"Set config {internalName} value to {valueString}", Color.Green); + if (SaveConfigValue(setting) is { IsFailed: true } res) + { + _logger.LogMessage($"Failed to save new config data to disk. Reasons: {res.ToString()}"); + } + } + else + { + _logger.LogError($"Failed to set config value"); + } + }, getValidArgs: () => new[] + { + ContentPackageManager.RegularPackages.Select(p => p.Name).ToArray() + }); + + commandsService.RegisterCommand("cfg_setprofile", "cfg_setprofile [ContentPackage] [InternalProfileName]", + (string[] args) => + { + if (args.Length < 1 || args[0].IsNullOrWhiteSpace()) + { + _logger.LogError("Please specify the name of the package of the profile."); + return; + } + + if (args.Length < 2 || args[1].IsNullOrWhiteSpace()) + { + _logger.LogError("Please specify the name of the profile."); + return; + } + + var package = ContentPackageManager.RegularPackages.FirstOrDefault(p => p.Name == args[0], null); + if (package == null) + { + _logger.LogError($"Could not find the package {args[0]}!"); + return; + } + + var res = ApplyConfigProfile(package, args[1]); + if (res.IsFailed) + { + _logger.LogError($"Errors while applying profile {args[1]}!"); + _logger.LogResults(res); + return; + } + _logger.Log($"Profile {args[1]} applied successfully!", Color.Green); + }, getValidArgs: () => new[] + { + ContentPackageManager.RegularPackages.Select(p => p.Name).ToArray() + }, false); + } + + public void RegisterSettingTypeInitializer(string typeIdentifier, Func<(IConfigService ConfigService, IConfigInfo Info), T> settingFactory) where T : class, ISettingBase + { + Guard.IsNotNullOrWhiteSpace(typeIdentifier, nameof(typeIdentifier)); + Guard.IsNotNull(settingFactory, nameof(settingFactory)); + using var lck = _operationLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult(); + IService.CheckDisposed(this); + + if (_instanceFactory.ContainsKey(typeIdentifier)) + { + ThrowHelper.ThrowArgumentException($"{nameof(RegisterSettingTypeInitializer)}: The type identifier {typeIdentifier} is already registered."); + } + + _instanceFactory[typeIdentifier] = settingFactory; + } + + private static ImmutableArray SelectCompatible(ImmutableArray resources) where T : IBaseResourceInfo + { + return resources + .Where(r => r.SupportedPlatforms.HasFlag(ModUtils.Environment.CurrentPlatform)) + .Where(r => r.SupportedTargets.HasFlag(ModUtils.Environment.CurrentTarget)) + .OrderBy(r => r.Optional ? 1 : 0) // optional content last + .ThenBy(r => r.LoadPriority) + .ToImmutableArray(); + } + + public async Task LoadConfigsAsync(ImmutableArray configResources) + { + using var lck = await _operationLock.AcquireReaderLock(); + IService.CheckDisposed(this); + if (configResources.IsDefaultOrEmpty) + { + return FluentResults.Result.Ok(); + } + + var result = new FluentResults.Result(); + + var taskBuilder = ImmutableArray.CreateBuilder>>(); + var toProcessErrors = new ConcurrentStack(); + + foreach (var resource in SelectCompatible(configResources)) + { + taskBuilder.Add(await Task.Factory.StartNew>>(async Task> () => + { + var r = await _configInfoParserService.TryParseResourcesAsync(resource); + if (r.IsFailed) + { + toProcessErrors.PushRange(r.Errors.ToArray()); + return ImmutableArray.Empty; + } + return r.Value; + })); + } + + var taskResults = await Task.WhenAll(taskBuilder.ToImmutable()); + + if (toProcessErrors.Count > 0) + { + return FluentResults.Result.Fail($"{nameof(LoadConfigsAsync)}: Errors while loading configuration info: ").WithErrors(toProcessErrors.ToArray()); + } + + var toProcessDocs = taskResults + .Where(tr => !tr.IsDefaultOrEmpty) + .SelectMany(tr => tr) + .Where(icf => icf is not null) + .ToImmutableArray(); + + var instanceQueue = new Queue<(IConfigInfo configInfo, Func<(IConfigService ConfigService, IConfigInfo Info), ISettingBase> factory)>(); + + foreach (var info in toProcessDocs) + { + if (!_instanceFactory.TryGetValue(info.DataType, out var factory)) + { + result.WithError( + $"{nameof(LoadConfigsAsync)}: Could not retrieve the instance factory for the data type of '{info.DataType}'!"); + continue; + } + if (_settingsInstances.ContainsKey((info.OwnerPackage, info.InternalName))) + { + // duplicate for some reason (ie. double loading). This should never happen. + ThrowHelper.ThrowInvalidOperationException($"{nameof(LoadConfigsAsync)}: A setting for the [ContentPackage].[InternalName] of '[{info.OwnerPackage.Name}].[{info.InternalName}]' already exists!"); + } + + instanceQueue.Enqueue((info, factory)); + } + + var toProcessInstanceQueue = new Queue<(IConfigInfo info, ISettingBase instance)>(); + + while (instanceQueue.TryDequeue(out var instanceFactoryInfo)) + { + try + { + toProcessInstanceQueue.Enqueue((instanceFactoryInfo.configInfo, instanceFactoryInfo.factory((this, instanceFactoryInfo.configInfo)))); + } + catch (Exception e) + { + result.WithError( + $"{nameof(LoadConfigsAsync)}: Error while instancing setting for '{instanceFactoryInfo.configInfo.OwnerPackage}.{instanceFactoryInfo.configInfo.InternalName}': {e.Message}!"); + continue; + } + } + + using var settingsLck = await _settingsByPackageLock.AcquireWriterLock(); // block to protect new bag instance creation + + while (toProcessInstanceQueue.TryDequeue(out var newInstanceData)) + { + _settingsInstances[(newInstanceData.info.OwnerPackage, newInstanceData.info.InternalName)] = newInstanceData.instance; + if (!_settingsInstancesByPackage.TryGetValue(newInstanceData.info.OwnerPackage, out _)) + { + _settingsInstancesByPackage[newInstanceData.info.OwnerPackage] = new ConcurrentBag(); + } + _settingsInstancesByPackage[newInstanceData.info.OwnerPackage].Add(newInstanceData.instance); + result.WithReasons(_eventService.PublishEvent(sub => + sub.OnSettingInstanceCreated(newInstanceData.instance)).Reasons); + } + + return result; + } + + public async Task LoadConfigsProfilesAsync(ImmutableArray configProfileResources) + { + using var _ = await _operationLock.AcquireReaderLock(); + IService.CheckDisposed(this); + if (configProfileResources.IsDefaultOrEmpty) + { + ThrowHelper.ThrowArgumentNullException($"{nameof(LoadConfigsProfilesAsync)}: {nameof(configProfileResources)} is empty."); + } + + var result = new FluentResults.Result(); + + foreach (var resource in SelectCompatible(configProfileResources)) + { + var r = await _configProfileInfoParserService.TryParseResourcesAsync(resource); + if (r.IsFailed) + { + result.WithErrors(r.Errors); + continue; + } + + foreach (var info in r.Value) + { + if (!_settingsProfiles.TryAdd((info.OwnerPackage, info.InternalName), info)) + { + result.WithErrors(r.Errors); + continue; + } + + if (info.InternalName.Equals("default", StringComparison.InvariantCultureIgnoreCase)) + { + //apply it + foreach (var value in info.ProfileValues) + { + if (_settingsInstances.TryGetValue((info.OwnerPackage, value.SettingName), out var instance)) + { + instance.TrySetSerializedValue(value.Element); + } + } + } + } + } + + return result; + } + + public FluentResults.Result LoadSavedValueForConfig(ISettingBase setting) + { + Guard.IsNotNull(setting, nameof(setting)); + using var lck = _operationLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult(); + IService.CheckDisposed(this); + + if (_storageService.LoadLocalXml(setting.OwnerPackage, SaveDataFileName) is not { } saveFileResult) + { +#if DEBUG + return FluentResults.Result.Fail( + $"{nameof(LoadSavedValueForConfig)}: Could not open save file for setting [{setting.OwnerPackage.Name}.{setting.InternalName}]"); +#endif + return FluentResults.Result.Ok(); + } + + if (saveFileResult is { IsFailed: true }) + { +#if DEBUG + _logger.LogResults(saveFileResult.ToResult()); + return FluentResults.Result.Fail( + $"{nameof(LoadSavedValueForConfig)}: Could not open save file for setting [{setting.OwnerPackage.Name}.{setting.InternalName}]"); +#endif + return FluentResults.Result.Ok(); + } + + if (saveFileResult.Value.Root is not {} rootElement + || !string.Equals(rootElement.Name.LocalName, "Configuration", StringComparison.InvariantCultureIgnoreCase)) + { + return FluentResults.Result.Fail($"{nameof(LoadSavedValueForConfig)}: Root invalid for setting [{setting.OwnerPackage.Name}.{setting.InternalName}]"); + } + + if (rootElement.GetChildElement(XmlConvert.EncodeLocalName(setting.OwnerPackage.Name.Trim()), StringComparison.InvariantCultureIgnoreCase) + ?.GetChildElement(setting.InternalName, StringComparison.InvariantCultureIgnoreCase) is not {} cfgValueElement) + { +#if DEBUG + return FluentResults.Result.Fail($"{nameof(LoadSavedValueForConfig)}: Could not find saved value for setting:[{setting.OwnerPackage.Name}.{setting.InternalName}]"); +#endif + return FluentResults.Result.Ok(); + } + + return FluentResults.Result.OkIf(setting.TrySetSerializedValue(cfgValueElement), new Error($"Failed to set value for [{setting.OwnerPackage.Name}.{setting.InternalName}]")); + } + + public FluentResults.Result LoadSavedConfigsValues() + { + ImmutableArray cfgValues; + using (var lck = _operationLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult()) + { + IService.CheckDisposed(this); + cfgValues = _settingsInstances.Select(kvp => kvp.Value).ToImmutableArray(); + } + + var ret = new FluentResults.Result(); + + foreach (var settingBase in cfgValues) + { +#if DEBUG + // log in debug only. + ret.WithReasons(LoadSavedValueForConfig(settingBase).Reasons); +#else + LoadSavedValueForConfig(settingBase); +#endif + } + + return ret; + } + + public FluentResults.Result ApplyConfigProfile(ContentPackage package, string internalName) + { + Guard.IsNotNull(package, nameof(package)); + Guard.IsNotNullOrWhiteSpace(internalName, nameof(internalName)); + using var _ = _operationLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult(); + IService.CheckDisposed(this); + + if (!_settingsProfiles.TryGetValue((package, internalName), out var setting)) + { + return FluentResults.Result.Fail($"{nameof(ApplyConfigProfile)}: Could not find profile [{package.Name}.{internalName}]"); + } + + var result = new FluentResults.Result(); + + foreach (var profileValue in setting.ProfileValues) + { + if (!_settingsInstances.TryGetValue((package, profileValue.SettingName), out var instance)) + { + result.WithError(new Error($"{nameof(ApplyConfigProfile)}: Could not find setting [{profileValue.SettingName}].")); + continue; + } + + if (!instance.TrySetSerializedValue(profileValue.Element)) + { + result.WithError(new Error($"{nameof(ApplyConfigProfile)}: Failed to set value for [{profileValue.SettingName}].")); + } + } + + return result; + } + + public FluentResults.Result SaveConfigValue(ISettingBase setting) + { + XDocument cpCfgValues; + if (_storageService.LoadLocalXml(setting.OwnerPackage, SaveDataFileName) is not {} saveFileResult) + { + return FluentResults.Result.Fail($"{nameof(SaveConfigValue)}: Storage Service Failure while trying to load file for setting [{setting.OwnerPackage.Name}.{setting.InternalName}]"); + } + + // get Configuration + if (saveFileResult.IsFailed) + { + cpCfgValues = new XDocument(new XDeclaration("1.0", "utf-8", "yes"), new XElement("Configuration")); + } + else + { + cpCfgValues = saveFileResult.Value; + } + + if (cpCfgValues.Root is null || cpCfgValues.Root.Name != "Configuration") + { + return FluentResults.Result.Fail($"{nameof(SaveConfigValue)}: Bad save file format for setting: [{setting.OwnerPackage.Name}.{setting.InternalName}]"); + } + + XElement currentTarget = GetOrAddElement(cpCfgValues.Root, XmlConvert.EncodeLocalName(setting.OwnerPackage.Name.Trim()), name => new XElement(name)); + currentTarget = GetOrAddElement(currentTarget, setting.InternalName, name => new XElement(name)); + + var ret = setting.GetSerializableValue().Match(str => + { + var tgt = currentTarget.Attribute("Value"); + if (tgt is null) + { + var attr = new XAttribute("Value", str); + currentTarget.Add(attr); + } + else + { + tgt.Value = str; + } + + return FluentResults.Result.Ok(); + }, + elem => + { + currentTarget.ReplaceNodes(new XElement("Value", elem)); + return FluentResults.Result.Ok(); + }); + + ret.WithReasons(_storageService.SaveLocalXml(setting.OwnerPackage, SaveDataFileName, cpCfgValues).Reasons); + return ret; + + XElement GetOrAddElement(XElement containerElement, string elementName, Func factory) + { + var element = containerElement.Element(elementName); + if (element is null) + { + element = factory(elementName); + containerElement.Add(element); + } + return element; + } + } + + + public FluentResults.Result DisposePackageData(ContentPackage package) + { + Guard.IsNotNull(package, nameof(package)); + using var lck = _operationLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult(); + IService.CheckDisposed(this); + + ConcurrentBag toDispose; + using (var settingsLck = _settingsByPackageLock.AcquireWriterLock().ConfigureAwait(false).GetAwaiter().GetResult()) + { + if (!_settingsInstancesByPackage.TryRemove(package, out toDispose) || toDispose is null) + { + return FluentResults.Result.Ok(); + } + } + + var result = new FluentResults.Result(); + + foreach (var setting in toDispose) + { + result.WithReasons(_eventService.PublishEvent(sub => sub.OnSettingInstanceDisposed(setting)).Reasons); + try + { + _settingsInstances.TryRemove((setting.OwnerPackage, setting.InternalName), out _); + setting.Dispose(); + } + catch (Exception e) + { + result.WithError(new ExceptionalError(e)); + } + } + + return result; + } + + public FluentResults.Result DisposeAllPackageData() + { + return this.Reset(); + } + + public bool TryGetConfig(ContentPackage package, string internalName, out T instance) where T : ISettingBase + { + Guard.IsNotNull(package, nameof(package)); + Guard.IsNotNullOrWhiteSpace(internalName, nameof(internalName)); + using var lck = _operationLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult(); + using var settingsLck = + _settingsByPackageLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult(); + IService.CheckDisposed(this); + + instance = default; + + if(!_settingsInstances.TryGetValue((package, internalName), out var inst)) + { + return false; + } + + if (inst is not T instanceT) + { + return false; + } + + instance = instanceT; + return true; + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/ConsoleCommandsService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/ConsoleCommandsService.cs new file mode 100644 index 000000000..242938a16 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/ConsoleCommandsService.cs @@ -0,0 +1,97 @@ +using Barotrauma.LuaCs.Events; +using Barotrauma.Networking; +using Microsoft.Xna.Framework; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; + +namespace Barotrauma.LuaCs; + +internal class ConsoleCommandsService : IConsoleCommandsService +{ + private readonly List _registeredCommands = new(); + + public void Dispose() + { + if (!ModUtils.Threading.CheckIfClearAndSetBool(ref _isDisposed)) + { + return; + } + + foreach (var cmd in _registeredCommands.ToImmutableArray()) + { + DebugConsole.Commands.Remove(cmd); + } + + _registeredCommands.Clear(); + } + + private int _isDisposed = 0; + public bool IsDisposed + { + get => ModUtils.Threading.GetBool(ref _isDisposed); + private set => ModUtils.Threading.SetBool(ref _isDisposed, value); + } + + public void RegisterCommand(string name, string help, Action onExecute, Func getValidArgs = null, bool isCheat = false) + { + IService.CheckDisposed(this); + + if (DebugConsole.Commands.Any(cmd => cmd.Names.Contains(name))) + { + LuaCsSetup.Instance.Logger.LogWarning($"Registering console command {name} more than once!"); + } + + var cmd = new DebugConsole.Command(name, help, onExecute, getValidArgs, isCheat); + _registeredCommands.Add(cmd); + DebugConsole.Commands.Add(cmd); + } + + public void AssignOnExecute(string names, Action onExecute) + { + var matchingCommand = DebugConsole.Commands.Find(c => c.Names.Intersect(names.Split('|').ToIdentifiers()).Any()); + if (matchingCommand == null) + { + throw new Exception("AssignOnExecute failed. Command matching the name(s) \"" + names + "\" not found."); + } + else + { + matchingCommand.OnExecute = onExecute; + } + } + +#if SERVER + public void AssignOnClientRequestExecute(string names, Action onClientRequestExecute) + { + var matchingCommand = DebugConsole.Commands.Find(c => c.Names.Intersect(names.Split('|').ToIdentifiers()).Any()); + if (matchingCommand == null) + { + throw new Exception("AssignOnClientRequestExecute failed. Command matching the name(s) \"" + names + "\" not found."); + } + else + { + matchingCommand.OnClientRequestExecute = onClientRequestExecute; + } + } +#endif + + public void RemoveCommand(string name) + { + IService.CheckDisposed(this); + + _registeredCommands.RemoveAll(cmd => cmd.Names.Contains(name)); + DebugConsole.Commands.RemoveAll(cmd => cmd.Names.Contains(name)); + } + + public void RemoveRegisteredCommands() + { + IService.CheckDisposed(this); + foreach (var cmd in _registeredCommands.ToImmutableArray()) + { + DebugConsole.Commands.Remove(cmd); + } + _registeredCommands.Clear(); + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/EventService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/EventService.cs new file mode 100644 index 000000000..22eec4a69 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/EventService.cs @@ -0,0 +1,426 @@ +using Barotrauma.LuaCs.Events; +using FluentResults; +using Microsoft.Toolkit.Diagnostics; +using MoonSharp.Interpreter; +using OneOf; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; + +namespace Barotrauma.LuaCs; + +public partial class EventService : IEventService +{ + private readonly record struct TypeStringKey : IEqualityComparer, IEquatable + { + public Type Type { get; init; } + public string TypeName { get; init; } + public readonly int HashCode; + + public TypeStringKey(Type type) + { + Type = type ?? throw new ArgumentNullException(nameof(type)); + TypeName = type.Name.ToLowerInvariant(); + HashCode = TypeName.GetHashCode(); + } + + public TypeStringKey(string typeName) + { + Type = null; + TypeName = typeName?.ToLowerInvariant() ?? throw new ArgumentNullException(nameof(typeName)); + HashCode = TypeName.GetHashCode(); + } + + public bool Equals(TypeStringKey x, TypeStringKey y) + { + if (x.Type is not null && y.Type is not null) + return x.Type == y.Type; + return x.TypeName == y.TypeName; + } + + public int GetHashCode(TypeStringKey obj) + { + return obj.HashCode; + } + + public static implicit operator TypeStringKey(Type type) => new(type); + public static implicit operator TypeStringKey(string typeName) => new(typeName); + } + + private readonly ILoggerService _loggerService; + private readonly ILuaPatcher _luaPatcher; + private readonly AsyncReaderWriterLock _operationsLock = new(); + private readonly ConcurrentDictionary, IEvent>> _subscribers = new(); + private readonly ConcurrentDictionary RunnerFactory)> _luaAliasEventFactory = new(); + private readonly ConcurrentDictionary> _luaLegacyEventsSubscribers = new(); + private readonly ConcurrentDictionary _subscribedEventDispatchers = new(); + + #region LifeCycle + + public void Dispose() + { + using var lck = _operationsLock.AcquireWriterLock().ConfigureAwait(false).GetAwaiter().GetResult(); + if (!ModUtils.Threading.CheckIfClearAndSetBool(ref _isDisposed)) + { + return; + } + + _luaLegacyEventsSubscribers.Clear(); + _luaAliasEventFactory.Clear(); + _subscribers.Clear(); + _luaPatcher.Dispose(); + } + + private int _isDisposed; + + public EventService(ILoggerService loggerService, ILuaPatcher luaPatcher) + { + _loggerService = loggerService; + _luaPatcher = luaPatcher; + } + + public bool IsDisposed + { + get => ModUtils.Threading.GetBool(ref _isDisposed); + private set => ModUtils.Threading.SetBool(ref _isDisposed, value); + } + public FluentResults.Result Reset() + { + using var lck = _operationsLock.AcquireWriterLock().ConfigureAwait(false).GetAwaiter().GetResult(); + IService.CheckDisposed(this); + _luaLegacyEventsSubscribers.Clear(); + _luaAliasEventFactory.Clear(); + _subscribers.Clear(); + _luaPatcher.Reset(); + return FluentResults.Result.Ok(); + } + + #endregion + + #region LuaEventSystem + + public void Add(string eventName, string identifier, LuaCsFunc callback, object owner = null) + { + Guard.IsNotNullOrWhiteSpace(eventName, nameof(eventName)); + Guard.IsNotNullOrWhiteSpace(identifier, nameof(identifier)); + Guard.IsNotNull(callback, nameof(callback)); + using var lck = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult(); + IService.CheckDisposed(this); + + if (_luaAliasEventFactory.TryGetValue(eventName, out var eventFunc)) + { + var eventSubs = _subscribers.GetOrAdd(eventFunc.Event, key => new ConcurrentDictionary, IEvent>()); + eventSubs[identifier] = eventFunc.RunnerFactory(callback); + } + else + { + var eventSubs = _luaLegacyEventsSubscribers.GetOrAdd(eventName, key => new ConcurrentDictionary()); + eventSubs[identifier] = callback; + } + } + + public void Add(string eventName, LuaCsFunc callback, object owner = null) + { + // random ident, we hope for no conflicts :barodev:. + Add(eventName, Random.Shared.NextInt64().ToString() ,callback); + } + + public object Call(string eventName, params object[] args) + { + return Call(eventName, args); + } + + [MoonSharpHidden] // Needs to be hidden so Lua doesn't accidentally use this instead of the above + public T Call(string eventName, params object[] args) + { + Guard.IsNotNullOrWhiteSpace(eventName, nameof(eventName)); + using var lck = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult(); + IService.CheckDisposed(this); + + if (!_luaLegacyEventsSubscribers.TryGetValue(eventName, out var eventSubscribers) + || eventSubscribers.IsEmpty) + { + return default; + } + + T returnValue = default; + + foreach (var subscriber in eventSubscribers) + { + try + { + object result = subscriber.Value.Invoke(args); + if (result is DynValue luaResult) + { + if (luaResult.Type == DataType.Tuple) + { + bool replaceNil = luaResult.Tuple.Length > 1 && luaResult.Tuple[1].CastToBool(); + + if (!luaResult.Tuple[0].IsNil() || replaceNil) + { + returnValue = luaResult.ToObject(); + } + } + else if (!luaResult.IsNil()) + { + returnValue = luaResult.ToObject(); + } + } + else + { + returnValue = (T)result; + } + } + catch (Exception e) + { + _loggerService.LogError(e.Message); +#if DEBUG + throw; +#endif + } + } + + return returnValue; + } + + public void Subscribe(string identifier, IDictionary callbacks) where T : class, IEvent + { + Guard.IsNotNullOrWhiteSpace(identifier, nameof(identifier)); + Guard.IsNotNull(callbacks, nameof(callbacks)); + Guard.IsNotEmpty(callbacks, nameof(callbacks)); + using var lck = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult(); + IService.CheckDisposed(this); + + var eventSubs = _subscribers.GetOrAdd(typeof(T), key => new ConcurrentDictionary, IEvent>()); + eventSubs[identifier] = T.GetLuaRunner(callbacks); + } + + public void Remove(string eventName, string identifier) + { + Guard.IsNotNullOrWhiteSpace(eventName, nameof(eventName)); + Guard.IsNotNullOrWhiteSpace(identifier, nameof(identifier)); + + using var lck = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult(); + IService.CheckDisposed(this); + + if (_luaAliasEventFactory.TryGetValue(eventName, out var eventFunc)) + { + if (_subscribers.TryGetValue(eventFunc.Event, out var eventSubs)) + { + eventSubs.TryRemove(identifier, out _); + } + } + else + { + if (_luaLegacyEventsSubscribers.TryGetValue(eventName, out var eventSubs)) + { + eventSubs.TryRemove(identifier, out _); + } + } + } + public void Unsubscribe(string eventName, string identifier) + { + Guard.IsNotNullOrWhiteSpace(eventName, nameof(eventName)); + Guard.IsNotNullOrWhiteSpace(identifier, nameof(identifier)); + using var lck = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult(); + + if (!_subscribers.TryGetValue(eventName, out var evtSubscribers)) + { + return; + } + + evtSubscribers.TryRemove(identifier, out _); + } + + public void PublishLuaEvent(LuaCsFunc subscriberRunner) where T : class, IEvent + { + this.PublishEvent(sub => subscriberRunner(sub)); + } + + public FluentResults.Result RegisterLuaEventAlias(string luaEventName, string targetMethod) where T : class, IEvent + { + Guard.IsNotNullOrWhiteSpace(luaEventName, nameof(luaEventName)); + Guard.IsNotNullOrWhiteSpace(targetMethod, nameof(targetMethod)); + using var lck = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult(); + IService.CheckDisposed(this); + + if (_luaAliasEventFactory.ContainsKey(luaEventName)) + { +#if DEBUG + ThrowHelper.ThrowInvalidOperationException($"{nameof(RegisterLuaEventAlias)}: An alias already exists for the event of {luaEventName}."); +#endif + return FluentResults.Result.Fail($"{nameof(RegisterLuaEventAlias)}: An alias already exists for the event of {luaEventName}."); + } + + var eventRunnerFactory = (LuaCsFunc function) => (IEvent)T.GetLuaRunner(new Dictionary + { + { targetMethod, function } + }); + + _luaAliasEventFactory[luaEventName] = (Event: typeof(T), RunnerFactory: eventRunnerFactory); + // create the group + _subscribers.GetOrAdd(typeof(T), key => new ConcurrentDictionary, IEvent>()); + return FluentResults.Result.Ok(); + } + + #endregion + + public FluentResults.Result Subscribe(T subscriber) where T : class, IEvent + { + Guard.IsNotNull(subscriber, nameof(subscriber)); + using var lck = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult(); + IService.CheckDisposed(this); + + var eventSubs = + _subscribers.GetOrAdd(typeof(T), (type) => new ConcurrentDictionary, IEvent>()); + + if (eventSubs.ContainsKey(subscriber)) + { + ThrowHelper.ThrowInvalidOperationException($"{nameof(Subscribe)}: The instance is already registered!"); + } + + return eventSubs.TryAdd(subscriber, subscriber) + ? FluentResults.Result.Ok() + : FluentResults.Result.Fail($"{nameof(Subscribe)}: Failed to add subscriber."); + } + + public void Unsubscribe(T subscriber) where T : class, IEvent + { + Guard.IsNotNull(subscriber, nameof(subscriber)); + using var lck = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult(); + IService.CheckDisposed(this); + + if (!_subscribers.TryGetValue(typeof(T), out var evtSubscribers)) + { + return; + } + + evtSubscribers.TryRemove(subscriber, out _); + } + + public void ClearAllEventSubscribers() where T : class, IEvent + { + using var lck = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult(); + IService.CheckDisposed(this); + _subscribers.TryRemove(typeof(T), out _); + } + + public void ClearAllSubscribers() + { + using var lck = _operationsLock.AcquireWriterLock().ConfigureAwait(false).GetAwaiter().GetResult(); + IService.CheckDisposed(this); + _subscribers.Clear(); + } + + public FluentResults.Result PublishEvent(Action action) where T : class, IEvent + { + Guard.IsNotNull(action, nameof(action)); + using var lck = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult(); + IService.CheckDisposed(this); + + if (!_subscribers.TryGetValue(typeof(T), out var subs) || subs.IsEmpty) + { + return FluentResults.Result.Ok(); + } + + var results = new FluentResults.Result(); + + foreach (var sub in subs) + { + try + { + action.Invoke(Unsafe.As(sub.Value)); + } + catch (Exception e) + { + results.WithError(new ExceptionalError(e)); + _loggerService.LogError(e.Message); + continue; + } + } + + foreach (var dispatchers in _subscribedEventDispatchers.ToImmutableArray()) + { + dispatchers.Value.PublishEvent(action); + } + + return results; + } + + public void AddDispatcherEventService(IEventService eventService) + { + using var lck = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult(); + IService.CheckDisposed(this); + + _subscribedEventDispatchers.TryAdd(eventService, eventService); + } + + public void RemoveDispatcherEventService(IEventService eventService) + { + using var lck = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult(); + IService.CheckDisposed(this); + + _subscribedEventDispatchers.TryRemove(eventService, out _); + } + + #region LuaPatcherAdapter + public string Patch(string identifier, string className, string methodName, string[] parameterTypes, LuaCsPatchFunc patch, LuaCsHook.HookMethodType hookType = LuaCsHook.HookMethodType.Before) + { + return _luaPatcher.Patch(identifier, className, methodName, parameterTypes, patch, hookType); + } + + public string Patch(string identifier, string className, string methodName, LuaCsPatchFunc patch, LuaCsHook.HookMethodType hookType = LuaCsHook.HookMethodType.Before) + { + return _luaPatcher.Patch(identifier, className, methodName, patch, hookType); + } + + public string Patch(string className, string methodName, string[] parameterTypes, LuaCsPatchFunc patch, LuaCsHook.HookMethodType hookType = LuaCsHook.HookMethodType.Before) + { + return _luaPatcher.Patch(className, methodName, parameterTypes, patch, hookType); + } + + public string Patch(string className, string methodName, LuaCsPatchFunc patch, LuaCsHook.HookMethodType hookType = LuaCsHook.HookMethodType.Before) + { + return _luaPatcher.Patch(className, methodName, patch, hookType); + } + + public bool RemovePatch(string identifier, string className, string methodName, string[] parameterTypes, LuaCsHook.HookMethodType hookType) + { + return _luaPatcher.RemovePatch(className, className, methodName, parameterTypes, hookType); + } + + public bool RemovePatch(string identifier, string className, string methodName, LuaCsHook.HookMethodType hookType) + { + return _luaPatcher.RemovePatch(className, className, methodName, hookType); + } + + public void HookMethod(string identifier, MethodBase method, LuaCsPatch patch, LuaCsHook.HookMethodType hookType = LuaCsHook.HookMethodType.Before, IAssemblyPlugin owner = null) + { + _luaPatcher.HookMethod(identifier, method, patch, hookType, owner); + } + + public void HookMethod(string identifier, string className, string methodName, string[] parameterNames, LuaCsPatch patch, LuaCsHook.HookMethodType hookMethodType = LuaCsHook.HookMethodType.Before) + { + _luaPatcher.HookMethod(identifier, className, methodName, parameterNames, patch, hookMethodType); + } + + public void HookMethod(string identifier, string className, string methodName, LuaCsPatch patch, LuaCsHook.HookMethodType hookMethodType = LuaCsHook.HookMethodType.Before) + { + _luaPatcher.HookMethod(identifier, className, methodName, patch, hookMethodType); + } + + public void HookMethod(string className, string methodName, LuaCsPatch patch, LuaCsHook.HookMethodType hookMethodType = LuaCsHook.HookMethodType.Before) + { + _luaPatcher.HookMethod(className, methodName, patch, hookMethodType); + } + + public void HookMethod(string className, string methodName, string[] parameterNames, LuaCsPatch patch, LuaCsHook.HookMethodType hookMethodType = LuaCsHook.HookMethodType.Before) + { + _luaPatcher.HookMethod(className, methodName, parameterNames, patch, hookMethodType); + } + #endregion +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/HarmonyEventPatchesService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/HarmonyEventPatchesService.cs new file mode 100644 index 000000000..fe54d9096 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/HarmonyEventPatchesService.cs @@ -0,0 +1,416 @@ +using Barotrauma.Items.Components; +using Barotrauma.LuaCs; +using Barotrauma.LuaCs.Events; +using Barotrauma.Networking; +using Barotrauma.Steam; +using HarmonyLib; +using Microsoft.Xna.Framework; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using static Barotrauma.ContentPackageManager; + +namespace Barotrauma.LuaCs; + +[HarmonyPatch] +internal class HarmonyEventPatchesService : ISystem +{ + public bool IsDisposed { get; private set; } + public FluentResults.Result Reset() + { + Unpatch(); + Patch(); + return FluentResults.Result.Ok(); + } + + private static IEventService _eventService; + private static ILoggerService _loggerService; + private readonly Harmony Harmony; + + public HarmonyEventPatchesService(IEventService eventService, ILoggerService loggerService) + { + _eventService = eventService; + _loggerService = loggerService; + Harmony = new Harmony("LuaCsForBarotrauma.Events"); + Patch(); + } + + private void Patch() + { + this.Harmony?.PatchAll(typeof(HarmonyEventPatchesService)); +#if SERVER + this.Harmony?.PatchAll(typeof(HarmonyEventPatchesService.Patch_StartGame_End)); +#endif + } + + private void Unpatch() + { + this.Harmony?.UnpatchSelf(); + } + + + [HarmonyPatch(typeof(CoroutineManager), nameof(CoroutineManager.Update)), HarmonyPostfix] + public static void CoroutineManager_Update_Post() + { + _eventService.PublishEvent(x => x.OnUpdate(CoroutineManager.DeltaTime)); + _loggerService.ProcessLogs(); + } + +#if CLIENT + [HarmonyPatch(typeof(GameSession), nameof(GameSession.StartRound), new Type[] + { + typeof(LevelData), typeof(bool), typeof(SubmarineInfo), typeof(SubmarineInfo) + }), HarmonyPostfix] + public static void GameSession_StartRound_Post() + { + _eventService.PublishEvent(x => x.OnRoundStart()); + } +#endif + + [HarmonyPatch(typeof(GameSession), nameof(GameSession.EndRound)), HarmonyPrefix] + public static void GameSession_EndRound_Pre() + { + _eventService.PublishEvent(x => x.OnRoundEnd()); + } + + [HarmonyPatch(typeof(GameSession), nameof(GameSession.LoadPreviousSave)), HarmonyPrefix] + public static void GameSession_LoadPreviousSave_Pre() + { + _eventService.PublishEvent(x => x.OnRoundEnd()); + } + + [HarmonyPatch(typeof(GameSession), nameof(GameSession.EndMissions)), HarmonyPostfix] + public static void GameSession_EndMission_Post(GameSession __instance) + { + _eventService.PublishEvent(x => x.OnMissionsEnded(__instance.Missions.ToList())); + } + + [HarmonyPatch(typeof(Screen), nameof(Screen.Select)), HarmonyPostfix] + public static void Screen_Selected_Post(Screen __instance) + { + _eventService.PublishEvent(x => x.OnScreenSelected(__instance)); + } + +#if CLIENT + [HarmonyPatch(typeof(MainMenuScreen), "StartGame"), HarmonyPostfix] + public static void MainMenuScreen_StartGame_Pre(Screen __instance) + { + LuaCsSetup.Instance.SetRunState(RunState.Running); + } + + [HarmonyPatch(typeof(MainMenuScreen), "LoadGame"), HarmonyPostfix] + public static void MainMenuScreen_LoadGame_Pre(Screen __instance) + { + LuaCsSetup.Instance.SetRunState(RunState.Running); + } + + [HarmonyPatch(typeof(MutableWorkshopMenu), nameof(MutableWorkshopMenu.Apply)), HarmonyPostfix] + public static void MutableWorkshopMenu_Apply_Post(Screen __instance) + { + LuaCsSetup.Instance.PromptCSharpMods(selection => { }, joiningServer: false); + } + +#endif + + [HarmonyPatch(typeof(ContentPackageManager.PackageSource), nameof(ContentPackageManager.PackageSource.Refresh)), HarmonyPostfix] + public static void PackageSource_Refresh_Post() + { + _eventService.PublishEvent(x => x.OnAllPackageListChanged(ContentPackageManager.CorePackages, ContentPackageManager.RegularPackages)); + } + + [HarmonyPatch(typeof(ContentPackageManager), nameof(ContentPackageManager.Init)), HarmonyPostfix] + public static void ContentPackageManager_Init_Post() + { + _eventService.PublishEvent(x => x.OnAllPackageListChanged(ContentPackageManager.CorePackages, ContentPackageManager.RegularPackages)); + _eventService.PublishEvent(sub => sub.OnEnabledPackageListChanged(EnabledPackages.Core, EnabledPackages.Regular)); + } + + [HarmonyPatch(typeof(ContentPackageManager.EnabledPackages), nameof(ContentPackageManager.EnabledPackages.SetCore)), HarmonyPostfix] + public static void EnabledPackages_SetCore_Post() + { + _eventService.PublishEvent(sub => sub.OnEnabledPackageListChanged(EnabledPackages.Core, EnabledPackages.Regular)); + } + + [HarmonyPatch(typeof(ContentPackageManager.EnabledPackages), nameof(ContentPackageManager.EnabledPackages.SetRegular)), HarmonyPostfix] + public static void EnabledPackages_SetRegular_Post() + { + _eventService.PublishEvent(sub => sub.OnEnabledPackageListChanged(EnabledPackages.Core, EnabledPackages.Regular)); + } + +#if CLIENT + [HarmonyPatch(typeof(GameClient), "ReadDataMessage"), HarmonyPrefix] + public static bool GameClient_ReadDataMessage_Pre(IReadMessage inc) + { + int prevBitPosition = inc.BitPosition; + ServerPacketHeader header = (ServerPacketHeader)inc.ReadByte(); + bool? skip = null; + _eventService.PublishEvent(x => skip = x.OnReceivedServerNetMessage(inc, header) ?? skip); + + if (skip == true) + { + return false; + } + + inc.BitPosition = prevBitPosition; // rewind so the game can read the message + return true; + } + + [HarmonyPatch(typeof(SubEditorScreen), nameof(SubEditorScreen.Select), new Type[] { }), HarmonyPostfix] + public static void SubEditorScreen_Selected_Post(Screen __instance) + { + _eventService.PublishEvent(x => x.OnScreenSelected(__instance)); + } + + [HarmonyPatch(typeof(PlayerInput), nameof(PlayerInput.Update)), HarmonyPrefix] + public static void PlayerInput_Update_Pre(double deltaTime) + { + _eventService.PublishEvent(x => x.OnKeyUpdate(deltaTime)); + } + + [HarmonyPatch(typeof(DebugConsole), "IsCommandPermitted"), HarmonyPrefix] + public static bool DebugConsole_IsCommandPermitted(Identifier command, ref bool __result) + { + DebugConsole.Command c = DebugConsole.FindCommand(command.Value); + + if (DebugConsole.Commands.IndexOf(c) >= LuaCsSetup.DebugConsoleCommandVanillaIndex) + { + __result = true; + return false; + } + + return true; + } + + +#elif SERVER + [HarmonyPatch(typeof(GameServer), "ReadDataMessage"), HarmonyPrefix] + public static bool GameServer_ReadDataMessage_Pre(NetworkConnection sender, IReadMessage inc) + { + int prevBitPosition = inc.BitPosition; + ClientPacketHeader header = (ClientPacketHeader)inc.ReadByte(); + + bool? skip = null; + _eventService.PublishEvent(x => skip = x.OnReceivedClientNetMessage(inc, header, sender) ?? skip); + + if (skip == true) + { + return false; + } + + inc.BitPosition = prevBitPosition; // rewind so the game can read the message + return true; + } + + [HarmonyPatch(typeof(GameServer), "OnInitializationComplete"), HarmonyPostfix] + public static void GameServer_OnInitializationComplete_Post(GameServer __instance) + { + Client client = __instance.ConnectedClients.LastOrDefault(); + if (client == null) { return; } + _eventService.PublishEvent(x => x.OnClientConnected(client)); + } + + [HarmonyPatch(typeof(GameServer), nameof(GameServer.DisconnectClient), new Type[] { typeof(Client), typeof(PeerDisconnectPacket) }), HarmonyPrefix] + public static void GameServer_DisconnectClient_Pre(Client client, PeerDisconnectPacket peerDisconnectPacket) + { + if (client == null) { return; } + + _eventService.PublishEvent(x => x.OnClientDisconnected(client)); + } + + [HarmonyPatch(typeof(GameServer), nameof(GameServer.AssignJobs)), HarmonyPostfix] + public static void GameServer_AssignJobs_Post(List unassigned) + { + _eventService.PublishEvent(x => x.OnJobsAssigned(unassigned)); + } +#endif + + [HarmonyPatch(typeof(Character), nameof(Character.Create), new[] { + typeof(CharacterPrefab), + typeof(Vector2), + typeof(string), + typeof(CharacterInfo), + typeof(ushort), + typeof(bool), + typeof(bool), + typeof(bool), + typeof(RagdollParams), + typeof(bool) + }), HarmonyPostfix] + public static void Character_Create_Post(Character __result) + { + _eventService.PublishEvent(x => x.OnCharacterCreated(__result)); + } + + [HarmonyPatch(typeof(Character), "KillProjSpecific"), HarmonyPostfix] + public static void Character_Kill_Post(Character __instance, Affliction causeOfDeathAffliction, CauseOfDeathType causeOfDeath) + { + _eventService.PublishEvent(x => x.OnCharacterDeath(__instance, causeOfDeathAffliction, causeOfDeath)); + } + + [HarmonyPatch(typeof(Character), nameof(Character.GiveJobItems)), HarmonyPostfix] + public static void Character_GiveJobItems_Post(Character __instance, WayPoint spawnPoint, bool isPvPMode) + { + _eventService.PublishEvent(x => x.OnGiveCharacterJobItems(__instance, spawnPoint, isPvPMode)); + } + + [HarmonyPatch(typeof(Character), nameof(Character.DamageLimb)), HarmonyPrefix] + public static bool Character_DamageLimb_Pre(AttackResult __result, Character __instance, Vector2 worldPosition, Limb hitLimb, IEnumerable afflictions, float stun, bool playSound, Vector2 attackImpulse, Character attacker, float damageMultiplier, bool allowStacking, float penetration, bool shouldImplode, bool ignoreDamageOverlay, bool recalculateVitality) + { + AttackResult? result = null; + _eventService.PublishEvent(x => result = x.OnCharacterDamageLimb(__instance, worldPosition, hitLimb, afflictions, stun, playSound, attackImpulse, attacker, damageMultiplier, allowStacking, penetration, shouldImplode)); + if (result != null) + { + __result = (AttackResult)result; + return false; // skip + } + + return true; + } + + [HarmonyPatch(typeof(Affliction), nameof(Affliction.Update)), HarmonyPostfix] + public static void Affliction_Update_Post(Affliction __instance, CharacterHealth characterHealth, Limb targetLimb, float deltaTime) + { + _eventService.PublishEvent(x => x.OnAfflictionUpdate(__instance, characterHealth, targetLimb, deltaTime)); + } + + [HarmonyPatch(typeof(Connection), nameof(Connection.SendSignal)), HarmonyPostfix] + public static void Connection_SendSignal_Post(Connection __instance, Signal signal) + { + foreach (var wire in __instance.Wires) + { + Connection recipient = wire.OtherConnection(__instance); + if (recipient == null) { continue; } + + _eventService.PublishEvent(x => x.OnSignalReceived(signal, recipient)); + _eventService.Call("signalReceived." + recipient.Item.Prefab.Identifier, signal, recipient); + } + + foreach (CircuitBoxConnection connection in __instance.CircuitBoxConnections) + { + _eventService.PublishEvent(x => x.OnSignalReceived(signal, connection.Connection)); + _eventService.Call("signalReceived." + connection.Connection.Item.Prefab.Identifier, signal, connection.Connection); + } + } + + [HarmonyPatch(typeof(Item), MethodType.Constructor, new Type[] { typeof(Rectangle), typeof(ItemPrefab), typeof(Submarine), typeof(bool), typeof(ushort) }), HarmonyPostfix] + public static void Item_Ctor_Post(Item __instance) + { + _eventService.PublishEvent(x => x.OnItemCreated(__instance)); + } + + [HarmonyPatch(typeof(Item), nameof(Item.Remove)), HarmonyPostfix] + public static void Item_Remove_Post(Item __instance) + { + _eventService.PublishEvent(x => x.OnItemRemoved(__instance)); + } + + [HarmonyPatch(typeof(Item), nameof(Item.Remove)), HarmonyPostfix] + public static void Item_ShallowRemove_Post(Item __instance) + { + _eventService.PublishEvent(x => x.OnItemRemoved(__instance)); + } + + [HarmonyPatch(typeof(Item), nameof(Item.Use)), HarmonyPrefix] + public static bool Item_Use_Pre(Item __instance, Character user, Limb targetLimb, Entity useTarget) + { + if (__instance.RequireAimToUse && (user == null || !user.IsKeyDown(InputType.Aim))) + { + return true; + } + + if (__instance.Condition <= 0.0f) { return true; } + + bool? result = null; + _eventService.PublishEvent(x => result = x.OnItemUsed(__instance, user, targetLimb, useTarget)); + if (result == true) + { + return false; // skip + } + + return true; + } + + [HarmonyPatch(typeof(Item), nameof(Item.SecondaryUse)), HarmonyPrefix] + public static bool Item_SecondaryUse_Pre(Item __instance, Character character) + { + if (__instance.Condition <= 0.0f) { return true; } + + bool? result = null; + _eventService.PublishEvent(x => result = x.OnItemSecondaryUsed(__instance, character)); + if (result == true) + { + return false; // skip + } + + return true; + } + + [HarmonyPatch(typeof(Inventory), "PutItem"), HarmonyPrefix] + public static bool Inventory_PutItem_Prefix(Inventory __instance, Item item, int i, Character user, bool removeItem) + { + bool? result = null; + _eventService.PublishEvent(x => result = x.OnInventoryPutItem(__instance, item, user, i, removeItem)); + if (result == true) + { + return false; // skip + } + + return true; + } + + [HarmonyPatch(typeof(Inventory), "TrySwapping"), HarmonyPrefix] + public static bool Inventory_TrySwapping_Prefix(Inventory __instance, Item item, int index, Character user, bool swapWholeStack, ref bool __result) + { + // uncomment when we are plugin + // if (item?.ParentInventory == null || !__instance.slots[index].Any()) { return false; } + // if (__instance.slots[index].Items.Any(it => !it.IsInteractable(user))) { return false; } + if (!__instance.AllowSwappingContainedItems) { return false; } + + bool? result = null; + _eventService.PublishEvent(x => result = x.OnInventoryItemSwap(__instance, item, user, index, swapWholeStack)); + if (result != null) + { + __result = (bool)result; + return false; // skip + } + + return true; + } + + public void Dispose() + { + IsDisposed = true; + this.Harmony?.UnpatchSelf(); + } + +#if SERVER + [HarmonyPatch] + class Patch_StartGame_End + { + static MethodBase TargetMethod() + { + var original = AccessTools.Method( + typeof(GameServer), + "StartGame" + ); + + return AccessTools.EnumeratorMoveNext(original); + } + + [HarmonyPostfix] + static void Postfix(object __instance, bool __result) + { + if (!__result) { return; } + + var enumerator = __instance as IEnumerator; + if (enumerator == null) { return; } + + if (enumerator.Current == CoroutineStatus.Success) + { + _eventService.PublishEvent(x => x.OnRoundStart()); + } + } + } +#endif +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/LoggerService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/LoggerService.cs new file mode 100644 index 000000000..1c09be0e1 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/LoggerService.cs @@ -0,0 +1,218 @@ +using Barotrauma.LuaCs.Events; +using Barotrauma.Networking; +using FluentResults; +using HarmonyLib; +using Microsoft.Xna.Framework; +using MoonSharp.Interpreter; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; + +namespace Barotrauma.LuaCs; + +public partial class LoggerService : ILoggerService +{ + private List logSubscribers = []; + private ConcurrentQueue logQueue = []; + +#if SERVER + private const string TargetPrefix = "[SV]"; + private const int NetMaxLength = 1024; // character limit of vanilla Barotrauma's chat system. + private const int NetMaxMessages = 60; + + // This is used so it's possible to call logging functions inside the serverLog + // hook without creating an infinite loop + private bool _isInsideLogCall = false; +#else + private const string TargetPrefix = "[CL]"; +#endif + + public LoggerService() { } + + public void Subscribe(ILoggerSubscriber subscriber) + { + logSubscribers.Add(subscriber); + } + + public void Unsubscribe(ILoggerSubscriber subscriber) + { + logSubscribers.Remove(subscriber); + } + + public void ProcessLogs() + { + while (logQueue.TryDequeue(out PendingLog log)) + { + logSubscribers.ForEach(s => s.OnLog(log)); + + DebugConsole.NewMessage(log.Message, log.Color); + +#if SERVER + if (GameMain.Server != null) + { + if (GameMain.Server.ServerSettings.SaveServerLogs) + { + string logMessage = "[LuaCs] " + log.Message; + GameMain.Server.ServerSettings.ServerLog.WriteLine(logMessage, log.MessageType, false); + + if (!_isInsideLogCall) + { + _isInsideLogCall = true; + LuaCsSetup.Instance?.EventService.PublishEvent(x => x.OnServerLog(logMessage, log.MessageType)); + _isInsideLogCall = false; + } + } + + for (int i = 0; i < log.Message.Length; i += NetMaxLength) + { + string subStr = log.Message.Substring(i, Math.Min(1024, log.Message.Length - i)); + BroadcastMessage(subStr); + } + } + + void BroadcastMessage(string m) + { + foreach (var client in GameMain.Server.ConnectedClients) + { + ChatMessage consoleMessage = ChatMessage.Create("", m, ChatMessageType.Console, null, textColor: log.Color); + GameMain.Server.SendDirectChatMessage(consoleMessage, client); + + if (!GameMain.Server.ServerSettings.SaveServerLogs || !client.HasPermission(ClientPermissions.ServerLog)) + { + continue; + } + + ChatMessage logMessage = ChatMessage.Create(log.MessageType.ToString(), "[LuaCs] " + m, ChatMessageType.ServerLog, null); + GameMain.Server.SendDirectChatMessage(logMessage, client); + } + } +#endif + } + } + + public void Log(string message, Color? color = null, ServerLog.MessageType messageType = ServerLog.MessageType.ServerMessage) + { + if (LuaCsSetup.Instance.HideUserNamesInLogs && !Environment.UserName.IsNullOrEmpty()) + { + message = message.Replace(Environment.UserName, "USERNAME"); + } + + message = $"{TargetPrefix} {message}"; + + logQueue.Enqueue(new PendingLog(message, color, messageType)); + } + + public void LogError(string message) + { + Log($"{message}", Color.Red, ServerLog.MessageType.Error); + } + + public void LogWarning(string message) + { + Log($"{message}", Color.Yellow, ServerLog.MessageType.ServerMessage); + } + + public void LogMessage(string message, Color? serverColor = null, Color? clientColor = null) + { + serverColor ??= Color.MediumPurple; + clientColor ??= Color.Purple; + +#if SERVER + Log(message, serverColor); +#else + Log(message, clientColor); +#endif + } + + public void HandleException(Exception exception, string prefix = null) + { + string errorString = ""; + switch (exception) + { + case NetRuntimeException netRuntimeException: + if (netRuntimeException.DecoratedMessage == null) + { + errorString = $"{prefix ?? ""}{netRuntimeException.ToString()}"; + } + else + { + // FIXME: netRuntimeException.ToString() doesn't print the InnerException's stack trace... + errorString = $"{prefix ?? ""}{netRuntimeException.DecoratedMessage}: {netRuntimeException}"; + } + break; + case InterpreterException interpreterException: + if (interpreterException.DecoratedMessage == null) + { + errorString = $"{prefix ?? ""}{interpreterException.ToString()}"; + } + else + { + errorString = $"{prefix ?? ""}{interpreterException.DecoratedMessage}"; + } + break; + default: + string s = exception.StackTrace != null ? exception.ToString() : $"{exception}\n{Environment.StackTrace}"; + errorString = $"{prefix ?? ""}{s}"; + break; + } + + LogError(prefix + Environment.UserName + " " + errorString); + } + + + public void LogResults(FluentResults.Result result) + { + if (result == null) + { + LogError("Result is null"); + return; + } + + if (result.IsSuccess) + { + return; + } + + if (result.IsFailed) + { + foreach (var error in result.Errors) + { + if (error is ExceptionalError exceptionalError) + { + HandleException(exceptionalError.Exception); + } + else + { + LogError($"FluentResults::IError: {error.Message}"); + /*if (error.Reasons != null) + { + foreach (var reason in error.Reasons) + { + LogError($" - {reason.Message}"); + } + }*/ + } + } + } + } + + public void LogDebug(string message, Color? color = null) + { + Log(message, color ?? Color.Purple); + } + + public void LogDebugWarning(string message) + { + Log(message, Color.Yellow); + } + + public void LogDebugError(string message) + { + Log(message, Color.Red); + } + + public void Dispose() { } + public FluentResults.Result Reset() => FluentResults.Result.Ok(); + + public bool IsDisposed { get; } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/LuaCsInfoProvider.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/LuaCsInfoProvider.cs new file mode 100644 index 000000000..25e5ab0b2 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/LuaCsInfoProvider.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Barotrauma.LuaCs; + +public sealed class LuaCsInfoProvider : ILuaCsInfoProvider +{ + public void Dispose() + { + // stateless service + } + + public bool IsDisposed => false; + public bool IsCsEnabled => LuaCsSetup.Instance.IsCsEnabled; + public bool HideUserNamesInLogs => LuaCsSetup.Instance.HideUserNamesInLogs; + public bool UseCaching => LuaCsSetup.Instance.UseCaching; + public RunState CurrentRunState => LuaCsSetup.Instance.CurrentRunState; + public ContentPackage LuaCsForBarotraumaPackage + { + get + { + return ContentPackageManager.EnabledPackages.Regular.FirstOrDefault(cp => cp.NameMatches(LuaCsSetup.PackageName), null) + ?? ContentPackageManager.LocalPackages.FirstOrDefault(cp => cp.NameMatches(LuaCsSetup.PackageName)) + ?? ContentPackageManager.WorkshopPackages.FirstOrDefault(cp => cp.NameMatches(LuaCsSetup.PackageName)); + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/LuaScriptManagementService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/LuaScriptManagementService.cs new file mode 100644 index 000000000..99032700d --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/LuaScriptManagementService.cs @@ -0,0 +1,670 @@ +#nullable enable + +using Barotrauma.LuaCs.Compatibility; +using Barotrauma.LuaCs.Data; +using Barotrauma.LuaCs.Events; +using Barotrauma.Networking; +using FluentResults; +using Microsoft.CodeAnalysis; +using Microsoft.Toolkit.Diagnostics; +using MoonSharp.Interpreter; +using MoonSharp.Interpreter.Interop; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Reflection; +using System.Threading.Tasks; + +namespace Barotrauma.LuaCs; + +class LuaScriptManagementService : ILuaScriptManagementService, ILuaDataService, IEventAssemblyUnloading +{ + public Script? InternalScript => _script; + + private Script? _script; + private bool _isRunning; + [MemberNotNullWhen(true, nameof(_script))] + public bool IsRunning => _isRunning; + private List _resourcesInfo = new List(); + + private readonly AsyncReaderWriterLock _operationsLock = new (); + + private readonly ILuaUserDataService _userDataService; + private readonly ISafeLuaUserDataService _safeUserDataService; + + private readonly ILuaScriptLoader _luaScriptLoader; + private readonly ILuaScriptServicesConfig _luaScriptServicesConfig; + private readonly ILoggerService _loggerService; + private readonly LuaGame _luaGame; + private readonly IEventService _eventService; + private readonly ILuaCsTimer _luaCsTimer; + private readonly IDefaultLuaRegistrar _defaultLuaRegistrar; + private readonly IPluginManagementService _pluginManagementService; + private readonly INetworkingService _networkingService; + private readonly IConsoleCommandsService _commandsService; + private readonly ILuaConfigService _configService; + private readonly ILuaCsInfoProvider _luaCsInfoProvider; + private readonly Lazy _packageManagementService; + //private readonly ILuaCsUtility _luaCsUtility; + + public LuaScriptManagementService( + ILoggerService loggerService, + ILuaScriptLoader loader, + ILuaUserDataService userDataService, + ISafeLuaUserDataService safeUserDataService, + IDefaultLuaRegistrar defaultLuaRegistrar, + ILuaScriptServicesConfig luaScriptServicesConfig, + IPluginManagementService pluginManagementService, + INetworkingService networkingService, + LuaGame luaGame, + IEventService eventService, + //ILuaCsUtility luaCsUtility, + ILuaCsTimer luaCsTimer, + IConsoleCommandsService commandsService, + ILuaCsInfoProvider luaCsInfoProvider, + ILuaConfigService configService, + Lazy packageManagementService) + { + _luaScriptLoader = loader; + _userDataService = userDataService; + _safeUserDataService = safeUserDataService; + _defaultLuaRegistrar = defaultLuaRegistrar; + _luaScriptServicesConfig = luaScriptServicesConfig; + _loggerService = loggerService; + _pluginManagementService = pluginManagementService; + _networkingService = networkingService; + + _luaGame = luaGame; + _eventService = eventService; + _commandsService = commandsService; + _luaCsInfoProvider = luaCsInfoProvider; + _configService = configService; + _packageManagementService = packageManagementService; + _luaCsTimer = luaCsTimer; + + RegisterLuaEvents(); + RegisterConsoleCommands(_commandsService); + } + + private void RegisterConsoleCommands(IConsoleCommandsService commands) + { +#if CLIENT + commands.RegisterCommand("cl_reloadlua|cl_reloadcs|cl_reloadluacs", "Re-initializes the LuaCs environment.", (string[] args) => + { + LuaCsSetup.Instance.EventService.PublishEvent(sub => sub.OnReloadAllPackages()); + }); + + commands.RegisterCommand("cl_lua", $"cl_lua: Runs a string on the client.", (string[] args) => + { + if (GameMain.Client != null && !GameMain.Client.HasPermission(ClientPermissions.ConsoleCommands)) + { + DebugConsole.ThrowError("Command not permitted."); + return; + } + + if (LuaCsSetup.Instance.CurrentRunState != RunState.Running) + { + DebugConsole.ThrowError("LuaCs not initialized, use the console command cl_reloadluacs to force initialization."); + return; + } + + var result = LuaCsSetup.Instance.LuaScriptManagementService.DoString(string.Join(" ", args)); + LuaCsSetup.Instance.Logger.LogResults(result.ToResult()); + }); + + commands.RegisterCommand("cl_toggleluadebug", "Toggles the MoonSharp Debug Server.", (string[] args) => + { + DebugConsole.Log($"This command is currently not implemented. Please open a github issue if you need this feature."); + /*int port = 41912; + + if (args.Length > 0) + { + int.TryParse(args[0], out port); + } + + throw new NotImplementedException(); + //GameMain.LuaCs.ToggleDebugger(port);*/ + }); + +#elif SERVER + commands.RegisterCommand("lua", "lua: Runs a string.", (string[] args) => + { + var result = LuaCsSetup.Instance.LuaScriptManagementService.DoString(string.Join(" ", args)); + LuaCsSetup.Instance.Logger.LogResults(result.ToResult()); + }); + + commands.RegisterCommand("reloadlua|reloadcs|reloadluacs", "Re-initializes the LuaCs environment.", (string[] args) => + { + LuaCsSetup.Instance.EventService.PublishEvent(sub => sub.OnReloadAllPackages()); + }); + + commands.RegisterCommand("toggleluadebug", "Toggles the MoonSharp Debug Server.", (string[] args) => + { + int port = 41912; + + if (args.Length > 0) + { + int.TryParse(args[0], out port); + } + + throw new NotImplementedException(); + //GameMain.LuaCs.ToggleDebugger(port); + }); +#endif + +#if SERVER + commands.RegisterCommand("install_cl_lua|install_cl|install_cl_cs|install_cl_luacs", "Installs Client-Side LuaCs into your client.", (string[] args) => + { + LuaCsInstaller.Install(); + }); +#endif + } + + public bool IsDisposed { get; private set; } + + public void SetCachingPolicy(bool useCaching) + { + _luaScriptLoader?.SetCachingPolicy(useCaching); + } + + public async Task LoadScriptResourcesAsync(ImmutableArray resourcesInfo) + { + if (!_luaCsInfoProvider.UseCaching) + { + return FluentResults.Result.Ok(); + } + + // Do any exception checks you can before acquiring a lock to avoid needlessly holding up resources. + if (resourcesInfo.IsDefaultOrEmpty) + { + ThrowHelper.ThrowArgumentNullException($"{nameof(LoadScriptResourcesAsync)}: The parameter is empty!"); + } + + // Acquire a lock: + // Reader = Allow parallel operations (try to avoid nesting acquiring the lock when possible) + // Writer = Exclusive use (ie. executing scripts or Dispose()) + using var lck = await _operationsLock.AcquireWriterLock(); // IDisposable using with generate a try-finally and release for you. + IService.CheckDisposed(this); // Check disposed after you have the lock + + // If you use a ConcurrentDictionary instead of a List, it will handle threading issues for you. + _resourcesInfo.AddRange(resourcesInfo.OrderBy(static r => r.LoadPriority)); + + // Use the StorageService's caching function by just loading the file with caching turned on. + // Right now the LuaScriptLoader has this on by default. + var cacheRes = await _luaScriptLoader.CacheResourcesAsync(resourcesInfo); + + // Aggregate and return results to the caller to deal with. Optionally, log here if you want. + // Automatically converted to a Task when 'async' is in the method declaration. + if (cacheRes.IsFailed) + { + return cacheRes.ToResult(); + } + return new FluentResults.Result().WithReasons(cacheRes.Value.SelectMany(cr => cr.Item2.Reasons)); + } + + public FluentResults.Result DoString(string code) + { + IService.CheckDisposed(this); + if (_script == null || !IsRunning) { throw new Exception("Disposed"); } + + try + { + var result = _script.DoString(code); + return FluentResults.Result.Ok(result); + } + catch (Exception ex) + { + return FluentResults.Result.Fail(new ExceptionalError(ex)); + } + } + + private DynValue DoFile(string file, Table? globalContext = null, string? codeStringFriendly = null) + { + if (_script == null) + { + throw new Exception("Not running"); + } + + if (!LuaCsFile.CanReadFromPath(file)) + { + // TODO: Replace with LuaScriptLoader IsFileAccessible. + throw new ScriptRuntimeException($"dofile: File access to {file} not allowed."); + } + + if (!LuaCsFile.Exists(file)) + { + // TODO: Replace with LuaScriptLoader IsFileAccessible. + throw new ScriptRuntimeException($"dofile: File {file} not found."); + } + + return _script.DoFile(file, globalContext, codeStringFriendly); + } + + private DynValue LoadFile(string file, Table? globalContext = null, string? codeStringFriendly = null) + { + if (_script == null) + { + throw new Exception("Not running"); + } + + if (!LuaCsFile.CanReadFromPath(file)) + { + throw new ScriptRuntimeException($"loadfile: File access to {file} not allowed."); + } + + if (!LuaCsFile.Exists(file)) + { + throw new ScriptRuntimeException($"loadfile: File {file} not found."); + } + + return _script.LoadFile(file, globalContext, codeStringFriendly); + } + + private void RegisterLuaEvents() + { + _eventService.Subscribe(this); + + _eventService.RegisterLuaEventAlias("think", nameof(IEventUpdate.OnUpdate)); + _eventService.RegisterLuaEventAlias("keyUpdate", nameof(IEventKeyUpdate.OnKeyUpdate)); + _eventService.RegisterLuaEventAlias("afflictionUpdate", nameof(IEventAfflictionUpdate.OnAfflictionUpdate)); + + _eventService.RegisterLuaEventAlias("character.created", nameof(IEventCharacterCreated.OnCharacterCreated)); + _eventService.RegisterLuaEventAlias("character.death", nameof(IEventCharacterDeath.OnCharacterDeath)); + _eventService.RegisterLuaEventAlias("character.damageLimb", nameof(IEventCharacterDamageLimb.OnCharacterDamageLimb)); + _eventService.RegisterLuaEventAlias("character.giveJobItems", nameof(IEventGiveCharacterJobItems.OnGiveCharacterJobItems)); + _eventService.RegisterLuaEventAlias("character.CPRSuccess", nameof(IEventHumanCPRSuccess.OnCharacterCPRSuccess)); + _eventService.RegisterLuaEventAlias("character.CPRFailed", nameof(IEventHumanCPRFailed.OnCharacterCPRFailed)); + _eventService.RegisterLuaEventAlias("human.CPRSuccess", nameof(IEventHumanCPRSuccess.OnCharacterCPRSuccess)); + _eventService.RegisterLuaEventAlias("human.CPRFailed", nameof(IEventHumanCPRFailed.OnCharacterCPRFailed)); + _eventService.RegisterLuaEventAlias("character.applyDamage", nameof(IEventCharacterApplyDamage.OnCharacterApplyDamage)); + _eventService.RegisterLuaEventAlias("character.applyAffliction", nameof(IEventCharacterApplyAffliction.OnCharacterApplyAffliction)); + + _eventService.RegisterLuaEventAlias("gapOxygenUpdate", nameof(IEventGapOxygenUpdate.OnGapOxygenUpdate)); + + _eventService.RegisterLuaEventAlias("husk.clientControlHusk", nameof(IEventClientControlHusk.OnClientControlHusk)); + + _eventService.RegisterLuaEventAlias("meleeWeapon.handleImpact", nameof(IEventMeleeWeaponHandleImpact.OnMeleeWeaponHandleImpact)); + + _eventService.RegisterLuaEventAlias("serverLog", nameof(IEventServerLog.OnServerLog)); + + _eventService.RegisterLuaEventAlias("tryChangeClientName", nameof(IEventTryClientChangeName.OnTryClienChangeName)); + + _eventService.RegisterLuaEventAlias("changeFallDamage", nameof(IEventChangeFallDamage.OnChangeFallDamage)); + + _eventService.RegisterLuaEventAlias("chatMessage", nameof(IEventChatMessage.OnChatMessage)); + + _eventService.RegisterLuaEventAlias("canUseVoiceRadio", nameof(IEventCanUseVoiceRadio.OnCanUseVoiceRadio)); + _eventService.RegisterLuaEventAlias("changeLocalVoiceRange", nameof(IEventChangeLocalVoiceRange.OnChangeLocalVoiceRange)); + + _eventService.RegisterLuaEventAlias("roundStart", nameof(IEventRoundStarted.OnRoundStart)); + _eventService.RegisterLuaEventAlias("roundEnd", nameof(IEventRoundEnded.OnRoundEnd)); + _eventService.RegisterLuaEventAlias("missionsEnded", nameof(IEventMissionsEnded.OnMissionsEnded)); + + _eventService.RegisterLuaEventAlias("signalReceived", nameof(IEventSignalReceived.OnSignalReceived)); + + _eventService.RegisterLuaEventAlias("item.created", nameof(IEventItemCreated.OnItemCreated)); + _eventService.RegisterLuaEventAlias("item.removed", nameof(IEventItemRemoved.OnItemRemoved)); + _eventService.RegisterLuaEventAlias("item.use", nameof(IEventItemUse.OnItemUsed)); + _eventService.RegisterLuaEventAlias("item.secondaryUse", nameof(IEventItemSecondaryUse.OnItemSecondaryUsed)); + _eventService.RegisterLuaEventAlias("item.readPropertyChange", nameof(IEventItemReadPropertyChange.OnItemReadPropertyChange)); + _eventService.RegisterLuaEventAlias("item.deconstructed", nameof(IEventItemDeconstructed.OnItemDeconstructed)); + + _eventService.RegisterLuaEventAlias("inventoryPutItem", nameof(IEventInventoryPutItem.OnInventoryPutItem)); + _eventService.RegisterLuaEventAlias("inventoryItemSwap", nameof(IEventInventoryItemSwap.OnInventoryItemSwap)); + + // Compatibility + _eventService.RegisterLuaEventAlias("characterCreated", nameof(IEventCharacterCreated.OnCharacterCreated)); + _eventService.RegisterLuaEventAlias("characterDeath", nameof(IEventCharacterDeath.OnCharacterDeath)); + +#if SERVER + _eventService.RegisterLuaEventAlias("client.connected", nameof(IEventClientConnected.OnClientConnected)); + _eventService.RegisterLuaEventAlias("client.disconnected", nameof(IEventClientDisconnected.OnClientDisconnected)); + _eventService.RegisterLuaEventAlias("jobsAssigned", nameof(IEventJobsAssigned.OnJobsAssigned)); + + _eventService.RegisterLuaEventAlias("netMessageReceived", nameof(IEventClientRawNetMessageReceived.OnReceivedClientNetMessage)); + + // Compatibility + _eventService.RegisterLuaEventAlias("clientConnected", nameof(IEventClientConnected.OnClientConnected)); + _eventService.RegisterLuaEventAlias("clientDisconnected", nameof(IEventClientDisconnected.OnClientDisconnected)); + _eventService.RegisterLuaEventAlias("modifyChatMessage", nameof(IEventModifyChatMessage.OnModifyMessagePredicate)); +#elif CLIENT + _eventService.RegisterLuaEventAlias("netMessageReceived", nameof(IEventServerRawNetMessageReceived.OnReceivedServerNetMessage)); +#endif + } + + private void SetupEnvironment(bool enableSandbox) + { + _script = new Script(CoreModules.Preset_SoftSandbox | CoreModules.Debug | CoreModules.IO | CoreModules.OS_System); + _script.Options.DebugPrint = (string msg) => + { + _loggerService.LogMessage($"[Lua] {msg}"); + }; + SetCachingPolicy(_luaCsInfoProvider.UseCaching); + + _script.Options.ScriptLoader = _luaScriptLoader; + _script.Options.CheckThreadAccess = false; + + Script.GlobalOptions.ShouldPCallCatchException = (Exception ex) => { return true; }; + + UserData.RegisterType(); + UserData.RegisterType(typeof(LuaGame)); + StandardUserDataDescriptor descriptor = (StandardUserDataDescriptor)UserData.RegisterType(typeof(EventService)); + descriptor.AddDynValue("HookMethodType", UserData.CreateStatic()); + UserData.RegisterType(typeof(ILuaCsNetworking)); + UserData.RegisterType(typeof(ILuaCsUtility)); + UserData.RegisterType(typeof(ILuaCsTimer)); + UserData.RegisterType(typeof(LuaCsFile)); + UserData.RegisterType(typeof(ILuaScriptResourceInfo)); + UserData.RegisterType(typeof(IResourceInfo)); + UserData.RegisterType(typeof(IUserDataDescriptor)); + UserData.RegisterType(typeof(INetworkingService)); + UserData.RegisterType(typeof(ILuaConfigService)); + UserData.RegisterType(typeof(ILoggerService)); + + UserData.RegisterType(typeof(ISettingBase)); + UserData.RegisterType(typeof(IDataInfo)); + + Type[] settingBaseTypes = [ + typeof(ISettingBase), + typeof(ISettingBase), + typeof(ISettingBase), + typeof(ISettingBase), + typeof(ISettingBase), + typeof(ISettingBase), + typeof(ISettingBase), + typeof(ISettingBase), + typeof(ISettingBase), + typeof(ISettingBase), + typeof(ISettingBase), + typeof(ISettingBase), + typeof(ISettingBase), + + typeof(ISettingRangeBase), + typeof(ISettingRangeBase), + + typeof(ISettingList), + typeof(ISettingList), + typeof(ISettingList), + typeof(ISettingList), + typeof(ISettingList), + typeof(ISettingList), + typeof(ISettingList), + typeof(ISettingList), + typeof(ISettingList), + typeof(ISettingList), + typeof(ISettingList), + typeof(ISettingList), + ]; + + Dictionary> settingsTable = []; + + foreach (Type type in settingBaseTypes) + { + UserData.RegisterType(type); + + string baseName = type.Name.RemoveFromEnd("`1").Substring(1); + + if (!settingsTable.ContainsKey(baseName)) + { + settingsTable[baseName] = new Dictionary(); + } + + settingsTable[baseName][type.GetGenericArguments()[0].Name] = UserData.CreateStatic(type); + } + + foreach (var keyPair in settingsTable) + { + _script.Globals[keyPair.Key] = keyPair.Value; + } + + UserData.RegisterType(typeof(ISettingRangeBase)); +#if CLIENT + UserData.RegisterType(typeof(ISettingControl)); +#endif + + new LuaConverters(this).RegisterLuaConverters(); + + var luaRequire = new LuaRequire(_script); + + _script.Globals["setmodulepaths"] = (string[] str) => ((LuaScriptLoader)_luaScriptLoader).ModulePaths = str; + + _script.Globals["dofile"] = (Func)DoFile; + _script.Globals["loadfile"] = (Func)LoadFile; + _script.Globals["require"] = (Func)luaRequire.Require; + + _script.Globals["printerror"] = (DynValue o) => { _loggerService.LogError($"[Lua] {o.ToString()}"); }; + + _script.Globals["dostring"] = (Func)_script.DoString; + _script.Globals["load"] = (Func)_script.LoadString; + _script.Globals["Game"] = _luaGame; + _script.Globals["Hook"] = _eventService; + _script.Globals["Timer"] = _luaCsTimer; + _script.Globals["File"] = UserData.CreateStatic(); + _script.Globals["ConfigService"] = _configService; + _script.Globals["Networking"] = _networkingService; + _script.Globals["trygetpackage"] = (string name, out ContentPackage package) => + _packageManagementService.Value.TryGetLoadedPackageByName(name, out package); + _script.Globals["Logger"] = _loggerService; + //_script.Globals["Steam"] = Steam; + + if (enableSandbox) + { + UserData.RegisterType(typeof(SafeLuaUserDataService)); + _script.Globals["LuaUserData"] = _safeUserDataService; + } + else + { + UserData.RegisterType(typeof(LuaUserDataService)); + _script.Globals["LuaUserData"] = _userDataService; + } + + Table eventsTable = new Table(_script); + + var typesValue = _pluginManagementService.GetImplementingTypes(includeInterfaces: true, includeAbstractTypes: true); + if (typesValue.IsSuccess) + { + foreach (var eventType in typesValue.Value) + { + if (eventType.IsGenericType) { continue; } + if (!eventType.IsInterface) { continue; } + + UserData.RegisterType(eventType); + eventsTable[eventType.Name] = UserData.CreateStatic(eventType); + } + } + + _script.Globals["Events"] = eventsTable; + + _script.Globals["ExecutionNumber"] = 0; + _script.Globals["CSActive"] = !enableSandbox; + ((Table)_script.Globals["debug"])["breakpoint"] = () => { Debugger.Break(); }; + + _script.Globals["SERVER"] = LuaCsSetup.IsServer; + _script.Globals["CLIENT"] = LuaCsSetup.IsClient; + + _defaultLuaRegistrar.RegisterAll(); + } + + public FluentResults.Result ExecuteLoadedScripts(ImmutableArray executionOrder, bool enableSandbox) + { + if (_isRunning) + { + return FluentResults.Result.Fail("Tried to execute Lua scripts without unloading first."); + } + + _loggerService.LogMessage("[Lua] Executing scripts"); + + SetupEnvironment(enableSandbox); + + if (_script == null) { return FluentResults.Result.Ok(); } // never happens + + var result = FluentResults.Result.Ok(); + + _isRunning = true; + + var packages = executionOrder.Select(r => r.OwnerPackage) + .Distinct() + .Select(p => $"{p.Dir}/Lua/?.lua") + .ToArray(); + + ((LuaScriptLoader)_luaScriptLoader).ModulePaths = packages; + Table package = (Table)_script.Globals["package"]; + package.Set("path", DynValue.FromObject(_script, packages)); + +#if CLIENT + if (GameMain.NetworkMember is { IsClient: true }) + { + var startMessage = _networkingService.Start("_luastart"); + + var packagesToReport = ContentPackageManager.EnabledPackages.All + .Where(p => _packageManagementService.Value.PackageContainsAnyRunnableResource(p)) + .Where(p => !p.NameMatches(LuaCsSetup.PackageName)) + .ToList(); + + startMessage.WriteUInt16((UInt16)packagesToReport.Count()); + + foreach (var enabledPackage in packagesToReport) + { + var id = enabledPackage.UgcId; + string hash = enabledPackage.Hash.StringRepresentation ?? ""; + + startMessage.WriteString(enabledPackage.Name); + startMessage.WriteString(enabledPackage.ModVersion); + if (id.TryUnwrap(out ContentPackageId? packageId) && packageId is SteamWorkshopId steamId) + { + startMessage.WriteUInt64(steamId.Value); + } + else + { + startMessage.WriteUInt64(0); + } + startMessage.WriteString(hash); + } + + _networkingService.Send(startMessage); + } +#elif SERVER + _networkingService.Receive("_luastart", (message, client) => + { + var num = message.ReadUInt16(); + List packages = new List
(); + + for (int i = 0; i < num; i++) + { + Table table = new Table(_script); + + table.Set("Name", DynValue.NewString(message.ReadString())); + table.Set("Version", DynValue.NewString(message.ReadString())); + table.Set("Id", DynValue.NewString(message.ReadUInt64().ToString())); + table.Set("Hash", DynValue.NewString(message.ReadString())); + + packages.Add(table); + } + + _eventService.Call("client.packages", client, packages); + }); +#endif + + + foreach (ILuaScriptResourceInfo resource in executionOrder.Where(l => l.IsAutorun)) + { + foreach (ContentPath filePath in resource.FilePaths) + { + try + { + _loggerService.LogMessage($"[Lua] - Run {filePath.Value}"); + _script.Call(_script.LoadFile(filePath.FullPath), resource.OwnerPackage.Dir); + } + catch(Exception e) + { + result = result.WithError(new ExceptionalError(e)); + } + } + } + + _eventService.Call("loaded"); + + return result; + } + + public DynValue? CallFunctionSafe(object luaFunction, params object[] args) + { + if (!IsRunning) { return null; } + + lock (_script) + { + try + { + return _script.Call(luaFunction, args); + } + catch (Exception e) + { + _loggerService.HandleException(e); + } + return null; + } + } + + public FluentResults.Result UnloadActiveScripts() + { + _isRunning = false; + + _script = null; + + return FluentResults.Result.Ok(); + } + + public FluentResults.Result DisposePackageResources(ContentPackage package) + { + return FluentResults.Result.Ok(); + } + + public FluentResults.Result DisposeAllPackageResources() + { + if (IsRunning) + { + UnloadActiveScripts(); + } + + _resourcesInfo.Clear(); + _luaScriptLoader.ClearCaches(); + + return FluentResults.Result.Ok(); + } + + public FluentResults.Result Reset() + { + IService.CheckDisposed(this); + _luaScriptLoader.ClearCaches(); + _userDataService.Reset(); + _luaCsTimer.Reset(); + RegisterLuaEvents(); + return DisposeAllPackageResources(); + } + + public void Dispose() + { + IsDisposed = true; + _userDataService.Dispose(); + _luaScriptLoader.Dispose(); + _commandsService.Dispose(); + } + + public object? GetGlobalTableValue(string tableName) + { + if (!IsRunning) { return null; } + + return _script.Globals[tableName]; + } + + public void OnAssemblyUnloading(Assembly assembly) + { + foreach (Type type in assembly.SafeGetTypes()) + { + UserData.UnregisterType(type, deleteHistory: true); + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/MainMenuPatch.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/MainMenuPatch.cs new file mode 100644 index 000000000..798a9a249 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/MainMenuPatch.cs @@ -0,0 +1,94 @@ +using Barotrauma; +using Barotrauma.LuaCs; +using Barotrauma.LuaCs.Events; +using FluentResults; +using HarmonyLib; +using Microsoft.Xna.Framework; + +[HarmonyPatch] +internal class MainMenuPatch : ISystem, IEventScreenSelected +{ + public bool IsDisposed { get; private set; } + + private readonly IEventService _eventService; + + private bool mainMenuUIAdded = false; + + public MainMenuPatch(IEventService eventService) + { + _eventService = eventService; + + RegisterEvents(); + +#if CLIENT + if (Screen.Selected is MainMenuScreen mainMenuScreen) + { + AddToMainMenu(mainMenuScreen); + } +#endif + } + + public void OnScreenSelected(Screen screen) + { +#if CLIENT + if (screen is MainMenuScreen mainMenuScreen) + { + AddToMainMenu(mainMenuScreen); + } +#endif + } + +#if CLIENT + private void AddToMainMenu(MainMenuScreen screen) + { + if (mainMenuUIAdded) { return; } + + var textBlock = new GUITextBlock(new RectTransform(new Point(300, 30), screen.Frame.RectTransform, Anchor.TopLeft) { AbsoluteOffset = new Point(10, 10) }, "", Color.Red) + { + IgnoreLayoutGroups = false + }; + + textBlock.OnAddedToGUIUpdateList = (GUIComponent component) => + { + string mode = LuaCsSetup.Instance.CsRunPolicyValue; + + if (mode is "Prompt") + { + string sessionState = LuaCsSetup.Instance.IsCsEnabledForSession ? "yes" : "no"; + mode = $"enabled (prompt mode, allowed for this session: {sessionState})"; + } + else if (mode is "Enabled") + { + mode = "always enabled"; + } + else + { + mode = "disabled"; + } + + textBlock.Text = $"LuaCsForBarotrauma active (revision {AssemblyInfo.GitRevision}), C# is currently {mode}\nNew settings available in the game settings menu."; + }; + + mainMenuUIAdded = true; + } +#endif + + private void RegisterEvents() + { + _eventService.Subscribe(this); + } + + public void Dispose() + { + _eventService.Unsubscribe(this); + + IsDisposed = true; + } + + public FluentResults.Result Reset() + { + RegisterEvents(); + + return FluentResults.Result.Ok(); + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/ModConfigFileParserService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/ModConfigFileParserService.cs new file mode 100644 index 000000000..e1dc53c7a --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/ModConfigFileParserService.cs @@ -0,0 +1,281 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Runtime.CompilerServices; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using System.Xml.Linq; +using Barotrauma.LuaCs.Data; +using FarseerPhysics.Common; +using FluentResults; +using Microsoft.Toolkit.Diagnostics; + +namespace Barotrauma.LuaCs; + +public sealed partial class ModConfigFileParserService : + IParserServiceAsync, + IParserServiceAsync, + IParserServiceAsync +{ + private IStorageService _storageService; + private readonly AsyncReaderWriterLock _operationsLock = new(); + + public ModConfigFileParserService(IStorageService storageService) + { + _storageService = storageService; + } + + #region Dispose + + public void Dispose() + { + using var lck = _operationsLock.AcquireWriterLock().ConfigureAwait(false).GetAwaiter().GetResult(); + if (!ModUtils.Threading.CheckIfClearAndSetBool(ref _isDisposed)) + return; + try + { + _storageService.Dispose(); + this._storageService = null; + } + catch + { + // ignored + } + } + + private int _isDisposed = 0; + public bool IsDisposed + { + get => ModUtils.Threading.GetBool(ref _isDisposed); + private set => ModUtils.Threading.SetBool(ref _isDisposed, value); + } + + #endregion + + // --- Assemblies + async Task> IParserServiceAsync.TryParseResourceAsync(ResourceParserInfo src) + { + using var lck = await _operationsLock.AcquireReaderLock(); + IService.CheckDisposed(this); + + if (CheckThrowNullRefs(src, "Assembly") is { IsFailed: true } fail) + return fail; + + var isScript = src.Element.GetAttributeBool("IsScript", false); + var runtimeEnv = GetRuntimeEnvironment(src.Element); + var fileResults = await UnsafeGetCheckedFiles(src.Element, src.Owner, isScript ? ".cs" : ".dll"); + + if (fileResults.IsFailed) + return FluentResults.Result.Fail(fileResults.Errors); + + return new AssemblyResourceInfo() + { + SupportedPlatforms = runtimeEnv.Platform, + SupportedTargets = runtimeEnv.Target, + LoadPriority = src.Element.GetAttributeInt("LoadPriority", 0), + FilePaths = fileResults.Value, + Optional = src.Element.GetAttributeBool("Optional", false), + InternalName = src.Element.GetAttributeString("Name", string.Empty), + OwnerPackage = src.Owner, + RequiredPackages = src.Required, + IncompatiblePackages = src.Incompatible, + // Type Specific + FriendlyName = src.Element.GetAttributeString("FriendlyName", GetFallbackCompliantAssemblyName(src.Owner)), + IsScript = isScript, + UseInternalAccessName = src.Element.GetAttributeBool("UseInternalAccessName", false), + IsReferenceModeOnly = src.Element.GetAttributeBool("IsReferenceModeOnly", false) + }; + + + // helper methods + string GetFallbackCompliantAssemblyName(ContentPackage package) + { + if (package.Name.IsNullOrWhiteSpace()) + { + return "FallbackAssemblyName"; + } + + // replace non az chars with '_' + var sanitizedPackageName = Regex.Replace(package.Name, @"[^a-zA-Z0-9_]", "_"); + if (char.IsDigit(sanitizedPackageName[0])) + { + sanitizedPackageName = "ASM" + sanitizedPackageName; + } + + // replace consecutive '_' + return Regex.Replace(sanitizedPackageName, @"[_.]{2,}", "_"); + } + } + + async Task>> IParserServiceAsync.TryParseResourcesAsync(IEnumerable sources) + { + return await this.TryParseGenericResourcesAsync(sources); + } + + // --- Config + + async Task> IParserServiceAsync.TryParseResourceAsync(ResourceParserInfo src) + { + using var lck = await _operationsLock.AcquireReaderLock(); + IService.CheckDisposed(this); + + if (CheckThrowNullRefs(src, "Config") is { IsFailed: true } fail) + return fail; + + var runtimeEnv = GetRuntimeEnvironment(src.Element); + var fileResults = await UnsafeGetCheckedFiles(src.Element, src.Owner, ".xml"); + + if (fileResults.IsFailed) + return FluentResults.Result.Fail(fileResults.Errors); + + return new ConfigResourceInfo() + { + SupportedPlatforms = runtimeEnv.Platform, + SupportedTargets = runtimeEnv.Target, + LoadPriority = src.Element.GetAttributeInt("LoadPriority", 0), + FilePaths = fileResults.Value, + Optional = src.Element.GetAttributeBool("Optional", false), + InternalName = src.Element.GetAttributeString("Name", string.Empty), + OwnerPackage = src.Owner, + RequiredPackages = src.Required, + IncompatiblePackages = src.Incompatible + }; + } + + async Task>> IParserServiceAsync.TryParseResourcesAsync(IEnumerable sources) + { + return await this.TryParseGenericResourcesAsync(sources); + } + + // --- Lua Scripts + async Task> IParserServiceAsync.TryParseResourceAsync(ResourceParserInfo src) + { + using var lck = await _operationsLock.AcquireReaderLock(); + IService.CheckDisposed(this); + + if (CheckThrowNullRefs(src, "Lua") is { IsFailed: true } fail) + return fail; + + var runtimeEnv = GetRuntimeEnvironment(src.Element); + var fileResults = await UnsafeGetCheckedFiles(src.Element, src.Owner, ".lua"); + + if (fileResults.IsFailed) + return FluentResults.Result.Fail(fileResults.Errors); + + return new LuaScriptsResourceInfo() + { + SupportedPlatforms = runtimeEnv.Platform, + SupportedTargets = runtimeEnv.Target, + LoadPriority = src.Element.GetAttributeInt("LoadPriority", 0), + FilePaths = fileResults.Value, + Optional = src.Element.GetAttributeBool("Optional", false), + InternalName = src.Element.GetAttributeString("Name", string.Empty), + OwnerPackage = src.Owner, + RequiredPackages = src.Required, + IncompatiblePackages = src.Incompatible, + // Type Specific + IsAutorun = src.Element.GetAttributeBool("IsAutorun", false), + RunUnrestricted = src.Element.GetAttributeBool("RunUnrestricted", false) + }; + } + + private FluentResults.Result CheckThrowNullRefs(ResourceParserInfo src, string elementName) + { + Guard.IsNotNull(src, nameof(src)); + Guard.IsNotNull(src.Owner, nameof(src.Owner)); + Guard.IsNotNull(src.Element, nameof(src.Element)); + + if (src.Element.Name != elementName) + { + return FluentResults.Result.Fail($"Element name '{elementName}' is incorrect"); + } + + return FluentResults.Result.Ok(); + } + + async Task>> IParserServiceAsync.TryParseResourcesAsync(IEnumerable sources) + { + return await this.TryParseGenericResourcesAsync(sources); + } + + // --- Helpers + private async Task>> UnsafeGetCheckedFiles(XElement srcElement, ContentPackage srcOwner, string fileExtension) + { + var builder = ImmutableArray.CreateBuilder(); + var filePath = srcElement.GetAttributeContentPath("File", srcOwner); + var folderPath = srcElement.GetAttributeContentPath("Folder", srcOwner); + + var res = new FluentResults.Result>(); + + if ((!filePath?.Value.IsNullOrWhiteSpace()) ?? false) + { + if (_storageService.FileExists(filePath.FullPath) is { IsSuccess: true, Value: true }) + { + builder.Add(filePath); + } + else + { + if (srcElement.GetAttributeBool("IsFileRequired", true)) + { + res.WithError($"{srcOwner.Name}: The file '{filePath}' is missing!"); + } + else + { + res.WithSuccess($"Skipped missing not-required file: '{filePath}'"); + } + } + } + + if ((!folderPath?.Value.IsNullOrWhiteSpace()) ?? false) + { + if (_storageService.DirectoryExists(folderPath.FullPath) is { IsSuccess: true, Value: true }) + { + var searchLocation = System.IO.Path.GetRelativePath(srcOwner.Dir, folderPath.Value); + var files = _storageService.FindFilesInPackage(srcOwner, searchLocation, "*"+fileExtension, true); + if (files.IsFailed) + { + res.WithError($"{srcOwner.Name}: Failed to load files from {folderPath}!"); + } + else + { + foreach (var file in files.Value) + { + builder.Add(ContentPath.FromRaw(srcOwner, $"%ModDir%/{System.IO.Path.GetRelativePath(System.IO.Path.GetFullPath(srcOwner.Dir), file)}")); + } + } + } + else + { + if (srcElement.GetAttributeBool("IsFileRequired", true)) + { + res.WithError($"{srcOwner.Name}: The file '{folderPath}' is missing!"); + } + else + { + res.WithSuccess($"Skipped missing not-required folder: '{folderPath}'"); + } + } + } + + return res.WithValue(builder.ToImmutable()); + } + private (Platform Platform, Target Target) GetRuntimeEnvironment(XElement element) + { + return ( + Platform: element.GetAttributeEnum("Platform", Platform.Any), + Target: element.GetAttributeEnum("Target", Target.Any)); + } + + private async Task>> TryParseGenericResourcesAsync(IEnumerable sources) + { + // ReSharper disable once PossibleMultipleEnumeration + Guard.IsNotNull(sources, nameof(IParserServiceAsync.TryParseResourcesAsync)); + var builder = ImmutableArray.CreateBuilder>(); + foreach (var info in sources) + { + builder.Add(await Unsafe.As>(this).TryParseResourceAsync(info)); + } + return builder.ToImmutable(); + } + +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/ModConfigService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/ModConfigService.cs new file mode 100644 index 000000000..d7278c72b --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/ModConfigService.cs @@ -0,0 +1,430 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using System.Xml.Linq; +using Barotrauma.Extensions; +using Barotrauma.LuaCs.Data; +using FluentResults; +using Microsoft.Toolkit.Diagnostics; +using MoonSharp.VsCodeDebugger.SDK; + +namespace Barotrauma.LuaCs; + +public sealed class ModConfigService : IModConfigService +{ + private IStorageService _storageService; + private ILoggerService _logger; + private IParserServiceAsync _assemblyParserService; + private IParserServiceAsync _luaScriptParserService; + private IParserServiceAsync _configParserService; +#if CLIENT + private IParserServiceAsync _stylesParserService; +#endif + private readonly AsyncReaderWriterLock _operationsLock = new(); + + public ModConfigService(IStorageService storageService, + IParserServiceAsync assemblyParserService, + IParserServiceAsync luaScriptParserService, + IParserServiceAsync configParserService, +#if CLIENT + IParserServiceAsync stylesParserService, +#endif + ILoggerService logger) + { + _storageService = storageService; + _assemblyParserService = assemblyParserService; + _luaScriptParserService = luaScriptParserService; + _configParserService = configParserService; + _logger = logger; +#if CLIENT + _stylesParserService = stylesParserService; +#endif + } + + #region Dispose + + public void Dispose() + { + using var lck = _operationsLock.AcquireWriterLock().ConfigureAwait(false).GetAwaiter().GetResult(); + if (!ModUtils.Threading.CheckIfClearAndSetBool(ref _isDisposed)) + return; + + try + { + _storageService.Dispose(); + _logger.Dispose(); + _assemblyParserService.Dispose(); + _luaScriptParserService.Dispose(); + _configParserService.Dispose(); + + _storageService = null; + _logger = null; + _assemblyParserService = null; + _luaScriptParserService = null; + _configParserService = null; + +#if CLIENT + _stylesParserService.Dispose(); + _stylesParserService = null; +#endif + } + catch + { + // ignored + } + } + + private int _isDisposed = 0; + public bool IsDisposed + { + get => ModUtils.Threading.GetBool(ref _isDisposed); + private set => ModUtils.Threading.SetBool(ref _isDisposed, value); + } + + #endregion + + public async Task> CreateConfigAsync(ContentPackage src) + { + Guard.IsNotNull(src, nameof(src)); + using var lck = await _operationsLock.AcquireReaderLock(); + IService.CheckDisposed(this); + + if (await TryGetModConfigXmlAsync(src) is { IsSuccess: true, Value: { } config }) + { + return await CreateFromConfigXmlAsync(src, config); + } + + return await CreateFromLegacyAsync(src); + } + + public async Task Config)>> CreateConfigsAsync(ImmutableArray src) + { + if (src.IsDefaultOrEmpty) + ThrowHelper.ThrowArgumentNullException($"{nameof(CreateConfigsAsync)}: The supplied array is default or empty!"); + using var lck = await _operationsLock.AcquireReaderLock(); + IService.CheckDisposed(this); + + var builder = ImmutableArray.CreateBuilder>>>(src.Length); + foreach (var srcItem in src) + { + builder.Add(Task.Factory.StartNew(async Task> () => await CreateConfigAsync(srcItem))); + } + var taskResults = await Task.WhenAll(builder.ToImmutable()); + var returnResults = ImmutableArray.CreateBuilder<(ContentPackage Source, Result Config)>(); + foreach (var taskResult in taskResults) + { + if (taskResult.IsFaulted) + { + ThrowHelper.ThrowInvalidOperationException($"{nameof(CreateConfigsAsync)}: Task failed: {taskResult.Exception?.Message}"); + } + + var r = await taskResult; + returnResults.Add((r.Value.Package, r)); + } + + return returnResults.ToImmutable(); + } + + //--- Helpers + private async Task> TryGetModConfigXmlAsync(ContentPackage src) + { + return await _storageService.LoadPackageXmlAsync(ContentPath.FromRaw(src, "%ModDir%/ModConfig.xml")) is { IsSuccess: true, Value: { Root: {} config} } + ? FluentResults.Result.Ok(config) + : FluentResults.Result.Fail("ModConfig.xml not found"); + } + + private async Task> CreateFromConfigXmlAsync(ContentPackage owner, XElement src) + { + var asmTask = Task.Factory.StartNew(async () => await GetAssembliesFromXml(owner, src)); + var cfgTask = Task.Factory.StartNew(async () => await GetConfigsFromXml(owner, src)); + var luaTask = Task.Factory.StartNew(async () => await GetLuaScriptsFromXml(owner, src)); +#if CLIENT + var styleTask = Task.Factory.StartNew(async () => await GetStylesFromXml(owner, src)); +#endif + + await Task.WhenAll( + asmTask, + cfgTask, +#if CLIENT + styleTask, +#endif + luaTask); + + return FluentResults.Result.Ok(new ModConfigInfo() + { + Package = owner, + Assemblies = await await asmTask, + Configs = await await cfgTask, +#if CLIENT + Styles = await await styleTask, +#endif + LuaScripts = await await luaTask + }); + + async Task> GetLuaScriptsFromXml(ContentPackage contentPackage, + XElement cfgElement) + { + return await GetResourceFromXml(contentPackage, cfgElement, "Lua", "FileGroup", _luaScriptParserService); + } + + async Task> GetConfigsFromXml(ContentPackage contentPackage, + XElement cfgElement) + { + return await GetResourceFromXml(contentPackage, cfgElement, "Config", "FileGroup", _configParserService); + } + + async Task> GetAssembliesFromXml(ContentPackage contentPackage, + XElement cfgElement) + { + return await GetResourceFromXml(contentPackage, cfgElement, "Assembly", "FileGroup", _assemblyParserService); + } + +#if CLIENT + async Task> GetStylesFromXml(ContentPackage contentPackage, + XElement cfgElement) + { + return await GetResourceFromXml(contentPackage, cfgElement, "Style", "FileGroup", _stylesParserService); + } +#endif + + async Task> GetResourceFromXml(ContentPackage contentPackage, XElement cfgElement, string elemName, string fileGroupName, IParserServiceAsync resourceService) + { + var elems = GetResourceElementsWithName(owner, cfgElement, elemName, fileGroupName); + if (elems.IsDefaultOrEmpty) + return ImmutableArray.Empty; + + var results = await resourceService.TryParseResourcesAsync(elems); + Guard.IsNotEmpty((IReadOnlyCollection>)results, nameof(results)); + + var resources = ImmutableArray.CreateBuilder(); + foreach (var result in results) + { + if (result.Errors.Count > 0) + { + _logger.LogResults(result.ToResult()); + continue; + } + resources.Add(result.Value); + } + return resources.ToImmutable(); + } + + ImmutableArray GetResourceElementsWithName(ContentPackage package, XElement root, string elemName, string groupName) + { + var elems = ImmutableArray.CreateBuilder(); + + elems.AddRange(root.GetChildElements(elemName) + .Select(e => new ResourceParserInfo(package, e, ImmutableArray.Empty, ImmutableArray.Empty)) + .ToImmutableArray()); + + if (root.GetChildElements(groupName).ToImmutableArray() is { IsDefaultOrEmpty: false } fileGroups) + { + foreach (var fileGroup in fileGroups) + { + if (fileGroup.GetChildElements(elemName).ToImmutableArray() is { IsDefaultOrEmpty: false } subLuaElems) + { + var cond = GetDependencyIdentifiers(fileGroup, true); + var negCond = GetDependencyIdentifiers(fileGroup, false); + + foreach (var element in subLuaElems) + { + elems.Add(new ResourceParserInfo(package, element, cond, negCond)); + } + } + } + } + + return elems.ToImmutable(); + } + + ImmutableArray GetDependencyIdentifiers(XElement fg, bool depsLoadedSetting) + { + return fg.GetChildElements("Conditional") + .Where(cElem => bool.TryParse(cElem.GetAttribute("IsLoaded").Value, out bool isLoaded) && isLoaded == depsLoadedSetting) + .SelectMany(cElem2 => cElem2.GetAttributeString("Dependencies", String.Empty) + .Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries) + .Select(ident => new Identifier(ident))) + .ToImmutableArray(); + } + } + + + + private async Task> CreateFromLegacyAsync(ContentPackage src) + { + return new ModConfigInfo() + { + Package = src, + Assemblies = GetAssembliesLegacy(src), + Configs = GetConfigsLegacy(src), + LuaScripts = GetLuaScriptsLegacy(src) + }; + + ImmutableArray GetAssembliesLegacy(ContentPackage srcPackage) + { + var binSearchInd = new (string SubFolder, Target Targets, Platform Platforms)[] + { + ("bin/Client/Windows", Target.Client, Platform.Windows), + ("bin/Client/Linux", Target.Client, Platform.Linux), + ("bin/Client/OSX", Target.Client, Platform.OSX), + ("bin/Server/Windows", Target.Server, Platform.Windows), + ("bin/Server/Linux", Target.Server, Platform.Linux), + ("bin/Server/OSX", Target.Server, Platform.OSX) + }; + + var builder = ImmutableArray.CreateBuilder(); + + foreach (var searchPathways in binSearchInd) + { + if (_storageService.FindFilesInPackage(srcPackage, searchPathways.SubFolder, "*.dll", + true) is { IsSuccess: true, Value.IsDefaultOrEmpty: false } result) + { + builder.Add(new AssemblyResourceInfo() + { + OwnerPackage = srcPackage, + InternalName = searchPathways.SubFolder, + SupportedPlatforms = searchPathways.Platforms, + SupportedTargets = searchPathways.Targets, + LoadPriority = 0, + FilePaths = result.Value.Select(fp => ContentPath.FromRaw(srcPackage, $"%ModDir%/{Path.GetRelativePath(srcPackage.Dir, fp)}".CleanUpPathCrossPlatform())) + .ToImmutableArray(), + FriendlyName = $"{srcPackage.Name}.{searchPathways.SubFolder.Replace('/','.')}", + IncompatiblePackages = ImmutableArray.Empty, + RequiredPackages = ImmutableArray.Empty, + IsScript = false, + IsReferenceModeOnly = false + }); + } + } + + var sharedResult = _storageService.FindFilesInPackage(srcPackage, + Path.Combine("CSharp/Shared"), + "*.cs", true); + var sharedFiles = sharedResult.IsSuccess && !sharedResult.Value.IsDefaultOrEmpty + ? sharedResult.Value.Select(fp => + ContentPath.FromRaw(srcPackage, $"%ModDir%/{Path.GetRelativePath(srcPackage.Dir, fp)}".CleanUpPathCrossPlatform())) + .ToImmutableArray() + : ImmutableArray.Empty; + + var srcSearchInd = new (string SubFolder, Target Targets, Platform Platforms)[] + { + ("CSharp/Client", Target.Client, Platform.Any), + ("CSharp/Server", Target.Server, Platform.Any) + }; + + foreach (var searchPathways in srcSearchInd) + { + // we have architecture dependent files as well + if (_storageService.FindFilesInPackage(srcPackage, searchPathways.SubFolder, "*.cs", + true) is { IsSuccess: true, Value.IsDefaultOrEmpty: false } result) + { + builder.Add(new AssemblyResourceInfo() + { + OwnerPackage = srcPackage, + InternalName = searchPathways.SubFolder, + SupportedPlatforms = searchPathways.Platforms, + SupportedTargets = searchPathways.Targets, + LoadPriority = 0, + FilePaths = result.Value + .Select(fp => ContentPath.FromRaw(srcPackage, + $"%ModDir%/{Path.GetRelativePath(srcPackage.Dir, fp)}".CleanUpPathCrossPlatform())) + .Concat(sharedFiles).ToImmutableArray(), + FriendlyName = IAssemblyLoaderService.InternalsAwareAssemblyName, // give the best chance of success (InternalsAware + Publicizer) + IncompatiblePackages = ImmutableArray.Empty, + RequiredPackages = ImmutableArray.Empty, + UseInternalAccessName = false, //compile as public and then fallback to internals + IsScript = true, + IsReferenceModeOnly = false + }); + } + // add the shared files by themselves + else if (!sharedFiles.IsDefaultOrEmpty) + { + builder.Add(new AssemblyResourceInfo() + { + OwnerPackage = srcPackage, + InternalName = searchPathways.SubFolder, + SupportedPlatforms = searchPathways.Platforms, + SupportedTargets = searchPathways.Targets, + LoadPriority = 0, + FilePaths = sharedFiles, + FriendlyName = IAssemblyLoaderService.InternalsAwareAssemblyName, + IncompatiblePackages = ImmutableArray.Empty, + RequiredPackages = ImmutableArray.Empty, + UseInternalAccessName = false, + IsScript = true, + IsReferenceModeOnly = false + }); + } + } + + return builder.ToImmutable(); + } + + ImmutableArray GetConfigsLegacy(ContentPackage src) + { + return ImmutableArray.Empty; + } + + ImmutableArray GetLuaScriptsLegacy(ContentPackage src) + { + var builder = ImmutableArray.CreateBuilder(); + + if (_storageService.FindFilesInPackage(src, "Lua", "*.lua", true) + is { IsSuccess: true, Value.IsDefaultOrEmpty: false } result) + { + ImmutableArray cleanedResult = result.Value.Select(fp => fp.CleanUpPathCrossPlatform()).ToImmutableArray(); + + ImmutableArray autorun = cleanedResult + .Where(fp => fp.Contains("Lua/ForcedAutorun/") || fp.Contains("Lua/Autorun/")) + .ToImmutableArray(); + + ImmutableArray autorunFP = autorun.Select(fp => ContentPath.FromRaw(src, + $"%ModDir%/{Path.GetRelativePath(src.Dir, fp)}".CleanUpPathCrossPlatform())) + .ToImmutableArray(); + + ImmutableArray reg = cleanedResult.Except(autorun) + .Select(fp => ContentPath.FromRaw(src, + $"%ModDir%/{Path.GetRelativePath(src.Dir, fp)}".CleanUpPathCrossPlatform())) + .ToImmutableArray(); + + builder.Add(new LuaScriptsResourceInfo() + { + OwnerPackage = src, + InternalName = "LegacyAutorun", + SupportedPlatforms = Platform.Any, + SupportedTargets = Target.Any, + LoadPriority = 1, // autorun should be last to ensure that dependent code in other files are loaded first + FilePaths = autorunFP, + IncompatiblePackages = ImmutableArray.Empty, + RequiredPackages = ImmutableArray.Empty, + IsAutorun = true, + RunUnrestricted = false + }); + + builder.Add(new LuaScriptsResourceInfo() + { + OwnerPackage = src, + InternalName = "Legacy", + SupportedPlatforms = Platform.Any, + SupportedTargets = Target.Any, + LoadPriority = 0, // should be included first to ensure that dependent code in these files are available + FilePaths = reg, + IncompatiblePackages = ImmutableArray.Empty, + RequiredPackages = ImmutableArray.Empty, + IsAutorun = false, + RunUnrestricted = false + }); + } + + return builder.ToImmutable(); + } + + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/NetworkingService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/NetworkingService.cs new file mode 100644 index 000000000..42511ed01 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/NetworkingService.cs @@ -0,0 +1,421 @@ +using Barotrauma.LuaCs; +using Barotrauma.LuaCs.Compatibility; +using Barotrauma.LuaCs.Events; +using Barotrauma.Networking; +using FluentResults; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Security.Cryptography; +using System.Text; +using Barotrauma.LuaCs.Data; + +namespace Barotrauma.LuaCs; + +internal partial class NetworkingService : INetworkingService, IEventSettingInstanceLifetime +{ + public readonly record struct NetId + { + private readonly string _value; + + public NetId(string netId) + { + _value = netId; + } + + public static void Write(IWriteMessage message, NetId netId) + { + message.WriteString(netId._value); + } + + public static NetId Read(IReadMessage message) + { + return new NetId(message.ReadString()); + } + } + + private enum ClientToServer + { + NetMessageInternalId, + NetMessageNetId, + RequestSingleNetId, + RequestSync, + } + + private enum ServerToClient + { + NetMessageInternalId, + NetMessageNetId, + ReceiveNetIds + } + + private ClientPacketHeader? clientHeader = null; + public ClientPacketHeader ClientHeader + { + get + { + if (clientHeader == null) + { + byte lastHeader = (byte)Enum.GetValues(typeof(ClientPacketHeader)).Cast().Last(); + clientHeader = (ClientPacketHeader)(lastHeader + 1); + } + + return (ClientPacketHeader)clientHeader; + } + } + + private ServerPacketHeader? serverHeader = null; + public ServerPacketHeader ServerHeader + { + get + { + if (serverHeader == null) + { + byte lastHeader = (byte)Enum.GetValues(typeof(ServerPacketHeader)).Cast().Last(); + serverHeader = (ServerPacketHeader)(lastHeader + 1); + } + + return (ServerPacketHeader)serverHeader; + } + } + + + private ConcurrentDictionary netVars = []; + + private ConcurrentDictionary netReceives = []; + private ConcurrentDictionary packetToId = []; + private ConcurrentDictionary idToPacket = []; + + public bool IsActive + { + get + { + return GameMain.NetworkMember != null; + } + } + + public bool IsSynchronized { get; private set; } + public bool IsDisposed { get; private set; } + + private readonly IEventService _eventService; + private readonly ILoggerService _loggerService; + private readonly INetworkIdProvider _networkIdProvider; + + public NetworkingService(IEventService eventService, INetworkIdProvider networkIdProvider, ILoggerService loggerService) + { + _eventService = eventService; + _networkIdProvider = networkIdProvider; + _loggerService = loggerService; + +#if SERVER + IsSynchronized = true; +#endif + SubscribeToEvents(); + } + + public void Receive(string netIdString, LuaCsAction callback) + { +#if SERVER + Receive(new NetId(netIdString), (IReadMessage message, Client client) => callback(message, client)); +#elif CLIENT + Receive(new NetId(netIdString), (IReadMessage message) => callback(message, null)); +#endif + } + + public void Receive(string netIdString, NetMessageReceived callback) => Receive(new NetId(netIdString), callback); + public void Receive(Guid netIdGuid, NetMessageReceived callback) => Receive(new NetId(netIdGuid.ToString()), callback); + public IWriteMessage Start(string netIdString) + { + if (netIdString == null) + { + // idk why but Lua calls this method with null instead of the Start method with no arguments + return new WriteOnlyMessage(); + } + + return Start(new NetId(netIdString)); + } + public IWriteMessage Start(Guid netIdGuid) => Start(new NetId(netIdGuid.ToString())); + public IWriteMessage Start() => new WriteOnlyMessage(); + + internal void Receive(NetId netId, NetMessageReceived callback) + { +#if SERVER + RegisterId(netId); +#elif CLIENT + RequestId(netId); +#endif + netReceives[netId] = callback; + } + + private void HandleNetMessage(IReadMessage netMessage, NetId netId, Client client = null) + { + if (netReceives.ContainsKey(netId)) + { + try + { +#if CLIENT + netReceives[netId](netMessage); +#elif SERVER + netReceives[netId](netMessage, client); +#endif + } + catch (Exception e) + { + _loggerService.LogResults(new ExceptionalError("Exception thrown inside NetMessageReceive({netId})", e)); + } + } + else + { + if (GameSettings.CurrentConfig.VerboseLogging) + { +#if SERVER + _loggerService.LogError($"Received NetMessage for unknown netid {netId} from {GameServer.ClientLogName(client)}."); +#else + _loggerService.LogError($"Received NetMessage for unknown netid {netId} from server."); +#endif + } + } + } + + private void HandleNetMessageString(IReadMessage netMessage, Client client = null) + { + NetId netId = NetId.Read(netMessage); + + HandleNetMessage(netMessage, netId, client); + } + + private void SubscribeToEvents() + { + _eventService.Subscribe(this); +#if CLIENT + _eventService.Subscribe(this); + _eventService.Subscribe(this); +#elif SERVER + _eventService.Subscribe(this); +#endif + } + + public Guid GetNetworkIdForInstance(INetworkSyncVar var) + { + return _networkIdProvider.GetNetworkIdForInstance(var); + } + + public void RegisterNetVar(INetworkSyncVar netVar) + { + netVar.SetNetworkOwner(this); + + NetId netId = new NetId(netVar.InstanceId.ToString()); + netVars[netVar] = netId; + +#if CLIENT + Receive(netId, (IReadMessage message) => + { + if (netVar.SyncType == NetSync.None) + { + _loggerService.LogWarning($"Received net var from server but {nameof(NetSync)} is {netVar.SyncType.ToString()}"); + return; + } + + netVar.ReadNetMessage(message); + }); +#elif SERVER + Receive(netId, (IReadMessage message, Client client) => + { + if (netVar.SyncType == NetSync.None || netVar.SyncType == NetSync.ServerAuthority) + { + _loggerService.LogWarning($"Received net var from {GameServer.ClientLogName(client)} but {nameof(NetSync)} is {netVar.SyncType.ToString()}"); + return; + } + + if (!client.HasPermission(netVar.WritePermissions)) + { + _loggerService.LogWarning($"Received net var from {GameServer.ClientLogName(client)} but the client lacks permissions to modify it"); + return; + } + + netVar.ReadNetMessage(message); + + // Sync back to all clients + if (netVar.SyncType != NetSync.ClientOneWay) + { + SendNetVar(netVar); + } + }); +#endif + } + + public void DeregisterNetVar(INetworkSyncVar netVar) + { + if (netVar is null) + { + return; + } + + netVar.SetNetworkOwner(null); + netVars.TryRemove(netVar, out _); + } + + public void SendNetVar(INetworkSyncVar netVar) => SendNetVar(netVar, null); + + public void SendNetVar(INetworkSyncVar netVar, NetworkConnection connection = null) + { + if (!netVars.TryGetValue(netVar, out NetId netId)) + { + throw new InvalidOperationException("Tried to send net var across network without registering first"); + } + + if (netVar.SyncType == NetSync.None) { return; } +#if CLIENT + if (netVar.SyncType == NetSync.ServerAuthority) { return; } +#elif SERVER + if (netVar.SyncType == NetSync.ClientOneWay) { return; } +#endif + + IWriteMessage message = Start(netId); + netVar.WriteNetMessage(message); +#if CLIENT + SendToServer(message); +#elif SERVER + SendToClient(message, connection); +#endif + } + + public FluentResults.Result Reset() + { + IsSynchronized = false; + netReceives = new ConcurrentDictionary(); + packetToId = new ConcurrentDictionary(); + idToPacket = new ConcurrentDictionary(); + netVars = new ConcurrentDictionary(); + + SubscribeToEvents(); + return FluentResults.Result.Ok(); + } + + public void Dispose() + { + IsDisposed = true; + } + + #region Compatiblity + + private static readonly HttpClient client = new HttpClient(); + + public async void HttpRequest(string url, LuaCsAction callback, string data = null, string method = "POST", string contentType = "application/json", Dictionary headers = null, string savePath = null) + { + try + { + HttpRequestMessage request = new HttpRequestMessage(new HttpMethod(method), url); + + if (headers != null) + { + foreach (var header in headers) + { + request.Headers.Add(header.Key, header.Value); + } + } + + if (data != null) + { + request.Content = new StringContent(data, Encoding.UTF8, contentType); + } + + HttpResponseMessage response = await client.SendAsync(request); + + if (savePath != null) + { + if (LuaCsFile.IsPathAllowedException(savePath)) + { + byte[] responseData = await response.Content.ReadAsByteArrayAsync(); + + using (var fileStream = new FileStream(savePath, FileMode.Create, FileAccess.Write)) + { + fileStream.Write(responseData, 0, responseData.Length); + } + } + } + + string responseBody = await response.Content.ReadAsStringAsync(); + + CrossThread.RequestExecutionOnMainThread(() => + { + callback(responseBody, (int)response.StatusCode, response.Headers); + }); + } + catch (HttpRequestException e) + { + CrossThread.RequestExecutionOnMainThread(() => { callback(e.Message, e.StatusCode, null); }); + } + catch (Exception e) + { + CrossThread.RequestExecutionOnMainThread(() => { callback(e.Message, null, null); }); + } + } + + public void HttpPost(string url, LuaCsAction callback, string data, string contentType = "application/json", Dictionary headers = null, string savePath = null) + { + HttpRequest(url, callback, data, "POST", contentType, headers, savePath); + } + + public void RequestPostHTTP(string url, LuaCsAction callback, string data, string contentType = "application/json", Dictionary headers = null, string savePath = null) + { + HttpRequest(url, callback, data, "POST", contentType, headers, savePath); + } + + public void HttpGet(string url, LuaCsAction callback, Dictionary headers = null, string savePath = null) + { + HttpRequest(url, callback, null, "GET", null, headers, savePath); + } + + public void RequestGetHTTP(string url, LuaCsAction callback, Dictionary headers = null, string savePath = null) + { + HttpRequest(url, callback, null, "GET", null, headers, savePath); + } + + public void CreateEntityEvent(INetSerializable entity, NetEntityEvent.IData extraData) + { + GameMain.NetworkMember.CreateEntityEvent(entity, extraData); + } + + public ushort LastClientListUpdateID + { + get { return GameMain.NetworkMember.LastClientListUpdateID; } + set { GameMain.NetworkMember.LastClientListUpdateID = value; } + } + +#if SERVER + public void ClientWriteLobby(Client client) => GameMain.Server.ClientWriteLobby(client); + + public void UpdateClientPermissions(Client client) + { + GameMain.Server.UpdateClientPermissions(client); + } + + public int FileSenderMaxPacketsPerUpdate + { + get { return FileSender.FileTransferOut.MaxPacketsPerUpdate; } + set { FileSender.FileTransferOut.MaxPacketsPerUpdate = value; } + } +#endif + + #endregion + + public void OnSettingInstanceCreated(T configInstance) where T : ISettingBase + { + if (configInstance is INetworkSyncVar syncVar) + { + RegisterNetVar(syncVar); + } + } + + public void OnSettingInstanceDisposed(T configInstance) where T : ISettingBase + { + if (configInstance is INetworkSyncVar syncVar) + { + DeregisterNetVar(syncVar); + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/PackageManagementService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/PackageManagementService.cs new file mode 100644 index 000000000..14b0d141f --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/PackageManagementService.cs @@ -0,0 +1,558 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; +using System.Xml; +using Barotrauma.Extensions; +using Barotrauma.LuaCs.Data; +using FluentResults; +using Microsoft.Toolkit.Diagnostics; + +namespace Barotrauma.LuaCs; + +public sealed class PackageManagementService : IPackageManagementService +{ + // svc + private ILoggerService _logger; + private IModConfigService _modConfigService; + private IConfigService _configService; + private ILuaScriptManagementService _luaScriptManagementService; + private IPluginManagementService _pluginManagementService; + private IConsoleCommandsService _commandsService; +#if CLIENT + private IUIStylesService _uiStylesService; +#endif + private IPackageManagementServiceConfig _runConfig; + // state + private readonly ConcurrentDictionary _loadedPackages = new(); + private readonly ConcurrentDictionary _runningPackages = new(); + private readonly ConcurrentDictionary _packageNameCache = new(); + // control + /// + /// Service Disposal Lock. + /// + private readonly AsyncReaderWriterLock _operationsLock = new(); + /// + /// Execution of packages lock. + ///
Read: Package loading/unloading (Multi-operation mode). + ///
Write: Package execution (exclusive mode). + ///
+ private readonly AsyncReaderWriterLock _executionLock = new(); + + public PackageManagementService(ILoggerService logger, + IModConfigService modConfigService, + ILuaScriptManagementService luaScriptManagementService, + IPluginManagementService pluginManagementService, + IConfigService configService, + IConsoleCommandsService commandsService, +#if CLIENT + IUIStylesService uiStylesService, +#endif + IPackageManagementServiceConfig runConfig) + { + _logger = logger; + _modConfigService = modConfigService; + _luaScriptManagementService = luaScriptManagementService; + _pluginManagementService = pluginManagementService; + _configService = configService; + _runConfig = runConfig; +#if CLIENT + _uiStylesService = uiStylesService; +#endif + _commandsService = commandsService; + commandsService.RegisterCommand("pms_getxmlname", + "Gets the XML encoded name for the given package, as used in localization.", + onExecute: args => + { + if (args.Length < 1) + { + _logger.LogError("Please specify the name of the package."); + return; + } + + if (ContentPackageManager.AllPackages.FirstOrDefault(p => p.Name == args[0]) is { } pkg) + { + _logger.Log($"Package Xml Name: '{XmlConvert.EncodeLocalName(pkg.Name)}'"); + return; + } + _logger.Log($"Could not find package with the name '{args[0]}'"); + }, + getValidArgs: () => + { + return new[] + { + this._loadedPackages.Keys.Select(p => p.Name).ToArray() + }; + }); + } + + public void Dispose() + { + using var lck = _operationsLock.AcquireWriterLock().ConfigureAwait(false).GetAwaiter().GetResult(); + if (!ModUtils.Threading.CheckIfClearAndSetBool(ref _isDisposed)) + return; + + _logger.LogMessage($"{nameof(PackageManagementService)} is disposing."); + _luaScriptManagementService.Dispose(); + _pluginManagementService.Dispose(); + _modConfigService.Dispose(); + _logger.Dispose(); +#if CLIENT + _uiStylesService.Dispose(); +#endif + + _logger = null; + _luaScriptManagementService = null; + _pluginManagementService = null; + _modConfigService = null; +#if CLIENT + _uiStylesService = null; +#endif + + + _loadedPackages.Clear(); + _runningPackages.Clear(); + } + + private int _isDisposed = 0; + public bool IsDisposed + { + get => ModUtils.Threading.GetBool(ref _isDisposed); + set => ModUtils.Threading.SetBool(ref _isDisposed, value); + } + + public FluentResults.Result Reset() + { + using var lck = _operationsLock.AcquireWriterLock().ConfigureAwait(false).GetAwaiter().GetResult(); + if (IsDisposed) + return FluentResults.Result.Fail($"{nameof(PackageManagementService)}failed to reset. Has already been disposed."); + + try + { + var operationResult = new FluentResults.Result(); + + operationResult.WithReasons(_luaScriptManagementService.Reset().Reasons); + operationResult.WithReasons(_pluginManagementService.Reset().Reasons); + operationResult.WithReasons(_configService.Reset().Reasons); +#if CLIENT + operationResult.WithReasons(_uiStylesService.Reset().Reasons); +#endif + _runningPackages.Clear(); + _loadedPackages.Clear(); + _packageNameCache.Clear(); + return operationResult; + } + catch (Exception e) + { + return FluentResults.Result.Fail(new ExceptionalError(e)); + } + } + + public bool TryGetLoadedPackageByName(string name, out ContentPackage package) + { + package = null; + if (name.IsNullOrWhiteSpace()) + { + return false; + } + + using var _ = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult(); + return _packageNameCache.TryGetValue(name, out package); + } + + public FluentResults.Result LoadPackageInfo(ContentPackage package) + { + Guard.IsNotNull(package, nameof(package)); + using var lck = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult(); + using var executeLock = _executionLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult(); + + IService.CheckDisposed(this); + if (_loadedPackages.TryGetValue(package, out var result)) + { + _logger.LogWarning($"{nameof(LoadPackageInfo)}: Tried to load already-loaded package {package.Name}."); + return FluentResults.Result.Ok(); + } + + var pkgCfgInfo = _modConfigService.CreateConfigAsync(package).ConfigureAwait(false).GetAwaiter().GetResult(); + if (pkgCfgInfo.IsFailed) + { + _logger.LogResults(pkgCfgInfo.ToResult()); + return pkgCfgInfo.ToResult(); + } + return UnsafeAddPackageInternal(package, pkgCfgInfo.Value); + } + + public FluentResults.Result LoadPackagesInfo(ImmutableArray packages) + { + if (packages.IsDefaultOrEmpty) + ThrowHelper.ThrowArgumentException($"{nameof(LoadPackagesInfo)}: packages list is empty."); + using var lck = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult(); + using var executeLock = _executionLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult(); + + IService.CheckDisposed(this); + var result = new FluentResults.Result(); + var packages2 = packages.OrderBy(pkg => pkg.Name == "LuaCsForBarotrauma" ? 0 : 1) // always run lua cs first. + .ThenBy(packages.IndexOf) + .ToImmutableArray(); + + var pkgConfigs = _modConfigService.CreateConfigsAsync([..packages2]).ConfigureAwait(false).GetAwaiter().GetResult(); + foreach (var pkgConfig in pkgConfigs) + { + result.WithReasons(pkgConfig.Config.Reasons); + if (pkgConfig.Config.IsSuccess) + { + result.WithReasons(UnsafeAddPackageInternal(pkgConfig.Source, pkgConfig.Config.Value).Reasons); + } + } + + return result; + } + + private FluentResults.Result UnsafeAddPackageInternal(ContentPackage package, IModConfigInfo config) + { + if (_loadedPackages.TryGetValue(package, out _)) + { + _logger.LogWarning($"Tried to load already-loaded package {package.Name}."); + return FluentResults.Result.Ok(); + } + + // We need to touch ContentPath.Fullpath once in a single-threaded context to make it thread-safe. + foreach (var info in config.Assemblies) + { + TouchMeFullPaths(info); + } + + foreach (var info in config.Configs) + { + TouchMeFullPaths(info); + } + + foreach (var info in config.LuaScripts) + { + TouchMeFullPaths(info); + } + + // We need to touch ContentPath.Fullpath once in a single-threaded context to make it thread-safe. + [MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.PreserveSig)] + void TouchMeFullPaths(IBaseResourceInfo info) + { + foreach (var contentPath in info.FilePaths) + { + var s = contentPath.FullPath; + } + } + + _loadedPackages[package] = config; + _packageNameCache[package.Name] = package; + try + { + var res = new FluentResults.Result(); + var tasks = ImmutableArray.CreateBuilder>>(); + + if (!config.Configs.IsDefaultOrEmpty) + { + tasks.Add(Task.Factory.StartNew(async Task () => + new FluentResults.Result() + .WithReasons((await _configService.LoadConfigsAsync(config.Configs)).Reasons) + .WithReasons((await _configService.LoadConfigsProfilesAsync(config.Configs)).Reasons))); + } + + if (!config.LuaScripts.IsDefaultOrEmpty) + { + tasks.Add(Task.Factory.StartNew(async () => + await _luaScriptManagementService.LoadScriptResourcesAsync(config.LuaScripts))); + } + + if (tasks.Count == 0) + { + return FluentResults.Result.Ok(); + } + +#if CLIENT + if (!config.Styles.IsDefaultOrEmpty) + { + res.WithReasons(_uiStylesService.LoadAssets(config.Styles).Reasons); + } +#endif + var r = Task.WhenAll(tasks.ToArray()).ConfigureAwait(false).GetAwaiter().GetResult(); + + foreach (var task in r) + { + res.WithReasons(task.ConfigureAwait(false).GetAwaiter().GetResult().Reasons); + } + return res; + } + catch (Exception e) + { + return FluentResults.Result.Fail(new ExceptionalError(e)); + } + } + + public FluentResults.Result ExecuteLoadedPackages(ImmutableArray executionOrder, bool executeCsAssemblies) + { + using var lck = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult(); + using var executeLock = _executionLock.AcquireWriterLock().ConfigureAwait(false).GetAwaiter().GetResult(); + IService.CheckDisposed(this); + + if (executionOrder.IsDefaultOrEmpty) + { + return FluentResults.Result.Fail($"{nameof(ExecuteLoadedPackages)}: No packages in the execution order list."); + } + + if (!_runningPackages.IsEmpty) + { + return FluentResults.Result.Fail( + $"{nameof(ExecuteLoadedPackages)}: There are already packages running! List: { + _runningPackages.Aggregate(string.Empty, (acc, kvp) => "-" + kvp + "\n" + kvp.Key.Name)}"); + } + + if (_loadedPackages.IsEmpty) + { + return FluentResults.Result.Fail($"{nameof(ExecuteLoadedPackages)}: No packages loaded. Nothing to run!)"); + } + + var result = new FluentResults.Result(); + + // get loading order. Note: packages not in the execution order list will load first. + var loadingOrderedPackages = _loadedPackages + .OrderBy(pkg => pkg.Key.Name == "LuaCsForBarotrauma" ? 0 : 1) // always run lua cs first. + .ThenBy(pkg => executionOrder.IndexOf(pkg.Key)) + .ToImmutableArray(); + var loadOrderByPackage = loadingOrderedPackages.Select(p => p.Key).ToImmutableArray(); + var toLoadPackagesIndents = loadingOrderedPackages + .SelectMany(p => p.Key.AltNames.Union(new []{ p.Key.Name }).ToIdentifiers()) + .ToImmutableHashSet(); + + + // NOTE: Config/Settings are instanced in LoadPackages() + + if (executeCsAssemblies) + { + var plugins = SelectCompatible(loadingOrderedPackages + .SelectMany(pkg => pkg.Value.Assemblies) + .ToImmutableArray(), toLoadPackagesIndents, loadOrderByPackage); + + if (!plugins.IsDefaultOrEmpty) + { + result.WithReasons(_pluginManagementService.LoadAssemblyResources(plugins).Reasons); + result.WithReasons(_pluginManagementService.ActivatePluginInstances( + plugins.Select(p => p.OwnerPackage).ToImmutableArray(), false).Reasons); + } + } + + //lua scripts + var luaScripts = SelectCompatible(loadingOrderedPackages + .Where(pkg => executeCsAssemblies + || !pkg.Value.LuaScripts.Any(scr => scr.RunUnrestricted)) + .SelectMany(pkg => pkg.Value.LuaScripts) + .ToImmutableArray(), toLoadPackagesIndents, loadOrderByPackage); + + if (!luaScripts.IsDefaultOrEmpty) + { + result.WithReasons(_luaScriptManagementService.ExecuteLoadedScripts(luaScripts, enableSandbox: !executeCsAssemblies).Reasons); + } + + foreach (var package in loadingOrderedPackages) + { + _runningPackages[package.Key] = package.Value; + } + + return result; + } + + private static ImmutableArray SelectCompatible(ImmutableArray resources, + ImmutableHashSet enabledPackagesIdents, + ImmutableArray loadingOrder) + where T : IBaseResourceInfo + { + return resources + .Where(r => r.SupportedPlatforms.HasFlag(ModUtils.Environment.CurrentPlatform)) + .Where(r => r.SupportedTargets.HasFlag(ModUtils.Environment.CurrentTarget)) + .Where(r => !r.Optional || ( + (r.RequiredPackages.IsDefaultOrEmpty || enabledPackagesIdents.Intersect(r.RequiredPackages).Any()) + && (r.IncompatiblePackages.IsDefaultOrEmpty || enabledPackagesIdents.Intersect(r.IncompatiblePackages).None()))) + .OrderBy(r => r.Optional ? 1 : 0) // optional content last + .ThenBy(r => loadingOrder.IndexOf(r.OwnerPackage)) + .ThenBy(r => r.LoadPriority) + .ToImmutableArray(); + } + + + public FluentResults.Result SyncLoadedPackagesList(ImmutableArray packages) + { + if (packages.IsDefaultOrEmpty) + ThrowHelper.ThrowArgumentNullException(nameof(packages)); + if (!_runningPackages.IsEmpty) + ThrowHelper.ThrowInvalidOperationException($"{nameof(SyncLoadedPackagesList)}: There are packages running!"); + + var toRemove = _loadedPackages.Keys.Except(packages).ToImmutableArray(); + var toAdd = packages.Except(_loadedPackages.Keys) + .OrderBy(pack => packages.IndexOf(pack)).ToImmutableArray(); + + var result = new FluentResults.Result(); + + if (!toRemove.IsDefaultOrEmpty) + { + result.WithReasons(UnloadPackages(toRemove).Reasons); + } + + if (!toAdd.IsDefaultOrEmpty) + { + result.WithReasons(LoadPackagesInfo(toAdd).Reasons); + } + + return result; + } + + public FluentResults.Result StopRunningPackages() + { + using var lck = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult(); + using var executeLock = _executionLock.AcquireWriterLock().ConfigureAwait(false).GetAwaiter().GetResult(); + IService.CheckDisposed(this); + + if (_loadedPackages.IsEmpty || _runningPackages.IsEmpty) + { + _logger.LogWarning($"{nameof(StopRunningPackages)}: No packages are currently executing."); + return FluentResults.Result.Ok(); + } + + var res = new FluentResults.Result(); + res.WithReasons(_luaScriptManagementService.UnloadActiveScripts().Reasons); + res.WithReasons(_pluginManagementService.UnloadManagedAssemblies().Reasons); + _runningPackages.Clear(); + return res; + } + + public FluentResults.Result UnloadPackage(ContentPackage package) + { + Guard.IsNotNull(package, nameof(package)); + using var lck = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult(); + using var executeLock = _executionLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult(); + IService.CheckDisposed(this); + + if (!_loadedPackages.ContainsKey(package)) + { + return FluentResults.Result.Fail($"{nameof(UnloadPackage)}: The package is not loaded."); + } + if (!_runningPackages.IsEmpty) + { + return FluentResults.Result.Fail($"{nameof(UnloadPackage)}: Packages are currently executing."); + } + var result = new FluentResults.Result(); + result.WithReasons(_luaScriptManagementService.DisposePackageResources(package).Reasons); + result.WithReasons(_configService.DisposePackageData(package).Reasons); +#if CLIENT + result.WithReasons(_uiStylesService.UnloadPackage(package).Reasons); +#endif + _loadedPackages.TryRemove(package, out _); + _packageNameCache.TryRemove(package.Name, out _); + return result; + } + + public FluentResults.Result UnloadPackages(ImmutableArray packages) + { + if (packages.IsDefaultOrEmpty) + return FluentResults.Result.Fail($"{nameof(UnloadPackages)}: Package list is empty."); + + using var lck = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult(); + using var executeLock = _executionLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult(); + IService.CheckDisposed(this); + + var result = new FluentResults.Result(); + foreach (var package in packages) + { + result.WithReasons(UnloadPackage(package).Reasons); + } + return result; + } + + public FluentResults.Result UnloadAllPackages() + { + using var lck = _operationsLock.AcquireWriterLock().ConfigureAwait(false).GetAwaiter().GetResult(); + using var executeLock = _executionLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult(); + IService.CheckDisposed(this); + + if (_loadedPackages.IsEmpty) + return FluentResults.Result.Ok(); + if (!_runningPackages.IsEmpty) + return FluentResults.Result.Fail($"{nameof(UnloadAllPackages)}: Packages are currently executing."); + var result = new FluentResults.Result(); + result.WithReasons(_luaScriptManagementService.DisposeAllPackageResources().Reasons); + result.WithReasons(_configService.DisposeAllPackageData().Reasons); + _loadedPackages.Clear(); + return result; + } + + public ImmutableArray GetAllLoadedPackages() + { + using var lck = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult(); + IService.CheckDisposed(this); + return [.._loadedPackages.Keys]; + } + + public bool IsPackageRunning(ContentPackage package) + { + Guard.IsNotNull(package, nameof(package)); + using var lck = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult(); + IService.CheckDisposed(this); + return _runningPackages.ContainsKey(package); + } + + public bool IsAnyPackageLoaded() + { + using var lck = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult(); + IService.CheckDisposed(this); + return !_loadedPackages.IsEmpty; + } + + public bool IsAnyPackageRunning() + { + using var lck = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult(); + IService.CheckDisposed(this); + return !_runningPackages.IsEmpty; + } + + public ImmutableArray GetLoadedUnrestrictedPackages() + { + using var lck = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult(); + IService.CheckDisposed(this); + if (_loadedPackages.IsEmpty) + return ImmutableArray.Empty; + return [.._loadedPackages.Values + .Where(cfg => !cfg.Assemblies.IsDefaultOrEmpty || cfg.LuaScripts.Any(scr => scr.RunUnrestricted)) + .Select(cfg => cfg.Package)]; + } + + public bool PackageContainsAnyRunnableResource(ContentPackage package) + { + using var lck = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult(); + IService.CheckDisposed(this); + + var result = GetModConfigForPackage(package); + + if (result.IsSuccess) + { + return result.Value.Assemblies.Any() || result.Value.LuaScripts.Any(); + } + else + { + return false; + } + } + + public Result GetModConfigForPackage(ContentPackage package) + { + using var lck = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult(); + IService.CheckDisposed(this); + + if (!_loadedPackages.TryGetValue(package, out var modConfig)) + { + return FluentResults.Result.Fail($"Failed to find mod config for package {package.Name}"); + } + + return new FluentResults.Result().WithValue(modConfig); + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/PluginManagementService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/PluginManagementService.cs new file mode 100644 index 000000000..30210fc6f --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/PluginManagementService.cs @@ -0,0 +1,962 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Loader; +using System.Text; +using System.Threading; +using System.Xml.Serialization; +using Barotrauma.Extensions; +using Barotrauma.IO; +using Barotrauma.LuaCs.Data; +using Barotrauma.LuaCs.Events; +using FluentResults; +using FluentResults.LuaCs; +using LightInject; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Text; +using Microsoft.Extensions.Logging; +using Microsoft.Toolkit.Diagnostics; +using OneOf; + +namespace Barotrauma.LuaCs; + +public class PluginManagementService : IAssemblyManagementService +{ + #region CSHARP_COMPILATION_OPTIONS + + private static readonly CSharpParseOptions ScriptParseOptions = CSharpParseOptions.Default + .WithPreprocessorSymbols(new[] + { +#if SERVER + "SERVER" +#elif CLIENT + "CLIENT" +#else + "UNDEFINED" +#endif +#if DEBUG + ,"DEBUG" +#endif + }); + +#if WINDOWS + private const string PLATFORM_TARGET = "Windows"; +#elif OSX + private const string PLATFORM_TARGET = "OSX"; +#elif LINUX + private const string PLATFORM_TARGET = "Linux"; +#endif + +#if CLIENT + private const string ARCHITECTURE_TARGET = "Client"; +#elif SERVER + private const string ARCHITECTURE_TARGET = "Server"; +#endif + + private static readonly CSharpCompilationOptions CompilationOptions = new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary) + .WithMetadataImportOptions(MetadataImportOptions.All) +#if DEBUG + .WithOptimizationLevel(OptimizationLevel.Debug) +#else + .WithOptimizationLevel(OptimizationLevel.Release) +#endif + .WithAllowUnsafe(true); + + private static readonly SyntaxTree BaseAssemblyImports = CSharpSyntaxTree.ParseText( + new StringBuilder() + .AppendLine("global using LuaCsHook = Barotrauma.LuaCs.Compatibility.ILuaCsHook;") + .AppendLine("global using System.Reflection;") + .AppendLine("global using Barotrauma;") + .AppendLine("global using Barotrauma.LuaCs;") + .AppendLine("global using Barotrauma.LuaCs.Compatibility;") + .AppendLine("using System.Runtime.CompilerServices;") + .AppendLine("[assembly: IgnoresAccessChecksTo(\"BarotraumaCore\")]") +#if CLIENT + .AppendLine("[assembly: IgnoresAccessChecksTo(\"Barotrauma\")]") +#elif SERVER + .AppendLine("[assembly: IgnoresAccessChecksTo(\"DedicatedServer\")]") +#endif + .ToString(), + ScriptParseOptions); + + private ImmutableArray _baseMetadataReferences = ImmutableArray.Empty; + private ImmutableArray _baseMetadataReferencesNonPublicized = ImmutableArray.Empty; + + + private IEnumerable BaseMetadataReferences + { + get + { + if (_baseMetadataReferences.IsDefaultOrEmpty) + { + _baseMetadataReferences = Basic.Reference.Assemblies.Net80.References.All + .Union(AssemblyLoadContext.Default.Assemblies + .Where(ass => + !ass.IsDynamic && + !ass.GetName().FullName.StartsWith("BarotraumaCore") && + !ass.GetName().FullName.StartsWith("Barotrauma") && + !ass.GetName().FullName.StartsWith("DedicatedServer")) + .Where(ass => !ass.Location.IsNullOrWhiteSpace()) + .Select(MetadataReference (ass) => MetadataReference.CreateFromFile(ass.Location))) + .Where(ar => ar is not null) + .ToImmutableArray(); + } + + return _baseMetadataReferences; + } + } + + private IEnumerable BaseMetadataReferencesWithBarotrauma + { + get + { + if (_baseMetadataReferencesNonPublicized.IsDefaultOrEmpty) + { + _baseMetadataReferencesNonPublicized = Basic.Reference.Assemblies.Net80.References.All + .Union(AssemblyLoadContext.Default.Assemblies + .Where(ass => !ass.IsDynamic) + .Where(ass => !ass.Location.IsNullOrWhiteSpace()) + .Select(MetadataReference (ass) => MetadataReference.CreateFromFile(ass.Location))) + .Where(ar => ar is not null) + .ToImmutableArray(); + } + + return _baseMetadataReferencesNonPublicized; + } + } + + #endregion + + #region Disposal + + public void Dispose() + { + using var lck = _operationsLock.AcquireWriterLock().ConfigureAwait(false).GetAwaiter().GetResult(); + if (!ModUtils.Threading.CheckIfClearAndSetBool(ref _isDisposed)) + { + return; + } + + UnsafeDisposeResourcesInternal(); + _assemblyLoaderFactory = null; + _storageService = null; + _eventService = null; + _logger = null; + _configService = null; + _luaScriptManagementService = null; + _luaCsInfoProvider = null; + + GC.SuppressFinalize(this); + } + + private void UnsafeDisposeResourcesInternal() + { + foreach (var packPlugin in _pluginInstances.SelectMany(kvp => kvp.Value.Select(pluginInst => (kvp.Key, pluginInst)))) + { + try + { + packPlugin.pluginInst.Dispose(); + } + catch (Exception e) + { + _logger.LogError($"Error while disposing plugin for ContentPackage {packPlugin.Key.Name}: \n{e.Message}"); + } + } + _pluginInstances.Clear(); + _pluginPackageLookup.Clear(); + _pluginInjectorContainer?.Dispose(); + _pluginInjectorContainer = null; + + foreach (var loader in _assemblyLoaders) + { + try + { + loader.Value.Dispose(); + _unloadingAssemblyLoaders.Add(loader.Value, loader.Key); + } + catch (Exception e) + { + _logger?.LogError($"Failed to dispose of {nameof(IAssemblyLoaderService)} for ContentPackage {loader.Key.Name}: \n{e.Message}"); + if (loader.Value.Assemblies.Any()) + { + foreach (var ass in loader.Value.Assemblies) + { + _logger?.LogWarning($"{nameof(PluginManagementService)}: Fallback manual unsubscription of assemblies: {ass.GetName()}"); + ReflectionUtils.RemoveAssemblyFromCache(ass); + } + } + } + } + _assemblyLoaders.Clear(); + } + + private int _isDisposed = 0; + public bool IsDisposed + { + get => ModUtils.Threading.GetBool(ref _isDisposed); + private set => ModUtils.Threading.SetBool(ref _isDisposed, value); + } + public FluentResults.Result Reset() + { + using var lck = _operationsLock.AcquireWriterLock().ConfigureAwait(false).GetAwaiter().GetResult(); + IService.CheckDisposed(this); + UnsafeDisposeResourcesInternal(); + return FluentResults.Result.Ok(); + } + + #endregion + + private IAssemblyLoaderService.IFactory _assemblyLoaderFactory; + private IStorageService _storageService; + private ILoggerService _logger; + private Lazy _eventService; + private Lazy _configService; + private Lazy _luaScriptManagementService; + private IEventService _pluginEventService; + private Lazy _pluginLuaPatcherService; + private Func _consoleCommandServiceFactory; + private ILuaCsInfoProvider _luaCsInfoProvider; + private readonly ConcurrentDictionary _assemblyLoaders = new(); + private readonly ConcurrentDictionary _pluginPackageLookup = new(); + private readonly ConcurrentDictionary> _pluginInstances = new(); + private readonly ConditionalWeakTable _unloadingAssemblyLoaders = new(); + private readonly ConcurrentBag _loadedNativeLibraries = new(); + private readonly AsyncReaderWriterLock _operationsLock = new(); + private ServiceContainer _pluginInjectorContainer; + + public PluginManagementService( + IAssemblyLoaderService.IFactory assemblyLoaderFactory, + IStorageService storageService, + ILoggerService logger, + Lazy eventService, + Lazy luaScriptManagementService, + Lazy configService, + Lazy pluginLuaPatcherService, + Func consoleCommandServiceFactory, + ILuaCsInfoProvider luaCsInfoProvider) + { + _assemblyLoaderFactory = assemblyLoaderFactory; + _storageService = storageService; + _logger = logger; + _eventService = eventService; + _luaScriptManagementService = luaScriptManagementService; + _configService = configService; + _pluginLuaPatcherService = pluginLuaPatcherService; + _consoleCommandServiceFactory = consoleCommandServiceFactory; + _luaCsInfoProvider = luaCsInfoProvider; + } + + private ServiceContainer CreatePluginServiceContainer() + { + var container = new ServiceContainer(new ContainerOptions() + { + EnablePropertyInjection = true + }); + + _pluginEventService ??= new EventService(_logger, _pluginLuaPatcherService.Value); + _eventService.Value.AddDispatcherEventService(_pluginEventService); + + container.Register(fac => _logger); + container.Register(fac => _storageService); + container.Register(fac => _pluginEventService); + container.Register(fac => this); + container.Register(fac => _luaScriptManagementService.Value); + container.Register(fac => _configService.Value); + container.Register(fac => _consoleCommandServiceFactory?.Invoke()); + + return container; + } + + public Result> GetImplementingTypes(bool includeInterfaces = false, bool includeAbstractTypes = false, + bool includeDefaultContext = true) + { + if (includeInterfaces) + { + includeAbstractTypes = true; + } + + using var lck = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult(); + IService.CheckDisposed(this); + + var builder = ImmutableArray.CreateBuilder(); + + if (includeDefaultContext) + { + foreach (var ass in AssemblyLoadContext.Default.Assemblies) + { + AddTypesFromAssembly(ass); + } + } + + foreach (var ass in _assemblyLoaders.Values.Where(al => !al.IsReferenceOnlyMode).SelectMany(al => al.Assemblies)) + { + AddTypesFromAssembly(ass); + } + + return builder.ToImmutable(); + + + void AddTypesFromAssembly(Assembly assembly) + { + foreach (var type in assembly.GetSafeTypes()) + { + if ((includeInterfaces || !type.IsInterface) + && (includeAbstractTypes || !type.IsAbstract) + && type.IsAssignableTo(typeof(T))) + { + builder.Add(type); + } + } + } + } + + public bool TryGetPackageForPlugin(out ContentPackage ownerPackage) + { + return _pluginPackageLookup.TryGetValue(typeof(TPlugin), out ownerPackage); + } + + public Type GetType(string typeName, bool isByRefType = false, bool includeInterfaces = false, + bool includeDefaultContext = true) + { + if (typeName.StartsWith("out ") || typeName.StartsWith("ref ")) + { + typeName = typeName.Remove(0, 4); + isByRefType = true; + } + + if (includeDefaultContext) + { + var type = Type.GetType(typeName, false, false); + if (type is not null && (includeInterfaces || !type.IsInterface)) + { + if (isByRefType) + { + return type.MakeByRefType(); + } + + return type; + } + + foreach (var ass in AssemblyLoadContext.Default.Assemblies) + { + if (ass.GetType(typeName, false, false) is not {} type2 || (!includeInterfaces && type2.IsInterface)) + { + continue; + } + + return isByRefType ? type2.MakeByRefType() : type2; + } + } + + foreach (var ass in AssemblyLoadContext.All + .Where(alc => alc != AssemblyLoadContext.Default) + .SelectMany(alc => alc.Assemblies)) + { + if (ass.GetType(typeName, false, false) is not {} type || (!includeInterfaces && type.IsInterface)) + { + continue; + } + + return isByRefType ? type.MakeByRefType() : type; + } + + return null; + } + + public FluentResults.Result ActivatePluginInstances(ImmutableArray executionOrder, bool excludeAlreadyRunningPackages = true) + { + if (executionOrder.IsDefaultOrEmpty) + { + ThrowHelper.ThrowArgumentNullException($"{nameof(ActivatePluginInstances)}: The ececution list provided is empty."); + } + using var lck = _operationsLock.AcquireWriterLock().ConfigureAwait(false).GetAwaiter().GetResult(); + IService.CheckDisposed(this); + + if (_assemblyLoaders.IsEmpty) + { + return FluentResults.Result.Ok(); + } + + var results = new FluentResults.Result(); + + var toLoad = _assemblyLoaders + .Where(al => executionOrder.Contains(al.Key)) + .Where(al => !excludeAlreadyRunningPackages || !_pluginInstances.ContainsKey(al.Key)) + .SelectMany(al => al.Value.Assemblies.Select(ass => (al.Key, ass))) + .SelectMany<(ContentPackage Key, Assembly ass), (ContentPackage Key, Type type)>(kvp => + { + try + { + return kvp.ass.GetTypes() + .Where(type => + type is { IsInterface: false, IsAbstract: false, IsGenericType: false } + && type.IsAssignableTo(typeof(IAssemblyPlugin))) + .Select(type => (kvp.Key, type)); + } + catch (ReflectionTypeLoadException re) + { + results.WithError(new Error($"Failed to get types from Package '{kvp.Key.Name}'")); + results.WithError(new ExceptionalError(re)); + } + catch (Exception e) + { + results.WithError(new Error($"Failed to get types from Package '{kvp.Key.Name}'")); + results.WithError(new ExceptionalError(e)); + } + return new List<(ContentPackage Key, Type type)>(); + }) + .GroupBy(kvp => kvp.Key, kvp => kvp.type) + .OrderBy(exeGrp => executionOrder.IndexOf(exeGrp.Key)) + .ToImmutableArray(); + + if (toLoad.Length == 0) + { + return results; + } + + _logger.LogMessage($"Activating {nameof(IAssemblyPlugin)} instances"); + + var loadedPackagePlugins = + ImmutableArray.CreateBuilder<(ContentPackage Package, ImmutableArray Plugins)>(); + _pluginInjectorContainer ??= CreatePluginServiceContainer(); + + foreach (var packageTypes in toLoad) + { + var loadedTypes = ImmutableArray.CreateBuilder(); + foreach (var pluginType in packageTypes) + { + try + { + _logger.LogMessage($"- Instantiating {pluginType.Name}"); + var plugin = (IAssemblyPlugin)Activator.CreateInstance(pluginType); + _pluginInjectorContainer.InjectProperties(plugin); + _pluginInjectorContainer.Register(pluginType, fac => plugin); + loadedTypes.Add(plugin); + _pluginPackageLookup.TryAdd(pluginType, packageTypes.Key); + } + catch (Exception e) + { + results.WithError(new ExceptionalError($"Failed to instantiate mod: {packageTypes.Key.Name}", e)); + continue; + } + } + loadedPackagePlugins.Add((packageTypes.Key, loadedTypes.ToImmutable())); + } + + var packPluginGroups = loadedPackagePlugins.ToImmutable(); + foreach (var packagePluginGrp in packPluginGroups) + { + if (_pluginInstances.TryGetValue(packagePluginGrp.Package, out var plugins)) + { + _pluginInstances[packagePluginGrp.Package] = plugins.Concat(packagePluginGrp.Plugins).ToImmutableArray(); + continue; + } + + _pluginInstances[packagePluginGrp.Package] = packagePluginGrp.Plugins; + } + + var pluginsToInit = packPluginGroups.SelectMany(ppg => ppg.Plugins).ToImmutableArray(); + + foreach (var plugin in pluginsToInit) + { + results.WithReasons(PluginInitRunner(plugin, p => p.PreInitPatching()).Reasons); + } + + _eventService.Value.PublishEvent(sub => sub.PreInitPatching()); + + foreach (var plugin in pluginsToInit) + { + results.WithReasons(PluginInitRunner(plugin, p => p.Initialize()).Reasons); + } + + _eventService.Value.PublishEvent(sub => sub.Initialize()); + + foreach (var plugin in pluginsToInit) + { + results.WithReasons(PluginInitRunner(plugin, p => p.OnLoadCompleted()).Reasons); + } + + _eventService.Value.PublishEvent(sub => sub.OnLoadCompleted()); + + return results; + + // helper + FluentResults.Result PluginInitRunner(IAssemblyPlugin plugin, Action action) + { + try + { + action(plugin); + return FluentResults.Result.Ok(); + } + catch (Exception e) + { + return FluentResults.Result.Fail(new ExceptionalError(e)); + } + } + } + + + public FluentResults.Result LoadAssemblyResources(ImmutableArray resources) + { + if (resources.IsDefaultOrEmpty) + { + ThrowHelper.ThrowArgumentNullException($"{nameof(LoadAssemblyResources)} The resource list is empty.)"); + } + using var lck = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult(); + IService.CheckDisposed(this); + + _storageService.UseCaching = _luaCsInfoProvider.UseCaching; + if (!_luaCsInfoProvider.UseCaching) + { + _storageService.PurgeCache(); + } + + var orderedContentPacks = resources.GroupBy(res => res.OwnerPackage) + .OrderBy(res => resources.FindIndex(r2 => r2.OwnerPackage == res.Key)) + .ToImmutableArray(); + + var result = new FluentResults.Result(); + + foreach (var contentPack in orderedContentPacks) + { + LoadBinaries(contentPack); + LoadAndCompileScriptAssemblies(contentPack); + foreach (var ass in _assemblyLoaders[contentPack.Key].Assemblies) + { + ReflectionUtils.AddNonAbstractAssemblyTypes(ass); + } + } + + return result; + + // --- helper methods + void LoadBinaries(IGrouping contentPackRes) + { + var binaries = contentPackRes.Where(cRes => !cRes.IsScript) + .OrderBy(bin => bin.LoadPriority) + .SelectMany(bin => bin.FilePaths) + .ToImmutableArray(); + + if (binaries.IsDefaultOrEmpty) + { + return; + } + + var assemblyLoader = _assemblyLoaders.GetOrAdd(contentPackRes.Key, (cp) => _assemblyLoaderFactory.CreateInstance( + new IAssemblyLoaderService.LoaderInitData( + InstanceId: Guid.NewGuid(), + contentPackRes.Key.Name, + IsReferenceMode: contentPackRes.Any(r => r.IsReferenceModeOnly), + OwnerPackage: contentPackRes.Key, + OnUnload: OnAssemblyLoaderUnloading, + OnResolvingManaged: OnAssemblyLoaderResolvingManaged, + OnResolvingUnmanagedDll: OnAssemblyLoaderResolvingUnmanaged + ))); + + var dependencyPaths = binaries + .Select(bin => System.IO.Path.GetDirectoryName(bin.FullPath)) + .Distinct() + .ToImmutableArray(); + + foreach (var binResource in binaries) + { + var res = assemblyLoader.LoadAssemblyFromFile(binResource.FullPath, dependencyPaths); + result.WithReasons(res.Reasons); + _logger.LogResults(res.ToResult()); + } + } + + void LoadAndCompileScriptAssemblies(IGrouping contentPackRes) + { + var scriptsGrp = contentPackRes.Where(cRes => cRes.IsScript) + .Select(scr => (scr.OwnerPackage, scr.FriendlyName, scr.FilePaths, scr.UseInternalAccessName, scr.LoadPriority)) + .OrderBy(scr => scr.LoadPriority) + .GroupBy(scr => scr.FriendlyName) + .ToImmutableArray(); + + if (scriptsGrp.IsDefaultOrEmpty) + { + return; + } + + var metadataReferences = GetMetadataReferences(false).ToImmutableArray(); + var metadataReferencesNonPublicized = GetMetadataReferences(true).ToImmutableArray(); + + var assemblyLoader = _assemblyLoaders.GetOrAdd(contentPackRes.Key, (cp) => _assemblyLoaderFactory.CreateInstance( + new IAssemblyLoaderService.LoaderInitData( + InstanceId: Guid.NewGuid(), + contentPackRes.Key.Name, + IsReferenceMode: contentPackRes.Any(r => r.IsReferenceModeOnly), + OwnerPackage: contentPackRes.Key, + OnUnload: OnAssemblyLoaderUnloading, + OnResolvingManaged: OnAssemblyLoaderResolvingManaged, + OnResolvingUnmanagedDll: OnAssemblyLoaderResolvingUnmanaged + ))); + + // create syntax trees + + foreach (var scripts in scriptsGrp) + { + var syntaxTreesBuilder = ImmutableArray.CreateBuilder(); + + bool hasInternalsAwareBeenAdded = false; + bool compileWithInternalName = true; + + foreach (var resourceInfo in scripts) + { + // this should be the same for the entire collection of src files so we just grab it from the collection + compileWithInternalName = resourceInfo.UseInternalAccessName; + + if (!hasInternalsAwareBeenAdded) + { + hasInternalsAwareBeenAdded = true; + syntaxTreesBuilder.Add(BaseAssemblyImports); + } + + if (resourceInfo.FilePaths.IsDefaultOrEmpty) + { + ThrowHelper.ThrowArgumentNullException($"{nameof(LoadAndCompileScriptAssemblies)} The resource list is empty for package {resourceInfo.OwnerPackage}."); + } + + foreach (var resourcePath in resourceInfo.FilePaths) + { + var loadRes = GetSourceFilesText(resourcePath); + if (loadRes.IsFailed) + { + _logger.LogResults(loadRes.ToResult()); + continue; + } + + CancellationToken token = CancellationToken.None; + + string sourceCode = loadRes.Value; + sourceCode = DoSourceCodeTextCompatibilityPass(sourceCode); + + syntaxTreesBuilder.Add(SyntaxFactory.ParseSyntaxTree( + text: sourceCode, + options: ScriptParseOptions, + path: resourcePath.FullPath, + encoding: Encoding.Default, + cancellationToken: token + )); + } + } + + if (syntaxTreesBuilder.Count < 1) + { + continue; + } + + _logger.LogMessage($"Compiling assembly for {scripts.Key}, in ContentPackage {contentPackRes.Key.Name}"); + + var res = assemblyLoader.CompileScriptAssembly( + assemblyName: scripts.Key, + compileWithInternalAccess: compileWithInternalName, + syntaxTrees: syntaxTreesBuilder.ToImmutable(), + metadataReferences: compileWithInternalName ? metadataReferencesNonPublicized : metadataReferences, + compilationOptions: CompilationOptions); + + // try with internal access instead for legacy mods + if (!compileWithInternalName && res.IsFailed) + { + _logger.LogMessage($"Attempted compilation of {scripts.Key} for package {contentPackRes.Key.Name}. Trying fallback method."); + var res2 = assemblyLoader.CompileScriptAssembly( + assemblyName: scripts.Key, + compileWithInternalAccess: true, + syntaxTrees: syntaxTreesBuilder.ToImmutable(), + metadataReferences: metadataReferencesNonPublicized, + compilationOptions: CompilationOptions); + + // overwrite result with good compilation + if (res2.IsSuccess) + { + var reasonsStr = res.Reasons.Aggregate("", (accum, reason) => accum + "\n" + reason.Message); + _logger.LogWarning($"Attempted compilation of {scripts.Key} for package {contentPackRes.Key.Name} succeeded. Original errors were: \n {reasonsStr}"); + res = res2; + } + } + + result.WithReasons(res.Reasons); + } + } + + Result GetSourceFilesText(ContentPath resourceInfoFilePath) + { + if (_storageService.LoadPackageText(resourceInfoFilePath) is not { IsFailed: false } res) + { + _logger.LogError($"{nameof(GetSourceFilesText)}: Failed to load source file for ContentPackage {resourceInfoFilePath.ContentPackage?.Name}."); + return FluentResults.Result.Fail($"{nameof(GetSourceFilesText)}: Failed to load source files for ContentPackage {resourceInfoFilePath.ContentPackage?.Name}."); + } + + return res; + } + + IEnumerable GetMetadataReferences(bool useNonPublicizedAssemblies) + { + var builder = ImmutableArray.CreateBuilder(); + if (useNonPublicizedAssemblies) + { + builder.AddRange(BaseMetadataReferencesWithBarotrauma); + foreach (var loaderService in _assemblyLoaders + .Where(asl => !asl.Key.Name.Equals("LuaCsForBarotrauma", StringComparison.InvariantCultureIgnoreCase)) + .ToImmutableArray()) + { + builder.AddRange(loaderService.Value.AssemblyReferences.Where(ar => ar is not null)); + } + } + else + { + builder.AddRange(BaseMetadataReferences); + foreach (var loaderService in _assemblyLoaders) + { + builder.AddRange(loaderService.Value.AssemblyReferences.Where(ar => ar is not null)); + } + } + + return builder.ToImmutable(); + } + } + + private string DoSourceCodeTextCompatibilityPass(string sourceCode) + { + return sourceCode + .Replace("GameMain.LuaCs", "LuaCsSetup.Instance") + .Replace(" Client.ClientList", " ModUtils.Client.ClientList") + .Replace(" Barotrauma.Networking.Client.ClientList", " ModUtils.Client.ClientList") + .Replace("ItemPrefab.GetItemPrefab", "ModUtils.ItemPrefab.GetItemPrefab"); + } + + private IntPtr OnAssemblyLoaderResolvingUnmanaged(Assembly callerAssembly, string targetAssemblyName) + { + Guard.IsNull(callerAssembly, nameof(callerAssembly)); + Guard.IsNullOrWhiteSpace(targetAssemblyName, nameof(targetAssemblyName)); + + if (AssemblyLoadContext.GetLoadContext(callerAssembly) is not IAssemblyLoaderService loaderService) + { + return IntPtr.Zero; + } + + var targetDirectory = Path.GetFullPath(loaderService.OwnerPackage.Dir); + if (!targetAssemblyName.TrimEnd().EndsWith(".dll")) + { + targetAssemblyName += ".dll"; + } + + var res = _storageService.FindFilesInPackage(loaderService.OwnerPackage, string.Empty, targetAssemblyName, true); + + if (res.IsFailed || !res.Value.Any()) + { + return IntPtr.Zero; + } + + foreach (var path in res.Value) + { + if (System.Runtime.InteropServices.NativeLibrary.TryLoad(path, out IntPtr asmPtr)) + { + _loadedNativeLibraries.Add(asmPtr); + return asmPtr; + } + } + + return IntPtr.Zero; + } + + private Assembly OnAssemblyLoaderResolvingManaged(IAssemblyLoaderService requestingLoader, AssemblyName searchName) + { + // This method is used during assembly instantiation, we cannot put a lock here. + //using var lck = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult(); + IService.CheckDisposed(this); + + foreach (var loader in _assemblyLoaders.Where(kvp => kvp.Value != requestingLoader) + .Select(kvp => kvp.Value).ToImmutableArray()) + { + if (loader.IsReferenceOnlyMode || !loader.Assemblies.Any()) + { + continue; + } + + foreach (var assembly in loader.Assemblies) + { + if (assembly.GetName().FullName == searchName.FullName) + { + return assembly; + } + } + } + + return null; + } + + private void OnAssemblyLoaderUnloading(IAssemblyLoaderService loader) + { + if (!loader.Assemblies.Any()) + { + return; + } + + foreach (var assembly in loader.Assemblies) + { + _eventService?.Value?.PublishEvent(sub => sub.OnAssemblyUnloading(assembly)); + } + } + + public FluentResults.Result UnloadManagedAssemblies() + { + using var lck = _operationsLock.AcquireWriterLock().ConfigureAwait(false).GetAwaiter().GetResult(); + IService.CheckDisposed(this); + + if (_assemblyLoaders.Count == 0) + { + return FluentResults.Result.Ok(); + } + + var results = new FluentResults.Result(); + + results.WithReasons(UnsafeDisposeManagedTypeInstances().Reasons); + + ReflectionUtils.ResetCache(); + foreach (var loaderService in _assemblyLoaders) + { + try + { + loaderService.Value.Dispose(); + _unloadingAssemblyLoaders.Add(loaderService.Value, loaderService.Key); + } + catch (Exception e) + { + results.WithError(new ExceptionalError(e)); + } + } + + _assemblyLoaders.Clear(); + _storageService.PurgeCache(); + GC.Collect(GC.MaxGeneration, GCCollectionMode.Aggressive, true); + +#if DEBUG + // Print still loaded assembly load ctx after giving some time + CoroutineManager.Invoke(() => + { + if (!_unloadingAssemblyLoaders.Any()) + { + return; + } + + StringBuilder sb = new StringBuilder(); + + sb.AppendLine("The following ContentPackages have not unloaded their assemblies:"); + + foreach (var kvp in _unloadingAssemblyLoaders.ToImmutableArray()) + { + sb.AppendLine($"- '{kvp.Value.Name}'"); + } + + + // Use DebugConsole in case logger is null by the time this executes. + if (_logger is null) + { + DebugConsole.LogError(sb.ToString()); + } + else + { + _logger.LogWarning(sb.ToString()); + } + }, 3.0f); +#endif + + // clear native libraries + if (_loadedNativeLibraries.Any()) + { + foreach (var ptr in _loadedNativeLibraries) + { + try + { + System.Runtime.InteropServices.NativeLibrary.Free(ptr); + } + catch + { + // ignored + continue; + } + } + + _loadedNativeLibraries.Clear(); + } + + return results; + } + + private FluentResults.Result UnsafeDisposeManagedTypeInstances() + { + var results = new FluentResults.Result(); + + if (!_pluginInstances.IsEmpty) + { + foreach (var instance in _pluginInstances.SelectMany(kvp => kvp.Value)) + { + try + { + instance.Dispose(); + } + catch (Exception e) + { + results.WithError(new ExceptionalError(e)); + continue; + } + } + } + + if (_pluginEventService is not null) + { + _eventService.Value.RemoveDispatcherEventService(_pluginEventService); + _pluginEventService = null; + } + _pluginInjectorContainer = null; + + _pluginInstances.Clear(); + _pluginPackageLookup.Clear(); + + return results; + } + + public Result GetLoadedAssembly(OneOf assemblyName, in Guid[] excludedContexts) + { + using var _ = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult(); + IService.CheckDisposed(this); + + var guids = excludedContexts; + return assemblyName.Match((AssemblyName asm) => + { + foreach (var ass in _assemblyLoaders.Values + .Where(al => guids.Length == 0 || !guids.Contains(al.Id)) + .SelectMany(al => al.Assemblies) + .ToImmutableArray()) + { + if (ass.GetName() == asm) + { + return ass; + } + } + + return null; + }, + (string asmName) => + { + foreach (var ass in _assemblyLoaders.Values.SelectMany(al => al.Assemblies)) + { + if (ass.GetName().Name?.Equals(asmName) ?? ass.GetName().FullName.Equals(asmName)) + { + return ass; + } + } + + return null; + }); + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/PluginService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/PluginService.cs new file mode 100644 index 000000000..1c42ebcd4 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/PluginService.cs @@ -0,0 +1,6 @@ +namespace Barotrauma.LuaCs; + +public class PluginService +{ + +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/SafeStorageService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/SafeStorageService.cs new file mode 100644 index 000000000..983a13b32 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/SafeStorageService.cs @@ -0,0 +1,230 @@ +using Barotrauma.IO; +using Barotrauma.LuaCs.Data; +using Barotrauma.Networking; +using FarseerPhysics.Common; +using FluentResults; +using FluentResults.LuaCs; +using Microsoft.Toolkit.Diagnostics; +using System; +using System.Collections.Concurrent; +using System.Collections.Immutable; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Xml.Linq; +using Path = System.IO.Path; + +namespace Barotrauma.LuaCs; + +public class SafeStorageService : StorageService, ISafeStorageService +{ + private ConcurrentDictionary + _fileListRead = new (), + _fileListWrite = new(); + private readonly AsyncReaderWriterLock _higherOperationsLock = new(); + + public SafeStorageService(IStorageServiceConfig configData) : base(configData) + { + IsReadOperationAllowedEval = (fp) => IsFileAccessible(fp, true, true); + IsWriteOperationAllowedEval = (fp) => IsFileAccessible(fp, false, true); + } + + private string GetFullPath(string path) => System.IO.Path.GetFullPath(path).CleanUpPathCrossPlatform(); + + public bool IsFileAccessible(string path, bool readOnly, bool checkWhitelistOnly = true) + { + Guard.IsNotNullOrWhiteSpace(path, nameof(path)); + using var lck = _higherOperationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult(); + IService.CheckDisposed(this); + + try + { + path = GetFullPath(path); + + if (path.StartsWith(ConfigData.WorkshopModsDirectory) + || path.StartsWith(ConfigData.LocalModsDirectory) +#if CLIENT + || path.StartsWith(ConfigData.TempDownloadsDirectory) +#endif + ) + { + return true; + } + + if (!_fileListRead.ContainsKey(path)) + { + return false; + } + if (!readOnly && !_fileListWrite.ContainsKey(path)) + { + return false; + } + if (checkWhitelistOnly) + { + return true; + } + using var fs = System.IO.File.Open( + path, FileMode.Open, readOnly ? FileAccess.Read : FileAccess.ReadWrite, FileShare.ReadWrite); + return readOnly ? fs.CanRead : fs.CanWrite; + } + catch + { + return false; + } + } + + public void AddFileToWhitelist(string path, bool readOnly = true) + { + Guard.IsNotNullOrWhiteSpace(path, nameof(path)); + using var lck = _higherOperationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult(); + IService.CheckDisposed(this); + + try + { + path = GetFullPath(path); + _fileListRead.AddOrUpdate(path, s => 0, (s, b) => 0); + if (!readOnly) + { + _fileListWrite.AddOrUpdate(path, s => 0, (s, b) => 0); + } + } + catch + { + return; + } + } + + public void AddFilesToWhitelist(ImmutableArray paths, bool readOnly = true) + { + if (paths.IsDefaultOrEmpty) + ThrowHelper.ThrowArgumentNullException(nameof(paths)); + foreach (var path in paths) + { + AddFileToWhitelist(path, readOnly); + } + } + + + public void RemoveFileFromAllWhitelists(string path) + { + Guard.IsNotNullOrWhiteSpace(path, nameof(path)); + using var lck = _higherOperationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult(); + IService.CheckDisposed(this); + + try + { + path = GetFullPath(path); + _fileListRead.TryRemove(path, out _); + _fileListWrite.TryRemove(path, out _); + } + catch + { + return; + } + } + + public FluentResults.Result SetReadOnlyWhitelist(ImmutableArray filePaths) + { + using var lck = _higherOperationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult(); + IService.CheckDisposed(this); + if (filePaths.IsDefaultOrEmpty) + { + return FluentResults.Result.Fail($"{nameof(SetReadOnlyWhitelist)}: FilePaths cannot be empty."); + } + + _fileListRead.Clear(); + var res = new FluentResults.Result(); + foreach (var path in filePaths) + { + Guard.IsNotNullOrWhiteSpace(path, nameof(path)); + try + { + var p = Path.GetFullPath(path.CleanUpPathCrossPlatform()); + if (_fileListRead.ContainsKey(p)) + { + res = res.WithReason(new Success($"Path already in whitelist: {p}")); + continue; + } + + if (_fileListRead.TryAdd(p, 0)) + { + res = res.WithSuccess($"Added path successfully: {p}"); + continue; + } + + res = res.WithError(new Error($"Failed to add path to list: {p}")); + } + catch (Exception e) + { + res = res.WithError(new ExceptionalError(e) + .WithMetadata(MetadataType.ExceptionObject, this) + .WithMetadata(MetadataType.ExceptionDetails, e.Message) + .WithMetadata(MetadataType.RootObject, path) + ); + continue; + } + } + + return res; + } + + public FluentResults.Result SetReadWriteWhitelist(ImmutableArray filePaths) + { + if (filePaths.IsDefaultOrEmpty) + { + return FluentResults.Result.Fail($"{nameof(SetReadOnlyWhitelist)}: FilePaths cannot be empty."); + } + using var lck = _higherOperationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult(); + IService.CheckDisposed(this); + + _fileListRead.Clear(); + _fileListWrite.Clear(); + var res = new FluentResults.Result(); + foreach (var path in filePaths) + { + Guard.IsNotNullOrWhiteSpace(path, nameof(path)); + try + { + var p = Path.GetFullPath(path.CleanUpPathCrossPlatform()); + TryAddToList(_fileListRead, p); + TryAddToList(_fileListWrite, p); + res = res.WithError(new Error($"Failed to add path to list: {p}")); + } + catch (Exception e) + { + res = res.WithError(new ExceptionalError(e) + .WithMetadata(MetadataType.ExceptionObject, this) + .WithMetadata(MetadataType.ExceptionDetails, e.Message) + .WithMetadata(MetadataType.RootObject, path) + ); + continue; + } + } + + void TryAddToList(ConcurrentDictionary dict, string p) + { + if (dict.ContainsKey(p)) + { + res = res.WithReason(new Success($"Path already in whitelist: {p}")); + return; + } + + if (dict.TryAdd(p, 0)) + { + res = res.WithSuccess($"Added path successfully: {p}"); + return; + } + } + + return res; + } + + public void ClearAllWhitelists() + { + using var lck = _higherOperationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult(); + IService.CheckDisposed(this); + _fileListRead.Clear(); + _fileListWrite.Clear(); + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/ServicesProvider.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/ServicesProvider.cs new file mode 100644 index 000000000..8bfad23f2 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/ServicesProvider.cs @@ -0,0 +1,295 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Threading; +using LightInject; +using Microsoft.Toolkit.Diagnostics; + +namespace Barotrauma.LuaCs; + + +public class ServicesProvider : IServicesProvider +{ + private ServiceContainer _serviceContainerInst; + private ServiceContainer ServiceContainer => _serviceContainerInst; + + /// + /// Definition: [Key: ConcreteType, Value: TypeInstance] + /// + private ImmutableArray _systemInstances = ImmutableArray.Empty; + private readonly ReaderWriterLockSlim _serviceLock = new(); + + public ServicesProvider() + { + _serviceContainerInst = new ServiceContainer(new ContainerOptions() + { + EnablePropertyInjection = false + }); + + //_serviceContainerInst.Register((f) => this); + } + + public void RegisterServiceType(ServiceLifetime lifetime, ILifetime lifetimeInstance = null) where TSvcInterface : class, IService where TService : class, IService, TSvcInterface + { + // ISystem services must run as a lifetime singleton + if (typeof(TSvcInterface).IsAssignableTo(typeof(ISystem))) + { + lifetimeInstance = new PerContainerLifetime(); + } + + if (lifetimeInstance is null) + { + switch (lifetime) + { + case ServiceLifetime.Singleton: + lifetimeInstance = new PerContainerLifetime(); + break; + case ServiceLifetime.PerThread: + lifetimeInstance = new PerThreadLifetime(); + break; + // treat these as transient + case ServiceLifetime.Transient: + case ServiceLifetime.Invalid: + case ServiceLifetime.Custom: + default: + lifetimeInstance = null; + break; + } + } + + try + { + _serviceLock.EnterReadLock(); + if (lifetimeInstance is not null) + ServiceContainer.Register(lifetimeInstance); + else + ServiceContainer.Register(); + } + finally + { + _serviceLock.ExitReadLock(); + } + } + + public void RegisterServiceType(string name, ServiceLifetime lifetime, + ILifetime lifetimeInstance = null) where TSvcInterface : class, IService where TService : class, IService, TSvcInterface + { + if (name.IsNullOrWhiteSpace()) + { + throw new ArgumentNullException($"Tried to register a service of type {typeof(TService).Name} but the name provided is null or empty." ); + } + + // ISystem services must run as a lifetime singleton + if (typeof(TService).IsAssignableTo(typeof(ISystem))) + { + lifetimeInstance = new PerContainerLifetime(); + } + + if (lifetimeInstance is null) + { + switch (lifetime) + { + case ServiceLifetime.Singleton: + lifetimeInstance = new PerContainerLifetime(); + break; + case ServiceLifetime.PerThread: + lifetimeInstance = new PerThreadLifetime(); + break; + // treat these as transient + case ServiceLifetime.Transient: + case ServiceLifetime.Invalid: + case ServiceLifetime.Custom: // lifetime should not be null here + default: + lifetimeInstance = new PerRequestLifeTime(); + break; + } + } + + try + { + _serviceLock.EnterReadLock(); + ServiceContainer.Register(name, lifetimeInstance); + } + finally + { + _serviceLock.ExitReadLock(); + } + } + + public void RegisterServiceResolver(Func factory) where TSvcInterface : class, IService + { + try + { + _serviceLock.EnterReadLock(); + ServiceContainer.Register(f => factory(ServiceContainer)); + } + finally + { + _serviceLock.ExitReadLock(); + } + } + + public void CompileAndRun() + { + try + { + _serviceLock.EnterWriteLock(); + ServiceContainer!.Compile(); + if (!_systemInstances.IsDefaultOrEmpty) + { + ThrowHelper.ThrowInvalidOperationException($"Systems are already instanced!"); + } + + _systemInstances = ServiceContainer.GetAllInstances(typeof(ISystem)) + .Select(obj => (ISystem)obj) + .ToImmutableArray(); + } + finally + { + _serviceLock.ExitWriteLock(); + } + } + + public void InjectServices(T inst) where T : class + { + try + { + _serviceLock.EnterReadLock(); + ServiceContainer.InjectProperties(inst); + } + finally + { + _serviceLock.ExitReadLock(); + } + } + + public bool TryGetService(out TSvcInterface service) where TSvcInterface : class, IService + { + try + { + _serviceLock.EnterReadLock(); + service = ServiceContainer.TryGetInstance(); + return service is not null; + } + catch + { + service = null; + return false; + } + finally + { + _serviceLock.ExitReadLock(); + } + } + + public TSvcInterface GetService() where TSvcInterface : class, IService + { + try + { + _serviceLock.EnterReadLock(); + return ServiceContainer.GetInstance(); + } + finally + { + _serviceLock.ExitReadLock(); + } + } + + public bool TryGetService(string name, out TSvcInterface service) where TSvcInterface : class, IService + { + try + { + _serviceLock.EnterReadLock(); + service = ServiceContainer.TryGetInstance(name); + return service is not null; + } + catch + { + service = null; + return false; + } + finally + { + _serviceLock.ExitReadLock(); + } + } + + public event Action OnServiceInstanced; + + public ImmutableArray GetAllServices() where TSvc : class, IService + { + try + { + _serviceLock.EnterReadLock(); + return ServiceContainer.GetAllInstances().ToImmutableArray(); + } + finally + { + _serviceLock.ExitReadLock(); + } + } + + [MethodImpl(MethodImplOptions.PreserveSig | MethodImplOptions.Synchronized)] + public void DisposeAndReset() + { + // Plugins should never be allowed to execute this. + if (Assembly.GetCallingAssembly() != Assembly.GetExecutingAssembly()) + { + throw new MethodAccessException( + $"Assembly {Assembly.GetCallingAssembly().FullName} attempted to call {nameof(DisposeAndReset)}()."); + } + + try + { + _serviceLock.EnterWriteLock(); + foreach (var system in _systemInstances) + { + try + { + system.Dispose(); + } + catch (Exception e) + { + // ignored, no logging services available. + } + } + _systemInstances = ImmutableArray.Empty; + _serviceContainerInst?.Dispose(); + _serviceContainerInst = new ServiceContainer(); + } + finally + { + _serviceLock.ExitWriteLock(); + } + } +} + +public class PerThreadLifetime : ILifetime +{ + private readonly ThreadLocal _instance = new(); + + public object GetInstance(Func createInstance, Scope scope) + { + if (_instance.Value is null) + { + var inst = createInstance.Invoke(); + // IDisposable dispatch + if (inst is IDisposable disposable) + { + if (scope is null) + { + throw new InvalidOperationException("Attempt disposable object without a valid scope."); + } + scope.TrackInstance(disposable); + } + + _instance.Value = inst; + } + + return _instance.Value; + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/SettingsFileParserService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/SettingsFileParserService.cs new file mode 100644 index 000000000..8ef819f26 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/SettingsFileParserService.cs @@ -0,0 +1,212 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Threading.Tasks; +using System.Xml; +using System.Xml.Linq; +using Barotrauma.LuaCs.Data; +using FluentResults; +using Microsoft.Toolkit.Diagnostics; +using OneOf; + +namespace Barotrauma.LuaCs; + +public sealed class SettingsFileParserService : + IParserServiceOneToManyAsync, + IParserServiceOneToManyAsync +{ + #region DisposalControl + + private AsyncReaderWriterLock _operationLock = new(); + + public void Dispose() + { + using var lck = _operationLock.AcquireWriterLock().ConfigureAwait(false).GetAwaiter().GetResult(); + if (!ModUtils.Threading.CheckIfClearAndSetBool(ref _isDisposed)) + { + return; + } + _storageService.Dispose(); + _storageService = null; + } + + private int _isDisposed = 0; + public bool IsDisposed + { + get => ModUtils.Threading.GetBool(ref _isDisposed); + private set => ModUtils.Threading.SetBool(ref _isDisposed, value); + } + + #endregion + + private IStorageService _storageService; + + public SettingsFileParserService(IStorageService storageService) + { + _storageService = storageService; + } + + async Task>> IParserServiceOneToManyAsync + .TryParseResourcesAsync(IConfigResourceInfo src) + { + Guard.IsNotNull(src, nameof(src)); + Guard.IsNotNull(src.OwnerPackage, nameof(src.OwnerPackage)); + using var lck = await _operationLock.AcquireReaderLock(); + IService.CheckDisposed(this); + + if (src.FilePaths.IsDefaultOrEmpty) + { + return ReturnFail($"The config file list is empty."); + } + + var parsedInfo = ImmutableArray.CreateBuilder(); + + foreach ((ContentPath path, Result docLoadResult) res in await _storageService.LoadPackageXmlFilesAsync(src.FilePaths)) + { + if (res.docLoadResult.IsFailed) + { + return ReturnFail($"Failed to load document for {src.OwnerPackage.Name}").WithErrors(res.docLoadResult.Errors); + } + + var settingElements = res.docLoadResult.Value.GetChildElement("Configuration") + .GetChildElements("Settings").SelectMany(e => e.GetChildElements("Setting")).ToImmutableArray(); + if (settingElements.IsDefaultOrEmpty) + { + continue; + } + + var packageIdent = XmlConvert.EncodeLocalName(res.path.ContentPackage!.Name); + + foreach (var element in settingElements) + { + var name = element.GetAttributeString("Name", string.Empty); + if (name.IsNullOrWhiteSpace()) + { + return ReturnFail( + $"The internal name for a setting in the config file '{res.path.FullPath}' is empty!"); + } + + var newSetting = new ConfigInfo() + { + InternalName = name, + OwnerPackage = res.path.ContentPackage, + DataType = element.GetAttributeString("Type", string.Empty), + Element = element, + EditableStates = element.GetAttributeBool("ReadOnly", false) ? RunState.Unloaded : + element.GetAttributeBool("AllowChangesWhileExecuting", true) ? RunState.Running : + RunState.LoadedNoExec, + NetSync = element.GetAttributeEnum("NetSync", NetSync.None), +#if CLIENT + DisplayName = $"{packageIdent}.{name}.DisplayName", + Description = $"{packageIdent}.{name}.Description", + DisplayCategory = $"{packageIdent}.{name}.DisplayCategory", + ShowInMenus = element.GetAttributeBool("ShowInMenus", true), + Tooltip = $"{packageIdent}.{name}.Tooltip", + ImageIconPath = element.GetAttributeString("ImageIcon", string.Empty) is {} val && !val.IsNullOrWhiteSpace() ? + ContentPath.FromRaw(res.path.ContentPackage, val) : ContentPath.Empty +#endif + }; + if (!IsInfoValid(newSetting)) + { + return ReturnFail($"A setting was invalid. ContentPackage: {res.path.ContentPackage.Name}. Name: {newSetting?.InternalName}"); + } + parsedInfo.Add(newSetting); + } + } + + return FluentResults.Result.Ok(parsedInfo.ToImmutable()); + + // Helpers + + FluentResults.Result ReturnFail(string msg) + { + return FluentResults.Result.Fail($"{nameof(IParserServiceOneToManyAsync.TryParseResourcesAsync)}: {msg}"); + } + + bool IsInfoValid(ConfigInfo info) + { + return info.OwnerPackage != null + && !info.InternalName.IsNullOrWhiteSpace() + && !info.DataType.IsNullOrWhiteSpace() + && info.Element != null +#if CLIENT + && !info.DisplayName.IsNullOrWhiteSpace() + && !info.Description.IsNullOrWhiteSpace() + && !info.DisplayCategory.IsNullOrWhiteSpace() + && !info.Tooltip.IsNullOrWhiteSpace() +#endif + ; + } + } + + async Task>> + IParserServiceOneToManyAsync + .TryParseResourcesAsync(IConfigResourceInfo src) + { + Guard.IsNotNull(src, nameof(src)); + Guard.IsNotNull(src.OwnerPackage, nameof(src.OwnerPackage)); + using var lck = await _operationLock.AcquireReaderLock(); + IService.CheckDisposed(this); + + if (src.FilePaths.IsDefaultOrEmpty) + { + return ReturnFail($"The config file list is empty."); + } + + var parsedInfo = ImmutableArray.CreateBuilder(); + + foreach ((ContentPath path, Result docLoadResult) res in await _storageService + .LoadPackageXmlFilesAsync(src.FilePaths)) + { + if (res.docLoadResult.IsFailed) + { + return ReturnFail($"Failed to load document for {src.OwnerPackage.Name}") + .WithErrors(res.docLoadResult.Errors); + } + + var profileCollection = res.docLoadResult.Value.GetChildElement("Configuration") + .GetChildElement("Profiles"); + if (profileCollection == null) + { + continue; + } + + foreach (var profile in profileCollection.GetChildElements("Profile")) + { + var profileName = profile.GetAttributeString("Name", string.Empty); + Guard.IsNotNullOrWhiteSpace(profileName, nameof(profileName)); + + var settingValues = profile.GetChildElements("SettingValue").ToImmutableArray(); + if (settingValues.IsDefaultOrEmpty) + { + ThrowHelper.ThrowArgumentNullException(nameof(settingValues)); + } + + var profileValuesBuilder = ImmutableArray.CreateBuilder<(string ConfigName, XElement Value)>(); + + foreach (var settingValue in settingValues) + { + var cfgName = settingValue.GetAttributeString("Name", string.Empty); + Guard.IsNotNullOrWhiteSpace(cfgName, nameof(cfgName)); + profileValuesBuilder.Add((cfgName, settingValue)); + } + + parsedInfo.Add(new ConfigProfileInfo() + { + InternalName = profileName, + OwnerPackage = res.path.ContentPackage, + ProfileValues = profileValuesBuilder.ToImmutable() + }); + } + } + + return parsedInfo.ToImmutable(); + + FluentResults.Result ReturnFail(string msg) + { + return FluentResults.Result.Fail($"{nameof(IParserServiceOneToManyAsync.TryParseResourcesAsync)}: {msg}"); + } + } + +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/StorageService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/StorageService.cs new file mode 100644 index 000000000..a55fd8506 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/StorageService.cs @@ -0,0 +1,728 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.IO; +using System.Reflection; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using System.Xml; +using System.Xml.Linq; +using Barotrauma.LuaCs.Data; +using Barotrauma.Networking; +using FluentResults; +using FluentResults.LuaCs; +using Microsoft.CodeAnalysis; +using Microsoft.Toolkit.Diagnostics; +using Error = FluentResults.Error; +using Path = System.IO.Path; + +namespace Barotrauma.LuaCs; + +public class StorageService : IStorageService +{ + public StorageService(IStorageServiceConfig configData) + { + ConfigData = configData; + IsReadOperationAllowedEval = bool (str) => true; + IsWriteOperationAllowedEval = bool (str) => true; + } + + private readonly ConcurrentDictionary> _fsCache = new(); + protected readonly IStorageServiceConfig ConfigData; + protected readonly AsyncReaderWriterLock OperationsLock = new(); + + private Func _isReadOperationAllowedEval; + protected Func IsReadOperationAllowedEval + { + get => _isReadOperationAllowedEval; + set + { + if (value is not null) + _isReadOperationAllowedEval = value; + } + } + + private Func _isWriteOperationAllowedEval; + protected Func IsWriteOperationAllowedEval + { + get => _isWriteOperationAllowedEval; + set + { + if (value is not null) + _isWriteOperationAllowedEval = value; + } + } + + public bool IsDisposed => ModUtils.Threading.GetBool(ref _isDisposed); + private int _isDisposed = 0; + public virtual void Dispose() + { + using var lck = OperationsLock.AcquireWriterLock().ConfigureAwait(false).GetAwaiter().GetResult(); + if (!ModUtils.Threading.CheckIfClearAndSetBool(ref _isDisposed)) + return; + _fsCache.Clear(); + } + + public void PurgeCache() + { + using var lck = OperationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult(); + IService.CheckDisposed(this); + _fsCache.Clear(); + } + + public void PurgeFileFromCache(string absolutePath) + { + using var lck = OperationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult(); + IService.CheckDisposed(this); + + if (absolutePath.IsNullOrWhiteSpace()) + return; + + try + { + //sanitation pass + absolutePath = System.IO.Path.GetFullPath(absolutePath).CleanUpPath(); + _fsCache.Remove(absolutePath, out _); + } + catch + { + // ignored + return; + } + } + + public void PurgeFilesFromCache(params string[] absolutePaths) + { + using var lck = OperationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult(); + IService.CheckDisposed(this); + + if (absolutePaths.Length < 1) + return; + + foreach (var path in absolutePaths) + { + try + { + if (path.IsNullOrWhiteSpace()) + continue; + + //sanitation pass + var path2 = System.IO.Path.GetFullPath(path).CleanUpPath(); + _fsCache.Remove(path2, out _); + } + catch + { + // ignored + continue; + } + } + } + + // --- Local Game Content + protected Result GetAbsolutePathForLocal(ContentPackage package, string localFilePath) + { + if (Path.IsPathRooted(localFilePath)) + ThrowHelper.ThrowArgumentException($"{nameof(GetAbsolutePathForLocal)}: The path {localFilePath} is an absolute path."); + + try + { + var path = System.IO.Path.GetFullPath(Path.Combine( + ConfigData.LocalPackageDataPath.Replace(ConfigData.LocalDataPathRegex, XmlConvert.EncodeLocalName(package.Name)).CleanUpPathCrossPlatform(), + localFilePath.CleanUpPathCrossPlatform())); + if (!path.StartsWith(Path.GetFullPath(ConfigData.LocalDataSavePath))) + ThrowHelper.ThrowUnauthorizedAccessException($"{nameof(GetAbsolutePathForLocal)}: The local path of '{path}' is not a local path!"); + return path; + } + catch (Exception e) + { + if (e is ArgumentNullException or ArgumentException or UnauthorizedAccessException) + throw; // these are dev errors and should be propagated. + return FluentResults.Result.Fail(new ExceptionalError(e)); + } + } + + private Result LoadLocalData(ContentPackage package, string localFilePath, Func> dataLoader) + { + Guard.IsNotNull(package, nameof(package)); + Guard.IsNotNullOrWhiteSpace(localFilePath, nameof(localFilePath)); + using var lck = OperationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult(); + IService.CheckDisposed(this); + var res = GetAbsolutePathForLocal(package, localFilePath); + return res is { IsFailed: true } ? res.ToResult() : dataLoader(res.Value); + } + + public Result LoadLocalXml(ContentPackage package, string localFilePath) => LoadLocalData(package, localFilePath, TryLoadXml); + public Result LoadLocalBinary(ContentPackage package, string localFilePath) => LoadLocalData(package, localFilePath, TryLoadBinary); + public Result LoadLocalText(ContentPackage package, string localFilePath) => LoadLocalData(package, localFilePath, TryLoadText); + + + private FluentResults.Result SaveLocalData(ContentPackage package, string localFilePath, in T data, Func dataSaver) + { + Guard.IsNotNull(package, nameof(package)); + Guard.IsNotNullOrWhiteSpace(localFilePath, nameof(localFilePath)); + using var lck = OperationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult(); + IService.CheckDisposed(this); + var res = GetAbsolutePathForLocal(package, localFilePath); + return res is { IsFailed: true } ? res.ToResult() : dataSaver(res.Value, data); + } + + public FluentResults.Result SaveLocalXml(ContentPackage package, string localFilePath, XDocument document) + => SaveLocalData(package, localFilePath, document, (path, data) => TrySaveXml(path, in data)); + public FluentResults.Result SaveLocalBinary(ContentPackage package, string localFilePath, in byte[] bytes) + => SaveLocalData(package, localFilePath, bytes, (path, data) => TrySaveBinary(path, in data)); + public FluentResults.Result SaveLocalText(ContentPackage package, string localFilePath, in string text) + => SaveLocalData(package, localFilePath, text, (path, data) => TrySaveText(path, in data)); + + private async Task> LoadLocalDataAsync(ContentPackage package, string localFilePath, + Func>> dataLoader) + { + Guard.IsNotNull(package, nameof(package)); + Guard.IsNotNullOrWhiteSpace(localFilePath, nameof(localFilePath)); + using var lck = await OperationsLock.AcquireReaderLock(); + IService.CheckDisposed(this); + var res = GetAbsolutePathForLocal(package, localFilePath); + return res is { IsFailed: true } ? res.ToResult() : await dataLoader(res.Value); + } + + public async Task> LoadLocalXmlAsync(ContentPackage package, string localFilePath) + => await LoadLocalDataAsync(package, localFilePath, async path => await TryLoadXmlAsync(path)); + public async Task> LoadLocalBinaryAsync(ContentPackage package, string localFilePath) + => await LoadLocalDataAsync(package, localFilePath, async path => await TryLoadBinaryAsync(path)); + public async Task> LoadLocalTextAsync(ContentPackage package, string localFilePath) + => await LoadLocalDataAsync(package, localFilePath, async path => await TryLoadTextAsync(path)); + + private async Task SaveLocalDataAsync(ContentPackage package, string localFilePath, + T data, Func> dataSaver) + { + Guard.IsNotNull(package, nameof(package)); + Guard.IsNotNullOrWhiteSpace(localFilePath, nameof(localFilePath)); + IService.CheckDisposed(this); + using var lck = await OperationsLock.AcquireReaderLock(); + var res = GetAbsolutePathForLocal(package, localFilePath); + return res is { IsFailed: true } ? res.ToResult() : await dataSaver(res.Value, data); + } + + public async Task SaveLocalXmlAsync(ContentPackage package, string localFilePath, XDocument document) + => await SaveLocalDataAsync(package, localFilePath, document, async (path, doc) => await TrySaveXmlAsync(path, doc)); + public async Task SaveLocalBinaryAsync(ContentPackage package, string localFilePath, byte[] bytes) + => await SaveLocalDataAsync(package, localFilePath, bytes, async (path, bin) => await TrySaveBinaryAsync(path, bin)); + public async Task SaveLocalTextAsync(ContentPackage package, string localFilePath, string text) + => await SaveLocalDataAsync(package, localFilePath, text, async (path, txt) => await TrySaveTextAsync(path, txt)); + + private bool IsPackagePathValid(ContentPath contentPath) + { + return contentPath.FullPath.StartsWith(ConfigData.WorkshopModsDirectory) + || contentPath.FullPath.StartsWith(ConfigData.LocalModsDirectory) +#if CLIENT + || contentPath.FullPath.StartsWith(ConfigData.TempDownloadsDirectory) +#endif + || contentPath.FullPath.StartsWith(Path.GetFullPath(ContentPackageManager.VanillaCorePackage!.Dir).CleanUpPathCrossPlatform()); + } + + // --- Package Content + private Result LoadPackageData(ContentPath contentPath, Func> dataLoader) + { + Guard.IsNotNull(contentPath, nameof(contentPath)); + Guard.IsNotNullOrWhiteSpace(contentPath.FullPath, nameof(contentPath.FullPath)); + using var lck = OperationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult(); + IService.CheckDisposed(this); + if (!IsPackagePathValid(contentPath)) + { + ThrowHelper.ThrowUnauthorizedAccessException($"{nameof(LoadPackageData)}: The filepath of `{contentPath.FullPath}' is not in a package directory!"); + } + return dataLoader(contentPath.FullPath); + } + + public Result LoadPackageXml(ContentPath filePath) + => LoadPackageData(filePath, path => TryLoadXml(filePath.FullPath)); + public Result LoadPackageBinary(ContentPath filePath) + => LoadPackageData(filePath, path => TryLoadBinary(filePath.FullPath)); + public Result LoadPackageText(ContentPath filePath) + => LoadPackageData(filePath, path => TryLoadText(filePath.FullPath)); + + private ImmutableArray<(ContentPath, Result)> LoadPackageDataFiles(ImmutableArray filePaths, Func> dataLoader) + { + if (filePaths.IsDefaultOrEmpty) + ThrowHelper.ThrowArgumentNullException($"{nameof(LoadPackageData)}: File paths is empty!"); + using var lck = OperationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult(); + var builder = ImmutableArray.CreateBuilder<(ContentPath, Result)>(); + foreach (var path in filePaths) + { + builder.Add((path, LoadPackageData(path, dataLoader))); + } + return builder.ToImmutable(); + } + + public ImmutableArray<(ContentPath, Result)> LoadPackageXmlFiles(ImmutableArray filePaths) + => LoadPackageDataFiles(filePaths, TryLoadXml); + public ImmutableArray<(ContentPath, Result)> LoadPackageBinaryFiles(ImmutableArray filePaths) + => LoadPackageDataFiles(filePaths, TryLoadBinary); + public ImmutableArray<(ContentPath, Result)> LoadPackageTextFiles(ImmutableArray filePaths) + => LoadPackageDataFiles(filePaths, TryLoadText); + + public Result> FindFilesInPackage(ContentPackage package, string localSubfolder, string regexFilter, bool searchRecursively) + { + Guard.IsNotNull(package, nameof(package)); + try + { + var cp = ContentPath.FromRaw(package, package.Dir); + var fullPath = localSubfolder.IsNullOrWhiteSpace() + ? Path.GetFullPath(cp.FullPath) + : Path.GetFullPath(localSubfolder, cp.FullPath); + return System.IO.Directory.GetFiles(fullPath, regexFilter, + searchRecursively ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly) + .ToImmutableArray(); + } + catch (Exception e) + { + if (e is ArgumentNullException or ArgumentException) + throw; + return FluentResults.Result.Fail(new ExceptionalError(e) + .WithMetadata(MetadataType.ExceptionObject, this) + .WithMetadata(MetadataType.RootObject, package)); + } + } + + + private async Task> LoadPackageDataAsync(ContentPath contentPath, Func>> dataLoader) + { + Guard.IsNotNull(contentPath, nameof(contentPath)); + Guard.IsNotNullOrWhiteSpace(contentPath.FullPath, nameof(contentPath.FullPath)); + using var lck = await OperationsLock.AcquireReaderLock(); + IService.CheckDisposed(this); + if (!IsPackagePathValid(contentPath)) + { + ThrowHelper.ThrowUnauthorizedAccessException($"{nameof(LoadPackageDataAsync)}: The filepath of `{contentPath.FullPath}' is not in a package directory!"); + } + return await dataLoader(contentPath.FullPath); + } + + public async Task> LoadPackageXmlAsync(ContentPath filePath) + => await LoadPackageDataAsync(filePath, async path => await TryLoadXmlAsync(path)); + public async Task> LoadPackageBinaryAsync(ContentPath filePath) + => await LoadPackageDataAsync(filePath, async path => await TryLoadBinaryAsync(path)); + public async Task> LoadPackageTextAsync(ContentPath filePath) + => await LoadPackageDataAsync(filePath, async path => await TryLoadTextAsync(path)); + + private async Task)>> LoadPackageDataFilesAsync( + ImmutableArray filePaths, Func>> dataLoader) + { + if (filePaths.IsDefaultOrEmpty) + { + ThrowHelper.ThrowArgumentNullException($"{nameof(LoadPackageData)}: File paths is empty!"); + } + using var lck = await OperationsLock.AcquireReaderLock(); + var builder = ImmutableArray.CreateBuilder<(ContentPath, Result)>(); + foreach (var path in filePaths) + { + builder.Add((path, await LoadPackageDataAsync(path, dataLoader))); + } + return builder.ToImmutable(); + } + + public async Task)>> LoadPackageXmlFilesAsync(ImmutableArray filePaths) + => await LoadPackageDataFilesAsync(filePaths, async path => await TryLoadXmlAsync(path)); + public async Task)>> LoadPackageBinaryFilesAsync(ImmutableArray filePaths) + => await LoadPackageDataFilesAsync(filePaths, async path => await TryLoadBinaryAsync(path)); + public async Task)>> LoadPackageTextFilesAsync(ImmutableArray filePaths) + => await LoadPackageDataFilesAsync(filePaths, async path => await TryLoadTextAsync(path)); + + + private int _useCaching; + public bool UseCaching + { + get => ModUtils.Threading.GetBool(ref _useCaching); + set => ModUtils.Threading.SetBool(ref _useCaching, value); + } + + // Method group redirect + private FluentResults.Result TryLoadXml(string filePath) => TryLoadXml(filePath, null); + + public virtual FluentResults.Result TryLoadXml(string filePath, Encoding encoding) + { + Guard.IsNotNullOrWhiteSpace(filePath, nameof(filePath)); + using var lck = OperationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult(); + IService.CheckDisposed(this); + + var r = TryLoadText(filePath, encoding); + if (r is { IsSuccess: true, Value: not null }) + return XDocument.Parse(r.Value); + else + { + return r.ToResult(s => null) + .WithError(GetGeneralError(nameof(LoadLocalXml), filePath)); + } + } + + // Method group redirect + private FluentResults.Result TryLoadText(string filePath) => TryLoadText(filePath, null); + public virtual FluentResults.Result TryLoadText(string filePath, Encoding encoding) + { + Guard.IsNotNullOrWhiteSpace(filePath, nameof(filePath)); + using var lck = OperationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult(); + IService.CheckDisposed(this); + + if (IsReadOperationAllowedEval?.Invoke(filePath) is not true) + { + return FluentResults.Result.Fail($"{nameof(TryLoadText)}: File '{filePath}' is not allowed."); + } + + if (UseCaching && _fsCache.TryGetValue(filePath, out var result) + && result.TryPickT1(out var cachedVal, out _)) + { + return FluentResults.Result.Ok(cachedVal); + } + + return IOExceptionsOperationRunner(nameof(TryLoadText), filePath, () => + { + var fp = filePath.CleanUpPath(); + fp = System.IO.Path.IsPathRooted(fp) ? fp : System.IO.Path.GetFullPath(fp); + var fileText = encoding is null ? System.IO.File.ReadAllText(fp) : System.IO.File.ReadAllText(fp, encoding); + if (UseCaching) + _fsCache[filePath] = fileText; + return new FluentResults.Result().WithSuccess($"Loaded file successfully").WithValue(fileText); + }); + } + + public virtual FluentResults.Result TryLoadBinary(string filePath) + { + Guard.IsNotNullOrWhiteSpace(filePath, nameof(filePath)); + using var lck = OperationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult(); + IService.CheckDisposed(this); + + if (IsReadOperationAllowedEval?.Invoke(filePath) is not true) + { + return FluentResults.Result.Fail($"{nameof(TryLoadBinary)}: File '{filePath}' is not allowed."); + } + + if (UseCaching && _fsCache.TryGetValue(filePath, out var result) + && result.TryPickT0(out var cachedVal, out _)) + { + return FluentResults.Result.Ok(cachedVal); + } + + return IOExceptionsOperationRunner(nameof(TryLoadBinary), filePath, () => + { + var fp = filePath.CleanUpPath(); + fp = System.IO.Path.IsPathRooted(fp) ? fp : System.IO.Path.GetFullPath(fp); + var fileData = System.IO.File.ReadAllBytes(fp); + if (UseCaching) + { + _fsCache[filePath] = fileData; + } + return new FluentResults.Result().WithSuccess($"Loaded file successfully").WithValue(fileData); + }); + } + + public virtual FluentResults.Result TrySaveXml(string filePath, in XDocument document, Encoding encoding = null) => TrySaveText(filePath, document.ToString(), encoding); + public virtual FluentResults.Result TrySaveText(string filePath, in string text, Encoding encoding = null) + { + Guard.IsNotNullOrWhiteSpace(text, nameof(text)); + using var lck = OperationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult(); + IService.CheckDisposed(this); + + if (IsWriteOperationAllowedEval?.Invoke(filePath) is not true) + { + return FluentResults.Result.Fail($"{nameof(TrySaveText)}: File '{filePath}' is not allowed."); + } + + string t = text; //copy + return IOExceptionsOperationRunner(nameof(TrySaveText), filePath, () => + { + var fp = filePath.CleanUpPath(); + fp = System.IO.Path.IsPathRooted(fp) ? fp : System.IO.Path.GetFullPath(fp); + Directory.CreateDirectory(Path.GetDirectoryName(fp)!); + System.IO.File.WriteAllText(fp, t, encoding ?? Encoding.UTF8); + if (UseCaching) + _fsCache[filePath] = t; + return new FluentResults.Result().WithSuccess($"Saved to file successfully"); + }); + } + + + public virtual FluentResults.Result TrySaveBinary(string filePath, in byte[] bytes) + { + Guard.IsNotNullOrWhiteSpace(filePath, nameof(filePath)); + Guard.IsNotNull(bytes, nameof(bytes)); + Guard.HasSizeGreaterThanOrEqualTo(bytes, 1, nameof(bytes)); + using var lck = OperationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult(); + IService.CheckDisposed(this); + + if (IsWriteOperationAllowedEval?.Invoke(filePath) is not true) + { + return FluentResults.Result.Fail($"{nameof(TrySaveBinary)}: File '{filePath}' is not allowed."); + } + + byte[] b = new byte[bytes.Length]; + System.Buffer.BlockCopy(bytes, 0, b, 0, bytes.Length); + return IOExceptionsOperationRunner(nameof(TrySaveBinary), filePath, () => + { + var fp = filePath.CleanUpPath(); + fp = System.IO.Path.IsPathRooted(fp) ? fp : System.IO.Path.GetFullPath(fp); + Directory.CreateDirectory(Path.GetDirectoryName(fp)!); + System.IO.File.WriteAllBytes(fp, b); + if (UseCaching) + _fsCache[filePath] = b; + return new FluentResults.Result().WithSuccess($"Saved to file successfully"); + }); + } + + public virtual FluentResults.Result FileExists(string filePath) + { + Guard.IsNotNullOrWhiteSpace(filePath, nameof(filePath)); + IService.CheckDisposed(this); + // lock not needed + if (IsReadOperationAllowedEval?.Invoke(filePath) is not true) + { + return FluentResults.Result.Fail($"{nameof(FileExists)}: File '{filePath}' is not allowed."); + } + + return IOExceptionsOperationRunner(nameof(FileExists), filePath, () => + { + var fp = filePath.CleanUpPath(); + fp = System.IO.Path.IsPathRooted(fp) ? fp : System.IO.Path.GetFullPath(fp); + return System.IO.File.Exists(fp); + }); + } + + public virtual FluentResults.Result DirectoryExists(string directoryPath) + { + Guard.IsNotNullOrWhiteSpace(directoryPath, nameof(directoryPath)); + IService.CheckDisposed(this); + // lock not needed + if (IsReadOperationAllowedEval?.Invoke(directoryPath) is not true) + { + return FluentResults.Result.Fail($"{nameof(DirectoryExists)}: File '{directoryPath}' is not allowed."); + } + + try + { + var di = new DirectoryInfo(directoryPath); + return di.Exists; + } + catch (Exception ex) + { + return new FluentResults.Result().WithError(ex.Message); + } + } + + public virtual async Task> TryLoadXmlAsync(string filePath, Encoding encoding = null) + { + Guard.IsNotNullOrWhiteSpace(filePath, nameof(filePath)); + using var lck = await OperationsLock.AcquireReaderLock(); + IService.CheckDisposed(this); + if (IsReadOperationAllowedEval.Invoke(filePath) is not true) + { + return FluentResults.Result.Fail($"{nameof(TryLoadXmlAsync)}: File '{filePath}' is not allowed."); + } + + if (UseCaching && _fsCache.TryGetValue(filePath, out var cachedVal) + && cachedVal.TryPickT2(out var cachedDoc, out _)) + { + return FluentResults.Result.Ok(cachedDoc); + } + + try + { + await using var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read); + var doc = await XDocument.LoadAsync(fs, LoadOptions.PreserveWhitespace, CancellationToken.None); + if (UseCaching) + _fsCache[filePath] = doc; + return FluentResults.Result.Ok(doc); + } + catch (Exception e) + { + return FluentResults.Result.Fail(GetGeneralError(nameof(TryLoadXmlAsync), filePath)); + } + } + + public virtual async Task> TryLoadTextAsync(string filePath, Encoding encoding = null) + { + Guard.IsNotNullOrWhiteSpace(filePath, nameof(filePath)); + using var lck = await OperationsLock.AcquireReaderLock(); + IService.CheckDisposed(this); + if (IsReadOperationAllowedEval.Invoke(filePath) is not true) + { + return FluentResults.Result.Fail($"{nameof(TryLoadTextAsync)}: File '{filePath}' is not allowed."); + } + + if (UseCaching && _fsCache.TryGetValue(filePath, out var cachedVal) + && cachedVal.TryPickT1(out var cachedTxt, out _)) + { + return FluentResults.Result.Ok(cachedTxt); + } + + return await IOExceptionsOperationRunnerAsync(nameof(TryLoadTextAsync), filePath, async () => + { + var fp = filePath.CleanUpPath(); + fp = System.IO.Path.IsPathRooted(fp) ? fp : System.IO.Path.GetFullPath(fp); + var txt = await System.IO.File.ReadAllTextAsync(fp); + if (UseCaching) + _fsCache[filePath] = txt; + return FluentResults.Result.Ok(txt); + }); + } + + public virtual async Task> TryLoadBinaryAsync(string filePath) + { + Guard.IsNotNullOrWhiteSpace(filePath, nameof(filePath)); + using var lck = await OperationsLock.AcquireReaderLock(); + IService.CheckDisposed(this); + if (IsReadOperationAllowedEval.Invoke(filePath) is not true) + { + return FluentResults.Result.Fail($"{nameof(TryLoadBinaryAsync)}: File '{filePath}' is not allowed."); + } + + if (UseCaching && _fsCache.TryGetValue(filePath, out var cachedVal) + && cachedVal.TryPickT0(out var cachedBin, out _)) + { + return cachedBin; + } + + return await IOExceptionsOperationRunnerAsync(nameof(TryLoadTextAsync), filePath, async () => + { + var fp = filePath.CleanUpPath(); + fp = System.IO.Path.IsPathRooted(fp) ? fp : System.IO.Path.GetFullPath(fp); + return await System.IO.File.ReadAllBytesAsync(fp); + }); + } + + // method group overload + public virtual async Task TrySaveXmlAsync(string filePath, XDocument document, Encoding encoding = null) => await TrySaveTextAsync(filePath, document.ToString(), encoding); + public virtual async Task TrySaveTextAsync(string filePath, string text, Encoding encoding = null) + { + Guard.IsNotNullOrWhiteSpace(text, nameof(text)); + using var lck = await OperationsLock.AcquireReaderLock(); + IService.CheckDisposed(this); + if (IsWriteOperationAllowedEval.Invoke(filePath) is not true) + { + return FluentResults.Result.Fail($"{nameof(TrySaveTextAsync)}: File '{filePath}' is not allowed."); + } + + string t = text.ToString(); //copy + return await IOExceptionsOperationRunnerAsync(nameof(TrySaveText), filePath, async () => + { + var fp = filePath.CleanUpPath(); + fp = System.IO.Path.IsPathRooted(fp) ? fp : System.IO.Path.GetFullPath(fp); + await System.IO.File.WriteAllTextAsync(fp, t, encoding); + if (UseCaching) + _fsCache[filePath] = t; + return new FluentResults.Result().WithSuccess($"Saved to file successfully"); + }); + } + + public virtual async Task TrySaveBinaryAsync(string filePath, byte[] bytes) + { + Guard.IsNotNullOrWhiteSpace(filePath, nameof(filePath)); + Guard.IsNotNull(bytes, nameof(bytes)); + Guard.HasSizeGreaterThanOrEqualTo(bytes, 1, nameof(bytes)); + using var lck = await OperationsLock.AcquireReaderLock(); + IService.CheckDisposed(this); + if (IsWriteOperationAllowedEval.Invoke(filePath) is not true) + { + return FluentResults.Result.Fail($"{nameof(TrySaveBinaryAsync)}: File '{filePath}' is not allowed."); + } + + byte[] b = new byte[bytes.Length]; + System.Buffer.BlockCopy(bytes, 0, b, 0, bytes.Length); + return await IOExceptionsOperationRunnerAsync(nameof(TrySaveBinary), filePath, async () => + { + var fp = filePath.CleanUpPath(); + fp = System.IO.Path.IsPathRooted(fp) ? fp : System.IO.Path.GetFullPath(fp); + await System.IO.File.WriteAllBytesAsync(fp, b); + if (UseCaching) + _fsCache[filePath] = b; + return new FluentResults.Result().WithSuccess($"Saved to file successfully"); + }); + } + + private async Task> IOExceptionsOperationRunnerAsync(string funcName, string filepath, Func>> operation) + { + try + { + return await operation?.Invoke()!; + } + catch (Exception e) + { + if (e is ArgumentException or ArgumentNullException) + throw; + return ReturnException(e, filepath).WithError(GetGeneralError(funcName, filepath)); + } + } + + private async Task IOExceptionsOperationRunnerAsync(string funcName, string filepath, Func> operation) + { + try + { + return await operation?.Invoke()!; + } + catch (Exception e) + { + if (e is ArgumentException or ArgumentNullException) + throw; + return ReturnException(e, filepath).WithError(GetGeneralError(funcName, filepath)); + } + } + + private FluentResults.Result IOExceptionsOperationRunner(string funcName, string filepath, Func> operation) + { + try + { + return operation?.Invoke(); + } + catch (Exception e) + { + if (e is ArgumentException or ArgumentNullException) + throw; + return ReturnException(e, filepath).WithError(GetGeneralError(funcName, filepath)); + } + } + + private FluentResults.Result IOExceptionsOperationRunner(string funcName, string filepath, Func operation) + { + try + { + return operation?.Invoke(); + } + catch (Exception e) + { + if (e is ArgumentException or ArgumentNullException) + throw; + return ReturnException(e, filepath).WithError(GetGeneralError(funcName, filepath)); + } + } + + private Error GetGeneralError(string funcName, string localfp, ContentPackage package) => + new Error($"{funcName}: Failed to load local file.") + .WithMetadata(MetadataType.ExceptionObject, this) + .WithMetadata(MetadataType.Sources, localfp) + .WithMetadata(MetadataType.RootObject, package); + + private Error GetGeneralError(string funcName, string localfp) => + new Error($"{funcName}: Failed to load local file.") + .WithMetadata(MetadataType.ExceptionObject, this) + .WithMetadata(MetadataType.Sources, localfp); + + private FluentResults.Result ReturnException(TException exception, ContentPackage package) where TException : Exception + { + return new FluentResults.Result().WithError(new ExceptionalError(exception) + .WithMetadata(MetadataType.ExceptionObject, this) + .WithMetadata(MetadataType.RootObject, package)); + } + + private FluentResults.Result ReturnException(TException exception, ContentPackage package) where TException : Exception + { + return new FluentResults.Result().WithError(new ExceptionalError(exception) + .WithMetadata(MetadataType.ExceptionObject, this) + .WithMetadata(MetadataType.RootObject, package)); + } + + private FluentResults.Result ReturnException(TException exception, string filePath) where TException : Exception + { + return new FluentResults.Result().WithError(new ExceptionalError(exception) + .WithMetadata(MetadataType.ExceptionObject, this) + .WithMetadata(MetadataType.RootObject, filePath)); + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/_Interfaces/IAssemblyManagementService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/_Interfaces/IAssemblyManagementService.cs new file mode 100644 index 000000000..dc5d8ab19 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/_Interfaces/IAssemblyManagementService.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; +using System.Reflection; +using System.Runtime.CompilerServices; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using OneOf; + +// ReSharper disable InconsistentNaming + +namespace Barotrauma.LuaCs; + +public interface IAssemblyManagementService : IPluginManagementService +{ + + /// + /// Searches for an assembly given it's fully qualified name, while excluding the contexts with the given Guids, if supplied. + /// + /// The assembly info. + /// Guids of excluded contexts. + /// On Success: The assembly.
On Failure: nothing.
+ FluentResults.Result GetLoadedAssembly(OneOf assemblyName, in Guid[] excludedContexts); +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/_Interfaces/IConfigService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/_Interfaces/IConfigService.cs new file mode 100644 index 000000000..09d571962 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/_Interfaces/IConfigService.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; +using System.Threading.Tasks; +using System.Xml.Linq; +using Barotrauma.LuaCs.Data; +using Barotrauma.LuaCs; +using Barotrauma.Networking; +using FluentResults; + +namespace Barotrauma.LuaCs; + +public partial interface IConfigService : IReusableService, ILuaConfigService +{ + void RegisterSettingTypeInitializer(string typeIdentifier, Func<(IConfigService ConfigService, IConfigInfo Info), T> settingFactory) + where T : class, ISettingBase; + Task LoadConfigsAsync(ImmutableArray configResources); + Task LoadConfigsProfilesAsync(ImmutableArray configProfileResources); + FluentResults.Result LoadSavedConfigsValues(); + FluentResults.Result ApplyConfigProfile(ContentPackage package, string internalName); + FluentResults.Result DisposePackageData(ContentPackage package); + FluentResults.Result DisposeAllPackageData(); +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/_Interfaces/IConsoleCommandsService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/_Interfaces/IConsoleCommandsService.cs new file mode 100644 index 000000000..2d4bad468 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/_Interfaces/IConsoleCommandsService.cs @@ -0,0 +1,16 @@ +using Barotrauma.Networking; +using Microsoft.Xna.Framework; +using System; + +namespace Barotrauma.LuaCs; + +public interface IConsoleCommandsService : IService +{ + void RegisterCommand(string name, string help, Action onExecute, Func getValidArgs = null, bool isCheat = false); + void AssignOnExecute(string names, Action onExecute); +#if SERVER + internal void AssignOnClientRequestExecute(string names, Action onClientRequestExecute); +#endif + void RemoveCommand(string name); + void RemoveRegisteredCommands(); +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/_Interfaces/IEventService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/_Interfaces/IEventService.cs new file mode 100644 index 000000000..867742321 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/_Interfaces/IEventService.cs @@ -0,0 +1,52 @@ +using System; +using System.Reflection; +using Barotrauma.LuaCs.Events; +using Barotrauma.LuaCs.Compatibility; +using Barotrauma.LuaCs; + +namespace Barotrauma.LuaCs; + +public interface IEventService : IReusableService, ILuaEventService +{ + /// + /// + /// + /// + /// + /// + FluentResults.Result Subscribe(T subscriber) where T : class, IEvent; + /// + /// + /// + /// + /// + void Unsubscribe(T subscriber) where T : class, IEvent; + /// + /// Clears all subscribers for a given event type and removes any registration to the type. + /// + /// The event type. + void ClearAllEventSubscribers() where T : class, IEvent; + /// + /// Clears all subscribers lists. + /// + void ClearAllSubscribers(); + /// + /// Invokes all alive subscribers of the given event using the provided invocation factory. + /// + /// + /// + /// + FluentResults.Result PublishEvent(Action action) where T : class, IEvent; + + /// + /// Adds an event service that will receive all published events. + /// + /// + void AddDispatcherEventService(IEventService eventService); + + /// + /// Removes an event service from the dispatcher list. + /// + /// + void RemoveDispatcherEventService(IEventService eventService); +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/_Interfaces/IHelperServiceDefinitions.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/_Interfaces/IHelperServiceDefinitions.cs new file mode 100644 index 000000000..23e623fc2 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/_Interfaces/IHelperServiceDefinitions.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Threading.Tasks; +using System.Xml.Linq; +using Barotrauma.LuaCs.Data; +using FluentResults; + +namespace Barotrauma.LuaCs; + +public interface IParserService : IService +{ + Result TryParseResource(TSrc src); + ImmutableArray> TryParseResources(IEnumerable sources); +} + +public interface IParserServiceAsync : IService +{ + Task> TryParseResourceAsync(TSrc src); + Task>> TryParseResourcesAsync(IEnumerable sources); +} + +public interface IParserServiceOneToManyAsync : IService +{ + Task>> TryParseResourcesAsync(TSrc src); +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/_Interfaces/ILoggerService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/_Interfaces/ILoggerService.cs new file mode 100644 index 000000000..7f2c3ed6e --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/_Interfaces/ILoggerService.cs @@ -0,0 +1,59 @@ +using System; +using Barotrauma.Networking; +using FluentResults; +using Microsoft.Xna.Framework; + +namespace Barotrauma.LuaCs; + +public readonly record struct PendingLog(string Message, Color? Color, ServerLog.MessageType MessageType); + +public interface ILoggerSubscriber +{ + void OnLog(PendingLog pendingLog); +} + +/// +/// Provides console and debug logging services +/// +public interface ILoggerService : IReusableService +{ + void Subscribe(ILoggerSubscriber subscriber); + void Unsubscribe(ILoggerSubscriber subscriber); + void ProcessLogs(); + void HandleException(Exception exception, string prefix = null); + void LogError(string message); + void LogWarning(string message); + void LogMessage(string message, Color? serverColor = null, Color? clientColor = null); + void Log(string message, Color? color = null, ServerLog.MessageType messageType = ServerLog.MessageType.ServerMessage); + void LogResults(FluentResults.Result result); + + #region DebugBuilds + + void LogDebug(string message, Color? color = null); + void LogDebugWarning(string message); + void LogDebugError(string message); + + #endregion + + #region LegacyCompat_LuaCsLogger + + public void HandleException(Exception ex, LuaCsMessageOrigin origin) + { + HandleException(ex, origin.ToString()); + } + + public void LogError(string message, LuaCsMessageOrigin origin) + { + LogError(message); + } + + #endregion +} + +public enum LuaCsMessageOrigin +{ + LuaCs, + Unknown, + LuaMod, + CSharpMod, +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/_Interfaces/ILuaCsInfoProvider.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/_Interfaces/ILuaCsInfoProvider.cs new file mode 100644 index 000000000..57a8a0251 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/_Interfaces/ILuaCsInfoProvider.cs @@ -0,0 +1,32 @@ +namespace Barotrauma.LuaCs; + +/// +/// Provides access to data from the current . +/// +public interface ILuaCsInfoProvider : IService +{ + /// + /// Whether C# plugin code is enabled. + /// + public bool IsCsEnabled { get; } + + /// + /// Whether usernames are anonymized or show in logs. + /// + public bool HideUserNamesInLogs { get; } + + /// + /// Whether file system caching is enabled. + /// + public bool UseCaching { get; } + + /// + /// The current state of the Execution State Machine. + /// + public RunState CurrentRunState { get; } + + /// + /// Returns the best-matching LuaCsForBarotrauma package (enabled list > localMods > WorkshopMods). + /// + public ContentPackage LuaCsForBarotraumaPackage { get; } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/_Interfaces/ILuaScriptManagementService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/_Interfaces/ILuaScriptManagementService.cs new file mode 100644 index 000000000..b5ee2cfc5 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/_Interfaces/ILuaScriptManagementService.cs @@ -0,0 +1,66 @@ +#nullable enable + +using System.Collections.Immutable; +using System.Threading.Tasks; +using Barotrauma.LuaCs.Data; +using MoonSharp.Interpreter; + +namespace Barotrauma.LuaCs; + +public interface ILuaScriptManagementService : IReusableService +{ + /// + /// The running instance, if available. + /// + /// + /// It is recommended to avoid using this directly if another API is available for the intended purposes. + /// + Script? InternalScript { get; } + + object? GetGlobalTableValue(string tableName); + FluentResults.Result DoString(string code); + DynValue? CallFunctionSafe(object luaFunction, params object[] args); + + /// + /// Whether to enable/disable the file system caching for lua. + /// + /// + void SetCachingPolicy(bool useCaching); + + /// + /// Parses and loads script sources (code) into a memory cache without executing it. + /// + /// + /// + // [Required] + Task LoadScriptResourcesAsync(ImmutableArray resourcesInfo); + + /// + /// Executes already loaded into memory scripts data, in the supplied order. + /// + /// + /// + // [Required] + FluentResults.Result ExecuteLoadedScripts(ImmutableArray executionOrder, bool enableSandbox); + + /// + /// + /// + /// + /// + // [Required] + FluentResults.Result DisposePackageResources(ContentPackage package); + + /// + /// Calls dispose on, and clears active refs for, currently running scripts. Does not clear caches. + /// + /// + FluentResults.Result UnloadActiveScripts(); + + /// + /// Unloads all scripts and clears all caches/references. + /// + /// + /// May be functionally equivalent to + FluentResults.Result DisposeAllPackageResources(); +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/_Interfaces/IModConfigService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/_Interfaces/IModConfigService.cs new file mode 100644 index 000000000..847c61020 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/_Interfaces/IModConfigService.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; +using System.Threading.Tasks; +using Barotrauma.LuaCs.Data; +using Barotrauma.LuaCs; +using FluentResults; + +namespace Barotrauma.LuaCs; + +public interface IModConfigService : IService +{ + /// + /// Loads or dynamically generates a for the given . + ///
Throws a if the package is null. + ///
+ /// + /// + Task> CreateConfigAsync([NotNull]ContentPackage src); + Task Config)>> CreateConfigsAsync(ImmutableArray src); +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/_Interfaces/INetworkingService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/_Interfaces/INetworkingService.cs new file mode 100644 index 000000000..007cd6ffe --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/_Interfaces/INetworkingService.cs @@ -0,0 +1,39 @@ +using System; +using Barotrauma.LuaCs.Data; +using Barotrauma.LuaCs; +using Barotrauma.LuaCs.Compatibility; +using Barotrauma.Networking; + +namespace Barotrauma.LuaCs; + +#if CLIENT +public delegate void NetMessageReceived(IReadMessage netMessage); +#elif SERVER +internal delegate void NetMessageReceived(IReadMessage netMessage, Client connection); +#endif + +internal interface INetworkingService : IReusableService, ILuaCsNetworking, IEntityNetworkingService +{ + bool IsActive { get; } + bool IsSynchronized { get; } + + IWriteMessage Start(string netId); + IWriteMessage Start(Guid netId); + void Receive(string netId, NetMessageReceived action); + void Receive(Guid netId, NetMessageReceived action); +#if SERVER + void SendToClient(IWriteMessage netMessage, NetworkConnection connection = null, DeliveryMethod deliveryMethod = DeliveryMethod.Reliable); +#elif CLIENT + void SendToServer(IWriteMessage netMessage, DeliveryMethod deliveryMethod = DeliveryMethod.Reliable); +#endif + +} + +public interface IEntityNetworkingService +{ + Guid GetNetworkIdForInstance(INetworkSyncVar var); + void RegisterNetVar(INetworkSyncVar netVar); + void DeregisterNetVar(INetworkSyncVar netVar); + void SendNetVar(INetworkSyncVar netVar); + void SendNetVar(INetworkSyncVar netVar, NetworkConnection connection); +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/_Interfaces/IPackageManagementService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/_Interfaces/IPackageManagementService.cs new file mode 100644 index 000000000..568e66078 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/_Interfaces/IPackageManagementService.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Threading.Tasks; +using Barotrauma.Extensions; +using Barotrauma.LuaCs.Data; +using FluentResults; + +namespace Barotrauma.LuaCs; + +public interface IPackageManagementService : IReusableService +{ + public bool TryGetLoadedPackageByName(string name, out ContentPackage package); + public FluentResults.Result LoadPackageInfo(ContentPackage package); + public FluentResults.Result LoadPackagesInfo(ImmutableArray packages); + public FluentResults.Result ExecuteLoadedPackages(ImmutableArray executionOrder, bool executeCsAssemblies); + public FluentResults.Result SyncLoadedPackagesList(ImmutableArray packages); + public FluentResults.Result StopRunningPackages(); + public FluentResults.Result UnloadPackage(ContentPackage package); + public FluentResults.Result UnloadPackages(ImmutableArray packages); + public FluentResults.Result UnloadAllPackages(); + public ImmutableArray GetAllLoadedPackages(); + public ImmutableArray GetLoadedUnrestrictedPackages(); + public bool IsPackageRunning(ContentPackage package); + public bool IsAnyPackageLoaded(); + public bool IsAnyPackageRunning(); + public bool PackageContainsAnyRunnableResource(ContentPackage package); + public Result GetModConfigForPackage(ContentPackage package); +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/_Interfaces/IPluginManagementService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/_Interfaces/IPluginManagementService.cs new file mode 100644 index 000000000..a26c49ff6 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/_Interfaces/IPluginManagementService.cs @@ -0,0 +1,63 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Reflection; +using Barotrauma.LuaCs.Data; +using Microsoft.CodeAnalysis; + +namespace Barotrauma.LuaCs; + +public interface IPluginManagementService : IReusableService +{ + /// + /// Gets all types in searched that implement the type supplied. + /// + /// + /// + /// + /// + /// + FluentResults.Result> GetImplementingTypes( + bool includeInterfaces = false, + bool includeAbstractTypes = false, + bool includeDefaultContext = true); + + /// + /// Gets the that contains the plugin type. + /// + /// + /// + /// + bool TryGetPackageForPlugin(out ContentPackage ownerPackage); + + /// + /// Tries to find the type given the fully qualified name and filters. + /// + /// + /// + /// + /// + /// + Type GetType(string typeName, bool isByRefType = false, bool includeInterfaces = false, bool includeDefaultContext = true); + + /// + /// + /// + /// + /// + /// + FluentResults.Result ActivatePluginInstances(ImmutableArray executionOrder, bool excludeAlreadyRunningPackages = true); + + /// + /// Loads the provided assembly resources in the order of their dependencies and intra-mod priority load order. + /// + /// + /// Success/Failure and list of failed resources, if any. + FluentResults.Result LoadAssemblyResources(ImmutableArray resources); + + /// + /// Unloads all managed , , and s. + /// + /// Success of the operation.
Note: does not guarantee .NET runtime assembly unloading success.
+ FluentResults.Result UnloadManagedAssemblies(); +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/_Interfaces/IPluginService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/_Interfaces/IPluginService.cs new file mode 100644 index 000000000..35cf1dfc4 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/_Interfaces/IPluginService.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Reflection; +using Barotrauma.LuaCs.Data; + +namespace Barotrauma.LuaCs; + +public interface IPluginService : IReusableService +{ + bool IsAssemblyLoaded(string friendlyName); + /// + /// Loads the assemblies for the given information + /// + /// + /// + /// + /// + /// + FluentResults.Result LoadAndInstanceTypes(IEnumerable assemblyResourcesInfo, bool injectServices, out ImmutableArray typeInstances) where T : class, IAssemblyPlugin; + FluentResults.Result> GetLoadedPluginTypesInPackage() where T : class, IAssemblyPlugin; + /// + /// Advances the loading/execution state of the plugin. IMPORTANT: You cannot set the execution state of plugins + /// to 'Disposed'. You must instead call the 'DisposePlugins' method. + /// + /// + /// + FluentResults.Result AdvancePluginStates(PluginRunState newState); + + /// + /// Disposes of all running plugins hosted by the service and releases their references to allow unloading. + /// + /// Success of the operation. Returns false if any plugin threw errors during disposal. + FluentResults.Result DisposePlugins(); + + /// + /// Gets the current plugin execution state. + /// + /// + PluginRunState GetPluginRunState(); +} + +public enum PluginRunState +{ + Instanced=0, + PreInitialization=1, + Initialized=2, + LoadingCompleted=3, + Disposed=4 +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/_Interfaces/ISafeStorageService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/_Interfaces/ISafeStorageService.cs new file mode 100644 index 000000000..e3e4428cc --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/_Interfaces/ISafeStorageService.cs @@ -0,0 +1,54 @@ +using System.Collections.Immutable; + +namespace Barotrauma.LuaCs; + +public interface ISafeStorageService : IStorageService, ISafeStorageValidation { } + +public interface ISafeStorageValidation +{ + /// + /// Checks the given file path to see if it can be read. This includes any permissions, whitelists and OS checks. + /// + /// The absolute path to the file. + /// Whether to only check for read permissions only, or full RWM if false. + /// Whether to only check if the file is safe to access, without checking accessibility at the OS level. + /// Whether the file is accessible. + bool IsFileAccessible(string path, bool readOnly, bool checkWhitelistOnly = true); + + /// + /// Adds the given path to the specified whitelists. + /// + /// The path to the file, exactly as it will be passed to the Try(Load|Save) methods in . + /// Whether to add it to the read whitelist only, or Read+Write whitelists. + void AddFileToWhitelist(string path, bool readOnly = true); + + /// + /// Adds the given collection of file paths to whitelists (Read|+Write) + /// + /// The paths to the files, formatted exactly as it will be passed to the Try(Load|Save) methods in . + /// Whether to add it to the read whitelist only, or Read+Write whitelists. + void AddFilesToWhitelist(ImmutableArray paths, bool readOnly = true); + + /// + /// Removes the given path from all whitelists (Read|+Write). + /// + /// + void RemoveFileFromAllWhitelists(string path); + + /// + /// Sets the whitelist filtering for read-only file permissions for the instance. Overwrites previous list. + /// + /// List of file paths allowed, as will be passed to the Try(Load|Save) methods. + FluentResults.Result SetReadOnlyWhitelist(ImmutableArray filePaths); + + /// + /// Sets the whitelist filtering for read & write file permissions for the instance. Overwrites previous lists. + /// + /// List of file paths allowed, as will be passed to the Try(Load|Save) methods. + FluentResults.Result SetReadWriteWhitelist(ImmutableArray filePaths); + + /// + /// Deletes all paths from all white lists. + /// + void ClearAllWhitelists(); +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/_Interfaces/IService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/_Interfaces/IService.cs new file mode 100644 index 000000000..78fd7c8ba --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/_Interfaces/IService.cs @@ -0,0 +1,43 @@ +using System; +using Microsoft.Toolkit.Diagnostics; + +namespace Barotrauma.LuaCs; + +/// +/// Represents a that is automatically instantiated at startup for the lifetime of the +/// instance. +/// +public interface ISystem : IReusableService { } + +/// +/// Defines a service that can be reset to it's post-constructor state and reused without needing to be disposed. +/// Intended for persistent services. +/// +public interface IReusableService : IService +{ + /// + /// Returns the service to its original state (post-instantiation). + /// Allows a service instance to be reused without disposing of the instance. + /// + FluentResults.Result Reset(); +} + +/// +/// Base interface inherited by all services. +/// +/// Throws exception if `IsDisposed` return true. +public interface IService : IDisposable +{ + bool IsDisposed { get; } + public void CheckDisposed() + { + if (IsDisposed) + ThrowHelper.ThrowObjectDisposedException($"Tried to call method on disposed object '{this.GetType().Name}'!"); + } + + static void CheckDisposed(IService service) + { + if (service.IsDisposed) + ThrowHelper.ThrowObjectDisposedException($"Tried to call method on disposed object '{service.GetType().Name}'!"); + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/_Interfaces/IServicesProvider.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/_Interfaces/IServicesProvider.cs new file mode 100644 index 000000000..29ad12045 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/_Interfaces/IServicesProvider.cs @@ -0,0 +1,118 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using LightInject; + +namespace Barotrauma.LuaCs; + +/// +/// Provides instancing and management of , , and +/// instances. +/// +public interface IServicesProvider +{ + #region Type_Registration + + /// + /// Registers a type as a service for a given interface. + /// + /// NOTE: services are forced to + /// The of the service when requested. + /// Custom lifetime instance. + /// Service interface. + /// Implementing service type. + void RegisterServiceType(ServiceLifetime lifetime, ILifetime lifetimeInstance = null) where TSvcInterface : class, IService where TService : class, IService, TSvcInterface; + + /// + /// Registers a type as a service for a given interface that can be requested by name. + /// + /// NOTE: services are forced to + /// Name of the service for lookup. + /// The of the service when requested. + /// Custom lifetime instance. + /// Service interface. + /// Implementing service type. + void RegisterServiceType(string name, ServiceLifetime lifetime, ILifetime lifetimeInstance = null) where TSvcInterface : class, IService where TService : class, IService, TSvcInterface; + + /// + /// Registers a factory for resolving the service type. + /// + /// + /// + void RegisterServiceResolver(Func factory) where TSvcInterface : class, IService; + + /// + /// Compiles/Generates IL for registered services and instantiates all registered types. + /// + public void CompileAndRun(); + + #endregion + + #region Services_Instancing_Injection + + /// + /// Injects services into the properties of already instanced objects. + /// + /// + /// + void InjectServices(T inst) where T : class; + + /// + /// Tries to get a service for the given interface, returns success/failure. + /// + /// + /// + /// + bool TryGetService(out TSvcInterface service) where TSvcInterface : class, IService; + + /// + /// Tries to get a service for the given interface, throws an exception upon failure. + /// + /// + /// + TSvcInterface GetService() where TSvcInterface : class, IService; + + /// + /// Tries to get a service for the given name and interface, returns success/failure. + /// + /// + /// + /// + /// + bool TryGetService(string name, out TSvcInterface service) where TSvcInterface : class, IService; + + /// + /// Called whenever a new service is created/instanced. + /// Args[0]: The interface type of the service. + /// Args[1]: The instance of the service. + /// + event System.Action OnServiceInstanced; + + #endregion + + #region ActiveServices + + /// + /// Returns all services for the given interface. + /// + /// + /// + ImmutableArray GetAllServices() where TSvc : class, IService; + + #endregion + + // Notes: Left public due to the common use of Publicizers + #region Internal_Use + + /// + /// Notes: Internal use only if hosted by LuaCsForBarotrauma. Disposes of all services and resets DI container. Warning: unable to dispose of services held by other objects. + /// + void DisposeAndReset(); + + #endregion +} + +public enum ServiceLifetime +{ + Transient, Singleton, PerThread, Invalid, Custom +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/_Interfaces/IStorageService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/_Interfaces/IStorageService.cs new file mode 100644 index 000000000..3a50e3dc2 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/_Interfaces/IStorageService.cs @@ -0,0 +1,84 @@ +using System; +using System.Collections.Immutable; +using System.Text; +using System.Threading.Tasks; +using System.Xml.Linq; +using FluentResults; + +namespace Barotrauma.LuaCs; + +public interface IStorageService : IService +{ + + bool UseCaching { get; set; } + + /// + /// Deletes all cached file data. + /// + void PurgeCache(); + + /// + /// Deletes the data for the supplied file path from the data cache. + /// + /// + void PurgeFileFromCache(string absolutePath); + + /// + /// Deletes the data from the supplied file paths from the data cache. + /// + /// + void PurgeFilesFromCache(params string[] absolutePaths); + + // -- local game folder storage + FluentResults.Result LoadLocalXml(ContentPackage package, string localFilePath); + FluentResults.Result LoadLocalBinary(ContentPackage package, string localFilePath); + FluentResults.Result LoadLocalText(ContentPackage package, string localFilePath); + FluentResults.Result SaveLocalXml(ContentPackage package, string localFilePath, XDocument document); + FluentResults.Result SaveLocalBinary(ContentPackage package, string localFilePath, in byte[] bytes); + FluentResults.Result SaveLocalText(ContentPackage package, string localFilePath, in string text); + // async + Task> LoadLocalXmlAsync(ContentPackage package, string localFilePath); + Task> LoadLocalBinaryAsync(ContentPackage package, string localFilePath); + Task> LoadLocalTextAsync(ContentPackage package, string localFilePath); + Task SaveLocalXmlAsync(ContentPackage package, string localFilePath, XDocument document); + Task SaveLocalBinaryAsync(ContentPackage package, string localFilePath, byte[] bytes); + Task SaveLocalTextAsync(ContentPackage package, string localFilePath, string text); + + // -- package directory + // singles + Result LoadPackageXml(ContentPath filePath); + Result LoadPackageBinary(ContentPath filePath); + Result LoadPackageText(ContentPath filePath); + // collections + ImmutableArray<(ContentPath, Result)> LoadPackageXmlFiles(ImmutableArray filePaths); + ImmutableArray<(ContentPath, Result)> LoadPackageBinaryFiles(ImmutableArray filePaths); + ImmutableArray<(ContentPath, Result)> LoadPackageTextFiles(ImmutableArray filePaths); + FluentResults.Result> FindFilesInPackage(ContentPackage package, string localSubfolder, string regexFilter, bool searchRecursively); + // async + // singles + Task> LoadPackageXmlAsync(ContentPath filePath); + Task> LoadPackageBinaryAsync(ContentPath filePath); + Task> LoadPackageTextAsync(ContentPath filePath); + // collections + Task)>> LoadPackageXmlFilesAsync(ImmutableArray filePaths); + Task)>> LoadPackageBinaryFilesAsync(ImmutableArray filePaths); + Task)>> LoadPackageTextFilesAsync(ImmutableArray filePaths); + + // -- absolute paths + FluentResults.Result TryLoadXml(string filePath, Encoding encoding = null); + FluentResults.Result TryLoadText(string filePath, Encoding encoding = null); + FluentResults.Result TryLoadBinary(string filePath); + FluentResults.Result TrySaveXml(string filePath, in XDocument document, Encoding encoding = null); + FluentResults.Result TrySaveText(string filePath, in string text, Encoding encoding = null); + FluentResults.Result TrySaveBinary(string filePath, in byte[] bytes); + FluentResults.Result FileExists(string filePath); + FluentResults.Result DirectoryExists(string directoryPath); + + //async + Task> TryLoadXmlAsync(string filePath, Encoding encoding = null); + Task> TryLoadTextAsync(string filePath, Encoding encoding = null); + Task> TryLoadBinaryAsync(string filePath); + Task TrySaveXmlAsync(string filePath, XDocument document, Encoding encoding = null); + Task TrySaveTextAsync(string filePath, string text, Encoding encoding = null); + Task TrySaveBinaryAsync(string filePath, byte[] bytes); +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/_Lua/DefaultLuaRegistrar.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/_Lua/DefaultLuaRegistrar.cs new file mode 100644 index 000000000..44e35bbdc --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/_Lua/DefaultLuaRegistrar.cs @@ -0,0 +1,240 @@ +using Barotrauma.LuaCs.Data; +using Barotrauma.Networking; +using MoonSharp.Interpreter; +using MoonSharp.Interpreter.Interop; +using MoonSharp.Interpreter.Interop.BasicDescriptors; +using Sigil; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using System.Numerics; +using System.Reflection; +using System.Runtime.CompilerServices; + +namespace Barotrauma.LuaCs; + +public interface IDefaultLuaRegistrar : IService +{ + public void RegisterAll(); +} + +public class DefaultLuaRegistrar : IDefaultLuaRegistrar +{ + public bool IsDisposed { get; private set; } + + private readonly ILuaUserDataService _userDataService; + private readonly ISafeLuaUserDataService _safeUserDataService; + private readonly ILoggerService _loggerService; + + private class SteamIDMemberDescriptor : IMemberDescriptor + { + public bool IsStatic => false; + + public string Name => "SteamID"; + + public MemberDescriptorAccess MemberAccess => MemberDescriptorAccess.CanRead; + + public DynValue GetValue(Script script, object obj) + { + if (obj is Client client) + { + return DynValue.FromObject(script, ModUtils.Client.GetSteamId(client)); + } + + throw new System.NotImplementedException(); + } + + public void SetValue(Script script, object obj, DynValue value) + { + throw new System.NotImplementedException(); + } + } + + public DefaultLuaRegistrar(ILoggerService loggerService, ILuaUserDataService userDataService, ISafeLuaUserDataService safeUserDataService) + { + _userDataService = userDataService; + _safeUserDataService = safeUserDataService; + _loggerService = loggerService; + } + + private void RegisterShared() + { + _userDataService.RegisterType("System.TimeSpan"); + _userDataService.RegisterType("System.Exception"); + _userDataService.RegisterType("System.Console"); + _userDataService.RegisterType("System.Exception"); + + _userDataService.RegisterType("Barotrauma.Success`2"); + _userDataService.RegisterType("Barotrauma.Failure`2"); + _userDataService.RegisterType("Barotrauma.Range`1"); + _userDataService.RegisterType("Barotrauma.ItemPrefab"); + + _userDataService.RegisterType("Barotrauma.InputType"); + + List assembliesToScan = [typeof(DefaultLuaRegistrar).Assembly, typeof(Identifier).Assembly, typeof(Microsoft.Xna.Framework.Vector2).Assembly]; + + foreach (var type in assembliesToScan.SelectMany(a => a.GetTypes())) + { + if (type.IsEnum || type.Name.StartsWith("<") || type.IsDefined(typeof(CompilerGeneratedAttribute)) || !_safeUserDataService.IsAllowed(type.FullName)) + { + continue; + } + + _userDataService.RegisterType(type.FullName); + } + + _userDataService.RegisterType("Barotrauma.LuaSByte"); + _userDataService.RegisterType("Barotrauma.LuaByte"); + _userDataService.RegisterType("Barotrauma.LuaInt16"); + _userDataService.RegisterType("Barotrauma.LuaUInt16"); + _userDataService.RegisterType("Barotrauma.LuaInt32"); + _userDataService.RegisterType("Barotrauma.LuaUInt32"); + _userDataService.RegisterType("Barotrauma.LuaInt64"); + _userDataService.RegisterType("Barotrauma.LuaUInt64"); + _userDataService.RegisterType("Barotrauma.LuaSingle"); + _userDataService.RegisterType("Barotrauma.LuaDouble"); + + _userDataService.RegisterType("Barotrauma.Level+InterestingPosition"); + _userDataService.RegisterType("Barotrauma.Networking.RespawnManager+TeamSpecificState"); + + _userDataService.RegisterType("Barotrauma.CharacterParams+AIParams"); + _userDataService.RegisterType("Barotrauma.CharacterParams+TargetParams"); + _userDataService.RegisterType("Barotrauma.CharacterParams+InventoryParams"); + _userDataService.RegisterType("Barotrauma.CharacterParams+HealthParams"); + _userDataService.RegisterType("Barotrauma.CharacterParams+ParticleParams"); + _userDataService.RegisterType("Barotrauma.CharacterParams+SoundParams"); + + _userDataService.RegisterType("Barotrauma.FabricationRecipe+RequiredItemByIdentifier"); + _userDataService.RegisterType("Barotrauma.FabricationRecipe+RequiredItemByTag"); + + _userDataService.MakeFieldAccessible(_userDataService.RegisterType("Barotrauma.StatusEffect"), "user"); + + + _userDataService.RegisterType("Barotrauma.ContentPackageManager+PackageSource"); + _userDataService.RegisterType("Barotrauma.ContentPackageManager+EnabledPackages"); + + _userDataService.RegisterType("System.Xml.Linq.XElement"); + _userDataService.RegisterType("System.Xml.Linq.XName"); + _userDataService.RegisterType("System.Xml.Linq.XAttribute"); + _userDataService.RegisterType("System.Xml.Linq.XContainer"); + _userDataService.RegisterType("System.Xml.Linq.XDocument"); + _userDataService.RegisterType("System.Xml.Linq.XNode"); + + + _userDataService.RegisterType("Barotrauma.Networking.ServerSettings+SavedClientPermission"); + _userDataService.RegisterType("Barotrauma.Inventory+ItemSlot"); + + + _userDataService.MakeFieldAccessible(_userDataService.RegisterType("Barotrauma.Items.Components.CustomInterface"), "customInterfaceElementList"); + _userDataService.RegisterType("Barotrauma.Items.Components.CustomInterface+CustomInterfaceElement"); + + _userDataService.RegisterType("Barotrauma.DebugConsole+Command"); + + { + var descriptor = _userDataService.RegisterType("Barotrauma.NetLobbyScreen"); + +#if SERVER + _userDataService.MakeFieldAccessible(descriptor, "subs"); +#endif + } + + _userDataService.RegisterType("FarseerPhysics.Dynamics.Body"); + _userDataService.RegisterType("FarseerPhysics.Dynamics.World"); + _userDataService.RegisterType("FarseerPhysics.Dynamics.Fixture"); + _userDataService.RegisterType("FarseerPhysics.ConvertUnits"); + _userDataService.RegisterType("FarseerPhysics.Collision.AABB"); + _userDataService.RegisterType("FarseerPhysics.Collision.ContactFeature"); + _userDataService.RegisterType("FarseerPhysics.Collision.ManifoldPoint"); + _userDataService.RegisterType("FarseerPhysics.Collision.ContactID"); + _userDataService.RegisterType("FarseerPhysics.Collision.Manifold"); + _userDataService.RegisterType("FarseerPhysics.Collision.RayCastInput"); + _userDataService.RegisterType("FarseerPhysics.Collision.ClipVertex"); + _userDataService.RegisterType("FarseerPhysics.Collision.RayCastOutput"); + _userDataService.RegisterType("FarseerPhysics.Collision.EPAxis"); + _userDataService.RegisterType("FarseerPhysics.Collision.ReferenceFace"); + _userDataService.RegisterType("FarseerPhysics.Collision.Collision"); + + _userDataService.RegisterType("Voronoi2.DoubleVector2"); + _userDataService.RegisterType("Voronoi2.Site"); + _userDataService.RegisterType("Voronoi2.Edge"); + _userDataService.RegisterType("Voronoi2.Halfedge"); + _userDataService.RegisterType("Voronoi2.VoronoiCell"); + _userDataService.RegisterType("Voronoi2.GraphEdge"); + + _userDataService.RegisterType("Barotrauma.PrefabCollection`1"); + _userDataService.RegisterType("Barotrauma.PrefabSelector`1"); + _userDataService.RegisterType("Barotrauma.Pair`2"); + + _userDataService.RegisterExtensionType("Barotrauma.MathUtils"); + _userDataService.RegisterExtensionType("Barotrauma.XMLExtensions"); + + var itemPrefabDescriptor = (StandardUserDataDescriptor)_userDataService.RegisterType("Barotrauma.ItemPrefab"); + itemPrefabDescriptor.AddMember("GetItemPrefab", new MethodMemberDescriptor(typeof(ModUtils.ItemPrefab).GetMethod(nameof(ModUtils.ItemPrefab.GetItemPrefab), BindingFlags.NonPublic | BindingFlags.Static))); + + var clientDescriptor = (StandardUserDataDescriptor)_userDataService.RegisterType("Barotrauma.Networking.Client"); + clientDescriptor.AddMember("ClientList", new PropertyMemberDescriptor(typeof(ModUtils.Client).GetProperty(nameof(ModUtils.Client.ClientList), BindingFlags.NonPublic | BindingFlags.Static), InteropAccessMode.LazyOptimized)); + clientDescriptor.AddMember("SteamID", new SteamIDMemberDescriptor()); + + +#if SERVER + clientDescriptor.AddMember("UnbanPlayer", new MethodMemberDescriptor(typeof(ModUtils.Client).GetMethod(nameof(ModUtils.Client.UnbanPlayer), BindingFlags.NonPublic | BindingFlags.Static), InteropAccessMode.LazyOptimized)); + clientDescriptor.AddMember("BanPlayer", new MethodMemberDescriptor(typeof(ModUtils.Client).GetMethod(nameof(ModUtils.Client.BanPlayer), BindingFlags.NonPublic | BindingFlags.Static), InteropAccessMode.LazyOptimized)); +#endif + + _userDataService.RegisterExtensionType(typeof(ClientExtensions).FullName); + _userDataService.RegisterExtensionType(typeof(ItemExtensions).FullName); + _userDataService.RegisterExtensionType(typeof(MapEntityExtensions).FullName); + _userDataService.RegisterExtensionType(typeof(QualityExtensions).FullName); + + + var toolBox = UserData.RegisterType(typeof(ToolBox)); +#if CLIENT + _userDataService.RemoveMember(toolBox, "OpenFileWithShell"); +#endif + } + +#if CLIENT + private void RegisterClient() + { + _userDataService.RegisterType("Microsoft.Xna.Framework.Graphics.Effect"); + _userDataService.RegisterType("Microsoft.Xna.Framework.Graphics.EffectParameterCollection"); + _userDataService.RegisterType("Microsoft.Xna.Framework.Graphics.EffectParameter"); + + _userDataService.RegisterType("Microsoft.Xna.Framework.Graphics.SpriteBatch"); + _userDataService.RegisterType("Microsoft.Xna.Framework.Graphics.Texture2D"); + _userDataService.RegisterType("EventInput.KeyboardDispatcher"); + _userDataService.RegisterType("EventInput.KeyEventArgs"); + _userDataService.RegisterType("Microsoft.Xna.Framework.Input.Keys"); + _userDataService.RegisterType("Microsoft.Xna.Framework.Input.KeyboardState"); + + _userDataService.RegisterType("Barotrauma.Anchor"); + _userDataService.RegisterType("Barotrauma.Alignment"); + _userDataService.RegisterType("Barotrauma.Pivot"); + _userDataService.RegisterType("Barotrauma.Key"); + _userDataService.RegisterType("Barotrauma.PlayerInput"); + + + _userDataService.RegisterType("Barotrauma.Inventory+SlotReference"); + } +#elif SERVER + private void RegisterServer() + { + _userDataService.RegisterType("Barotrauma.Character+TeamChangeEventData"); + } +#endif + + public void RegisterAll() + { + RegisterShared(); +#if CLIENT + RegisterClient(); +#elif SERVER + RegisterServer(); +#endif + } + + public void Dispose() + { + IsDisposed = true; + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/_Lua/ILuaConfigService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/_Lua/ILuaConfigService.cs new file mode 100644 index 000000000..29b851fc2 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/_Lua/ILuaConfigService.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; +using Barotrauma.LuaCs.Data; +using Microsoft.Xna.Framework; + +namespace Barotrauma.LuaCs; + +public interface ILuaConfigService : ILuaService +{ + FluentResults.Result LoadSavedValueForConfig(ISettingBase setting); + bool TryGetConfig(ContentPackage package, string internalName, out T instance) where T : ISettingBase; + FluentResults.Result SaveConfigValue(ISettingBase setting); +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/_Lua/ILuaDataService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/_Lua/ILuaDataService.cs new file mode 100644 index 000000000..4e3b49a83 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/_Lua/ILuaDataService.cs @@ -0,0 +1,11 @@ +using MoonSharp.Interpreter; + +namespace Barotrauma.LuaCs; + +/// +/// Service for providing stateful functions and in-memory storage for lua functions +/// +public interface ILuaDataService : ILuaService +{ + +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/_Lua/ILuaEventService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/_Lua/ILuaEventService.cs new file mode 100644 index 000000000..3e6ed307c --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/_Lua/ILuaEventService.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; +using Barotrauma.LuaCs.Events; +using Barotrauma.LuaCs.Compatibility; + +namespace Barotrauma.LuaCs; + +public interface ILuaSafeEventService : ILuaService, ILuaCsHook +{ + /// + /// Subscribes lua scripts via for the given interface. + /// + /// + /// + /// A 'method name'=='signature action' dictionary matching the interface method list. + void Subscribe(string identifier, IDictionary callbacks) where T : class, IEvent; + /// + /// Removes a subscriber from an event that subscribed under the given identifier. + /// + /// + /// + void Unsubscribe(string eventName, string identifier); + /// + /// Send an event to all subscribers to an interface. + /// + /// Interface type. + /// Execution runner, the subscriber is provided as the first argument in the lua runner. + /// + void PublishLuaEvent(LuaCsFunc subscriberRunner) where T : class, IEvent; + + /// + /// Defines the target method name for legacy to target on new + /// interfaces. + /// + /// The legacy event name. + /// . + /// The event interface type. + /// Operation success. + /// The is null or empty. + public FluentResults.Result RegisterLuaEventAlias(string luaEventName, string targetMethod) where T : class, IEvent; +} + +public interface ILuaEventService : ILuaSafeEventService +{ + +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/_Lua/ILuaNetworkingService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/_Lua/ILuaNetworkingService.cs new file mode 100644 index 000000000..0a7447fe1 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/_Lua/ILuaNetworkingService.cs @@ -0,0 +1,6 @@ +namespace Barotrauma.LuaCs; + +public interface ILuaNetworkingService : ILuaService +{ + +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/_Lua/ILuaPackageManagementService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/_Lua/ILuaPackageManagementService.cs new file mode 100644 index 000000000..c0b11ad49 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/_Lua/ILuaPackageManagementService.cs @@ -0,0 +1,6 @@ +namespace Barotrauma.LuaCs; + +public interface ILuaPackageManagementService : ILuaService +{ + +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/_Lua/ILuaPackageService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/_Lua/ILuaPackageService.cs new file mode 100644 index 000000000..f2c1b9162 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/_Lua/ILuaPackageService.cs @@ -0,0 +1,6 @@ +namespace Barotrauma.LuaCs; + +public interface ILuaPackageService : ILuaService +{ + +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/_Lua/ILuaPatcher.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/_Lua/ILuaPatcher.cs new file mode 100644 index 000000000..3d385a9d3 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/_Lua/ILuaPatcher.cs @@ -0,0 +1,23 @@ +using Barotrauma.LuaCs.Compatibility; +using System; +using System.Reflection; +using static Barotrauma.LuaCs.Compatibility.ILuaCsHook; +using LuaCsCompatPatchFunc = Barotrauma.LuaCsPatch; + +namespace Barotrauma.LuaCs; + +public interface ILuaPatcher : IReusableService +{ + string Patch(string identifier, string className, string methodName, string[] parameterTypes, LuaCsPatchFunc patch, HookMethodType hookType = HookMethodType.Before); + string Patch(string identifier, string className, string methodName, LuaCsPatchFunc patch, HookMethodType hookType = HookMethodType.Before); + string Patch(string className, string methodName, string[] parameterTypes, LuaCsPatchFunc patch, HookMethodType hookType = HookMethodType.Before); + string Patch(string className, string methodName, LuaCsPatchFunc patch, HookMethodType hookType = HookMethodType.Before); + bool RemovePatch(string identifier, string className, string methodName, string[] parameterTypes, HookMethodType hookType); + bool RemovePatch(string identifier, string className, string methodName, HookMethodType hookType); + + void HookMethod(string identifier, MethodBase method, LuaCsCompatPatchFunc patch, HookMethodType hookType = HookMethodType.Before, IAssemblyPlugin owner = null); + public void HookMethod(string identifier, string className, string methodName, string[] parameterNames, LuaCsCompatPatchFunc patch, ILuaCsHook.HookMethodType hookMethodType = ILuaCsHook.HookMethodType.Before); + public void HookMethod(string identifier, string className, string methodName, LuaCsCompatPatchFunc patch, ILuaCsHook.HookMethodType hookMethodType = ILuaCsHook.HookMethodType.Before); + public void HookMethod(string className, string methodName, LuaCsCompatPatchFunc patch, ILuaCsHook.HookMethodType hookMethodType = ILuaCsHook.HookMethodType.Before); + public void HookMethod(string className, string methodName, string[] parameterNames, LuaCsCompatPatchFunc patch, ILuaCsHook.HookMethodType hookMethodType = ILuaCsHook.HookMethodType.Before); +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/_Lua/ILuaScriptLoader.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/_Lua/ILuaScriptLoader.cs new file mode 100644 index 000000000..cca1d482d --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/_Lua/ILuaScriptLoader.cs @@ -0,0 +1,18 @@ +using System.Collections.Immutable; +using System.Threading.Tasks; +using Barotrauma.LuaCs.Data; +using FluentResults; +using MoonSharp.Interpreter.Loaders; + +namespace Barotrauma.LuaCs; + +public interface ILuaScriptLoader : IService, IScriptLoader, ISafeStorageValidation +{ + void ClearCaches(); + /// + /// Whether caching is enabled/disabled. + /// + /// + void SetCachingPolicy(bool useCaching); + Task)>>> CacheResourcesAsync(ImmutableArray resourceInfos); +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/_Lua/ILuaService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/_Lua/ILuaService.cs new file mode 100644 index 000000000..becb54333 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/_Lua/ILuaService.cs @@ -0,0 +1,6 @@ +namespace Barotrauma.LuaCs; + +public interface ILuaService : IService +{ + +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Lua/LuaConverters.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/_Lua/LuaClasses/LuaConverters.cs similarity index 90% rename from Barotrauma/BarotraumaShared/SharedSource/LuaCs/Lua/LuaConverters.cs rename to Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/_Lua/LuaClasses/LuaConverters.cs index 4b7845e85..381ddcac2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Lua/LuaConverters.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/_Lua/LuaClasses/LuaConverters.cs @@ -7,12 +7,22 @@ using FarseerPhysics.Dynamics; using LuaCsCompatPatchFunc = Barotrauma.LuaCsPatch; using Barotrauma.Networking; using System.Collections.Immutable; +using Barotrauma.LuaCs; namespace Barotrauma { - partial class LuaCsSetup + public class LuaConverters { - private void RegisterLuaConverters() + private readonly ILuaScriptManagementService _luaScriptManagementService; + + public LuaConverters(ILuaScriptManagementService luaScriptManagementService) + { + _luaScriptManagementService = luaScriptManagementService; + } + + private DynValue Call(object function, params object[] arguments) => _luaScriptManagementService.CallFunctionSafe(function, arguments); + + public void RegisterLuaConverters() { RegisterAction(); RegisterAction(); @@ -26,41 +36,40 @@ namespace Barotrauma Script.GlobalOptions.CustomConverters.SetScriptToClrCustomConversion(DataType.Function, typeof(LuaCsAction), v => (LuaCsAction)(args => { - if (v.Function.OwnerScript == Lua) + if (v.Function.OwnerScript == _luaScriptManagementService.InternalScript) { - CallLuaFunction(v.Function, args); + Call(v.Function, args); } })); Script.GlobalOptions.CustomConverters.SetScriptToClrCustomConversion(DataType.Function, typeof(LuaCsFunc), v => (LuaCsFunc)(args => { - if (v.Function.OwnerScript == Lua) + if (v.Function.OwnerScript == _luaScriptManagementService.InternalScript) { - return CallLuaFunction(v.Function, args); + return Call(v.Function, args); } return default; })); Script.GlobalOptions.CustomConverters.SetScriptToClrCustomConversion(DataType.Function, typeof(LuaCsCompatPatchFunc), v => (LuaCsCompatPatchFunc)((self, args) => { - if (v.Function.OwnerScript == Lua) + if (v.Function.OwnerScript == _luaScriptManagementService.InternalScript) { - return CallLuaFunction(v.Function, self, args); + return Call(v.Function, self, args); } return default; })); Script.GlobalOptions.CustomConverters.SetScriptToClrCustomConversion(DataType.Function, typeof(LuaCsPatchFunc), v => (LuaCsPatchFunc)((self, args) => { - if (v.Function.OwnerScript == Lua) + if (v.Function.OwnerScript == _luaScriptManagementService.InternalScript) { - return CallLuaFunction(v.Function, self, args); + return Call(v.Function, self, args); } return default; })); - DynValue Call(object function, params object[] arguments) => CallLuaFunction(function, arguments); void RegisterHandler(Func converter) => Script.GlobalOptions.CustomConverters.SetScriptToClrCustomConversion(DataType.Function, typeof(T), v => converter(v.Function)); RegisterHandler(f => (Character.OnDeathHandler)((a1, a2) => Call(f, a1, a2))); @@ -127,6 +136,22 @@ namespace Barotrauma RegisterHandler(f => (GUITextBlock.ClickableArea.OnClickDelegate)( (a1, a2) => Call(f, a1, a2))); } + + Script.GlobalOptions.CustomConverters.SetScriptToClrCustomConversion(DataType.Function, typeof(NetMessageReceived), v => (NetMessageReceived)((arg1) => + { + if (v.Function.OwnerScript == _luaScriptManagementService.InternalScript) + { + Call(v.Function, arg1); + } + })); +#elif SERVER + Script.GlobalOptions.CustomConverters.SetScriptToClrCustomConversion(DataType.Function, typeof(NetMessageReceived), v => (NetMessageReceived)((arg1, arg2) => + { + if (v.Function.OwnerScript == _luaScriptManagementService.InternalScript) + { + Call(v.Function, arg1, arg2); + } + })); #endif Script.GlobalOptions.CustomConverters.SetScriptToClrCustomConversion(DataType.Table, typeof(Pair), v => @@ -252,7 +277,7 @@ namespace Barotrauma RegisterImmutableDictionary>(); } - private void RegisterImmutableArray() + private static void RegisterImmutableArray() { Script.GlobalOptions.CustomConverters.SetScriptToClrCustomConversion(DataType.Table, typeof(ImmutableArray), v => { @@ -260,7 +285,7 @@ namespace Barotrauma }); } - private void RegisterEither() + private static void RegisterEither() { DynValue convertEitherIntoDynValue(Either either) { @@ -298,7 +323,7 @@ namespace Barotrauma }); } - private void RegisterOption(DataType dataType) + private static void RegisterOption(DataType dataType) { Script.GlobalOptions.CustomConverters.SetClrToScriptCustomConversion(typeof(Option), (Script v, object obj) => { @@ -329,13 +354,13 @@ namespace Barotrauma Script.GlobalOptions.CustomConverters.SetScriptToClrCustomConversion(DataType.Function, typeof(Action), v => { var function = v.Function; - return (Action)(p => CallLuaFunction(function, p)); + return (Action)(p => Call(function, p)); }); Script.GlobalOptions.CustomConverters.SetScriptToClrCustomConversion(DataType.ClrFunction, typeof(Action), v => { var function = v.Function; - return (Action)(p => CallLuaFunction(function, p)); + return (Action)(p => Call(function, p)); }); } @@ -344,13 +369,13 @@ namespace Barotrauma Script.GlobalOptions.CustomConverters.SetScriptToClrCustomConversion(DataType.Function, typeof(Action), v => { var function = v.Function; - return (Action)((a1, a2) => CallLuaFunction(function, a1, a2)); + return (Action)((a1, a2) => Call(function, a1, a2)); }); Script.GlobalOptions.CustomConverters.SetScriptToClrCustomConversion(DataType.ClrFunction, typeof(Action), v => { var function = v.Function; - return (Action)((a1, a2) => CallLuaFunction(function, a1, a2)); + return (Action)((a1, a2) => Call(function, a1, a2)); }); } @@ -359,13 +384,13 @@ namespace Barotrauma Script.GlobalOptions.CustomConverters.SetScriptToClrCustomConversion(DataType.Function, typeof(Action), v => { var function = v.Function; - return (Action)(() => CallLuaFunction(function)); + return (Action)(() => Call(function)); }); Script.GlobalOptions.CustomConverters.SetScriptToClrCustomConversion(DataType.ClrFunction, typeof(Action), v => { var function = v.Function; - return (Action)(() => CallLuaFunction(function)); + return (Action)(() => Call(function)); }); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/_Lua/LuaClasses/LuaCsLogger.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/_Lua/LuaClasses/LuaCsLogger.cs new file mode 100644 index 000000000..d46b6ec27 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/_Lua/LuaClasses/LuaCsLogger.cs @@ -0,0 +1,51 @@ +using System; +using Barotrauma.Networking; +using Microsoft.Xna.Framework; +using MoonSharp.Interpreter; +using Barotrauma.LuaCs; + +namespace Barotrauma +{ + + public partial class LuaCsLogger + { + public static void HandleException(Exception ex, LuaCsMessageOrigin origin) + { + LuaCsSetup.Instance.Logger.HandleException(ex); + } + + public static void LogError(string message, LuaCsMessageOrigin origin) + { + LuaCsSetup.Instance.Logger.LogError(message); + } + + public static void LogError(string message) + { + LuaCsSetup.Instance.Logger.LogError(message); + } + + public static void LogMessage(string message, Color? serverColor = null, Color? clientColor = null) + { + LuaCsSetup.Instance.Logger.LogMessage(message, serverColor, clientColor); + } + + public static void Log(string message, Color? color = null, ServerLog.MessageType messageType = ServerLog.MessageType.ServerMessage) + { + LuaCsSetup.Instance.Logger.Log(message, color, messageType); + } + } + + partial class LuaCsSetup + { + // Compatibility with cs mods that use this method. + public static void PrintLuaError(object message) => LuaCsSetup.Instance.Logger.LogError($"{message}"); + public static void PrintCsError(object message) => LuaCsSetup.Instance.Logger.LogError($"{message}"); + public static void PrintGenericError(object message) => LuaCsSetup.Instance.Logger.LogError($"{message}"); + + internal void PrintMessage(object message) => LuaCsSetup.Instance.Logger.LogMessage($"{message}"); + + public static void PrintCsMessage(object message) => LuaCsSetup.Instance.Logger.LogMessage($"{message}"); + + internal void HandleException(Exception ex, LuaCsMessageOrigin origin) => LuaCsSetup.Instance.Logger.HandleException(ex); + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/_Lua/LuaClasses/LuaCsPerformanceCounter.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/_Lua/LuaClasses/LuaCsPerformanceCounter.cs new file mode 100644 index 000000000..8d900e460 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/_Lua/LuaClasses/LuaCsPerformanceCounter.cs @@ -0,0 +1,82 @@ +using Barotrauma.LuaCs; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; + +namespace Barotrauma +{ + public interface IPerformanceData + { + public string Identifier { get; } + public long ElapsedTicks { get; } + } + + public class SimplePerformanceData : IPerformanceData + { + public string Identifier { get; } + public long ElapsedTicks { get; } + + public SimplePerformanceData(string identifier, long elapsedTicks) + { + Identifier = identifier; + ElapsedTicks = elapsedTicks; + } + } + + public class PerformanceCounterService : IReusableService + { + public bool EnablePerformanceCounter { get; set; } = false; + + private Dictionary> _data = new Dictionary>(); + + public void AddElapsedTicks(IPerformanceData data) + { + if (!EnablePerformanceCounter) { return; } + + if (!_data.ContainsKey(data.Identifier)) + { + _data.Add(data.Identifier, new List()); + } + + _data[data.Identifier].Add(data); + + Trim(data.Identifier, 100); + } + + public T GetLatestSnapshot(string identifier) where T : class, IPerformanceData + { + if (!_data.ContainsKey(identifier)) { return default; } + + return (T)_data[identifier].Last(); + } + + public T[] GetSnapshot(string identifier, int length) where T : class, IPerformanceData, new() + { + if (!_data.ContainsKey(identifier)) { return new T[] { }; } + + length = Math.Min(length, _data[identifier].Count); + + return _data[identifier].GetRange(_data[identifier].Count - length, length).Cast().ToArray(); + } + + public void Trim(string identifier, int maxSize) + { + if (!_data.ContainsKey(identifier)) { return; } + + if (_data[identifier].Count > maxSize) + { + _data[identifier].RemoveRange(0, _data[identifier].Count - maxSize); + } + } + + public FluentResults.Result Reset() + { + _data = new Dictionary>(); + return FluentResults.Result.Ok(); + } + + public void Dispose() { } + public bool IsDisposed { get; } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/LuaCsSteam.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/_Lua/LuaClasses/LuaCsSteam.cs similarity index 100% rename from Barotrauma/BarotraumaShared/SharedSource/LuaCs/LuaCsSteam.cs rename to Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/_Lua/LuaClasses/LuaCsSteam.cs diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/LuaCsTimer.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/_Lua/LuaClasses/LuaCsTimer.cs similarity index 75% rename from Barotrauma/BarotraumaShared/SharedSource/LuaCs/LuaCsTimer.cs rename to Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/_Lua/LuaClasses/LuaCsTimer.cs index a88f7c5b5..d888c4597 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/LuaCsTimer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/_Lua/LuaClasses/LuaCsTimer.cs @@ -1,10 +1,13 @@ -using System; +using Barotrauma.LuaCs.Events; +using Barotrauma.LuaCs; +using Barotrauma.LuaCs.Compatibility; +using System; using System.Collections.Generic; using System.Diagnostics; namespace Barotrauma { - public class LuaCsTimer + public class LuaCsTimer : ILuaCsTimer, IEventUpdate { public static double Time => Timing.TotalTime; public static double GetTime() => Time; @@ -53,6 +56,16 @@ namespace Barotrauma private List timedActions = new List(); + private readonly IEventService _eventService; + private readonly ILoggerService _loggerService; + + public LuaCsTimer(IEventService eventService, ILoggerService loggerService) + { + _eventService = eventService; + _loggerService = loggerService; + SubscribeToEvents(); + } + private void AddTimer(TimedAction timedAction) { if (timedAction == null) @@ -73,35 +86,6 @@ namespace Barotrauma } } - public void Update() - { - lock (timedActions) - { - TimedAction[] timedCopy = timedActions.ToArray(); - for (int i = 0; i < timedCopy.Length; i++) - { - TimedAction timedAction = timedCopy[i]; - if (Time >= timedAction.ExecutionTime) - { - try - { - timedAction.Action(); - } - catch (Exception e) - { - LuaCsLogger.HandleException(e, LuaCsMessageOrigin.CSharpMod); - } - - timedActions.Remove(timedAction); - } - else - { - break; - } - } - } - } - public void Clear() { timedActions = new List(); @@ -118,5 +102,52 @@ namespace Barotrauma TimedAction timedAction = new TimedAction(action, 0); AddTimer(timedAction); } + + public void OnUpdate(double fixedDeltaTime) + { + lock (timedActions) + { + TimedAction[] timedCopy = timedActions.ToArray(); + for (int i = 0; i < timedCopy.Length; i++) + { + TimedAction timedAction = timedCopy[i]; + if (Time >= timedAction.ExecutionTime) + { + try + { + timedAction.Action(); + } + catch (Exception e) + { + _loggerService.HandleException(e); + } + + timedActions.Remove(timedAction); + } + else + { + break; + } + } + } + } + + private void SubscribeToEvents() + { + _eventService.Subscribe(this); + } + + public FluentResults.Result Reset() + { + SubscribeToEvents(); + return FluentResults.Result.Ok(); + } + + public void Dispose() + { + _eventService.Unsubscribe(this); + } + + public bool IsDisposed => false; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/_Lua/LuaClasses/LuaCsUtility.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/_Lua/LuaClasses/LuaCsUtility.cs new file mode 100644 index 000000000..dc7b0b835 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/_Lua/LuaClasses/LuaCsUtility.cs @@ -0,0 +1,247 @@ +using Barotrauma.Items.Components; +using Barotrauma.Networking; +using MoonSharp.Interpreter; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Net; +using System.Reflection; +using System.Xml.Linq; +using Barotrauma.LuaCs; + +namespace Barotrauma +{ + partial class LuaCsFile + { + public static bool CanReadFromPath(string path) + { + string getFullPath(string p) => System.IO.Path.GetFullPath(p).CleanUpPath(); + + path = getFullPath(path); + + bool pathStartsWith(string prefix) => path.StartsWith(prefix, StringComparison.OrdinalIgnoreCase); + + string localModsDir = getFullPath(ContentPackage.LocalModsDir); + string workshopModsDir = getFullPath(ContentPackage.WorkshopModsDir); +#if CLIENT + string tempDownloadDir = getFullPath(ModReceiver.DownloadFolder); +#endif + if (pathStartsWith(getFullPath(string.IsNullOrEmpty(GameSettings.CurrentConfig.SavePath) ? SaveUtil.DefaultSaveFolder : GameSettings.CurrentConfig.SavePath))) + return true; + + if (pathStartsWith(localModsDir)) + return true; + + if (pathStartsWith(workshopModsDir)) + return true; + +#if CLIENT + if (pathStartsWith(tempDownloadDir)) + return true; +#endif + + if (pathStartsWith(getFullPath("."))) + return true; + + return false; + } + + public static bool CanWriteToPath(string path) + { + const long LuaCsPackageId = 2559634234; + + string getFullPath(string p) => System.IO.Path.GetFullPath(p).CleanUpPath(); + + path = getFullPath(path); + + bool pathStartsWith(string prefix) => path.StartsWith(prefix, StringComparison.OrdinalIgnoreCase); + + if (pathStartsWith(getFullPath(LuaCsSetup.GetLuaCsPackage().Path))) + { + return false; + } + + if (pathStartsWith(getFullPath(string.IsNullOrEmpty(GameSettings.CurrentConfig.SavePath) ? SaveUtil.DefaultSaveFolder : GameSettings.CurrentConfig.SavePath))) + return true; + + if (pathStartsWith(getFullPath(ContentPackage.LocalModsDir))) + return true; + + if (pathStartsWith(getFullPath(ContentPackage.WorkshopModsDir))) + return true; +#if CLIENT + if (pathStartsWith(getFullPath(ModReceiver.DownloadFolder))) + return true; +#endif + + return false; + } + + public static bool IsPathAllowedException(string path, bool write = true, LuaCsMessageOrigin origin = LuaCsMessageOrigin.Unknown) + { + if (write) + { + if (CanWriteToPath(path)) + { + return true; + } + else + { + throw new Exception("File access to \"" + path + "\" not allowed."); + } + } + else + { + if (CanReadFromPath(path)) + { + return true; + } + else + { + throw new Exception("File access to \"" + path + "\" not allowed."); + } + } + } + + public static bool IsPathAllowedLuaException(string path, bool write = true) => + IsPathAllowedException(path, write, LuaCsMessageOrigin.LuaMod); + public static bool IsPathAllowedCsException(string path, bool write = true) => + IsPathAllowedException(path, write, LuaCsMessageOrigin.CSharpMod); + + public static string Read(string path) + { + if (!IsPathAllowedException(path, false)) + return ""; + + return File.ReadAllText(path); + } + + public static void Write(string path, string text) + { + if (!IsPathAllowedException(path)) + return; + + File.WriteAllText(path, text); + } + + public static void Delete(string path) + { + if (!IsPathAllowedException(path)) + return; + + File.Delete(path); + } + + public static void DeleteDirectory(string path) + { + if (!IsPathAllowedException(path)) + return; + + Directory.Delete(path, true); + } + + public static void Move(string path, string destination) + { + if (!IsPathAllowedException(path)) + return; + + if (!IsPathAllowedException(destination)) + return; + + File.Move(path, destination, true); + } + + public static FileStream OpenRead(string path) + { + if (!IsPathAllowedException(path)) + return null; + + return File.Open(path, FileMode.Open, FileAccess.Read); + } + public static FileStream OpenWrite(string path) + { + if (!IsPathAllowedException(path)) + return null; + + if (File.Exists(path)) return File.Open(path, FileMode.Truncate, FileAccess.Write); + else return File.Open(path, FileMode.Create, FileAccess.Write); + } + + public static bool Exists(string path) + { + if (!IsPathAllowedException(path, false)) + return false; + + return File.Exists(path); + } + + public static bool CreateDirectory(string path) + { + if (!IsPathAllowedException(path)) + return false; + + Directory.CreateDirectory(path); + + return true; + } + + public static bool DirectoryExists(string path) + { + if (!IsPathAllowedException(path, false)) + return false; + + return Directory.Exists(path); + } + + public static string[] GetFiles(string path) + { + if (!IsPathAllowedException(path, false)) + return null; + + return Directory.GetFiles(path); + } + + public static string[] GetDirectories(string path) + { + if (!IsPathAllowedException(path, false)) + return new string[] { }; + + return Directory.GetDirectories(path); + } + + public static string[] DirSearch(string sDir) + { + if (!IsPathAllowedException(sDir, false)) + return new string[] { }; + + List files = new List(); + + try + { + foreach (string f in Directory.GetFiles(sDir)) + { + files.Add(f); + } + + foreach (string d in Directory.GetDirectories(sDir)) + { + foreach (string f in Directory.GetFiles(d)) + { + files.Add(f); + } + DirSearch(d); + } + } + catch (System.Exception excpt) + { + Console.WriteLine(excpt.Message); + } + + return files.ToArray(); + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Lua/LuaClasses/LuaGame.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/_Lua/LuaClasses/LuaGame.cs similarity index 86% rename from Barotrauma/BarotraumaShared/SharedSource/LuaCs/Lua/LuaClasses/LuaGame.cs rename to Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/_Lua/LuaClasses/LuaGame.cs index fc515c8bd..75bc2a3dc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Lua/LuaClasses/LuaGame.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/_Lua/LuaClasses/LuaGame.cs @@ -3,14 +3,15 @@ using System.Collections.Generic; using System.Linq; using System.Xml.Linq; using Barotrauma.Items.Components; +using Barotrauma.LuaCs; using Barotrauma.Networking; using FarseerPhysics.Dynamics; using Microsoft.Xna.Framework; using MoonSharp.Interpreter; -namespace Barotrauma +namespace Barotrauma.LuaCs { - partial class LuaGame + partial class LuaGame : IReusableService { public bool IsSingleplayer => GameMain.IsSingleplayer; public bool IsMultiplayer => GameMain.IsMultiplayer; @@ -134,6 +135,8 @@ namespace Barotrauma } } + public List Commands => DebugConsole.Commands; + public bool? ForceVoice = null; public bool? ForceLocalVoice = null; @@ -146,7 +149,6 @@ namespace Barotrauma public bool disableSpamFilter = false; public bool disableDisconnectCharacter = false; public bool enableControlHusk = false; - public int MapEntityUpdateInterval { get { return MapEntity.MapEntityUpdateInterval; } @@ -269,10 +271,13 @@ namespace Barotrauma } #endif - public LuaGame() + private readonly IConsoleCommandsService _consoleCommands; + + public LuaGame(IConsoleCommandsService consoleCommands) { - LuaUserData.MakeFieldAccessible(UserData.RegisterType(typeof(GameSettings)), "currentConfig"); + UserData.RegisterType(typeof(GameSettings)); Settings = UserData.CreateStatic(typeof(GameSettings)); + _consoleCommands = consoleCommands; } public void OverrideTraitors(bool o) @@ -404,38 +409,16 @@ namespace Barotrauma return new Signal(value, stepsTaken, sender, source, power, strength); } - private List luaAddedCommand = new List(); - public IEnumerable LuaAddedCommand { get { return luaAddedCommand; } } - - public bool IsCustomCommandPermitted(Identifier command) - { - DebugConsole.Command[] permitted = new DebugConsole.Command[] - { - DebugConsole.FindCommand("cl_reloadluacs"), - DebugConsole.FindCommand("cl_lua"), - DebugConsole.FindCommand("cl_toggleluadebug"), - }; - - foreach (var consoleCommand in LuaAddedCommand.Concat(permitted.AsEnumerable())) - { - if (consoleCommand.Names.Contains(command)) - { - return true; - } - } - - return false; - } - public void RemoveCommand(string name) { - for (var i = 0; i < DebugConsole.Commands.Count; i++) + _consoleCommands.RemoveCommand(name); + + for (var i = DebugConsole.Commands.Count - 1; i >= 0; i--) { foreach (var cmdname in DebugConsole.Commands[i].Names) { if (cmdname == name) { - luaAddedCommand.Remove(DebugConsole.Commands[i]); DebugConsole.Commands.RemoveAt(i); continue; } @@ -445,25 +428,50 @@ namespace Barotrauma public void AddCommand(string name, string help, LuaCsAction onExecute, LuaCsFunc getValidArgs = null, bool isCheat = false) { - var cmd = new DebugConsole.Command(name, help, (string[] arg1) => onExecute(new object[] { arg1 }), + _consoleCommands.RegisterCommand(name, help, + (string[] args) => + { + onExecute(new object[] { args }); + }, () => { - if (getValidArgs == null) return null; + if (getValidArgs == null) { return null; } var validArgs = getValidArgs(); if (validArgs is DynValue luaValue) { return luaValue.ToObject(); } return (string[][])validArgs; - }, isCheat); - - luaAddedCommand.Add(cmd); - DebugConsole.Commands.Add(cmd); + } + ); + } + + public void AddCommand(string name, LuaCsAction onExecute, LuaCsFunc getValidArgs = null, bool isCheat = false) + { + _consoleCommands.RegisterCommand(name, "", + (string[] args) => + { + onExecute(new object[] { args }); + }, + () => + { + if (getValidArgs == null) { return null; } + var validArgs = getValidArgs(); + if (validArgs is DynValue luaValue) + { + return luaValue.ToObject(); + } + return (string[][])validArgs; + } + ); } - public List Commands => DebugConsole.Commands; + public bool IsDisposed => throw new NotImplementedException(); - public void AssignOnExecute(string names, object onExecute) => DebugConsole.AssignOnExecute(names, (string[] a) => { GameMain.LuaCs.CallLuaFunction(onExecute, new object[] { a }); }); + public void AssignOnExecute(string names, object onExecute) => DebugConsole.AssignOnExecute(names, (string[] args) => + { + LuaCsSetup.Instance.LuaScriptManagementService.CallFunctionSafe(onExecute, new object[] { args }); + }); public void SaveGame(string path) { @@ -524,7 +532,8 @@ namespace Barotrauma GameMain.Server.EndGame(); } - public void AssignOnClientRequestExecute(string names, object onExecute) => DebugConsole.AssignOnClientRequestExecute(names, (Client a, Vector2 b, string[] c) => { GameMain.LuaCs.CallLuaFunction(onExecute, new object[] { a, b, c }); }); + public void AssignOnClientRequestExecute(string names, LuaCsAction onExecute) => + _consoleCommands.AssignOnClientRequestExecute(names, (Client client, Vector2 position, string[] args) => onExecute(client, position, args)); #endif @@ -533,10 +542,18 @@ namespace Barotrauma MapEntityUpdateInterval = 1; CharacterUpdateInterval = 1; - foreach (var cmd in luaAddedCommand) - { - DebugConsole.Commands.Remove(cmd); - } + _consoleCommands.RemoveRegisteredCommands(); + } + + public FluentResults.Result Reset() + { + Stop(); + return FluentResults.Result.Ok(); + } + + public void Dispose() + { + Stop(); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Lua/LuaPlatformAccessor.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/_Lua/LuaClasses/LuaPlatformAccessor.cs similarity index 100% rename from Barotrauma/BarotraumaShared/SharedSource/LuaCs/Lua/LuaPlatformAccessor.cs rename to Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/_Lua/LuaClasses/LuaPlatformAccessor.cs diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Lua/LuaRequire.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/_Lua/LuaClasses/LuaRequire.cs similarity index 100% rename from Barotrauma/BarotraumaShared/SharedSource/LuaCs/Lua/LuaRequire.cs rename to Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/_Lua/LuaClasses/LuaRequire.cs diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Lua/LuaClasses/LuaTypes.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/_Lua/LuaClasses/LuaTypes.cs similarity index 100% rename from Barotrauma/BarotraumaShared/SharedSource/LuaCs/Lua/LuaClasses/LuaTypes.cs rename to Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/_Lua/LuaClasses/LuaTypes.cs diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/LuaCsHookCompat.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/_Lua/LuaPatcherCompat.cs similarity index 66% rename from Barotrauma/BarotraumaShared/SharedSource/LuaCs/LuaCsHookCompat.cs rename to Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/_Lua/LuaPatcherCompat.cs index 957f77ba9..c3f56a272 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/LuaCsHookCompat.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/_Lua/LuaPatcherCompat.cs @@ -1,8 +1,11 @@ -using System; +global using LuaCsHook = Barotrauma.LuaCs.Compatibility.ILuaCsHook; + +using System; using System.Linq; using System.Reflection; using HarmonyLib; using System.Collections.Generic; +using Barotrauma.LuaCs.Compatibility; using MoonSharp.Interpreter; using LuaCsCompatPatchFunc = Barotrauma.LuaCsPatch; @@ -10,30 +13,35 @@ namespace Barotrauma { // XXX: this can't be renamed because of backward compatibility with C# mods public delegate object LuaCsPatch(object self, Dictionary args); +} - partial class LuaCsHook +namespace Barotrauma.LuaCs +{ + partial class LuaPatcherService { - private Dictionary> compatHookPrefixMethods = new Dictionary>(); - private Dictionary> compatHookPostfixMethods = new Dictionary>(); + private static LuaPatcherService instance; - private static void _hookLuaCsPatch(MethodBase __originalMethod, object[] __args, object __instance, out object result, HookMethodType hookType) + private Dictionary> compatHookPrefixMethods = new Dictionary>(); + private Dictionary> compatHookPostfixMethods = new Dictionary>(); + + private static void _hookLuaCsPatch(MethodBase __originalMethod, object[] __args, object __instance, out object result, ILuaCsHook.HookMethodType hookType) { result = null; try { var funcAddr = ((long)__originalMethod.MethodHandle.GetFunctionPointer()); - HashSet<(string, LuaCsCompatPatchFunc, ACsMod)> methodSet = null; + HashSet<(string, LuaCsCompatPatchFunc)> methodSet = null; switch (hookType) { - case HookMethodType.Before: + case ILuaCsHook.HookMethodType.Before: instance.compatHookPrefixMethods.TryGetValue(funcAddr, out methodSet); break; - case HookMethodType.After: + case ILuaCsHook.HookMethodType.After: instance.compatHookPostfixMethods.TryGetValue(funcAddr, out methodSet); break; default: - throw new ArgumentException($"Invalid {nameof(HookMethodType)} enum value.", nameof(hookType)); + throw new ArgumentException($"Invalid {nameof(ILuaCsHook.HookMethodType)} enum value.", nameof(hookType)); } if (methodSet != null) @@ -45,40 +53,31 @@ namespace Barotrauma args.Add(@params[i].Name, __args[i]); } - var outOfSocpe = new HashSet<(string, LuaCsCompatPatchFunc, ACsMod)>(); foreach (var tuple in methodSet) { - if (tuple.Item3 != null && tuple.Item3.IsDisposed) + var _result = tuple.Item2(__instance, args); + if (_result != null) { - outOfSocpe.Add(tuple); - } - else - { - var _result = tuple.Item2(__instance, args); - if (_result != null) + if (_result is DynValue res) { - if (_result is DynValue res) + if (!res.IsNil()) { - if (!res.IsNil()) + if (__originalMethod is MethodInfo mi && mi.ReturnType != typeof(void)) { - if (__originalMethod is MethodInfo mi && mi.ReturnType != typeof(void)) - { - result = res.ToObject(mi.ReturnType); - } - else - { - result = res.ToObject(); - } + result = res.ToObject(mi.ReturnType); + } + else + { + result = res.ToObject(); } } - else - { - result = _result; - } + } + else + { + result = _result; } } } - foreach (var tuple in outOfSocpe) { methodSet.Remove(tuple); } } } catch (Exception ex) @@ -91,16 +90,16 @@ namespace Barotrauma private static bool HookLuaCsPatchPrefix(MethodBase __originalMethod, object[] __args, object __instance) { - _hookLuaCsPatch(__originalMethod, __args, __instance, out object result, HookMethodType.Before); + _hookLuaCsPatch(__originalMethod, __args, __instance, out object result, ILuaCsHook.HookMethodType.Before); return result == null; } private static void HookLuaCsPatchPostfix(MethodBase __originalMethod, object[] __args, object __instance) => - _hookLuaCsPatch(__originalMethod, __args, __instance, out object _, HookMethodType.After); + _hookLuaCsPatch(__originalMethod, __args, __instance, out object _, ILuaCsHook.HookMethodType.After); private static bool HookLuaCsPatchRetPrefix(MethodBase __originalMethod, object[] __args, ref object __result, object __instance) { - _hookLuaCsPatch(__originalMethod, __args, __instance, out object result, HookMethodType.Before); + _hookLuaCsPatch(__originalMethod, __args, __instance, out object result, ILuaCsHook.HookMethodType.Before); if (result != null) { __result = result; @@ -111,17 +110,18 @@ namespace Barotrauma private static void HookLuaCsPatchRetPostfix(MethodBase __originalMethod, object[] __args, ref object __result, object __instance) { - _hookLuaCsPatch(__originalMethod, __args, __instance, out object result, HookMethodType.After); + _hookLuaCsPatch(__originalMethod, __args, __instance, out object result, ILuaCsHook.HookMethodType.After); if (result != null) __result = result; } - private static MethodInfo _miHookLuaCsPatchPrefix = typeof(LuaCsHook).GetMethod("HookLuaCsPatchPrefix", BindingFlags.NonPublic | BindingFlags.Static); - private static MethodInfo _miHookLuaCsPatchPostfix = typeof(LuaCsHook).GetMethod("HookLuaCsPatchPostfix", BindingFlags.NonPublic | BindingFlags.Static); - private static MethodInfo _miHookLuaCsPatchRetPrefix = typeof(LuaCsHook).GetMethod("HookLuaCsPatchRetPrefix", BindingFlags.NonPublic | BindingFlags.Static); - private static MethodInfo _miHookLuaCsPatchRetPostfix = typeof(LuaCsHook).GetMethod("HookLuaCsPatchRetPostfix", BindingFlags.NonPublic | BindingFlags.Static); + private static MethodInfo _miHookLuaCsPatchPrefix = typeof(LuaPatcherService).GetMethod("HookLuaCsPatchPrefix", BindingFlags.NonPublic | BindingFlags.Static); + private static MethodInfo _miHookLuaCsPatchPostfix = typeof(LuaPatcherService).GetMethod("HookLuaCsPatchPostfix", BindingFlags.NonPublic | BindingFlags.Static); + private static MethodInfo _miHookLuaCsPatchRetPrefix = typeof(LuaPatcherService).GetMethod("HookLuaCsPatchRetPrefix", BindingFlags.NonPublic | BindingFlags.Static); + private static MethodInfo _miHookLuaCsPatchRetPostfix = typeof(LuaPatcherService).GetMethod("HookLuaCsPatchRetPostfix", BindingFlags.NonPublic | BindingFlags.Static); // TODO: deprecate this - public void HookMethod(string identifier, MethodBase method, LuaCsCompatPatchFunc patch, HookMethodType hookType = HookMethodType.Before, ACsMod owner = null) + + public void HookMethod(string identifier, MethodBase method, LuaCsCompatPatchFunc patch, ILuaCsHook.HookMethodType hookType = ILuaCsHook.HookMethodType.Before, IAssemblyPlugin owner = null) { if (identifier == null || method == null || patch == null) { @@ -133,7 +133,7 @@ namespace Barotrauma var funcAddr = ((long)method.MethodHandle.GetFunctionPointer()); var patches = Harmony.GetPatchInfo(method); - if (hookType == HookMethodType.Before) + if (hookType == ILuaCsHook.HookMethodType.Before) { if (method is MethodInfo mi && mi.ReturnType != typeof(void)) { @@ -150,22 +150,22 @@ namespace Barotrauma } } - if (compatHookPrefixMethods.TryGetValue(funcAddr, out HashSet<(string, LuaCsCompatPatchFunc, ACsMod)> methodSet)) + if (compatHookPrefixMethods.TryGetValue(funcAddr, out HashSet<(string, LuaCsCompatPatchFunc)> methodSet)) { if (identifier != "") { methodSet.RemoveWhere(tuple => tuple.Item1 == identifier); } - methodSet.Add((identifier, patch, owner)); + methodSet.Add((identifier, patch)); } else if (patch != null) { - compatHookPrefixMethods.Add(funcAddr, new HashSet<(string, LuaCsCompatPatchFunc, ACsMod)>() { (identifier, patch, owner) }); + compatHookPrefixMethods.Add(funcAddr, new HashSet<(string, LuaCsCompatPatchFunc)>() { (identifier, patch) }); } } - else if (hookType == HookMethodType.After) + else if (hookType == ILuaCsHook.HookMethodType.After) { if (method is MethodInfo mi && mi.ReturnType != typeof(void)) { @@ -182,22 +182,22 @@ namespace Barotrauma } } - if (compatHookPostfixMethods.TryGetValue(funcAddr, out HashSet<(string, LuaCsCompatPatchFunc, ACsMod)> methodSet)) + if (compatHookPostfixMethods.TryGetValue(funcAddr, out HashSet<(string, LuaCsCompatPatchFunc)> methodSet)) { if (identifier != "") { methodSet.RemoveWhere(tuple => tuple.Item1 == identifier); } - methodSet.Add((identifier, patch, owner)); + methodSet.Add((identifier, patch)); } else if (patch != null) { - compatHookPostfixMethods.Add(funcAddr, new HashSet<(string, LuaCsCompatPatchFunc, ACsMod)>() { (identifier, patch, owner) }); + compatHookPostfixMethods.Add(funcAddr, new HashSet<(string, LuaCsCompatPatchFunc)>() { (identifier, patch) }); } } } - protected void HookMethod(string identifier, string className, string methodName, string[] parameterNames, LuaCsCompatPatchFunc patch, HookMethodType hookMethodType = HookMethodType.Before) + public void HookMethod(string identifier, string className, string methodName, string[] parameterNames, LuaCsCompatPatchFunc patch, ILuaCsHook.HookMethodType hookMethodType = ILuaCsHook.HookMethodType.Before) { var method = ResolveMethod(className, methodName, parameterNames); if (method == null) return; @@ -207,26 +207,26 @@ namespace Barotrauma } HookMethod(identifier, method, patch, hookMethodType); } - protected void HookMethod(string identifier, string className, string methodName, LuaCsCompatPatchFunc patch, HookMethodType hookMethodType = HookMethodType.Before) => + public void HookMethod(string identifier, string className, string methodName, LuaCsCompatPatchFunc patch, ILuaCsHook.HookMethodType hookMethodType = ILuaCsHook.HookMethodType.Before) => HookMethod(identifier, className, methodName, null, patch, hookMethodType); - protected void HookMethod(string className, string methodName, LuaCsCompatPatchFunc patch, HookMethodType hookMethodType = HookMethodType.Before) => + public void HookMethod(string className, string methodName, LuaCsCompatPatchFunc patch, ILuaCsHook.HookMethodType hookMethodType = ILuaCsHook.HookMethodType.Before) => HookMethod("", className, methodName, null, patch, hookMethodType); - protected void HookMethod(string className, string methodName, string[] parameterNames, LuaCsCompatPatchFunc patch, HookMethodType hookMethodType = HookMethodType.Before) => + public void HookMethod(string className, string methodName, string[] parameterNames, LuaCsCompatPatchFunc patch, ILuaCsHook.HookMethodType hookMethodType = ILuaCsHook.HookMethodType.Before) => HookMethod("", className, methodName, parameterNames, patch, hookMethodType); - public void UnhookMethod(string identifier, MethodBase method, HookMethodType hookType = HookMethodType.Before) + public void UnhookMethod(string identifier, MethodBase method, ILuaCsHook.HookMethodType hookType = ILuaCsHook.HookMethodType.Before) { var funcAddr = (long)method.MethodHandle.GetFunctionPointer(); - Dictionary> methods; - if (hookType == HookMethodType.Before) methods = compatHookPrefixMethods; - else if (hookType == HookMethodType.After) methods = compatHookPostfixMethods; + Dictionary> methods; + if (hookType == ILuaCsHook.HookMethodType.Before) methods = compatHookPrefixMethods; + else if (hookType == ILuaCsHook.HookMethodType.After) methods = compatHookPostfixMethods; else throw null; if (methods.ContainsKey(funcAddr)) methods[funcAddr]?.RemoveWhere(t => t.Item1 == identifier); } - protected void UnhookMethod(string identifier, string className, string methodName, string[] parameterNames, HookMethodType hookType = HookMethodType.Before) + protected void UnhookMethod(string identifier, string className, string methodName, string[] parameterNames, ILuaCsHook.HookMethodType hookType = ILuaCsHook.HookMethodType.Before) { var method = ResolveMethod(className, methodName, parameterNames); if (method == null) return; diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/LuaCsHook.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/_Lua/LuaPatcherService.cs similarity index 57% rename from Barotrauma/BarotraumaShared/SharedSource/LuaCs/LuaCsHook.cs rename to Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/_Lua/LuaPatcherService.cs index cce9de419..0bee7c96c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/LuaCsHook.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/_Lua/LuaPatcherService.cs @@ -1,4 +1,11 @@ -using System; +using Barotrauma.LuaCs; +using HarmonyLib; +using Microsoft.Xna.Framework; +using MoonSharp.Interpreter; +using MoonSharp.Interpreter.Interop; +using Sigil; +using Sigil.NonGeneric; +using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; @@ -8,415 +15,18 @@ using System.Reflection; using System.Reflection.Emit; using System.Text; using System.Text.RegularExpressions; -using HarmonyLib; -using Microsoft.Xna.Framework; -using MoonSharp.Interpreter; -using MoonSharp.Interpreter.Interop; -using Sigil; -using Sigil.NonGeneric; namespace Barotrauma { public delegate void LuaCsAction(params object[] args); public delegate object LuaCsFunc(params object[] args); - public delegate DynValue LuaCsPatchFunc(object instance, LuaCsHook.ParameterTable ptable); + public delegate DynValue LuaCsPatchFunc(object instance, LuaPatcherService.ParameterTable ptable); +} - internal static class SigilExtensions +namespace Barotrauma.LuaCs +{ + public partial class LuaPatcherService : ILuaPatcher { - /// - /// Puts a type on the stack, as a object instead of a - /// runtime type token. - /// - /// The IL emitter. - /// The type to put on the stack. - public static void LoadType(this Emit il, Type type) - { - if (type == null) throw new ArgumentNullException(nameof(type)); - il.LoadConstant(type); // ldtoken - // This converts the type token into a Type object - il.Call(typeof(Type).GetMethod( - name: nameof(Type.GetTypeFromHandle), - bindingAttr: BindingFlags.Public | BindingFlags.Static, - binder: null, - types: new Type[] { typeof(RuntimeTypeHandle) }, - modifiers: null)); - } - - /// - /// Converts the value on the stack to . - /// - /// The IL emitter. - /// The type of the value on the stack. - public static void ToObject(this Emit il, Type type) - { - if (type == null) throw new ArgumentNullException(nameof(type)); - il.DerefIfByRef(ref type); - if (type.IsValueType) - { - il.Box(type); - } - else if (type != typeof(object)) - { - il.CastClass(); - } - } - - /// - /// Deferences the value on stack if the provided type is ByRef. - /// - /// The IL emitter. - /// The type to check if ByRef. - public static void DerefIfByRef(this Emit il, Type type) => il.DerefIfByRef(ref type); - - /// - /// Deferences the value on stack if the provided type is ByRef. - /// - /// The IL emitter. - /// The type to check if ByRef. - public static void DerefIfByRef(this Emit il, ref Type type) - { - if (type == null) throw new ArgumentNullException(nameof(type)); - if (type.IsByRef) - { - type = type.GetElementType(); - if (type.IsValueType) - { - il.LoadObject(type); - } - else - { - il.LoadIndirect(type); - } - } - } - - // Copied from https://github.com/evilfactory/moonsharp/blob/5264656c6442e783f3c75082cce69a93d66d4cc0/src/MoonSharp.Interpreter/Interop/Converters/ScriptToClrConversions.cs#L79-L99 - private static MethodInfo GetImplicitOperatorMethod(Type baseType, Type targetType) - { - try - { - return Expression.Convert(Expression.Parameter(baseType, null), targetType).Method; - } - catch - { - if (baseType.BaseType != null) - { - return GetImplicitOperatorMethod(baseType.BaseType, targetType); - } - - if (targetType.BaseType != null) - { - return GetImplicitOperatorMethod(baseType, targetType.BaseType); - } - - return null; - } - } - - /// - /// Loads a local variable and casts it to the target type. - /// - /// The IL emitter. - /// The value to cast. Must be of type . - /// The type to cast into. - public static void LoadLocalAndCast(this Emit il, Local value, Type targetType) - { - if (value == null) throw new ArgumentNullException(nameof(value)); - if (targetType == null) throw new ArgumentNullException(nameof(targetType)); - if (value.LocalType != typeof(object)) - { - throw new ArgumentException($"Expected local type {typeof(object)}; got {value.LocalType}.", nameof(value)); - } - - var guid = Guid.NewGuid().ToString("N"); - - if (targetType.IsByRef) - { - targetType = targetType.GetElementType(); - } - - // IL: var baseType = value.GetType(); - var baseType = il.DeclareLocal(typeof(Type), $"cast_baseType_{guid}"); - il.LoadLocal(value); - il.Call(typeof(object).GetMethod("GetType")); - il.StoreLocal(baseType); - - // IL: var implicitOperatorMethod = SigilExtensions.GetImplicitOperatorMethod(baseType, ); - var implicitOperatorMethod = il.DeclareLocal(typeof(MethodInfo), $"cast_implicitOperatorMethod_{guid}"); - il.LoadLocal(baseType); - il.LoadType(targetType); - il.Call(typeof(SigilExtensions).GetMethod(nameof(GetImplicitOperatorMethod), BindingFlags.NonPublic | BindingFlags.Static)); - il.StoreLocal(implicitOperatorMethod); - - // IL: castValue; - var castValue = il.DeclareLocal(targetType, $"cast_castValue_{guid}"); - - // IL: if (implicitConversionMethod != null) - il.LoadLocal(implicitOperatorMethod); - il.Branch((il) => - { - // IL: var methodInvokeParams = new object[1]; - var methodInvokeParams = il.DeclareLocal(typeof(object[]), $"cast_methodInvokeParams_{guid}"); - il.LoadConstant(1); - il.NewArray(typeof(object)); - il.StoreLocal(methodInvokeParams); - - // IL: methodInvokeParams[0] = value; - il.LoadLocal(methodInvokeParams); - il.LoadConstant(0); - il.LoadLocal(value); - il.StoreElement(); - - // IL: castValue = ()implicitConversionMethod.Invoke(null, methodInvokeParams); - il.LoadLocal(implicitOperatorMethod); - il.LoadNull(); // first parameter is null because implicit cast operators are static - il.LoadLocal(methodInvokeParams); - il.Call(typeof(MethodInfo).GetMethod("Invoke", new[] { typeof(object), typeof(object[]) })); - if (targetType.IsValueType) - { - il.UnboxAny(targetType); - } - else - { - il.CastClass(targetType); - } - il.StoreLocal(castValue); - }, - (il) => - { - // IL: castValue = ()value; - il.LoadLocal(value); - if (targetType.IsValueType) - { - il.UnboxAny(targetType); - } - else - { - il.CastClass(targetType); - } - il.StoreLocal(castValue); - }); - - il.LoadLocal(castValue); - } - - /// - /// Emits a call to . - /// - /// The IL emitter. - /// The string format. - /// The local variables passed to string.Format. - public static void FormatString(this Emit il, string format, params Local[] args) - { - if (format == null) throw new ArgumentNullException(nameof(format)); - if (args == null) throw new ArgumentNullException(nameof(args)); - - var guid = Guid.NewGuid().ToString("N"); - - var listType = typeof(List<>).MakeGenericType(typeof(object)); - var list = il.DeclareLocal(listType, $"formatString_list_{guid}"); - il.NewObject(listType); - il.StoreLocal(list); - - foreach (var arg in args) - { - il.LoadLocal(list); - il.LoadLocal(arg); - il.ToObject(arg.LocalType); - il.CallVirtual(listType.GetMethod("Add", new[] { typeof(object) })); - } - - var arr = il.DeclareLocal($"formatString_arr_{guid}"); - il.LoadLocal(list); - il.CallVirtual(listType.GetMethod("ToArray", new Type[0])); - il.StoreLocal(arr); - - il.LoadConstant(format); - il.LoadLocal(arr); - il.Call(typeof(string).GetMethod("Format", new[] { typeof(string), typeof(object[]) })); - } - - /// - /// Emits a call to . - /// - /// The IL emitter. - /// The message to print. - public static void NewMessage(this Emit il, string message) - { - var newMessage = typeof(DebugConsole).GetMethod( - name: nameof(DebugConsole.NewMessage), - bindingAttr: BindingFlags.Public | BindingFlags.Static, - binder: null, - types: new Type[] { typeof(string), typeof(Color?), typeof(bool) }, - modifiers: null); - il.LoadConstant(message); - il.Call(typeof(Color).GetProperty(nameof(Color.LightBlue), BindingFlags.Public | BindingFlags.Static).GetGetMethod()); - il.LoadConstant(false); - il.Call(newMessage); - } - - /// - /// Emits a call to , - /// using the string on the stack. - /// - /// The IL emitter. - public static void NewMessage(this Emit il) - { - var newMessage = typeof(DebugConsole).GetMethod( - name: nameof(DebugConsole.NewMessage), - bindingAttr: BindingFlags.Public | BindingFlags.Static, - binder: null, - types: new Type[] { typeof(string), typeof(Color?), typeof(bool) }, - modifiers: null); - il.Call(typeof(Color).GetProperty(nameof(Color.LightBlue), BindingFlags.Public | BindingFlags.Static).GetGetMethod()); - il.LoadConstant(false); - il.Call(newMessage); - } - - /// - /// Emits a foreach loop that iterates over an local variable. - /// - /// The type of elements in the enumerable. - /// The IL emitter. - /// The enumerable. - /// The body of code to run on each iteration. - public static void ForEachEnumerable(this Emit il, Local enumerable, Action action) - { - if (enumerable == null) throw new ArgumentNullException(nameof(enumerable)); - if (action == null) throw new ArgumentNullException(nameof(action)); - if (!typeof(IEnumerable).IsAssignableFrom(enumerable.LocalType)) - { - throw new ArgumentException($"Expected local type {typeof(IEnumerator)}; got {enumerable.LocalType}.", nameof(enumerable)); - } - - var guid = Guid.NewGuid().ToString("N"); - - var enumerator = il.DeclareLocal>($"forEachEnumerable_enumerator_{guid}"); - il.LoadLocal(enumerable); - il.CallVirtual(typeof(IEnumerable).GetMethod("GetEnumerator")); - il.StoreLocal(enumerator); - ForEachEnumerator(il, enumerator, action); - } - - /// - /// Emits a foreach loop that iterates over an local variable. - /// - /// The type of elements in the enumerable. - /// The IL emitter. - /// The enumerator. - /// The body of code to run on each iteration. - public static void ForEachEnumerator(this Emit il, Local enumerator, Action action) - { - if (enumerator == null) throw new ArgumentNullException(nameof(enumerator)); - if (action == null) throw new ArgumentNullException(nameof(action)); - if (!typeof(IEnumerator).IsAssignableFrom(enumerator.LocalType)) - { - throw new ArgumentException($"Expected local type {typeof(IEnumerator)}; got {enumerator.LocalType}.", nameof(enumerator)); - } - - var guid = Guid.NewGuid().ToString("N"); - var labelLoopStart = il.DefineLabel($"forEach_loopStart_{guid}"); - var labelMoveNext = il.DefineLabel($"forEach_moveNext_{guid}"); - var labelLeave = il.DefineLabel($"forEach_leave_{guid}"); - - il.BeginExceptionBlock(out var exceptionBlock); - il.Branch(labelMoveNext); // MoveNext() needs to be called at least once before iterating - il.MarkLabel(labelLoopStart); - - // IL: var current = enumerator.Current; - var current = il.DeclareLocal($"forEachEnumerator_current_{guid}"); - il.LoadLocal(enumerator); - il.CallVirtual(enumerator.LocalType.GetProperty("Current").GetGetMethod()); - il.StoreLocal(current); - - action(il, current, labelLeave); - - il.MarkLabel(labelMoveNext); - il.LoadLocal(enumerator); - il.CallVirtual(typeof(IEnumerator).GetMethod("MoveNext")); - il.BranchIfTrue(labelLoopStart); // loop if MoveNext() returns true - - // IL: finally { enumerator.Dispose(); } - il.BeginFinallyBlock(exceptionBlock, out var finallyBlock); - il.LoadLocal(enumerator); - il.CallVirtual(typeof(IDisposable).GetMethod("Dispose")); - il.EndFinallyBlock(finallyBlock); - - il.EndExceptionBlock(exceptionBlock); - - il.MarkLabel(labelLeave); - } - - /// - /// Emits a branch that only executes if the last value on the stack - /// is truthy (e.g. non-null references, 1, etc). - /// - /// The IL emitter. - /// The body of code to run if the value is truthy. - public static void If(this Emit il, Action action) - { - if (action == null) throw new ArgumentNullException(nameof(action)); - il.Branch(@if: action); - } - - /// - /// Emits a branch that only executes if the last value on the stack - /// is falsy (e.g. null references, 0, etc). - /// - /// The IL emitter. - /// The body of code to run if the value is falsy. - public static void IfNot(this Emit il, Action action) - { - if (action == null) throw new ArgumentNullException(nameof(action)); - il.Branch(@else: action); - } - - /// - /// Emits two branches that diverge based on a condition -- analogous - /// to an if-else statement. If either - /// or are omitted, it behaves the same as - /// - /// and . - /// - /// The IL emitter. - /// The body of code to run if the value is truthy. - /// The body of code to run if the value is falsy. - public static void Branch(this Emit il, Action @if = null, Action @else = null) - { - if (@if == null && @else == null) throw new ArgumentException("At least one of the two branches must be defined."); - - var guid = Guid.NewGuid().ToString("N"); - var labelEnd = il.DefineLabel($"branch_end_{guid}"); - if (@if != null && @else != null) - { - var labelElse = il.DefineLabel($"branch_else_{guid}"); - il.BranchIfFalse(labelElse); - @if(il); - il.Branch(labelEnd); - il.MarkLabel(labelElse); - @else(il); - } - else if (@if != null) - { - il.BranchIfFalse(labelEnd); - @if(il); - } - else - { - il.BranchIfTrue(labelEnd); - @else(il); - } - il.MarkLabel(labelEnd); - } - } - - public partial class LuaCsHook - { - public enum HookMethodType - { - Before, After - } - private class LuaCsHookCallback { public string name; @@ -512,38 +122,6 @@ namespace Barotrauma public Dictionary ModifiedParameters { get; } = new Dictionary(); } - private static readonly string[] prohibitedHooks = - { - "Barotrauma.Lua", - "Barotrauma.Cs", - "Barotrauma.ContentPackageManager", - }; - - private static void ValidatePatchTarget(MethodBase method) - { - if (prohibitedHooks.Any(h => method.DeclaringType.FullName.StartsWith(h))) - { - throw new ArgumentException("Hooks into the modding environment are prohibited."); - } - } - - private static string NormalizeIdentifier(string identifier) - { - return identifier?.Trim().ToLowerInvariant(); - } - - private Harmony harmony; - - private Lazy patchModuleBuilder; - - private readonly Dictionary> hookFunctions = new Dictionary>(); - - private readonly Dictionary registeredPatches = new Dictionary(); - - private LuaCsSetup luaCs; - - private static LuaCsHook instance; - private struct MethodKey : IEquatable { public ModuleHandle ModuleHandle { get; set; } @@ -582,21 +160,32 @@ namespace Barotrauma }; } - internal LuaCsHook(LuaCsSetup luaCs) + private static readonly string[] prohibitedHooks = + { + "Barotrauma.Lua", + "Barotrauma.Cs", + "Barotrauma.ContentPackageManager", + }; + + + private Harmony harmony; + private Lazy patchModuleBuilder; + private readonly Dictionary registeredPatches = new Dictionary(); + + public LuaPatcherService() { instance = this; - this.luaCs = luaCs; - } - public void Initialize() - { harmony = new Harmony("LuaCsForBarotrauma"); patchModuleBuilder = new Lazy(CreateModuleBuilder); UserData.RegisterType(); - var hookType = UserData.RegisterType(); + + // whats this for? + /* + var hookType = UserData.RegisterType(); var hookDesc = (StandardUserDataDescriptor)hookType; - typeof(LuaCsHook).GetMethods(BindingFlags.NonPublic | BindingFlags.Instance).ToList().ForEach(m => { + typeof(EventService).GetMethods(BindingFlags.NonPublic | BindingFlags.Instance).ToList().ForEach(m => { if ( m.Name.Contains("HookMethod") || m.Name.Contains("UnhookMethod") || @@ -607,6 +196,20 @@ namespace Barotrauma hookDesc.AddMember(m.Name, new MethodMemberDescriptor(m, InteropAccessMode.Default)); } }); + */ + } + + private static void ValidatePatchTarget(MethodBase method) + { + if (prohibitedHooks.Any(h => method.DeclaringType.FullName.StartsWith(h))) + { + throw new ArgumentException("Hooks into the modding environment are prohibited."); + } + } + + private static string NormalizeIdentifier(string identifier) + { + return identifier?.Trim().ToLowerInvariant(); } private ModuleBuilder CreateModuleBuilder() @@ -689,158 +292,9 @@ namespace Barotrauma return moduleBuilder; } - public void Add(string name, LuaCsFunc func, ACsMod owner = null) => Add(name, name, func, owner); - - public void Add(string name, string identifier, LuaCsFunc func, ACsMod owner = null) - { - if (name == null) throw new ArgumentNullException(nameof(name)); - if (identifier == null) throw new ArgumentNullException(nameof(identifier)); - if (func == null) throw new ArgumentNullException(nameof(func)); - - name = NormalizeIdentifier(name); - identifier = NormalizeIdentifier(identifier); - - if (!hookFunctions.ContainsKey(name)) - { - hookFunctions.Add(name, new Dictionary()); - } - - hookFunctions[name][identifier] = (new LuaCsHookCallback(name, identifier, func), owner); - } - - public bool Exists(string name, string identifier) - { - if (name == null) throw new ArgumentNullException(nameof(name)); - if (identifier == null) throw new ArgumentNullException(nameof(identifier)); - - name = NormalizeIdentifier(name); - identifier = NormalizeIdentifier(identifier); - - if (!hookFunctions.ContainsKey(name)) - { - return false; - } - - return hookFunctions[name].ContainsKey(identifier); - } - - public void Remove(string name, string identifier) - { - if (name == null) throw new ArgumentNullException(nameof(name)); - if (identifier == null) throw new ArgumentNullException(nameof(identifier)); - - name = NormalizeIdentifier(name); - identifier = NormalizeIdentifier(identifier); - - if (hookFunctions.ContainsKey(name) && hookFunctions[name].ContainsKey(identifier)) - { - hookFunctions[name].Remove(identifier); - } - } - - public void Clear() - { - harmony?.UnpatchSelf(); - - foreach (var (_, patch) in registeredPatches) - { - // Remove references stored in our dynamic types so the generated - // assembly can be garbage-collected. - patch.HarmonyPrefixMethod.DeclaringType - .GetField(FIELD_LUACS, BindingFlags.Public | BindingFlags.Static) - .SetValue(null, null); - patch.HarmonyPostfixMethod.DeclaringType - .GetField(FIELD_LUACS, BindingFlags.Public | BindingFlags.Static) - .SetValue(null, null); - } - - hookFunctions.Clear(); - registeredPatches.Clear(); - patchModuleBuilder = null; - - compatHookPrefixMethods.Clear(); - compatHookPostfixMethods.Clear(); - } - - private Stopwatch performanceMeasurement = new Stopwatch(); - - [MoonSharpHidden] - public T Call(string name, params object[] args) - { - if (name == null) throw new ArgumentNullException(name); - if (args == null) args = new object[0]; - - name = NormalizeIdentifier(name); - if (!hookFunctions.ContainsKey(name)) return default; - - T lastResult = default; - - var hooks = hookFunctions[name].ToArray(); - foreach ((string key, var tuple) in hooks) - { - if (tuple.Item2 != null && tuple.Item2.IsDisposed) - { - hookFunctions[name].Remove(key); - continue; - } - - try - { - if (luaCs.PerformanceCounter.EnablePerformanceCounter) - { - performanceMeasurement.Start(); - } - - var result = tuple.Item1.func(args); - - if (result is DynValue luaResult) - { - if (luaResult.Type == DataType.Tuple) - { - bool replaceNil = luaResult.Tuple.Length > 1 && luaResult.Tuple[1].CastToBool(); - - if (!luaResult.Tuple[0].IsNil() || replaceNil) - { - lastResult = luaResult.ToObject(); - } - } - else if (!luaResult.IsNil()) - { - lastResult = luaResult.ToObject(); - } - } - else - { - lastResult = (T)result; - } - - if (luaCs.PerformanceCounter.EnablePerformanceCounter) - { - performanceMeasurement.Stop(); - luaCs.PerformanceCounter.SetHookElapsedTicks(name, key, performanceMeasurement.ElapsedTicks); - performanceMeasurement.Reset(); - } - } - catch (Exception e) - { - var argsSb = new StringBuilder(); - foreach (var arg in args) - { - argsSb.Append(arg + " "); - } - LuaCsLogger.LogError($"Error in Hook '{name}'->'{key}', with args '{argsSb}':\n{e}", LuaCsMessageOrigin.Unknown); - LuaCsLogger.HandleException(e, LuaCsMessageOrigin.Unknown); - } - } - - return lastResult; - } - - public object Call(string name, params object[] args) => Call(name, args); - private static MethodBase ResolveMethod(string className, string methodName, string[] parameters) { - var classType = LuaUserData.GetType(className); + var classType = LuaCsSetup.Instance.PluginManagementService.GetType(className); if (classType == null) throw new ScriptRuntimeException($"invalid class name '{className}'"); const BindingFlags BINDING_FLAGS = BindingFlags.Static | BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; @@ -855,7 +309,7 @@ namespace Barotrauma for (int i = 0; i < parameters.Length; i++) { - Type type = LuaUserData.GetType(parameters[i]); + Type type = LuaCsSetup.Instance.PluginManagementService.GetType(parameters[i]); if (type == null) { throw new ScriptRuntimeException($"invalid parameter type '{parameters[i]}'"); @@ -930,10 +384,12 @@ namespace Barotrauma private const string FIELD_LUACS = "LuaCs"; + public bool IsDisposed { get; private set; } + // If you need to debug this: // - use https://sharplab.io ; it's a very useful for resource for writing IL by hand. // - use il.NewMessage("") or il.WriteLine("") to see where the IL crashes at runtime. - private MethodInfo CreateDynamicHarmonyPatch(string identifier, MethodBase original, HookMethodType hookType) + private MethodInfo CreateDynamicHarmonyPatch(string identifier, MethodBase original, LuaCsHook.HookMethodType hookType) { var parameters = new List { @@ -968,9 +424,9 @@ namespace Barotrauma var luaCsField = typeBuilder.DefineField(FIELD_LUACS, typeof(LuaCsSetup), FieldAttributes.Public | FieldAttributes.Static); - var methodName = hookType == HookMethodType.Before ? "HarmonyPrefix" : "HarmonyPostfix"; + var methodName = hookType == LuaCsHook.HookMethodType.Before ? "HarmonyPrefix" : "HarmonyPostfix"; var il = Emit.BuildMethod( - returnType: hookType == HookMethodType.Before ? typeof(bool) : typeof(void), + returnType: hookType == LuaCsHook.HookMethodType.Before ? typeof(bool) : typeof(void), parameterTypes: parameters.Select(x => x.HarmonyPatchParamType).ToArray(), type: typeBuilder, name: methodName, @@ -996,8 +452,8 @@ namespace Barotrauma // IL: var patchExists = instance.registeredPatches.TryGetValue(patchKey, out MethodPatches patches) var patchExists = il.DeclareLocal("patchExists"); var patches = il.DeclareLocal("patches"); - il.LoadField(typeof(LuaCsHook).GetField(nameof(instance), BindingFlags.NonPublic | BindingFlags.Static)); - il.LoadField(typeof(LuaCsHook).GetField(nameof(registeredPatches), BindingFlags.NonPublic | BindingFlags.Instance)); + il.LoadField(typeof(LuaPatcherService).GetField(nameof(instance), BindingFlags.NonPublic | BindingFlags.Static)); + il.LoadField(typeof(LuaPatcherService).GetField(nameof(registeredPatches), BindingFlags.NonPublic | BindingFlags.Instance)); il.LoadLocal(patchKey); il.LoadLocalAddress(patches); // out parameter il.Call(typeof(Dictionary).GetMethod("TryGetValue")); @@ -1039,7 +495,7 @@ namespace Barotrauma il.NewObject(typeof(ParameterTable), typeof(Dictionary)); il.StoreLocal(ptable); - if (hasReturnType && hookType == HookMethodType.After) + if (hasReturnType && hookType == LuaCsHook.HookMethodType.After) { // IL: ptable.OriginalReturnValue = __result; il.LoadLocal(ptable); @@ -1052,7 +508,7 @@ namespace Barotrauma var enumerator = il.DeclareLocal>("enumerator"); il.LoadLocal(patches); il.CallVirtual(typeof(PatchedMethod).GetMethod( - name: hookType == HookMethodType.Before + name: hookType == LuaCsHook.HookMethodType.Before ? nameof(PatchedMethod.GetPrefixEnumerator) : nameof(PatchedMethod.GetPostfixEnumerator), bindingAttr: BindingFlags.Public | BindingFlags.Instance)); @@ -1201,7 +657,7 @@ namespace Barotrauma il.EndExceptionBlock(exceptionBlock); // Only prefixes return a bool - if (hookType == HookMethodType.Before) + if (hookType == LuaCsHook.HookMethodType.Before) { il.LoadLocal(harmonyReturnValue); } @@ -1214,11 +670,11 @@ namespace Barotrauma } var type = typeBuilder.CreateType(); - type.GetField(FIELD_LUACS, BindingFlags.Public | BindingFlags.Static).SetValue(null, luaCs); + type.GetField(FIELD_LUACS, BindingFlags.Public | BindingFlags.Static).SetValue(null, LuaCsSetup.Instance); return type.GetMethod(methodName, BindingFlags.Public | BindingFlags.Static); } - private string Patch(string identifier, MethodBase method, LuaCsPatchFunc patch, HookMethodType hookType = HookMethodType.Before) + private string Patch(string identifier, MethodBase method, LuaCsPatchFunc patch, LuaCsHook.HookMethodType hookType = LuaCsHook.HookMethodType.Before) { if (method == null) throw new ArgumentNullException(nameof(method)); if (patch == null) throw new ArgumentNullException(nameof(patch)); @@ -1230,13 +686,13 @@ namespace Barotrauma var patchKey = MethodKey.Create(method); if (!registeredPatches.TryGetValue(patchKey, out var methodPatches)) { - var harmonyPrefix = CreateDynamicHarmonyPatch(identifier, method, HookMethodType.Before); - var harmonyPostfix = CreateDynamicHarmonyPatch(identifier, method, HookMethodType.After); + var harmonyPrefix = CreateDynamicHarmonyPatch(identifier, method, LuaCsHook.HookMethodType.Before); + var harmonyPostfix = CreateDynamicHarmonyPatch(identifier, method, LuaCsHook.HookMethodType.After); harmony.Patch(method, prefix: new HarmonyMethod(harmonyPrefix), postfix: new HarmonyMethod(harmonyPostfix)); methodPatches = registeredPatches[patchKey] = new PatchedMethod(harmonyPrefix, harmonyPostfix); } - if (hookType == HookMethodType.Before) + if (hookType == LuaCsHook.HookMethodType.Before) { if (methodPatches.Prefixes.Remove(identifier)) { @@ -1249,7 +705,7 @@ namespace Barotrauma PatchFunc = patch, }); } - else if (hookType == HookMethodType.After) + else if (hookType == LuaCsHook.HookMethodType.After) { if (methodPatches.Postfixes.Remove(identifier)) { @@ -1266,31 +722,31 @@ namespace Barotrauma return identifier; } - public string Patch(string identifier, string className, string methodName, string[] parameterTypes, LuaCsPatchFunc patch, HookMethodType hookType = HookMethodType.Before) + public string Patch(string identifier, string className, string methodName, string[] parameterTypes, LuaCsPatchFunc patch, LuaCsHook.HookMethodType hookType = LuaCsHook.HookMethodType.Before) { var method = ResolveMethod(className, methodName, parameterTypes); return Patch(identifier, method, patch, hookType); } - public string Patch(string identifier, string className, string methodName, LuaCsPatchFunc patch, HookMethodType hookType = HookMethodType.Before) + public string Patch(string identifier, string className, string methodName, LuaCsPatchFunc patch, LuaCsHook.HookMethodType hookType = LuaCsHook.HookMethodType.Before) { var method = ResolveMethod(className, methodName, null); return Patch(identifier, method, patch, hookType); } - public string Patch(string className, string methodName, string[] parameterTypes, LuaCsPatchFunc patch, HookMethodType hookType = HookMethodType.Before) + public string Patch(string className, string methodName, string[] parameterTypes, LuaCsPatchFunc patch, LuaCsHook.HookMethodType hookType = LuaCsHook.HookMethodType.Before) { var method = ResolveMethod(className, methodName, parameterTypes); return Patch(null, method, patch, hookType); } - public string Patch(string className, string methodName, LuaCsPatchFunc patch, HookMethodType hookType = HookMethodType.Before) + public string Patch(string className, string methodName, LuaCsPatchFunc patch, LuaCsHook.HookMethodType hookType = LuaCsHook.HookMethodType.Before) { var method = ResolveMethod(className, methodName, null); return Patch(null, method, patch, hookType); } - private bool RemovePatch(string identifier, MethodBase method, HookMethodType hookType) + private bool RemovePatch(string identifier, MethodBase method, LuaCsHook.HookMethodType hookType) { if (identifier == null) throw new ArgumentNullException(nameof(identifier)); identifier = NormalizeIdentifier(identifier); @@ -1303,22 +759,58 @@ namespace Barotrauma return hookType switch { - HookMethodType.Before => methodPatches.Prefixes.Remove(identifier), - HookMethodType.After => methodPatches.Postfixes.Remove(identifier), - _ => throw new ArgumentException($"Invalid {nameof(HookMethodType)} enum value.", nameof(hookType)), + LuaCsHook.HookMethodType.Before => methodPatches.Prefixes.Remove(identifier), + LuaCsHook.HookMethodType.After => methodPatches.Postfixes.Remove(identifier), + _ => throw new ArgumentException($"Invalid {nameof(LuaCsHook.HookMethodType)} enum value.", nameof(hookType)), }; } - public bool RemovePatch(string identifier, string className, string methodName, string[] parameterTypes, HookMethodType hookType) + public bool RemovePatch(string identifier, string className, string methodName, string[] parameterTypes, LuaCsHook.HookMethodType hookType) { var method = ResolveMethod(className, methodName, parameterTypes); return RemovePatch(identifier, method, hookType); } - public bool RemovePatch(string identifier, string className, string methodName, HookMethodType hookType) + public bool RemovePatch(string identifier, string className, string methodName, LuaCsHook.HookMethodType hookType) { var method = ResolveMethod(className, methodName, null); return RemovePatch(identifier, method, hookType); } + + private void ClearAll() + { + harmony?.UnpatchSelf(); + + foreach (var (_, patch) in registeredPatches) + { + // Remove references stored in our dynamic types so the generated + // assembly can be garbage-collected. + patch.HarmonyPrefixMethod.DeclaringType + .GetField(FIELD_LUACS, BindingFlags.Public | BindingFlags.Static) + .SetValue(null, null); + patch.HarmonyPostfixMethod.DeclaringType + .GetField(FIELD_LUACS, BindingFlags.Public | BindingFlags.Static) + .SetValue(null, null); + } + + registeredPatches.Clear(); + + compatHookPrefixMethods.Clear(); + compatHookPostfixMethods.Clear(); + } + + public void Dispose() + { + IsDisposed = true; + + ClearAll(); + } + + public FluentResults.Result Reset() + { + ClearAll(); + + return FluentResults.Result.Ok(); + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/_Lua/LuaScriptLoader.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/_Lua/LuaScriptLoader.cs new file mode 100644 index 000000000..d1b751d23 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/_Lua/LuaScriptLoader.cs @@ -0,0 +1,167 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Text; +using System.IO; +using MoonSharp.Interpreter; +using MoonSharp.Interpreter.Loaders; +using System.Linq; +using System.Threading.Tasks; +using Barotrauma.LuaCs.Data; +using Barotrauma.LuaCs; +using FluentResults; + +namespace Barotrauma.LuaCs +{ + public class LuaScriptLoader : ScriptLoaderBase, ILuaScriptLoader + { + public LuaScriptLoader(ISafeStorageService storageService, Lazy loggerService) + { + this._storageService = storageService; + this._loggerService = loggerService; + storageService.UseCaching = true; + } + + private readonly ISafeStorageService _storageService; + private readonly Lazy _loggerService; + + public override object LoadFile(string file, Table globalContext) + { + IService.CheckDisposed(this); + if (file.IsNullOrWhiteSpace()) + { + return null; + } + + var res = _storageService.TryLoadText(file); + + if (res.IsFailed || res is not { Value: { } script}) + { + UnsafeLogErrors($"Failed to load file '{file}'.", res.ToResult()); + return null; + } + + if (script.IsNullOrWhiteSpace()) + { + UnsafeLogErrors($"The file '{file}' is empty. ", res.ToResult()); + return null; + } + + return script; + } + + public void ClearCaches() + { + IService.CheckDisposed(this); + _storageService?.PurgeCache(); + } + + public void SetCachingPolicy(bool useCaching) + { + if (_storageService is null) + { + return; + } + + if (!useCaching) + { + _storageService.PurgeCache(); + } + _storageService.UseCaching = useCaching; + } + + public async Task)>>> CacheResourcesAsync(ImmutableArray resourceInfos) + { + IService.CheckDisposed(this); + if (!_storageService.UseCaching) + { + return FluentResults.Result.Fail($"Caching is not enabled."); + } + + return await this._storageService.LoadPackageTextFilesAsync([..resourceInfos.SelectMany(ri => ri.FilePaths)]); + } + + public override bool ScriptFileExists(string file) + { + IService.CheckDisposed(this); + var result = _storageService.FileExists(file); + if (result is { IsFailed: true }) + { + UnsafeLogErrors($"Unable to find and load file \"{file}\".", result.ToResult()); + return false; + } + + return result.Value; + } + + private void UnsafeLogErrors(string message, FluentResults.Result result = null) + { + _loggerService.Value.LogError($"{nameof(LuaScriptLoader)}: {message}"); + if (result is null || result.Errors.Count <= 0) + { + return; + } + + foreach (var error in result.Errors) + { + _loggerService.Value.LogError($"{nameof(LuaScriptLoader)}: Error: {error.Message}."); + } + } + + public void Dispose() + { + if (!ModUtils.Threading.CheckIfClearAndSetBool(ref _isDisposed)) + { + return; + } + + _storageService?.Dispose(); + _loggerService?.Value.Dispose(); + } + + private int _isDisposed = 0; + public bool IsDisposed => ModUtils.Threading.GetBool(ref _isDisposed); + + public bool IsFileAccessible(string path, bool readOnly, bool checkWhitelistOnly = true) + { + IService.CheckDisposed(this); + return _storageService.IsFileAccessible(path, readOnly, checkWhitelistOnly); + } + + public void AddFileToWhitelist(string path, bool readOnly = true) + { + IService.CheckDisposed(this); + _storageService.AddFileToWhitelist(path, readOnly); + } + + public void AddFilesToWhitelist(ImmutableArray paths, bool readOnly = true) + { + IService.CheckDisposed(this); + _storageService.AddFilesToWhitelist(paths, readOnly); + } + + public void RemoveFileFromAllWhitelists(string path) + { + IService.CheckDisposed(this); + _storageService.RemoveFileFromAllWhitelists(path); + } + + public FluentResults.Result SetReadOnlyWhitelist(ImmutableArray filePaths) + { + IService.CheckDisposed(this); + return _storageService.SetReadOnlyWhitelist(filePaths); + } + + public FluentResults.Result SetReadWriteWhitelist(ImmutableArray filePaths) + { + IService.CheckDisposed(this); + return _storageService.SetReadWriteWhitelist(filePaths); + } + + public void ClearAllWhitelists() + { + IService.CheckDisposed(this); + _storageService.ClearAllWhitelists(); + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/_Lua/LuaUserDataService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/_Lua/LuaUserDataService.cs new file mode 100644 index 000000000..366249f21 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/_Lua/LuaUserDataService.cs @@ -0,0 +1,413 @@ +using MoonSharp.Interpreter; +using MoonSharp.Interpreter.Interop; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace Barotrauma.LuaCs; + +public interface ILuaUserDataService : IReusableService +{ + IReadOnlyDictionary Descriptors { get; } + IUserDataDescriptor RegisterType(string typeName); + void RegisterExtensionType(string typeName); + bool IsRegistered(string typeName); + void UnregisterType(string typeName, bool deleteHistory = false); + object CreateStatic(string typeName); + bool IsTargetType(object obj, string typeName); + string TypeOf(object obj); + object CreateEnumTable(string typeName); + void MakeFieldAccessible(IUserDataDescriptor IUUD, string fieldName); + void MakeMethodAccessible(IUserDataDescriptor IUUD, string methodName, string[] parameters = null); + void MakePropertyAccessible(IUserDataDescriptor IUUD, string propertyName); + void AddMethod(IUserDataDescriptor IUUD, string methodName, object function); + void AddField(IUserDataDescriptor IUUD, string fieldName, DynValue value); + void RemoveMember(IUserDataDescriptor IUUD, string memberName); + bool HasMember(object obj, string memberName); + /// + /// See . + /// + /// Lua value to convert and wrap in a userdata. + /// Descriptor of the type of the object to convert the Lua value to. Uses MoonSharp ScriptToClr converters. + /// A userdata that wraps the Lua value converted to an object of the desired type as described by . + DynValue CreateUserDataFromDescriptor(DynValue scriptObject, IUserDataDescriptor desiredTypeDescriptor); + + /// + /// Converts a Lua value to a CLR object of a desired type and wraps it in a userdata. + /// If the type is not registered, then a new will be created and used. + /// The goal of this method is to allow Lua scripts to create userdata to wrap certain data without having to register types. + /// Wrapping the value in a userdata preserves the original type during script-to-CLR conversions. + /// A Lua script needs to pass a List`1 to a CLR method expecting System.Object, MoonSharp gets + /// in the way by converting the List`1 to a MoonSharp.Interpreter.Table and breaking everything. + /// Registering the List`1 type can break other scripts relying on default converters, so instead + /// it is better to manually wrap the List`1 object into a userdata. + /// + /// + /// Lua value to convert and wrap in a userdata. + /// Type describing the CLR type of the object to convert the Lua value to. + /// A userdata that wraps the Lua value converted to an object of the desired type. + DynValue CreateUserDataFromType(DynValue scriptObject, Type desiredType); + + void AddCallMetaTable(object userdata); +} + +public class LuaUserDataService : ILuaUserDataService +{ + public bool IsDisposed { get; private set; } + + public IReadOnlyDictionary Descriptors => descriptors; + private ConcurrentDictionary descriptors; + + private readonly IPluginManagementService _pluginManagementService; + + public LuaUserDataService(IPluginManagementService pluginManagementService) + { + descriptors = new ConcurrentDictionary(); + _pluginManagementService = pluginManagementService; + } + + public IUserDataDescriptor this[string key] + { + get + { + return descriptors.GetValueOrDefault(key); + } + } + + private Type GetType(string typeName) => _pluginManagementService.GetType(typeName, includeInterfaces: true); + + public IUserDataDescriptor RegisterType(string typeName) + { + Type type = GetType(typeName); + + if (type == null) + { + throw new ScriptRuntimeException($"tried to register a type that doesn't exist: {typeName}."); + } + + var descriptor = UserData.RegisterType(type); + descriptors.TryAdd(typeName, descriptor); + + return descriptor; + } + + public void RegisterExtensionType(string typeName) + { + Type type = GetType(typeName); + + if (type == null) + { + throw new ScriptRuntimeException($"tried to register a type that doesn't exist: {typeName}."); + } + + UserData.RegisterExtensionType(type); + } + + public bool IsRegistered(string typeName) + { + Type type = GetType(typeName); + + if (type == null) + { + return false; + } + + return UserData.GetDescriptorForType(type, true) != null; + } + + public void UnregisterType(string typeName, bool deleteHistory = false) + { + Type type = GetType(typeName); + + if (type == null) + { + throw new ScriptRuntimeException($"tried to unregister a type that doesn't exist: {typeName}."); + } + + UserData.UnregisterType(type, deleteHistory); + } + + public bool IsTargetType(object obj, string typeName) + { + if (obj == null) { throw new ScriptRuntimeException("userdata is nil"); } + Type targetType = GetType(typeName); + if (targetType == null) { throw new ScriptRuntimeException("target type not found"); } + + Type type = obj is Type ? (Type)obj : obj.GetType(); + return targetType.IsAssignableFrom(type); + } + + public string TypeOf(object obj) + { + if (obj == null) { throw new ScriptRuntimeException("userdata is nil"); } + + return obj.GetType().FullName; + } + + public object CreateEnumTable(string typeName) + { + Type type = GetType(typeName); + + if (type == null) + { + throw new ScriptRuntimeException($"tried to create an enum table with a type that doesn't exist:: {typeName}."); + } + + Dictionary result = new Dictionary(); + + foreach (var value in Enum.GetValues(type)) + { + string name = Enum.GetName(type, value); + + result[name] = value; + } + + return result; + } + + public object CreateStatic(string typeName) + { + Type type = GetType(typeName); + + if (type == null) + { + throw new ScriptRuntimeException($"tried to create a static userdata of a type that doesn't exist: {typeName}."); + } + + MethodInfo method = typeof(UserData).GetMethod(nameof(UserData.CreateStatic), 1, new Type[0]); + MethodInfo generic = method.MakeGenericMethod(type); + return generic.Invoke(null, null); + } + + private FieldInfo FindFieldRecursively(Type type, string fieldName) + { + var field = type.GetField(fieldName, BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static); + + if (field == null && type.BaseType != null) + { + return FindFieldRecursively(type.BaseType, fieldName); + } + + return field; + } + + public void MakeFieldAccessible(IUserDataDescriptor IUUD, string fieldName) + { + if (IUUD == null) + { + throw new ScriptRuntimeException($"tried to use a UserDataDescriptor that is null to make {fieldName} accessible."); + } + + var descriptor = (StandardUserDataDescriptor)IUUD; + FieldInfo field = FindFieldRecursively(IUUD.Type, fieldName); + + if (field == null) + { + throw new ScriptRuntimeException($"tried to make field '{fieldName}' accessible, but the field doesn't exist."); + } + + descriptor.RemoveMember(fieldName); + descriptor.AddMember(fieldName, new FieldMemberDescriptor(field, InteropAccessMode.Default)); + } + + private MethodInfo FindMethodRecursively(Type type, string methodName, Type[] types = null) + { + MethodInfo method; + + if (types == null) + { + method = type.GetMethod(methodName, BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static); + } + else + { + method = type.GetMethod(methodName, BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static, types); + } + + if (method == null && type.BaseType != null) + { + return FindMethodRecursively(type.BaseType, methodName, types); + } + + return method; + } + + public void MakeMethodAccessible(IUserDataDescriptor IUUD, string methodName, string[] parameters = null) + { + if (IUUD == null) + { + throw new ScriptRuntimeException($"tried to use a UserDataDescriptor that is null to make {methodName} accessible."); + } + + Type[] parameterTypes = null; + + + if (parameters != null) + { + parameterTypes = new Type[parameters.Length]; + + for (int i = 0; i < parameters.Length; i++) + { + Type type = GetType(parameters[i]); + if (type == null) + { + throw new ScriptRuntimeException($"invalid parameter type '{parameters[i]}'"); + } + parameterTypes[i] = type; + } + } + + var descriptor = (StandardUserDataDescriptor)IUUD; + + MethodBase method; + + try + { + method = FindMethodRecursively(IUUD.Type, methodName, parameterTypes); + } + catch (AmbiguousMatchException ex) + { + throw new ScriptRuntimeException("ambiguous method signature."); + } + + if (method == null) + { + throw new ScriptRuntimeException($"tried to make method '{methodName}' accessible, but the method doesn't exist."); + } + + descriptor.AddMember(methodName, new MethodMemberDescriptor(method, InteropAccessMode.Default)); + } + + private PropertyInfo FindPropertyRecursively(Type type, string propertyName) + { + var property = type.GetProperty(propertyName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static); + + if (property == null && type.BaseType != null) + { + return FindPropertyRecursively(type.BaseType, propertyName); + } + + return property; + } + + public void MakePropertyAccessible(IUserDataDescriptor IUUD, string propertyName) + { + if (IUUD == null) + { + throw new ScriptRuntimeException($"tried to use a UserDataDescriptor that is null to make {propertyName} accessible."); + } + + var descriptor = (StandardUserDataDescriptor)IUUD; + PropertyInfo property = FindPropertyRecursively(IUUD.Type, propertyName); + + if (property == null) + { + throw new ScriptRuntimeException($"tried to make property '{propertyName}' accessible, but the property doesn't exist."); + } + + descriptor.RemoveMember(propertyName); + descriptor.AddMember(propertyName, new PropertyMemberDescriptor(property, InteropAccessMode.Default, property.GetGetMethod(true), property.GetSetMethod(true))); + } + + public void AddMethod(IUserDataDescriptor IUUD, string methodName, object function) + { + if (IUUD == null) + { + throw new ScriptRuntimeException($"tried to use a UserDataDescriptor that is null to add method {methodName}."); + } + + var descriptor = (StandardUserDataDescriptor)IUUD; + + descriptor.RemoveMember(methodName); + descriptor.AddMember(methodName, new ObjectCallbackMemberDescriptor(methodName, (object arg1, ScriptExecutionContext arg2, CallbackArguments arg3) => + { + if (LuaCsSetup.Instance != null) + { + return LuaCsSetup.Instance.CallLuaFunction(function, arg3.GetArray()); + } + return null; + })); + } + + public void AddField(IUserDataDescriptor IUUD, string fieldName, DynValue value) + { + if (IUUD == null) + { + throw new ScriptRuntimeException($"tried to use a UserDataDescriptor that is null to add field {fieldName}."); + } + + var descriptor = (StandardUserDataDescriptor)IUUD; + descriptor.RemoveMember(fieldName); + descriptor.AddMember(fieldName, new DynValueMemberDescriptor(fieldName, value)); + } + + public void RemoveMember(IUserDataDescriptor IUUD, string memberName) + { + if (IUUD == null) + { + throw new ScriptRuntimeException($"tried to use a UserDataDescriptor that is null to remove the member {memberName}."); + } + + var descriptor = (StandardUserDataDescriptor)IUUD; + descriptor.RemoveMember(memberName); + } + + public bool HasMember(object obj, string memberName) + { + if (obj == null) { throw new ScriptRuntimeException("object is nil"); } + + Type type; + if (obj is Type) + { + type = (Type)obj; + } + else if (obj is IUserDataDescriptor descriptor) + { + type = descriptor.Type; + + if (((StandardUserDataDescriptor)descriptor).HasMember(memberName)) + { + return true; + } + } + else + { + type = obj.GetType(); + } + + if (type.GetMember(memberName).Length == 0) + { + return false; + } + + return true; + } + + + public DynValue CreateUserDataFromDescriptor(DynValue scriptObject, IUserDataDescriptor desiredTypeDescriptor) + { + return UserData.Create(scriptObject.ToObject(desiredTypeDescriptor.Type), desiredTypeDescriptor); + } + + public DynValue CreateUserDataFromType(DynValue scriptObject, Type desiredType) + { + IUserDataDescriptor descriptor = UserData.GetDescriptorForType(desiredType, true); + descriptor ??= new StandardUserDataDescriptor(desiredType, InteropAccessMode.Default); + return CreateUserDataFromDescriptor(scriptObject, descriptor); + } + + public void AddCallMetaTable(object userdata) { } + + public void Dispose() + { + IsDisposed = true; + descriptors.Clear(); + } + + public FluentResults.Result Reset() + { + descriptors.Clear(); + return FluentResults.Result.Ok(); + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/_Lua/SafeLuaUserDataService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/_Lua/SafeLuaUserDataService.cs new file mode 100644 index 000000000..a31869e78 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/_Lua/SafeLuaUserDataService.cs @@ -0,0 +1,236 @@ +using Barotrauma; +using Barotrauma.LuaCs; +using MoonSharp.Interpreter; +using MoonSharp.Interpreter.Interop; +using System; +using System.Collections.Generic; +using System.Reflection; + +namespace Barotrauma.LuaCs; + +public interface ISafeLuaUserDataService : IService +{ + bool IsAllowed(string typeName); + IUserDataDescriptor RegisterType(string typeName); + void RegisterExtensionType(string typeName); + bool IsRegistered(string typeName); + void UnregisterType(string typeName, bool deleteHistory = false); + object CreateStatic(string typeName); + bool IsTargetType(object obj, string typeName); + string TypeOf(object obj); + object CreateEnumTable(string typeName); + void MakeFieldAccessible(IUserDataDescriptor IUUD, string fieldName); + void MakeMethodAccessible(IUserDataDescriptor IUUD, string methodName, string[] parameters = null); + void MakePropertyAccessible(IUserDataDescriptor IUUD, string propertyName); + void AddMethod(IUserDataDescriptor IUUD, string methodName, object function); + void AddField(IUserDataDescriptor IUUD, string fieldName, DynValue value); + void RemoveMember(IUserDataDescriptor IUUD, string memberName); + bool HasMember(object obj, string memberName); + /// + /// See . + /// + /// Lua value to convert and wrap in a userdata. + /// Descriptor of the type of the object to convert the Lua value to. Uses MoonSharp ScriptToClr converters. + /// A userdata that wraps the Lua value converted to an object of the desired type as described by . + DynValue CreateUserDataFromDescriptor(DynValue scriptObject, IUserDataDescriptor desiredTypeDescriptor); + + /// + /// Converts a Lua value to a CLR object of a desired type and wraps it in a userdata. + /// If the type is not registered, then a new will be created and used. + /// The goal of this method is to allow Lua scripts to create userdata to wrap certain data without having to register types. + /// Wrapping the value in a userdata preserves the original type during script-to-CLR conversions. + /// A Lua script needs to pass a List`1 to a CLR method expecting System.Object, MoonSharp gets + /// in the way by converting the List`1 to a MoonSharp.Interpreter.Table and breaking everything. + /// Registering the List`1 type can break other scripts relying on default converters, so instead + /// it is better to manually wrap the List`1 object into a userdata. + /// + /// + /// Lua value to convert and wrap in a userdata. + /// Type describing the CLR type of the object to convert the Lua value to. + /// A userdata that wraps the Lua value converted to an object of the desired type. + DynValue CreateUserDataFromType(DynValue scriptObject, Type desiredType); + void AddCallMetaTable(object userdata); +} + +public class SafeLuaUserDataService : ISafeLuaUserDataService +{ + private readonly ILuaUserDataService _userDataService; + + public bool IsDisposed { get; private set; } + + public SafeLuaUserDataService(ILuaUserDataService userDataService) + { + _userDataService = userDataService; + } + + public IUserDataDescriptor this[string key] + { + get + { + return _userDataService.Descriptors.GetValueOrDefault(key); + } + } + + private bool CanBeRegistered(string typeName) + { + if (typeName.StartsWith("Barotrauma.Lua", StringComparison.Ordinal) || + typeName.StartsWith("Barotrauma.Cs", StringComparison.Ordinal) || + typeName.StartsWith("Barotrauma.LuaCs", StringComparison.Ordinal)) + { + return false; + } + + if (typeName == "System.Single") { return true; } + + if (typeName == "System.Console") { return true; } + + if (typeName.StartsWith("System.Collections", StringComparison.Ordinal)) + return true; + + if (typeName.StartsWith("Microsoft.Xna", StringComparison.Ordinal)) + return true; + + if (typeName.StartsWith("Barotrauma.IO", StringComparison.Ordinal)) + return false; + + if (typeName.StartsWith("Barotrauma.ToolBox", StringComparison.Ordinal)) + return false; + + if (typeName.StartsWith("Barotrauma.SaveUtil", StringComparison.Ordinal)) + return false; + + if (typeName.StartsWith("Barotrauma.", StringComparison.Ordinal)) + return true; + + return false; + } + + private bool CanBeReRegistered(string typeName) + { + if (typeName.StartsWith("Barotrauma.Lua", StringComparison.Ordinal) || + typeName.StartsWith("Barotrauma.Cs", StringComparison.Ordinal) || + typeName.StartsWith("Barotrauma.LuaCs", StringComparison.Ordinal)) + { + return false; + } + + return true; + } + + public bool IsAllowed(string typeName) + { + if (!CanBeReRegistered(typeName) && IsRegistered(typeName)) + { + return false; + } + + if (!CanBeRegistered(typeName)) + { + return false; + } + + return true; + } + + private void CheckAllowed(string typeName) + { + if (!IsAllowed(typeName)) + { + throw new ScriptRuntimeException($"Type {typeName} can't be registered"); + } + } + + public IUserDataDescriptor RegisterType(string typeName) + { + CheckAllowed(typeName); + return _userDataService.RegisterType(typeName); + } + + public void RegisterExtensionType(string typeName) + { + CheckAllowed(typeName); + _userDataService.RegisterExtensionType(typeName); + } + + public bool IsRegistered(string typeName) + { + return _userDataService.IsRegistered(typeName); + } + + public void UnregisterType(string typeName, bool deleteHistory = false) + { + IsAllowed(typeName); + _userDataService.UnregisterType(typeName, deleteHistory); + } + public object CreateStatic(string typeName) + { + return _userDataService.CreateStatic(typeName); + } + + public bool IsTargetType(object obj, string typeName) + { + return _userDataService.IsTargetType(obj, typeName); + } + + public string TypeOf(object obj) + { + return _userDataService.TypeOf(obj); + } + + public object CreateEnumTable(string typeName) + { + return _userDataService.CreateEnumTable(typeName); + } + + public void MakeFieldAccessible(IUserDataDescriptor IUUD, string fieldName) + { + _userDataService.MakeFieldAccessible(IUUD, fieldName); + } + + public void MakeMethodAccessible(IUserDataDescriptor IUUD, string methodName, string[] parameters = null) + { + _userDataService.MakeMethodAccessible(IUUD, methodName, parameters); + } + + public void MakePropertyAccessible(IUserDataDescriptor IUUD, string propertyName) + { + _userDataService.MakePropertyAccessible(IUUD, propertyName); + } + + public void AddMethod(IUserDataDescriptor IUUD, string methodName, object function) + { + _userDataService.AddMethod(IUUD, methodName, function); + } + + public void AddField(IUserDataDescriptor IUUD, string fieldName, DynValue value) + { + _userDataService.AddField(IUUD, fieldName, value); + } + + public void RemoveMember(IUserDataDescriptor IUUD, string memberName) + { + _userDataService.RemoveMember(IUUD, memberName); + } + + public bool HasMember(object obj, string memberName) + { + return _userDataService.HasMember(obj, memberName); + } + + public DynValue CreateUserDataFromDescriptor(DynValue scriptObject, IUserDataDescriptor desiredTypeDescriptor) + { + return _userDataService.CreateUserDataFromDescriptor(scriptObject, desiredTypeDescriptor); + } + + public DynValue CreateUserDataFromType(DynValue scriptObject, Type desiredType) + { + return _userDataService.CreateUserDataFromType(scriptObject, desiredType); + } + + public void AddCallMetaTable(object userdata) { } + + public void Dispose() + { + IsDisposed = true; + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs index f617c584a..d86426493 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs @@ -1,5 +1,7 @@ using Barotrauma.Extensions; using Barotrauma.Items.Components; +using Barotrauma.LuaCs.Events; +using Barotrauma.Networking; using FarseerPhysics; using FarseerPhysics.Dynamics; using Microsoft.Xna.Framework; @@ -804,7 +806,6 @@ namespace Barotrauma waterFlowThisFrame = 0.0f; } - private static readonly ConcurrentBag checkedHulls = new ConcurrentBag(); /// /// Simulates water flow from the source to all the hulls it's connected to across the sub, as if the water was coming directly from outside. @@ -812,7 +813,7 @@ namespace Barotrauma /// void SimulateWaterFlowFromOutsideToConnectedHulls(Hull hull, float maxFlow, float deltaTime) { - checkedHulls.Clear(); + List checkedHulls = new List(); checkedHulls.Add(hull); foreach (var connectedGap in hull.ConnectedGaps) { @@ -823,7 +824,7 @@ namespace Barotrauma } } - static void SimulateWaterFlowFromOutsideToConnectedHullsRecursive(Hull targetHull, Gap gap, ConcurrentBag checkedHulls, Hull originHull, float maxFlow, float deltaTime) + static void SimulateWaterFlowFromOutsideToConnectedHullsRecursive(Hull targetHull, Gap gap, List checkedHulls, Hull originHull, float maxFlow, float deltaTime) { const float decay = 0.95f; @@ -871,7 +872,12 @@ namespace Barotrauma if (outsideCollisionBlocker == null) { return false; } if (IsRoomToRoom || Submarine == null || open <= 0.0f || linkedTo.Count == 0 || linkedTo[0] is not Hull) { - outsideCollisionBlocker.Enabled = false; + SingleThreadWorker.Instance.AddAction(() => + { + if (outsideCollisionBlocker == null) { return; } + outsideCollisionBlocker.Enabled = false; + }); + return false; } @@ -942,8 +948,8 @@ namespace Barotrauma if (Math.Max(hull1.WorldSurface + hull1.WaveY[hull1.WaveY.Length - 1], hull2.WorldSurface + hull2.WaveY[0]) > WorldRect.Y) { return; } } - var should = GameMain.LuaCs.Hook.Call("gapOxygenUpdate", this, hull1, hull2); - + bool? should = null; + LuaCsSetup.Instance.EventService.PublishEvent(x => should = x.OnGapOxygenUpdate(this, hull1, hull2) ?? should); if (should != null && should.Value) return; float totalOxygen = hull1.Oxygen + hull2.Oxygen; @@ -1054,8 +1060,6 @@ namespace Barotrauma base.Remove(); GapList.Remove(this); - checkedHulls.Clear(); - foreach (Hull hull in Hull.HullList) { hull.ConnectedGaps.Remove(this); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs index 08a7bad97..c48c5924d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs @@ -234,6 +234,8 @@ namespace Barotrauma public const float OxygenDeteriorationSpeed = 0.3f; public const float OxygenConsumptionSpeed = 700.0f; + private const float DecalAlphaRemoveThreshold = 0.001f; + public const int WaveWidth = 32; public static float WaveStiffness = 0.01f; public static float WaveSpread = 0.02f; @@ -997,22 +999,24 @@ namespace Barotrauma Oxygen -= OxygenDeteriorationSpeed * deltaTime; - if (FakeFireSources.Count > 0) + SingleThreadWorker.Instance.AddAction(() => { - if ((Character.Controlled?.CharacterHealth?.GetAffliction("psychosis")?.Strength ?? 0.0f) <= 0.0f) + if (FakeFireSources.Count > 0) { - for (int i = FakeFireSources.Count - 1; i >= 0; i--) + if ((Character.Controlled?.CharacterHealth?.GetAffliction("psychosis")?.Strength ?? 0.0f) <= 0.0f) { - if (FakeFireSources[i].CausedByPsychosis) + for (int i = FakeFireSources.Count - 1; i >= 0; i--) { - FakeFireSources[i].Remove(); + if (FakeFireSources[i].CausedByPsychosis) + { + FakeFireSources[i].Remove(); + } } } + FireSource.UpdateAll(FakeFireSources, deltaTime); } - FireSource.UpdateAll(FakeFireSources, deltaTime); - } - - FireSource.UpdateAll(FireSources, deltaTime); + FireSource.UpdateAll(FireSources, deltaTime); + }); foreach (Decal decal in decals) { @@ -1024,7 +1028,7 @@ namespace Barotrauma for (int i = decals.Count - 1; i >= 0; i--) { var decal = decals[i]; - if (decal.FadeTimer >= decal.LifeTime || decal.BaseAlpha <= 0.001f) + if (decal.FadeTimer >= decal.LifeTime || decal.BaseAlpha <= DecalAlphaRemoveThreshold) { decals.RemoveAt(i); #if SERVER @@ -1282,7 +1286,10 @@ namespace Barotrauma Hull currentHull = current.hull; Vector2 currentPos = current.pos; - if (currentDist > maxDistance) { return float.MaxValue; } + if (currentDist > maxDistance) + { + return float.MaxValue; + } // If we've reached the target, add the final segment from hull to endPos if (currentHull == targetHull) @@ -1290,7 +1297,7 @@ namespace Barotrauma return currentDist + Vector2.Distance(currentPos, endPos); } - foreach (Gap g in ConnectedGaps) + foreach (Gap g in currentHull.ConnectedGaps) { float distanceMultiplier = 1; if (g.ConnectedDoor != null && !g.ConnectedDoor.IsBroken) @@ -1766,9 +1773,18 @@ namespace Barotrauma bool decalsCleaned = false; foreach (Decal decal in decals) { + // Don't attempt to clean the decal if it's already below the remove threshold, since the server + // is already gonna remove the decal for us, sending another decal update event would result in + // us potentially modifying a different decal since the indices can briefly desync. + if (decal.BaseAlpha <= DecalAlphaRemoveThreshold) + { + continue; + } + if (decal.AffectsSection(section)) { decal.Clean(cleanVal); + decalsCleaned = true; #if SERVER decalUpdatePending = true; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs index f4d5e5d6a..003bacbee 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs @@ -7,6 +7,7 @@ using FarseerPhysics.Dynamics; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Diagnostics; using System.Globalization; using System.Linq; @@ -328,6 +329,7 @@ namespace Barotrauma public Submarine BeaconStation { get; private set; } private Sonar beaconSonar; + private ImmutableArray beaconTransducers = ImmutableArray.Empty; /// /// Special wall chunks that aren't part of the normal level geometry: includes things like the ocean floor, floating ice chunks and ice spires. @@ -4406,6 +4408,13 @@ namespace Barotrauma attempts++; } } + + foreach (var wreck in Wrecks) + { + wreck.SetCrushDepth(wreck.RealWorldDepth + 1000); + SetLinkedSubCrushDepth(wreck); + } + totalSW.Stop(); Debug.WriteLine($"{Wrecks.Count} wrecks created in { totalSW.ElapsedMilliseconds} (ms)"); } @@ -4790,6 +4799,7 @@ namespace Barotrauma return; } beaconSonar = sonarItem.GetComponent(); + beaconTransducers = sonarItem.GetConnectedComponents().ToImmutableArray(); } public void PrepareBeaconStation() @@ -4916,9 +4926,20 @@ namespace Barotrauma public bool CheckBeaconActive() { if (beaconSonar == null) { return false; } + if (beaconSonar.UseTransducers) + { + var connectedTransducers = beaconSonar.Item.GetConnectedComponents(); + foreach (var beaconTransducer in beaconTransducers) + { + if (!beaconTransducer.HasPower || !connectedTransducers.Contains(beaconTransducer)) { return false; } + } + } return beaconSonar.HasPower && beaconSonar.CurrentMode == Sonar.Mode.Active; } + /// + /// Set the crush depths of the connected subs to match the crush depth of the parent sub. + /// private void SetLinkedSubCrushDepth(Submarine parentSub) { foreach (var connectedSub in parentSub.GetConnectedSubs()) @@ -5157,6 +5178,7 @@ namespace Barotrauma BeaconStation = null; beaconSonar = null; + beaconTransducers = ImmutableArray.Empty; StartOutpost = null; EndOutpost = null; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs index 03e37859c..b7460e61c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs @@ -413,8 +413,7 @@ namespace Barotrauma new XAttribute("difficulty", Difficulty.ToString("G", CultureInfo.InvariantCulture)), new XAttribute("size", XMLExtensions.PointToString(Size)), new XAttribute("generationparams", GenerationParams.Identifier), - new XAttribute("initialdepth", InitialDepth), - new XAttribute("exhaustedeventsets", allEventsExhausted)); + new XAttribute("initialdepth", InitialDepth)); newElement.Add( new XAttribute(nameof(exhaustedEventSets), string.Join(',', exhaustedEventSets.Select(e => e.Value)))); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelTrigger.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelTrigger.cs index 4731c255a..0a32d80c3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelTrigger.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelTrigger.cs @@ -820,7 +820,7 @@ namespace Barotrauma public static float GetDistanceFactor(PhysicsBody triggererBody, PhysicsBody triggerBody, float colliderRadius) { - return 1.0f - ConvertUnits.ToDisplayUnits(Vector2.Distance(triggererBody.SimPosition, triggerBody.SimPosition)) / colliderRadius; + return 1.0f - ConvertUnits.ToDisplayUnits(Vector2.Distance(triggererBody.SimPosition, triggerBody.SimPosition) - triggererBody.GetMaxExtent() / 2) / colliderRadius; } public Vector2 GetWaterFlowVelocity(Vector2 viewPosition) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs index 43b8045b2..af181ef67 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs @@ -9,6 +9,7 @@ using System.Reflection; using System.Threading; using System.Threading.Tasks; using System.Xml.Linq; +using static OneOf.Types.TrueFalseOrNull; namespace Barotrauma { @@ -127,7 +128,7 @@ namespace Barotrauma protected readonly List Upgrades = new List(); public readonly HashSet DisallowedUpgradeSet = new HashSet(); - + [Editable, Serialize("", IsPropertySaveable.Yes)] public string DisallowedUpgrades { @@ -182,11 +183,11 @@ namespace Barotrauma public bool IsHighlighted { get { return isHighlighted || ExternalHighlight; } - set + set { if (value != isHighlighted) { - isHighlighted = value; + isHighlighted = value; CheckIsHighlighted(); } } @@ -629,7 +630,7 @@ namespace Barotrauma } if (originalWire.Connections.Any(c => c != null) && - (cloneWire.Connections[0] == null || cloneWire.Connections[1] == null) && + (cloneWire.Connections[0] == null || cloneWire.Connections[1] == null) && cloneItem.GetComponent() == null) { if (!clones.Any(c => (c as Item)?.GetComponent()?.DisconnectedWires.Contains(cloneWire) ?? false)) @@ -743,8 +744,9 @@ namespace Barotrauma /// /// Call Update() on every object in Entity.list /// - public static void UpdateAll(float deltaTime, Camera cam , ParallelOptions parallelOptions) + public static void UpdateAll(float deltaTime, Camera cam, ParallelOptions parallelOptions) { + Random rand = new Random(); #if CLIENT var sw = new System.Diagnostics.Stopwatch(); sw.Start(); @@ -753,26 +755,29 @@ namespace Barotrauma // Buffer lists to avoid repeated allocations var hullList = Hull.HullList.ToList(); var structureList = Structure.WallList.ToList(); - var gapList = Gap.GapList.ToList(); + + List gapList = Gap.GapList.ToList(); + + // This should never break again... right? + int n = gapList.Count; + while (n > 1) + { + n--; + int k = rand.Next(n + 1); + (gapList[n], gapList[k]) = (gapList[k], gapList[n]); + } + var itemList = Item.ItemList.ToList(); // First phase: parallel updates that have no order dependencies Parallel.Invoke(parallelOptions, - // Hull parallel update () => { Parallel.ForEach(hullList, parallelOptions, hull => { - PhysicsBodyQueue.IsInParallelContext = true; - try - { - hull.Update(deltaTime, cam); - } - finally - { - PhysicsBodyQueue.IsInParallelContext = false; - } + hull.Update(deltaTime, cam); }); + }, // Structure parallel update () => @@ -790,12 +795,19 @@ namespace Barotrauma } }); }, - // Gap reset (must be done before update) () => + //update gaps in random order, because otherwise in rooms with multiple gaps + //the water/air will always tend to flow through the first gap in the list, + //which may lead to weird behavior like water draining down only through + //one gap in a room even if there are several + + // moved waterflow reset here to see if we can reduce at least some time { + // PLEASE WORK Parallel.ForEach(gapList, parallelOptions, gap => { gap.ResetWaterFlowThisFrame(); + gap.Update(deltaTime, cam); }); }, // Powered components update @@ -814,24 +826,6 @@ namespace Barotrauma Hull.UpdateCheats(deltaTime, cam); #endif - // Gap update (has order dependencies, keep random order but execute sequentially) - var shuffledGaps = gapList.OrderBy(g => Rand.Int(int.MaxValue)).ToList(); - Parallel.ForEach(shuffledGaps, parallelOptions, gap => - { - PhysicsBodyQueue.IsInParallelContext = true; - try - { - gap.Update(deltaTime, cam); - } - finally - { - PhysicsBodyQueue.IsInParallelContext = false; - } - }); - - // Process any physics operations queued during Gap updates. - PhysicsBodyQueue.ProcessPendingOperations(); - #if CLIENT sw.Stop(); GameMain.PerformanceCounter.AddElapsedTicks("Update:MapEntity:Misc", sw.ElapsedTicks); @@ -847,16 +841,8 @@ namespace Barotrauma { Parallel.ForEach(itemList, parallelOptions, item => { - PhysicsBodyQueue.IsInParallelContext = true; - try - { - lastUpdatedItem = item; - item.Update(deltaTime, cam); - } - finally - { - PhysicsBodyQueue.IsInParallelContext = false; - } + lastUpdatedItem = item; + item.Update(deltaTime, cam); }); } catch (InvalidOperationException e) @@ -868,10 +854,6 @@ namespace Barotrauma throw new InvalidOperationException($"Error while updating item {lastUpdatedItem?.Name ?? "null"}", innerException: e); } - // Process any physics operations that were queued during the parallel update. - // This must be done on the main thread because Farseer Physics is not thread-safe. - PhysicsBodyQueue.ProcessPendingOperations(); - UpdateAllProjSpecific(deltaTime); Spawner?.Update(); @@ -931,7 +913,7 @@ namespace Barotrauma var tags = element.GetAttributeIdentifierArray("tags", Array.Empty()); if (tags.Contains(Tags.HiddenItemContainer)) { - containsHiddenContainers = true; + containsHiddenContainers = true; break; } } @@ -976,7 +958,7 @@ namespace Barotrauma } } } - else if (t == typeof(Item) && !containsHiddenContainers && identifier == "vent" && + else if (t == typeof(Item) && !containsHiddenContainers && identifier == "vent" && submarine.Info.Type == SubmarineType.Player && !submarine.Info.HasTag(SubmarineTag.Shuttle)) { if (!hiddenContainerCreated) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs index 9874a3dcb..4064cbdb4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs @@ -709,7 +709,7 @@ namespace Barotrauma newBody.Friction = 0.8f; newBody.UserData = this; - newBody.Position = ConvertUnits.ToSimUnits(stairPos) + BodyOffset * Scale; + newBody.Position = ConvertUnits.ToSimUnits(stairPos) + ConvertUnits.ToSimUnits(BodyOffset) * Scale; bodyDimensions.Add(newBody, new Vector2(bodyWidth, bodyHeight)); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs index da7483a78..a352baf1b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs @@ -2,18 +2,17 @@ using Barotrauma.IO; using Barotrauma.Items.Components; using Barotrauma.Networking; -using Barotrauma.PerkBehaviors; using FarseerPhysics; using FarseerPhysics.Dynamics; using Microsoft.Xna.Framework; using System; -using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics; using System.Linq; using System.Threading; using System.Xml.Linq; +using Barotrauma.PerkBehaviors; using Voronoi2; namespace Barotrauma @@ -545,9 +544,9 @@ namespace Barotrauma { Rectangle dockedBorders = Borders; checkSubmarineBorders.Add(this); - var connectedSubs = DockedTo.Where(s => - !checkSubmarineBorders.Contains(s) && - !s.Info.IsOutpost && + var connectedSubs = DockedTo.Where(s => + !checkSubmarineBorders.Contains(s) && + !s.Info.IsOutpost && (allowDifferentTeam || s.TeamID == TeamID)); foreach (Submarine dockedSub in connectedSubs) { @@ -569,16 +568,23 @@ namespace Barotrauma return dockedBorders; } - private readonly ConcurrentBag connectedSubs; + private readonly HashSet connectedSubs; /// /// Returns a list of all submarines that are connected to this one via docking ports, including this sub. /// - public ConcurrentBag GetConnectedSubs() + public IEnumerable GetConnectedSubs() { return connectedSubs; } - private void GetConnectedSubsRecursive(ConcurrentBag subs) + public void RefreshConnectedSubs() + { + connectedSubs.Clear(); + connectedSubs.Add(this); + GetConnectedSubsRecursive(connectedSubs); + } + + private void GetConnectedSubsRecursive(HashSet subs) { foreach (Submarine dockedSub in DockedTo) { @@ -588,12 +594,6 @@ namespace Barotrauma } } - public void RefreshConnectedSubs() - { - connectedSubs.Clear(); - connectedSubs.Add(this); - GetConnectedSubsRecursive(connectedSubs); - } /// /// Attempt to find a spawn position close to the specified position where the sub doesn't collide with walls/ruins /// @@ -611,7 +611,7 @@ namespace Barotrauma minWidth += padding; minHeight += padding; - int iterations = 0; + int iterations = 0; const int maxIterations = 5; do { @@ -640,9 +640,9 @@ namespace Barotrauma //if the raycast hit a wall, attempt to place the spawnpos there int offsetFromWall = 10 * -verticalMoveDir; float pickedPos = ConvertUnits.ToDisplayUnits(LastPickedPosition.Y) + offsetFromWall; - closestPickedPos.Y = - verticalMoveDir > 0 ? - Math.Min(closestPickedPos.Y, pickedPos) : + closestPickedPos.Y = + verticalMoveDir > 0 ? + Math.Min(closestPickedPos.Y, pickedPos) : Math.Max(closestPickedPos.Y, pickedPos); } } @@ -657,7 +657,7 @@ namespace Barotrauma bool couldMoveInVerticalMoveDir = Math.Sign(newSpawnPos.Y - spawnPos.Y) == Math.Sign(verticalMoveDir); if (!couldMoveInVerticalMoveDir) { break; } spawnPos = ClampToHorizontalLimits(newSpawnPos, limits); - } + } iterations++; } while (iterations < maxIterations); @@ -1078,7 +1078,7 @@ namespace Barotrauma /// Should plants' branches be ignored? /// If the predicate returns false, the fixture is ignored even if it would normally block visibility. /// A physics body that was between the points (or null) - public static Body CheckVisibility(Vector2 rayStart, Vector2 rayEnd, bool ignoreLevel = false, bool ignoreSubs = false, bool ignoreSensors = true, bool ignoreDisabledWalls = true, bool ignoreBranches = true, + public static Body CheckVisibility(Vector2 rayStart, Vector2 rayEnd, bool ignoreLevel = false, bool ignoreSubs = false, bool ignoreSensors = true, bool ignoreDisabledWalls = true, bool ignoreBranches = true, Predicate blocksVisibilityPredicate = null) { Body closestBody = null; @@ -1237,10 +1237,10 @@ namespace Barotrauma { //a little hacky: undock and redock to ensure the hulls and gaps between docking ports are correct //after all the parts of the submarine have been flipped and moved to correct places. - if (dockingPort.DockingTarget is { } dockingTarget) + if (dockingPort.DockingTarget is { } dockingTarget) { - dockingPort.Undock(); - dockingPort.Dock(dockingTarget); + dockingPort.Undock(); + dockingPort.Dock(dockingTarget); } } @@ -1564,7 +1564,7 @@ namespace Barotrauma { if (ignoreOutposts && sub.Info.IsOutpost) { continue; } if (ignoreOutsideLevel && Level.Loaded != null && sub.IsAboveLevel) { continue; } - if (ignoreRespawnShuttle && sub.IsRespawnShuttle) { continue; } + if (ignoreRespawnShuttle && sub.IsRespawnShuttle) { continue; } if (teamType.HasValue && sub.TeamID != teamType) { continue; } float dist = Vector2.DistanceSquared(worldPosition, sub.WorldPosition); if (closest == null || dist < closestDist) @@ -1623,7 +1623,6 @@ namespace Barotrauma if (includingConnectedSubs) { // Performance-sensitive code -> implemented without Linq. - foreach (Submarine s in connectedSubs) { if (s == entity.Submarine && (allowDifferentTeam || entity.Submarine.TeamID == TeamID) && (allowDifferentType || entity.Submarine.Info.Type == Info.Type)) @@ -1673,7 +1672,7 @@ namespace Barotrauma Vector4 bounds = new Vector4(float.MaxValue, float.MinValue, float.MinValue, float.MaxValue); foreach (XElement element in submarineElement.Elements()) { - if (element.Name == "Structure") + if (element.Name == "Structure") { string name = element.GetAttributeString("name", ""); Identifier identifier = element.GetAttributeIdentifier("identifier", ""); @@ -1715,7 +1714,7 @@ namespace Barotrauma { Stopwatch sw = Stopwatch.StartNew(); - connectedSubs = new ConcurrentBag + connectedSubs = new HashSet(2) { this }; @@ -1900,7 +1899,7 @@ namespace Barotrauma } } - if (Screen.Selected is { IsEditor: false }) + if (Screen.Selected is { IsEditor : false }) { foreach (Identifier layer in Info.LayersHiddenByDefault) { @@ -2082,7 +2081,7 @@ namespace Barotrauma Item itemToSwap = kvp.Key; ItemPrefab swapTo = kvp.Value; itemToSwap.PurchasedNewSwap = item.PurchasedNewSwap; - if (itemToSwap.Prefab != swapTo) { itemToSwap.PendingItemSwap = swapTo; } + if (itemToSwap.Prefab != swapTo) { itemToSwap.PendingItemSwap = swapTo; } } } @@ -2172,8 +2171,8 @@ namespace Barotrauma public static void Unload() { - if (Unloading) - { + if (Unloading) + { DebugConsole.AddWarning($"Called {nameof(Submarine.Unload)} when already unloading."); return; } @@ -2223,7 +2222,7 @@ namespace Barotrauma Ragdoll.RemoveAll(); PhysicsBody.RemoveAll(); - StatusEffect.StopAll(); + StatusEffect.StopAll(); GameMain.World = null; Powered.Grids.Clear(); @@ -2429,10 +2428,10 @@ namespace Barotrauma if (potentialContainer.Submarine == this && !isSecondary) { //valid primary container in the same sub -> perfect, let's use that one - return potentialContainer; + return potentialContainer; } selectedContainer = potentialContainer; - + } return selectedContainer; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs index b2f5a366f..0f0169f3d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs @@ -641,7 +641,11 @@ namespace Barotrauma { for (int dir = -1; dir <= 1; dir += 2) { - WayPoint closest = stairPoints[i].FindClosest(dir, horizontalSearch: true, new Vector2(minDist * 1.5f, minDist / 2)); + //connect to the closest waypoint, preferring non-stair waypoyints + //(it's easier for characters to fully get off stairs before moving on to the next set of stairs, than to move directly from one set of stairs to another) + WayPoint closest = + stairPoints[i].FindClosest(dir, horizontalSearch: true, new Vector2(minDist * 1.5f, minDist / 2), filter: wp => wp.Stairs == null) ?? + stairPoints[i].FindClosest(dir, horizontalSearch: true, new Vector2(minDist * 1.5f, minDist / 2)); if (closest == null) { continue; } stairPoints[i].ConnectTo(closest); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/NetConfig.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetConfig.cs index 0c60db650..bdaeb91dc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/NetConfig.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetConfig.cs @@ -39,6 +39,12 @@ namespace Barotrauma.Networking public static int MaxEventPacketsPerUpdate = 4; + /// + /// When enabled, uses more lenient Lidgren handshake timeouts (longer connection timeout, more retry attempts). + /// Useful for local testing when running multiple instances on the same machine under heavy load. + /// + public static bool UseLenientHandshake; + /// /// Interpolates the positional error of a physics body towards zero. /// diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/NetIdUtils.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetIdUtils.cs index e75202086..54f671521 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/NetIdUtils.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetIdUtils.cs @@ -33,13 +33,7 @@ namespace Barotrauma.Networking /// regarding its relation to values other than the input. /// public static ushort GetIdOlderThan(ushort id) -#if DEBUG - // Debug implementation has some RNG to discourage bad assumptions about the return value - => unchecked((ushort)(id - 1 - Rand.Int(500, sync: Rand.RandSync.Unsynced))); -#else - // Release implementation favors performance => unchecked((ushort)(id - 1)); -#endif public static ushort Difference(ushort id1, ushort id2) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs index cbea90c64..b8be054f1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs @@ -47,7 +47,6 @@ namespace Barotrauma.Networking TOGGLE_RESERVE_BENCH, REQUEST_BACKUP_INDICES, // client wants a list of available backups for a save file - LUA_NET_MESSAGE } enum ClientNetSegment @@ -106,8 +105,6 @@ namespace Barotrauma.Networking UNLOCKRECIPE, //unlocking a fabrication recipe SEND_BACKUP_INDICES, // the server sends a list of available backups for a save file - - LUA_NET_MESSAGE } enum ServerNetSegment { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/AccountInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/AccountInfo.cs index 5ef184891..1c9ff1907 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/AccountInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/AccountInfo.cs @@ -5,7 +5,7 @@ using System.Linq; namespace Barotrauma.Networking { [NetworkSerialize] - readonly struct AccountInfo : INetSerializableStruct + public readonly struct AccountInfo : INetSerializableStruct { public static readonly AccountInfo None = new AccountInfo(Option.None()); @@ -48,4 +48,4 @@ namespace Barotrauma.Networking public static bool operator !=(AccountInfo a, AccountInfo b) => !(a == b); } -} \ No newline at end of file +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Auth/SteamAuthTicketForEosHostAuthenticator.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Auth/SteamAuthTicketForEosHostAuthenticator.cs index ff2788158..aa2d46143 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Auth/SteamAuthTicketForEosHostAuthenticator.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Auth/SteamAuthTicketForEosHostAuthenticator.cs @@ -15,13 +15,18 @@ sealed class SteamAuthTicketForEosHostAuthenticator : Authenticator { string ticketData = ToolBoxCore.ByteArrayToHexString(ticket.Data); - var client = new RestClient(ServerUrl); - - var request = new RestRequest(ServerFile, Method.GET); + var client = RestFactory.CreateClient(ServerUrl); + var request = RestFactory.CreateRequest(ServerFile); request.AddParameter("authticket", ticketData); request.AddParameter("request_version", RemoteRequestVersion); var response = await client.ExecuteAsync(request, Method.GET); + if (response.ErrorException != null) + { + DebugConsole.AddWarning($"Connection error: Failed to verify Steam auth ticket for EOS host " + + $"({response.ErrorException.Message})."); + return AccountInfo.None; + } if (!response.IsSuccessful) { return AccountInfo.None; } try diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Endpoint/Endpoint.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Endpoint/Endpoint.cs index f1599e654..ad6ef6e00 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Endpoint/Endpoint.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Endpoint/Endpoint.cs @@ -2,7 +2,7 @@ namespace Barotrauma.Networking { - abstract class Endpoint + public abstract class Endpoint { public abstract string StringRepresentation { get; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Message/IReadMessage.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Message/IReadMessage.cs index 2bfc7eec4..6259e7678 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Message/IReadMessage.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Message/IReadMessage.cs @@ -4,7 +4,7 @@ using System.Text; namespace Barotrauma.Networking { - interface IReadMessage + public interface IReadMessage { bool ReadBoolean(); void ReadPadBits(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Message/IWriteMessage.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Message/IWriteMessage.cs index b5721153d..47580b4bb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Message/IWriteMessage.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Message/IWriteMessage.cs @@ -2,7 +2,7 @@ namespace Barotrauma.Networking { - interface IWriteMessage + public interface IWriteMessage { void WriteBoolean(bool val); void WritePadBits(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkConnection/NetworkConnection.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkConnection/NetworkConnection.cs index 040321b59..2189a166a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkConnection/NetworkConnection.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkConnection/NetworkConnection.cs @@ -8,14 +8,14 @@ namespace Barotrauma.Networking Disconnected = 0x2 } - abstract class NetworkConnection : NetworkConnection where T : Endpoint + public abstract class NetworkConnection : NetworkConnection where T : Endpoint { protected NetworkConnection(T endpoint) : base(endpoint) { } public new T Endpoint => (base.Endpoint as T)!; } - abstract class NetworkConnection + public abstract class NetworkConnection { public static double TimeoutThresholdNotInGame => GameMain.NetworkMember?.ServerSettings?.TimeoutThresholdNotInGame ?? 60.0; //full minute for timeout because loading screens can take quite a while public static double TimeoutThresholdInGame => GameMain.NetworkMember?.ServerSettings?.TimeoutThresholdInGame ?? 10.0; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/RespawnManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/RespawnManager.cs index 8f67b919d..a9d9ab0d6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/RespawnManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/RespawnManager.cs @@ -213,9 +213,6 @@ namespace Barotrauma.Networking public void Update(float deltaTime) { - var result = GameMain.LuaCs.Hook.Call("respawnManager.update"); - if (result != null && result.Value) { return; } - foreach (var teamSpecificState in teamSpecificStates.Values) { if (RespawnShuttles.None()) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs index 8918f9252..b6a2008ec 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs @@ -458,7 +458,7 @@ namespace Barotrauma.Networking private set; } - [Serialize(300.0f, IsPropertySaveable.Yes)] + [Serialize(30.0f, IsPropertySaveable.Yes)] public float RespawnInterval { get; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs b/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs index 2cb61b2a0..afa0978db 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs @@ -591,8 +591,10 @@ namespace Barotrauma default: throw new NotImplementedException(); } - return spritesheetRotation.HasValue ? Vector2.Transform(pos, Matrix.CreateRotationZ(-spritesheetRotation.Value)) : pos; + return spritesheetRotation.HasValue ? RotateVector(pos, spritesheetRotation.Value) : pos; } + + public static Vector2 RotateVector(Vector2 v, float rotation) => Vector2.Transform(v, Matrix.CreateRotationZ(-rotation)); public float GetMaxExtent() { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Prefabs/PrefabCollection.cs b/Barotrauma/BarotraumaShared/SharedSource/Prefabs/PrefabCollection.cs index 47b575be5..f3a0a4842 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Prefabs/PrefabCollection.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Prefabs/PrefabCollection.cs @@ -410,7 +410,7 @@ namespace Barotrauma && otherPrefab.UintIdentifier == prefabWithUintIdentifier.UintIdentifier); for (T? collision = findCollision(); collision != null; collision = findCollision()) { - DebugConsole.ThrowError($"Hashing collision when generating uint identifiers for {typeof(T).Name}: {prefab.Identifier} has the same UintIdentifier as {collision.Identifier} ({prefabWithUintIdentifier.UintIdentifier})"); + DebugConsole.AddWarning($"Hashing collision when generating uint identifiers for {typeof(T).Name}: {prefab.Identifier} has the same UintIdentifier as {collision.Identifier} ({prefabWithUintIdentifier.UintIdentifier})"); prefabWithUintIdentifier.UintIdentifier++; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Screens/GameScreen.cs b/Barotrauma/BarotraumaShared/SharedSource/Screens/GameScreen.cs index 3dc8dfac2..7e5620e14 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Screens/GameScreen.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Screens/GameScreen.cs @@ -1,6 +1,4 @@ -//#define RUN_PHYSICS_IN_SEPARATE_THREAD - -using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework; using System.Threading; using FarseerPhysics.Dynamics; using FarseerPhysics; @@ -8,6 +6,7 @@ using System.Threading.Tasks; using System.Linq; using System.Collections.Generic; using System; +using static Barotrauma.SingleThreadWorker; #if DEBUG && CLIENT @@ -20,12 +19,10 @@ namespace Barotrauma { partial class GameScreen : Screen { - private object updateLock = new object(); - private double physicsTime; - + // Use default instead. Hopefully this wont cause issues in long-running servers. private static readonly ParallelOptions parallelOptions = new ParallelOptions { - MaxDegreeOfParallelism = Math.Max(1,Environment.ProcessorCount - 1), + //MaxDegreeOfParallelism = Math.Max(4, Environment.ProcessorCount - 1) }; #if CLIENT @@ -63,13 +60,13 @@ namespace Barotrauma #if CLIENT if (Character.Controlled != null) { - cam.Position = Character.Controlled.WorldPosition; - cam.UpdateTransform(true); + Cam.Position = Character.Controlled.WorldPosition; + Cam.UpdateTransform(true); } else if (Submarine.MainSub != null) { - cam.Position = Submarine.MainSub.WorldPosition; - cam.UpdateTransform(true); + Cam.Position = Submarine.MainSub.WorldPosition; + Cam.UpdateTransform(true); } GameMain.GameSession?.CrewManager?.ResetCrewListOpenState(); ChatBox.ResetChatBoxOpenState(); @@ -78,14 +75,6 @@ namespace Barotrauma MapEntity.ClearHighlightedEntities(); -#if RUN_PHYSICS_IN_SEPARATE_THREAD - var physicsThread = new Thread(ExecutePhysics) - { - Name = "Physics thread", - IsBackground = true - }; - physicsThread.Start(); -#endif } public override void Deselect() @@ -114,21 +103,15 @@ namespace Barotrauma public override void Update(double deltaTime) { -#warning For now CL side performance counter is partly useless bucz multiple changes on such things. Need time to take care of it - -#if RUN_PHYSICS_IN_SEPARATE_THREAD - physicsTime += deltaTime; - lock (updateLock) - { -#endif - + var submarines = Submarine.Loaded.ToList(); + var physicsBodies = PhysicsBody.List.ToList(); #if DEBUG && CLIENT if (GameMain.GameSession != null && !DebugConsole.IsOpen && GUI.KeyboardDispatcher.Subscriber == null) { if (GameMain.GameSession.Level != null && GameMain.GameSession.Submarine != null) { - Submarine closestSub = Submarine.FindClosest(cam.WorldViewCenter) ?? GameMain.GameSession.Submarine; + Submarine closestSub = Submarine.FindClosest(Cam.WorldViewCenter) ?? GameMain.GameSession.Submarine; Vector2 targetMovement = Vector2.Zero; if (PlayerInput.KeyDown(Keys.I)) { targetMovement.Y += 1.0f; } @@ -150,8 +133,6 @@ namespace Barotrauma GameTime += deltaTime; - var physicsBodies = PhysicsBody.List.ToList(); - Parallel.ForEach(physicsBodies, parallelOptions, body => { if ((body.Enabled || body.UserData is Character) && @@ -160,49 +141,38 @@ namespace Barotrauma body.Update(); } }); - GameMain.GameSession?.Update((float)deltaTime); - - Parallel.ForEach(physicsBodies, parallelOptions, body => - { - if (body.Enabled && body.BodyType != BodyType.Static) - { - body.SetPrevTransform(body.SimPosition, body.Rotation); - } - }); MapEntity.ClearHighlightedEntities(); #if CLIENT var sw = new System.Diagnostics.Stopwatch(); sw.Start(); - - Parallel.Invoke(parallelOptions, - () => GameMain.ParticleManager.Update((float)deltaTime), - () => - { - PhysicsBodyQueue.IsInParallelContext = true; - try - { - if (Level.Loaded != null) Level.Loaded.Update((float)deltaTime, cam); - } - finally - { - PhysicsBodyQueue.IsInParallelContext = false; - } - } - ); - - // Process any physics operations queued during Level update - PhysicsBodyQueue.ProcessPendingOperations(); - +#endif + GameMain.GameSession?.Update((float)deltaTime); +#if CLIENT sw.Stop(); - GameMain.PerformanceCounter.AddElapsedTicks("Update:Particles+Level", sw.ElapsedTicks); + GameMain.PerformanceCounter.AddElapsedTicks("Update:GameSession", sw.ElapsedTicks); + sw.Restart(); + + GameMain.ParticleManager?.Update((float)deltaTime); + + sw.Stop(); + GameMain.PerformanceCounter.AddElapsedTicks("Update:Particle", sw.ElapsedTicks); + sw.Restart(); + +#endif + + if (Level.Loaded != null) { Level.Loaded.Update((float)deltaTime, Cam); } + +#if CLIENT + sw.Stop(); + GameMain.PerformanceCounter.AddElapsedTicks("Update:Level", sw.ElapsedTicks); if (Character.Controlled is { } controlled) { if (controlled.SelectedItem != null && controlled.CanInteractWith(controlled.SelectedItem)) { - controlled.SelectedItem.UpdateHUD(cam, controlled, (float)deltaTime); + controlled.SelectedItem.UpdateHUD(Cam, controlled, (float)deltaTime); } if (controlled.Inventory != null) { @@ -210,7 +180,7 @@ namespace Barotrauma { if (controlled.HasEquippedItem(item)) { - item.UpdateHUD(cam, controlled, (float)deltaTime); + item.UpdateHUD(Cam, controlled, (float)deltaTime); } } } @@ -218,11 +188,19 @@ namespace Barotrauma sw.Restart(); - Character.UpdateAll((float)deltaTime, cam); +#endif + Character.UpdateAll((float)deltaTime, Cam); +#if CLIENT sw.Stop(); GameMain.PerformanceCounter.AddElapsedTicks("Update:Character", sw.ElapsedTicks); sw.Restart(); +#endif + + //StatusEffect.UpdateAll is not thread-safe and must be executed on the main thread + StatusEffect.UpdateAll((float)deltaTime); + +#if CLIENT sw.Stop(); GameMain.PerformanceCounter.AddElapsedTicks("Update:StatusEffects", sw.ElapsedTicks); @@ -248,51 +226,31 @@ namespace Barotrauma } Vector2 screenOffset = screenTargetPos - new Vector2(GameMain.GraphicsWidth / 2, GameMain.GraphicsHeight / 2); screenOffset.Y = -screenOffset.Y; - targetPos -= screenOffset / cam.Zoom; + targetPos -= screenOffset / Cam.Zoom; } } - cam.TargetPos = targetPos; + Cam.TargetPos = targetPos; } - cam.MoveCamera((float)deltaTime, allowZoom: GUI.MouseOn == null && !Inventory.IsMouseOnInventory); + Cam.MoveCamera((float)deltaTime, allowZoom: GUI.MouseOn == null && !Inventory.IsMouseOnInventory); - Character.Controlled?.UpdateLocalCursor(cam); - -#elif SERVER - // DO NOT PARALLELIZE THESE TWO OR IT MAY STUCK HERE - // SO FOLLOW THE ORIGINAL SINGLE-THREAD LOGIC STRICTLY - - if (Level.Loaded != null) Level.Loaded.Update((float)deltaTime, Camera.Instance); - - Character.UpdateAll((float)deltaTime, Camera.Instance); - - StatusEffect.UpdateAll((float)deltaTime); - - PhysicsBodyQueue.ProcessPendingOperations(); + Character.Controlled?.UpdateLocalCursor(Cam); #endif - var submarines = Submarine.Loaded.ToList(); - - Parallel.ForEach(submarines, parallelOptions, sub => + foreach (Submarine sub in submarines) { sub.SetPrevTransform(sub.Position); - }); + } Parallel.ForEach(physicsBodies, parallelOptions, body => { - if (body.Enabled && body.BodyType != FarseerPhysics.BodyType.Static) - { - body.SetPrevTransform(body.SimPosition, body.Rotation); + if (body.Enabled && body.BodyType != FarseerPhysics.BodyType.Static) + { + body.SetPrevTransform(body.SimPosition, body.Rotation); } }); -#if CLIENT - MapEntity.UpdateAll((float)deltaTime, cam, parallelOptions); -#elif SERVER - - MapEntity.UpdateAll((float)deltaTime, Camera.Instance, parallelOptions); - -#endif + MapEntity.UpdateAll((float)deltaTime, Cam, parallelOptions); #if CLIENT sw.Stop(); @@ -303,11 +261,8 @@ namespace Barotrauma Character.UpdateAnimAll((float)deltaTime); PhysicsBodyQueue.ProcessPendingOperations(); -#if CLIENT - Ragdoll.UpdateAll((float)deltaTime, cam); -#elif SERVER - Ragdoll.UpdateAll((float)deltaTime, Camera.Instance); -#endif + + Ragdoll.UpdateAll((float)deltaTime, Cam); #if CLIENT sw.Stop(); @@ -315,7 +270,7 @@ namespace Barotrauma sw.Restart(); #endif - foreach(Submarine sub in submarines) + foreach (Submarine sub in submarines) { sub.Update((float)deltaTime); } @@ -326,7 +281,8 @@ namespace Barotrauma sw.Restart(); #endif -#if !RUN_PHYSICS_IN_SEPARATE_THREAD + SingleThreadActionStandbySignal.Wait(); + try { GameMain.World.Step((float)Timing.Step); @@ -337,34 +293,16 @@ namespace Barotrauma DebugConsole.ThrowError(errorMsg, e); GameAnalyticsManager.AddErrorEventOnce("GameScreen.Update:WorldLockedException" + e.Message, GameAnalyticsManager.ErrorSeverity.Critical, errorMsg); } -#endif + + SingleThreadActionStandbySignal.Release(); #if CLIENT sw.Stop(); GameMain.PerformanceCounter.AddElapsedTicks("Update:Physics", sw.ElapsedTicks); #endif UpdateProjSpecific(deltaTime); - -#if RUN_PHYSICS_IN_SEPARATE_THREAD - } -#endif } partial void UpdateProjSpecific(double deltaTime); - - private void ExecutePhysics() - { - while (true) - { - while (physicsTime >= Timing.Step) - { - lock (updateLock) - { - GameMain.World.Step((float)Timing.Step); - physicsTime -= Timing.Step; - } - } - } - } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs b/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs index b04914057..a3a19cddb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs @@ -36,6 +36,27 @@ namespace Barotrauma { typeof(Rectangle), (str, defVal) => ParseRect(str, true) } }.ToImmutableDictionary(); + /// + /// Check if the given value equals to the default value of the property. + /// Takes into account that certain default values (e.g. Vectors and other values that aren't compile-time constants) are defined as strings. + /// + public static bool DefaultValueEquals(object defaultValue, object value) + { + //if the value is given as a string, check if there's a converter for the type of the default value and attempt converting + if (defaultValue != null && value is string valueAsString && + Converters.TryGetKey(defaultValue.GetType(), out Type type)) + { + return Equals(Converters[type].Invoke(valueAsString, defaultValue), defaultValue); + } + //other way around: default values is given as a string, check if there's a converter for the type of the value + else if (value != null && defaultValue is string defaultValueAsString && + Converters.TryGetKey(value.GetType(), out Type type2)) + { + return Equals(Converters[type2].Invoke(defaultValueAsString, value), value); + } + return Equals(value, defaultValue); + } + public static string ParseContentPathFromUri(this XObject element) => !string.IsNullOrWhiteSpace(element.BaseUri) ? System.IO.Path.GetRelativePath(Environment.CurrentDirectory, element.BaseUri.CleanUpPath()) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs index eb44f9d6b..8f22a4da0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs @@ -72,6 +72,7 @@ namespace Barotrauma EnableSplashScreen = true, PauseOnFocusLost = true, RemoteMainMenuContentUrl = "https://www.barotraumagame.com/gamedata/", + RemoteContentTimeoutSeconds = 15f, AimAssistAmount = DefaultAimAssist, ShowEnemyHealthBars = EnemyHealthBarMode.ShowAll, ChatSpeechBubbles = true, @@ -167,6 +168,17 @@ namespace Barotrauma public bool EnableSubmarineAutoSave; public Identifier QuickStartSub; public string RemoteMainMenuContentUrl; + + /// + /// Timeout in seconds for HTTP requests to remote content servers. + /// + public float RemoteContentTimeoutSeconds; + + /// + /// Returns converted to milliseconds needed by eg. RestSharp. + /// + public readonly int RemoteContentTimeoutMs => (int)(RemoteContentTimeoutSeconds * 1000); + #if CLIENT public Eos.EosSteamPrimaryLogin.CrossplayChoice CrossplayChoice; public XElement SavedCampaignSettings; diff --git a/Barotrauma/BarotraumaShared/SharedSource/SingleThreadWorker.cs b/Barotrauma/BarotraumaShared/SharedSource/SingleThreadWorker.cs new file mode 100644 index 000000000..c16530aa8 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/SingleThreadWorker.cs @@ -0,0 +1,105 @@ +using Barotrauma.Networking; +using System; +using System.Collections.Concurrent; +using System.Threading; +using System.Threading.Tasks; +using static Barotrauma.EosInterface.Ownership; + +namespace Barotrauma +{ + public class SingleThreadWorker + { + private ConcurrentQueue ActionQueue; + + public static SingleThreadWorker Instance = new SingleThreadWorker(); + + private readonly CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(); + private readonly SemaphoreSlim actionSignal = new SemaphoreSlim(0); + private static Task WorkerTask; + + public static readonly SemaphoreSlim SingleThreadActionStandbySignal = new SemaphoreSlim(1); + + /// + /// Initilize a SingleThreadWorker + /// SingleThreadWorker or STW for short is a FIFO queue ensure single-thread execution of a series of actions. + /// + public SingleThreadWorker() + { + ActionQueue = new ConcurrentQueue(); + WorkerTask = CreateProcessTask(cancellationTokenSource.Token); + } + + public void Dispose() + { + cancellationTokenSource.Cancel(); + WorkerTask.Wait(); + WorkerTask.Dispose(); + Instance = null; + cancellationTokenSource.Dispose(); + actionSignal.Dispose(); + SingleThreadActionStandbySignal.Dispose(); + } + + private async Task CreateProcessTask(CancellationToken token) + { + while (!token.IsCancellationRequested) + { + try + { + await actionSignal.WaitAsync(100, token); + SingleThreadActionStandbySignal.Wait(CancellationToken.None); + RunActions(); + } + catch (OperationCanceledException) + { + break; + } + finally + { + SingleThreadActionStandbySignal.Release(); + } + } + } + + /// + /// Add a pending action in a STW queue. + /// DO NOT ABUSE IT OR IT WILL SLOW DOWN MAIN THREAD!!!! + /// + /// + public void AddAction(Action action) + { + // enqueue and let background task handle the rest + ActionQueue.Enqueue(action); + + if (actionSignal.CurrentCount == 0) + { + actionSignal.Release(); + } + } + + /// + /// Run all pending actions in the STW queue + /// + [STAThread] + private void RunActions() + { + while (ActionQueue.TryDequeue(out Action action)) + { + try + { + action.Invoke(); + } + catch (Exception e) + { + // Just try-catch and do nothing but print errorlogs. We cannot afford crashing the game. + ConsoleColor originalForeground = Console.ForegroundColor; + Console.ForegroundColor = ConsoleColor.Yellow; + Console.WriteLine($"WARNING: Error occurred when running Single Thread Actions." + + $"If the server didn't crash or stop responding then this should be fine \n{e}"); + Console.ForegroundColor = Console.ForegroundColor; + } + } + } + + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs index 7aaf375bc..98af771f9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs @@ -868,6 +868,12 @@ namespace Barotrauma /// public Vector2 Offset { get; private set; } + /// + /// Should be rotated, flipped and scaled based on the entity that this effect is executed by? + /// Currently only supports status effects in items. + /// + public bool OffsetCopiesEntityTransform { get; private set; } + /// /// An random offset (in a random direction) added to the position of the effect is executed at. Only relevant if the effect does something where position matters, /// for example emitting particles or explosions, spawning something or playing sounds. @@ -928,6 +934,7 @@ namespace Barotrauma Range = element.GetAttributeFloat("range", 0.0f); Offset = element.GetAttributeVector2("offset", Vector2.Zero); + OffsetCopiesEntityTransform = element.GetAttributeBool(nameof(OffsetCopiesEntityTransform), false); RandomOffset = element.GetAttributeFloat("randomoffset", 0.0f); string[] targetLimbNames = element.GetAttributeStringArray("targetlimb", null) ?? element.GetAttributeStringArray("targetlimbs", null); if (targetLimbNames != null) @@ -1830,6 +1837,7 @@ namespace Barotrauma protected Vector2 GetPosition(Entity entity, IReadOnlyList targets, Vector2? worldPosition = null) { Vector2 position = worldPosition ?? (entity == null || entity.Removed ? Vector2.Zero : entity.WorldPosition); + if (worldPosition == null) { if (entity is Character character && !character.Removed && targetLimbs != null) @@ -1866,9 +1874,22 @@ namespace Barotrauma } } } - } - position += Offset; + + Vector2 offset = Offset; + + if (OffsetCopiesEntityTransform) + { + if (entity is Item item) + { + offset *= item.Scale; + if (item.FlippedX) { offset.X *= -1; } + if (item.FlippedY) { offset.Y *= -1; } + offset = Vector2.Transform(offset, Matrix.CreateRotationZ(-item.RotationRad)); + } + } + + position += offset; position += Rand.Vector(Rand.Range(0.0f, RandomOffset)); return position; } @@ -1886,14 +1907,14 @@ namespace Barotrauma { if (entity is Item item) { - var result = GameMain.LuaCs.Hook.Call("statusEffect.apply." + item.Prefab.Identifier, this, deltaTime, entity, targets, worldPosition); + var result = LuaCsSetup.Instance.Hook.Call("statusEffect.apply." + item.Prefab.Identifier, this, deltaTime, entity, targets, worldPosition); if (result != null && result.Value) { return; } } if (entity is Character character) { - var result = GameMain.LuaCs.Hook.Call("statusEffect.apply." + character.SpeciesName, this, deltaTime, entity, targets, worldPosition); + var result = LuaCsSetup.Instance.Hook.Call("statusEffect.apply." + character.SpeciesName, this, deltaTime, entity, targets, worldPosition); if (result != null && result.Value) { return; } } @@ -1903,7 +1924,7 @@ namespace Barotrauma { foreach ((string hookName, ContentXElement element) in luaHook) { - var result = GameMain.LuaCs.Hook.Call(hookName, this, deltaTime, entity, targets, worldPosition, element); + var result = LuaCsSetup.Instance.Hook.Call(hookName, this, deltaTime, entity, targets, worldPosition, element); if (result != null && result.Value) { return; } } @@ -2193,24 +2214,9 @@ namespace Barotrauma { LocalizedString messageToSay = TextManager.Get(forceSayIdentifier).Fallback(forceSayIdentifier.Value); - if (!messageToSay.IsNullOrEmpty() && target is Character targetCharacter && targetCharacter.SpeechImpediment < 100.0f && !targetCharacter.IsDead) + if (!messageToSay.IsNullOrEmpty() && target is Character targetCharacter) { - ChatMessageType messageType = ChatMessageType.Default; - bool canUseRadio = ChatMessage.CanUseRadio(targetCharacter, out WifiComponent radio); - if (canUseRadio && forceSayInRadio) - { - messageType = ChatMessageType.Radio; - } -#if SERVER - GameMain.Server?.SendChatMessage(messageToSay.Value, messageType, senderClient: null, targetCharacter); -#elif CLIENT - //no need to create the message when playing as a client, the server will send it to us - if (isNotClient) - { - AIChatMessage message = new AIChatMessage(messageToSay.Value, messageType); - targetCharacter.SendSinglePlayerMessage(message, canUseRadio, radio); - } -#endif + targetCharacter.ForceSay(messageToSay, forceSayInRadio); } } @@ -2362,7 +2368,10 @@ namespace Barotrauma inheritedTeam = entity switch { Character c => c.TeamID, - Item it => it.GetRootInventoryOwner() is Character owner ? owner.TeamID : GetTeamFromSubmarine(it), + Item it => + (it.GetRootInventoryOwner() as Character ?? it.PreviousParentInventory?.Owner as Character) is { } owner ? + owner.TeamID : + GetTeamFromSubmarine(it), MapEntity e => GetTeamFromSubmarine(e), _ => null // Default to Team1, when we can't deduce the team (for example when spawning outside the sub AND character inventory). diff --git a/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/TrimLString.cs b/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/TrimLString.cs index 92c987b9e..4eca59969 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/TrimLString.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/TrimLString.cs @@ -9,19 +9,21 @@ namespace Barotrauma public enum Mode { Start = 0x1, End = 0x2, Both=0x3 } private readonly LocalizedString nestedStr; private readonly Mode mode; + private readonly char[]? trimCharacters; - public TrimLString(LocalizedString nestedStr, Mode mode) + public TrimLString(LocalizedString nestedStr, Mode mode, char[]? trimCharacters = null) { this.nestedStr = nestedStr; this.mode = mode; + this.trimCharacters = trimCharacters; } public override bool Loaded => nestedStr.Loaded; public override void RetrieveValue() { cachedValue = nestedStr.Value; - if (mode.HasFlag(Mode.Start)) { cachedValue = cachedValue.TrimStart(); } - if (mode.HasFlag(Mode.End)) { cachedValue = cachedValue.TrimEnd(); } + if (mode.HasFlag(Mode.Start)) { cachedValue = cachedValue.TrimStart(trimCharacters); } + if (mode.HasFlag(Mode.End)) { cachedValue = cachedValue.TrimEnd(trimCharacters); } UpdateLanguage(); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Text/TextPack.cs b/Barotrauma/BarotraumaShared/SharedSource/Text/TextPack.cs index 19ee41a2f..5ba27c155 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Text/TextPack.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Text/TextPack.cs @@ -242,7 +242,10 @@ namespace Barotrauma } - Barotrauma.IO.File.WriteAllText($"csv_{Language.ToString().ToLower()}_{index}.csv", sb.ToString()); + string fileName = $"csv_{Language.ToString().ToLower()}_{index}.csv"; + Barotrauma.IO.File.WriteAllText(fileName, sb.ToString()); + + DebugConsole.NewMessage($"Wrote \"{ContentFile.Path}\" to \"{fileName}\""); } #endif } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/RestFactory.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/RestFactory.cs new file mode 100644 index 000000000..1c3c38f97 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/RestFactory.cs @@ -0,0 +1,35 @@ +using RestSharp; + +namespace Barotrauma +{ + /// + /// Factory methods for creating RestSharp clients and requests with default timeout + /// settings, to avoid unforeseen connectivity issues hanging the game. + /// The timeout needs to be added to both the client and the request, due to known + /// issues with RestSharp 106.x that we use: https://github.com/restsharp/RestSharp/issues/1900 + /// + public static class RestFactory + { + /// + /// Creates a RestClient with applied. + /// + public static RestClient CreateClient(string baseUrl) + { + return new RestClient(baseUrl) + { + Timeout = GameSettings.CurrentConfig.RemoteContentTimeoutMs + }; + } + + /// + /// Creates a RestRequest with applied. + /// + public static RestRequest CreateRequest(string resource, Method method = Method.GET) + { + return new RestRequest(resource, method) + { + Timeout = GameSettings.CurrentConfig.RemoteContentTimeoutMs + }; + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs index 2ab344c36..a1e521bfb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs @@ -12,6 +12,7 @@ using System.Net; using System.Reflection; using System.Security.Cryptography; using System.Text; +using System.Collections.Concurrent; namespace Barotrauma { diff --git a/Barotrauma/BarotraumaShared/changelog.txt b/Barotrauma/BarotraumaShared/changelog.txt index fcd1b5730..1e029d35c 100644 --- a/Barotrauma/BarotraumaShared/changelog.txt +++ b/Barotrauma/BarotraumaShared/changelog.txt @@ -1,4 +1,107 @@ ------------------------------------------------------------------------------------------------------------------------------------------------- +v1.12.7.0 +------------------------------------------------------------------------------------------------------------------------------------------------- + +- Reduced how much the new weak points in the reworked subs push bots around to make them more capable of fixing broken weak points. +- Fixed selecting any item that forces the character to some pose (chairs, periscopes) getting logged in the server console. +- Mac only: added a button for settings mic permissions to the audio settings. It seems that on Mac, the game updates may cause the OS to revoke the permissions. +- Fixed some of the Workshop tags you can choose in-game not working on Steam's side. + +Modding: +- Fixed contained items being misaligned on attachable items (e.g. in mods that make diving suit cabinets attachable). +- Fixed monsters spawned by an event inside an outpost being unable to attack any items inside that outpost. To our knowledge, didn't affect vanilla events, but caused issues in certain mods. + +------------------------------------------------------------------------------------------------------------------------------------------------- +v1.12.6.2 +------------------------------------------------------------------------------------------------------------------------------------------------- + +- Updated localization files. + +------------------------------------------------------------------------------------------------------------------------------------------------- +v1.12.6.1 +------------------------------------------------------------------------------------------------------------------------------------------------- + +- Fixed some conversation prompts (such as the one with Artie Dolittle) being misaligned, causing parts the text to be cropped. More specifically, ones that start with some special event sprite but also show the speaker's face in the prompt. +- Fixed pets having trouble moving due to some of the navigation changes in the previous build. Also caused huskified containers to get launched off with enormous speed when they tried to eat something. +- Fixed navigation terminals in shuttles having their maintain position get messed up between level transitions. + +------------------------------------------------------------------------------------------------------------------------------------------------- +v1.12.6.0 (Spring Update 2026) +------------------------------------------------------------------------------------------------------------------------------------------------- + +Submarine reworks: +- Tier 1 submarines (Barsuk, Dugong, Orca and Camel) have all received a visual polish as well as gameplay polishing. +- Camel and Orca now uses the pipe weakpoints and valves system. +- Various improvements and additions to the vanilla item assemblies. + +Balance: +- Increase health of flare, alienflare and glowstick. Now doesn't get destroyed as quickly by monsters, allowing it be a more useful distraction again. Glowsticks don't aggro monsters from as far as flares. +- Sonar Beacon's sound range is reduced, and when dropped can now be destroyed by monsters (to avoid making flares redundant due to being a better and invulnerable version of monster distraction). + +Changes: +- Characters can now be "deconstructed" by dragging them into a deconstructor, producing small amounts of raw materials. Also a handy way to get rid of monster corpses on the submarine, and perhaps problematic crew mates as well. +- Stationary batteries can charge the battery cells inside them even when the output is disabled. +- A handful of missions in which you can earn a reward for getting through the level fast enough (which also serve as an example of the new custom mission functionality, see the Modding section for more information). +- Minor lighting optimizations. + +Multiplayer: +- Reduced the default respawn interval from 300 seconds to 30. +- Fixed an issue that sometimes caused the list of hidden subs to get out of sync in multiplayer, preventing some subs from being purchased. +- Fixed pickup sound not playing when picking up an item in multiplayer. +- Fixed karma system considering bandages and other medical items "dangerous" and giving a penalty for taking them from other players. +- Characters no longer drop items when the player disconnects (meaning you won't lose the items you were holding). + +Fixes: +- Another attempt to fix reported freezes at 80% in the loading screen, which seems to have been caused by Steam's servers or our master server refusing connection attempts from certain kinds of IPs, causing the game to hang waiting for the connection. +- Fixed conversation/event prompts sometimes getting stuck when you went rapidly pressing E. In particular, this happened with events that allow you to retrigger the same event by interacting with the NPC again. +- Fixed certain monsters (e.g. mudraptors) having trouble dropping through hatches inside the sub. +- Fixed monsters often failing to follow targets from sub to another (e.g. from Remora's drone to the main sub). +- Fixes to pathfinding bugs that sometimes caused bots to get stuck on stairs. +- Fixed closing the health interface while your cursor is on another character opening that character's health interface. +- Fixed an AI bug that often prevented outpost NPCs from putting out fires. +- Fixed projectiles that fire more than one raycast per shot (e.g. shotgun shells) only registering one hit if you're firing from inside to outside. +- Fixed implacable sometimes not triggering in time, causing a 5-second stun when vitality dropped below zero. +- Fixed radio jammer not having the traitormissionitem tag (unlike every other traitor mission item). +- Fixed ruin scan missions sometimes failing to choose all 3 positions to scan, making the mission impossible to complete. Happened with very small ruins in particular. +- Fixed Engineering_G4 module sometimes spawning with a ladder leading nowhere. +- Hide items inside non-interactable containers (e.g. decorative items not accessible to the player) showing up on the item finder. +- The achievement for killing a monster is also awarded if the monster is killed by an bot in single player. +- Fixed some items sometimes teleporting to the origin when saving and loading in the submarine editor. +- Fixed a broken waypoint near Berilia's engine which made bots sometimes get stuck there. +- Fixed shuttles/drones/elevators or other parts of a wreck getting crush depth damage in deep levels. +- Characters that respawn in a flooded hull (in either a submarine or an outpost) now spawn with diving gear. +- Fixed fabrication tooltip being unclear (previously showed "requires recipe to fabricate" in red even when the recipe is already learnt, now shows in green that is has been learned) +- Fixed characters sometimes not taking fall damage if they fall on a mirrored structure. +- Fixed portable pumps getting damaged by explosions, despite not being repairable. +- Fixed pet raptors getting assigned an incorrect team if they hatch in a hostile outpost. +- Fixed Linux systems failing to load content packages whose filelist.xml files aren't all lower-case. +- Fixed NPCs ignoring infected humans attacking them. +- Fixed mirrored items becoming unmirrored when swapped by perk points (e.g. a mirrored periscope base becoming an unmirrored periscope when purchasing a turret with a perk point). +- Fixed inability to rename already-hired bots if you no longer have the required reputation to hire the bot. +- Fixed WeaponDamageModifier (a multiplier in RangedWeapon which seems to be used for buffing the damage of variants of a weapon) not affecting explosion damage. Means that e.g. Harpoon-Coil Rifle or Autoshotgun's modifiers don't actually do much when using explosive ammo. +- Fixed creatures not being able to "properly leave a sub" if any of their severed limbs are still inside the sub. In practice, they'd still be considered to be inside the sub, and they would not collide with anything outside it. +- Fixed toggling layer visibility selecting that layer in the sub editor. +- Fixed pasting entities unhiding all of the layers in the sub editor. +- Fixed condition_out connections not taking into account the multipliers applied by the Tinkerer talent. +- Fixed bots still refusing to deconstruct items that yield nothing, even though you could order them to deconstruct those. + +Modding: +- Support for custom event-based missions. The mission simply triggers a specific event, and that event can control the success/failure of the mission using MissionStateAction. See the "TimeTrial" missions in Missions.xml for an usage example. +- Character, level and particle editors show fields set to the default values as gray. Makes it easier to tell which fields have been modified or are relevant for that specific character/level/particle. +- It's possible to add empty RequiredItem elements to item components to make them not have any requirements by default, but allowing them to be added in the submarine editor. +- Fixed limb's randomcolor attribute not working as expected: every character of the same type would get the same randomly chosen color, instead of a different color being chosen for each character. +- Added ForceSayAction which can be used by scripted events to make characters speak in the chat. ConversationAction can also now be used to display text in the chat in addition to the conversation prompt. +- CheckConditionalAction now fails instead of succeeding if it can't find the specified target. There's also a property called FailIfTargetNotFound to make it succeed instead. +- CountTargetsAction now fails instead of succeeding if it's trying to compare against the amount of some other target (e.g. "number of flooded hulls" is at least 30% of the "number of all hulls") and none of that other target can't be found. +- Fixed light offsets not being handled correctly on flipped items (did not affect any vanilla items). +- Adds a new status effect property called OffsetCopiesEntityTransform that can be used by status effects to configure the offset so it copies the current entity rotation/flipping/scaling. +- Fixed TargetSlot in RequiredItem not working properly on items that have multiple ItemContainers/inventories (only the first one was checked). +- Fixed melee weapons sometimes hitting characters whose limbs have been set to ignore collisions. More specifically, the weapon would still hit the character's "main collider". +- If a beacon station has a sonar transducer connected to the sonar monitor, and the monitor is set to use transducers, the transducer must be powered for the beacon mission to complete. +- Fix ContainedSpriteDepth being tied to the item's index in the container instead of the slot index. +- Fixed OnInserted StatusEffects triggering when you try to swap an item inside some other item but the swap fails. + +------------------------------------------------------------------------------------------------------------------------------------------------- v1.11.5.0 (Winter Update Hotfix 1) ------------------------------------------------------------------------------------------------------------------------------------------------- @@ -349,7 +452,6 @@ v1.8.8.1 Modding: - Fixed transferring afflictions to a newly spawned character using status effects causing a crash if the original character had already been removed. Didn't affect any vanilla content. ->>>>>>> master ------------------------------------------------------------------------------------------------------------------------------------------------- v1.8.7.0 diff --git a/Barotrauma/BarotraumaTest/LuaCs/HookPatchHelpers.cs b/Barotrauma/BarotraumaTest/LuaCs/HookPatchHelpers.cs index cdc4ed6f1..854f01140 100644 --- a/Barotrauma/BarotraumaTest/LuaCs/HookPatchHelpers.cs +++ b/Barotrauma/BarotraumaTest/LuaCs/HookPatchHelpers.cs @@ -1,12 +1,14 @@ extern alias Client; - -using Client::Barotrauma; +extern alias Server; +using Client::Barotrauma.LuaCs; +using Client::Barotrauma; using MoonSharp.Interpreter; using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Text; using System.Threading; +using Server::Barotrauma.LuaCs.Compatibility; using Xunit; namespace TestProject.LuaCs @@ -63,17 +65,18 @@ namespace TestProject.LuaCs string methodName, string[]? parameters, string function, - LuaCsHook.HookMethodType patchType) + ILuaCsHook.HookMethodType patchType) { var args = BuildHookPatchArgsList(patchId, className, methodName, parameters); args.Add(function); args.Add(patchType switch { - LuaCsHook.HookMethodType.Before => "Hook.HookMethodType.Before", - LuaCsHook.HookMethodType.After => "Hook.HookMethodType.After", + ILuaCsHook.HookMethodType.Before => "Hook.HookMethodType.Before", + ILuaCsHook.HookMethodType.After => "Hook.HookMethodType.After", _ => throw new NotImplementedException(), }); - return luaCs.Lua.DoString($"return Hook.Patch({string.Join(", ", args)})"); + throw new NotImplementedException(); + //return luaCs.Lua.DoString($"return Hook.Patch({string.Join(", ", args)})"); } private static DynValue DoHookRemovePatch( @@ -82,16 +85,17 @@ namespace TestProject.LuaCs string className, string methodName, string[]? parameters, - LuaCsHook.HookMethodType patchType) + ILuaCsHook.HookMethodType patchType) { var args = BuildHookPatchArgsList(patchId, className, methodName, parameters); args.Add(patchType switch { - LuaCsHook.HookMethodType.Before => "Hook.HookMethodType.Before", - LuaCsHook.HookMethodType.After => "Hook.HookMethodType.After", + ILuaCsHook.HookMethodType.Before => "Hook.HookMethodType.Before", + ILuaCsHook.HookMethodType.After => "Hook.HookMethodType.After", _ => throw new NotImplementedException(), }); - return luaCs.Lua.DoString($"return Hook.RemovePatch({string.Join(", ", args)})"); + throw new NotImplementedException(); + //return luaCs.Lua.DoString($"return Hook.RemovePatch({string.Join(", ", args)})"); } public static PatchHandle AddPrefix(this LuaCsSetup luaCs, string body, string methodName, string[]? parameters = null, string? patchId = null) @@ -101,7 +105,7 @@ namespace TestProject.LuaCs function(instance, ptable) {body} end - ", LuaCsHook.HookMethodType.Before); + ", ILuaCsHook.HookMethodType.Before); Assert.Equal(DataType.String, returnValue.Type); return new(returnValue.String, () => luaCs.RemovePrefix(returnValue.String, methodName, parameters)); } @@ -113,7 +117,7 @@ namespace TestProject.LuaCs function(instance, ptable) {body} end - ", LuaCsHook.HookMethodType.After); + ", ILuaCsHook.HookMethodType.After); Assert.Equal(DataType.String, returnValue.Type); return new(returnValue.String, () => luaCs.RemovePostfix(returnValue.String, methodName, parameters)); } @@ -121,7 +125,7 @@ namespace TestProject.LuaCs public static bool RemovePrefix(this LuaCsSetup luaCs, string patchId, string methodName, string[]? parameters = null) { var className = typeof(T).FullName!; - var returnValue = luaCs.DoHookRemovePatch(patchId, className, methodName, parameters, LuaCsHook.HookMethodType.Before); + var returnValue = luaCs.DoHookRemovePatch(patchId, className, methodName, parameters, ILuaCsHook.HookMethodType.Before); Assert.Equal(DataType.Boolean, returnValue.Type); return returnValue.Boolean; } @@ -129,7 +133,7 @@ namespace TestProject.LuaCs public static bool RemovePostfix(this LuaCsSetup luaCs, string patchId, string methodName, string[]? parameters = null) { var className = typeof(T).FullName!; - var returnValue = luaCs.DoHookRemovePatch(patchId, className, methodName, parameters, LuaCsHook.HookMethodType.After); + var returnValue = luaCs.DoHookRemovePatch(patchId, className, methodName, parameters, ILuaCsHook.HookMethodType.After); Assert.Equal(DataType.Boolean, returnValue.Type); return returnValue.Boolean; } diff --git a/Barotrauma/BarotraumaTest/LuaCs/HookPatchTests.cs b/Barotrauma/BarotraumaTest/LuaCs/HookPatchTests.cs index b9366b239..9608b804a 100644 --- a/Barotrauma/BarotraumaTest/LuaCs/HookPatchTests.cs +++ b/Barotrauma/BarotraumaTest/LuaCs/HookPatchTests.cs @@ -1,4 +1,5 @@ -extern alias Client; +/* +extern alias Client; using Client::Barotrauma; using Microsoft.Xna.Framework; @@ -7,6 +8,8 @@ using System; using Xunit; using Xunit.Abstractions; +// TODO: Rewrite all of this. + namespace TestProject.LuaCs { [Collection("LuaCs")] @@ -16,6 +19,7 @@ namespace TestProject.LuaCs public HookPatchTests(LuaCsFixture luaCsFixture, ITestOutputHelper output) { + // XXX: we can't have multiple instances of LuaCs patching the // same methods, otherwise we get script ownership exceptions. luaCs = luaCsFixture.LuaCs; @@ -36,10 +40,12 @@ namespace TestProject.LuaCs UserData.RegisterType(); UserData.RegisterType(); UserData.RegisterType(); - - luaCs.Initialize(); - luaCs.Lua.Globals["TestValueType"] = UserData.CreateStatic(); - luaCs.Lua.Globals["InterfaceImplementingType"] = UserData.CreateStatic(); + + luaCs.ForceRunState(RunState.Running); + throw new NotImplementedException(); + //luaCs.Initialize(); + //luaCs.Lua.Globals["TestValueType"] = UserData.CreateStatic(); + //luaCs.Lua.Globals["InterfaceImplementingType"] = UserData.CreateStatic(); } private class PatchTargetSimple @@ -664,3 +670,4 @@ namespace TestProject.LuaCs } } } +*/ diff --git a/Barotrauma/BarotraumaTest/LuaCs/LuaCsFixture.cs b/Barotrauma/BarotraumaTest/LuaCs/LuaCsFixture.cs index 70c2d9c1e..61fdc58a6 100644 --- a/Barotrauma/BarotraumaTest/LuaCs/LuaCsFixture.cs +++ b/Barotrauma/BarotraumaTest/LuaCs/LuaCsFixture.cs @@ -1,9 +1,12 @@ -extern alias Client; +/* +extern alias Client; using Client::Barotrauma; using System; using System.Runtime.ExceptionServices; +// TODO: Rewrite all of this. + namespace TestProject.LuaCs { /// @@ -31,3 +34,4 @@ namespace TestProject.LuaCs void IDisposable.Dispose() => LuaCs.Stop(); } } +*/ diff --git a/Libraries/BarotraumaLibs/BarotraumaCore/BarotraumaCore.csproj b/Libraries/BarotraumaLibs/BarotraumaCore/BarotraumaCore.csproj index 43147b2b5..7cc2b89a1 100644 --- a/Libraries/BarotraumaLibs/BarotraumaCore/BarotraumaCore.csproj +++ b/Libraries/BarotraumaLibs/BarotraumaCore/BarotraumaCore.csproj @@ -1,28 +1,28 @@ - - - - net8.0 - Barotrauma - disable - enable - - - - full - ;NU1605;CS0114;CS0108;CS8597;CS8600;CS8601;CS8602;CS8603;CS8604;CS8605;CS8606;CS8607;CS8608;CS8609;CS8610;CS8611;CS8612;CS8613;CS8614;CS8615;CS8616;CS8617;CS8618;CS8619;CS8620;CS8621;CS8622;CS8624;CS8625;CS8626;CS8629;CS8631;CS8632;CS8633;CS8634;CS8638;CS8643;CS8644;CS8645;CS8653;CS8654;CS8655;CS8667;CS8669;CS8670;CS8714;CS8717;CS8765 - true - x64 - - - - full - ;NU1605;CS0114;CS0108;CS8597;CS8600;CS8601;CS8602;CS8603;CS8604;CS8605;CS8606;CS8607;CS8608;CS8609;CS8610;CS8611;CS8612;CS8613;CS8614;CS8615;CS8616;CS8617;CS8618;CS8619;CS8620;CS8621;CS8622;CS8624;CS8625;CS8626;CS8629;CS8631;CS8632;CS8633;CS8634;CS8638;CS8643;CS8644;CS8645;CS8653;CS8654;CS8655;CS8667;CS8669;CS8670;CS8714;CS8717;CS8765 - true - x64 - - - - - - - + + + + net8.0 + Barotrauma + disable + enable + + + + full + ;NU1605;CS0114;CS0108;CS8597;CS8600;CS8601;CS8602;CS8603;CS8604;CS8605;CS8606;CS8607;CS8608;CS8609;CS8610;CS8611;CS8612;CS8613;CS8614;CS8615;CS8616;CS8617;CS8618;CS8619;CS8620;CS8621;CS8622;CS8624;CS8625;CS8626;CS8629;CS8631;CS8632;CS8633;CS8634;CS8638;CS8643;CS8644;CS8645;CS8653;CS8654;CS8655;CS8667;CS8669;CS8670;CS8714;CS8717;CS8765 + true + x64 + + + + full + ;NU1605;CS0114;CS0108;CS8597;CS8600;CS8601;CS8602;CS8603;CS8604;CS8605;CS8606;CS8607;CS8608;CS8609;CS8610;CS8611;CS8612;CS8613;CS8614;CS8615;CS8616;CS8617;CS8618;CS8619;CS8620;CS8621;CS8622;CS8624;CS8625;CS8626;CS8629;CS8631;CS8632;CS8633;CS8634;CS8638;CS8643;CS8644;CS8645;CS8653;CS8654;CS8655;CS8667;CS8669;CS8670;CS8714;CS8717;CS8765 + true + x64 + + + + + + + diff --git a/Libraries/BarotraumaLibs/BarotraumaCore/Utils/ToolBoxCore.cs b/Libraries/BarotraumaLibs/BarotraumaCore/Utils/ToolBoxCore.cs index d4c7f54f2..e22177450 100644 --- a/Libraries/BarotraumaLibs/BarotraumaCore/Utils/ToolBoxCore.cs +++ b/Libraries/BarotraumaLibs/BarotraumaCore/Utils/ToolBoxCore.cs @@ -47,11 +47,17 @@ public static class ToolBoxCore byte[] inputBytes = Encoding.UTF8.GetBytes(str); byte[] hash = md5.ComputeHash(inputBytes); - UInt32 key = (UInt32)((str.Length & 0xff) << 24); //could use more of the hash here instead? - key |= (UInt32)(hash[hash.Length - 3] << 16); - key |= (UInt32)(hash[hash.Length - 2] << 8); - key |= (UInt32)(hash[hash.Length - 1]); - + //xor all of the bits of the hash together + UInt32 key = 0; + foreach (byte b in hash) + { + // Rotate the 32-bit value left by 5 bits: + // (key << 5) moves everything left, + // (key >> 27) brings the 5 bits that overflowed back around (32 - 5 = 27), + // OR'ing them together completes the rotate. + key = (key << 5) | (key >> 27); + key ^= b; + } return key; } diff --git a/Libraries/BarotraumaLibs/EosInterface/EosInterface.csproj b/Libraries/BarotraumaLibs/EosInterface/EosInterface.csproj index e366bb52f..1976acfca 100644 --- a/Libraries/BarotraumaLibs/EosInterface/EosInterface.csproj +++ b/Libraries/BarotraumaLibs/EosInterface/EosInterface.csproj @@ -1,26 +1,26 @@ - - - - net8.0 - disable - enable - Barotrauma - - - - ;NU1605;CS0114;CS0108;CS8597;CS8600;CS8601;CS8602;CS8603;CS8604;CS8605;CS8606;CS8607;CS8608;CS8609;CS8610;CS8611;CS8612;CS8613;CS8614;CS8615;CS8616;CS8617;CS8618;CS8619;CS8620;CS8621;CS8622;CS8624;CS8625;CS8626;CS8629;CS8631;CS8632;CS8633;CS8634;CS8638;CS8643;CS8644;CS8645;CS8653;CS8654;CS8655;CS8667;CS8669;CS8670;CS8714;CS8717;CS8765 - true - x64 - - - - ;NU1605;CS0114;CS0108;CS8597;CS8600;CS8601;CS8602;CS8603;CS8604;CS8605;CS8606;CS8607;CS8608;CS8609;CS8610;CS8611;CS8612;CS8613;CS8614;CS8615;CS8616;CS8617;CS8618;CS8619;CS8620;CS8621;CS8622;CS8624;CS8625;CS8626;CS8629;CS8631;CS8632;CS8633;CS8634;CS8638;CS8643;CS8644;CS8645;CS8653;CS8654;CS8655;CS8667;CS8669;CS8670;CS8714;CS8717;CS8765 - true - x64 - - - - - - - + + + + net8.0 + disable + enable + Barotrauma + + + + ;NU1605;CS0114;CS0108;CS8597;CS8600;CS8601;CS8602;CS8603;CS8604;CS8605;CS8606;CS8607;CS8608;CS8609;CS8610;CS8611;CS8612;CS8613;CS8614;CS8615;CS8616;CS8617;CS8618;CS8619;CS8620;CS8621;CS8622;CS8624;CS8625;CS8626;CS8629;CS8631;CS8632;CS8633;CS8634;CS8638;CS8643;CS8644;CS8645;CS8653;CS8654;CS8655;CS8667;CS8669;CS8670;CS8714;CS8717;CS8765 + true + x64 + + + + ;NU1605;CS0114;CS0108;CS8597;CS8600;CS8601;CS8602;CS8603;CS8604;CS8605;CS8606;CS8607;CS8608;CS8609;CS8610;CS8611;CS8612;CS8613;CS8614;CS8615;CS8616;CS8617;CS8618;CS8619;CS8620;CS8621;CS8622;CS8624;CS8625;CS8626;CS8629;CS8631;CS8632;CS8633;CS8634;CS8638;CS8643;CS8644;CS8645;CS8653;CS8654;CS8655;CS8667;CS8669;CS8670;CS8714;CS8717;CS8765 + true + x64 + + + + + + + diff --git a/Libraries/Concentus/.gitignore b/Libraries/Concentus/.gitignore index 629fe1100..fcd9a1be9 100644 --- a/Libraries/Concentus/.gitignore +++ b/Libraries/Concentus/.gitignore @@ -1,240 +1,240 @@ -## Ignore Visual Studio temporary files, build results, and -## files generated by popular Visual Studio add-ons. - -# User-specific files -*.suo -*.user -*.userosscache -*.sln.docstates - -# User-specific files (MonoDevelop/Xamarin Studio) -*.userprefs - -# Build results -[Dd]ebug/ -[Dd]ebugPublic/ -[Rr]elease/ -[Rr]eleases/ -x64/ -x86/ -bld/ -[Bb]in/ -[Oo]bj/ - -# Visual Studio 2015 cache/options directory -.vs/ -# Uncomment if you have tasks that create the project's static files in wwwroot -#wwwroot/ - -# MSTest test Results -[Tt]est[Rr]esult*/ -[Bb]uild[Ll]og.* - -# NUNIT -*.VisualState.xml -TestResult.xml - -# Build Results of an ATL Project -[Dd]ebugPS/ -[Rr]eleasePS/ -dlldata.c - -# DNX -project.lock.json -artifacts/ - -*_i.c -*_p.c -*_i.h -*.ilk -*.meta -*.obj -*.pch -*.pdb -*.pgc -*.pgd -*.rsp -*.sbr -*.tlb -*.tli -*.tlh -*.tmp -*.tmp_proj -*.log -*.vspscc -*.vssscc -.builds -*.pidb -*.svclog -*.scc - -# Chutzpah Test files -_Chutzpah* - -# Visual C++ cache files -ipch/ -*.aps -*.ncb -*.opendb -*.opensdf -*.sdf -*.cachefile - -# Visual Studio profiler -*.psess -*.vsp -*.vspx -*.sap - -# TFS 2012 Local Workspace -$tf/ - -# Guidance Automation Toolkit -*.gpState - -# ReSharper is a .NET coding add-in -_ReSharper*/ -*.[Rr]e[Ss]harper -*.DotSettings.user - -# JustCode is a .NET coding add-in -.JustCode - -# TeamCity is a build add-in -_TeamCity* - -# DotCover is a Code Coverage Tool -*.dotCover - -# NCrunch -_NCrunch_* -.*crunch*.local.xml -nCrunchTemp_* - -# MightyMoose -*.mm.* -AutoTest.Net/ - -# Web workbench (sass) -.sass-cache/ - -# Installshield output folder -[Ee]xpress/ - -# DocProject is a documentation generator add-in -DocProject/buildhelp/ -DocProject/Help/*.HxT -DocProject/Help/*.HxC -DocProject/Help/*.hhc -DocProject/Help/*.hhk -DocProject/Help/*.hhp -DocProject/Help/Html2 -DocProject/Help/html - -# Click-Once directory -publish/ - -# Publish Web Output -*.[Pp]ublish.xml -*.azurePubxml -# TODO: Comment the next line if you want to checkin your web deploy settings -# but database connection strings (with potential passwords) will be unencrypted -*.pubxml -*.publishproj - -# NuGet Packages -*.nupkg -# The packages folder can be ignored because of Package Restore -**/packages/* -# except build/, which is used as an MSBuild target. -!**/packages/build/ -# Uncomment if necessary however generally it will be regenerated when needed -#!**/packages/repositories.config -# NuGet v3's project.json files produces more ignoreable files -*.nuget.props -*.nuget.targets - -# Microsoft Azure Build Output -csx/ -*.build.csdef - -# Microsoft Azure Emulator -ecf/ -rcf/ - -# Microsoft Azure ApplicationInsights config file -ApplicationInsights.config - -# Windows Store app package directory -AppPackages/ -BundleArtifacts/ - -# Visual Studio cache files -# files ending in .cache can be ignored -*.[Cc]ache -# but keep track of directories ending in .cache -!*.[Cc]ache/ - -# Others -ClientBin/ -~$* -*~ -*.dbmdl -*.dbproj.schemaview -*.pfx -*.publishsettings -node_modules/ -orleans.codegen.cs - -# RIA/Silverlight projects -Generated_Code/ - -# Backup & report files from converting an old project file -# to a newer Visual Studio version. Backup files are not needed, -# because we have git ;-) -_UpgradeReport_Files/ -Backup*/ -UpgradeLog*.XML -UpgradeLog*.htm - -# SQL Server files -*.mdf -*.ldf - -# Business Intelligence projects -*.rdl.data -*.bim.layout -*.bim_*.settings - -# Microsoft Fakes -FakesAssemblies/ - -# GhostDoc plugin setting file -*.GhostDoc.xml - -# Node.js Tools for Visual Studio -.ntvs_analysis.dat - -# Visual Studio 6 build log -*.plg - -# Visual Studio 6 workspace options file -*.opt - -# Visual Studio LightSwitch build output -**/*.HTMLClient/GeneratedArtifacts -**/*.DesktopClient/GeneratedArtifacts -**/*.DesktopClient/ModelManifest.xml -**/*.Server/GeneratedArtifacts -**/*.Server/ModelManifest.xml -_Pvt_Extensions - -# Paket dependency manager -.paket/paket.exe - -# FAKE - F# Make -.fake/ -/Java/Concentus/target/ -/Java/ContentusTestConsole/ContentusTestConsole/target/ -/Java/ContentusTestConsole/ConcentusTestConsole/target/ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ + +# Visual Studio 2015 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# DNX +project.lock.json +artifacts/ + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# TODO: Comment the next line if you want to checkin your web deploy settings +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config +# NuGet v3's project.json files produces more ignoreable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Microsoft Azure ApplicationInsights config file +ApplicationInsights.config + +# Windows Store app package directory +AppPackages/ +BundleArtifacts/ + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.pfx +*.publishsettings +node_modules/ +orleans.codegen.cs + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe + +# FAKE - F# Make +.fake/ +/Java/Concentus/target/ +/Java/ContentusTestConsole/ContentusTestConsole/target/ +/Java/ContentusTestConsole/ConcentusTestConsole/target/ /Java/ConcentusTestConsole/target/ \ No newline at end of file diff --git a/Libraries/Concentus/CSharp/Concentus/Concentus.NetStandard.csproj b/Libraries/Concentus/CSharp/Concentus/Concentus.NetStandard.csproj index 725dda808..99073a079 100644 --- a/Libraries/Concentus/CSharp/Concentus/Concentus.NetStandard.csproj +++ b/Libraries/Concentus/CSharp/Concentus/Concentus.NetStandard.csproj @@ -1,20 +1,20 @@ - - - - netstandard2.1 - AnyCPU;x64 - Concentus - Logan Stromberg - 1.1.6.0 - Copyright © Xiph.Org Foundation, Skype Limited, CSIRO, Microsoft Corp. - This package is a pure portable C# implementation of the Opus audio compression codec (see https://opus-codec.org/ for more details). This package contains the Opus encoder, decoder, multistream codecs, repacketizer, as well as a port of the libspeexdsp resampler. It does NOT contain code to parse .ogg or .opus container files or to manage RTP packet streams - - https://github.com/lostromb/concentus - - - - full - true - - - + + + + netstandard2.1 + AnyCPU;x64 + Concentus + Logan Stromberg + 1.1.6.0 + Copyright © Xiph.Org Foundation, Skype Limited, CSIRO, Microsoft Corp. + This package is a pure portable C# implementation of the Opus audio compression codec (see https://opus-codec.org/ for more details). This package contains the Opus encoder, decoder, multistream codecs, repacketizer, as well as a port of the libspeexdsp resampler. It does NOT contain code to parse .ogg or .opus container files or to manage RTP packet streams + + https://github.com/lostromb/concentus + + + + full + true + + + diff --git a/Libraries/Farseer Physics Engine 3.5/Dynamics/Body.cs b/Libraries/Farseer Physics Engine 3.5/Dynamics/Body.cs index a35808d92..9a7ad1056 100644 --- a/Libraries/Farseer Physics Engine 3.5/Dynamics/Body.cs +++ b/Libraries/Farseer Physics Engine 3.5/Dynamics/Body.cs @@ -29,6 +29,7 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.Linq; @@ -349,6 +350,7 @@ namespace FarseerPhysics.Dynamics } } + /// /// Create all proxies. /// diff --git a/Libraries/Farseer Physics Engine 3.5/Farseer.NetStandard.csproj b/Libraries/Farseer Physics Engine 3.5/Farseer.NetStandard.csproj index 96e8ca92c..69fbafa2f 100644 --- a/Libraries/Farseer Physics Engine 3.5/Farseer.NetStandard.csproj +++ b/Libraries/Farseer Physics Engine 3.5/Farseer.NetStandard.csproj @@ -1,40 +1,40 @@ - - - - netstandard2.1 - FarseerPhysics - Copyright Ian Qvist © 2013 - Farseer Physics Engine - - 3.5.0.0 - Ian Qvist - AnyCPU;x64 - - - - TRACE - portable - true - - - - - - - - - - - - - - - - - - - - - - - + + + + netstandard2.1 + FarseerPhysics + Copyright Ian Qvist © 2013 + Farseer Physics Engine + + 3.5.0.0 + Ian Qvist + AnyCPU;x64 + + + + TRACE + portable + true + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Libraries/GameAnalytics/GA_SDK_NETSTANDARD/GA_SDK_NETSTANDARD.csproj b/Libraries/GameAnalytics/GA_SDK_NETSTANDARD/GA_SDK_NETSTANDARD.csproj index 6b99d030a..1a989ea0f 100644 --- a/Libraries/GameAnalytics/GA_SDK_NETSTANDARD/GA_SDK_NETSTANDARD.csproj +++ b/Libraries/GameAnalytics/GA_SDK_NETSTANDARD/GA_SDK_NETSTANDARD.csproj @@ -1,35 +1,35 @@ - - - - netstandard2.1 - GameAnalytics.NetStandard - GameAnalytics.Net - AnyCPU;x64 - Game Analytics - Copyright (c) 2016 Game Analytics - - - - TRACE;MONO - - - - TRACE;MONO - - - - TRACE;MONO - - - - TRACE;MONO - - - - - - - - - - + + + + netstandard2.1 + GameAnalytics.NetStandard + GameAnalytics.Net + AnyCPU;x64 + Game Analytics + Copyright (c) 2016 Game Analytics + + + + TRACE;MONO + + + + TRACE;MONO + + + + TRACE;MONO + + + + TRACE;MONO + + + + + + + + + + diff --git a/Libraries/SharpFont/Source/SharpFont/SharpFont.NetStandard.csproj b/Libraries/SharpFont/Source/SharpFont/SharpFont.NetStandard.csproj index a4477adaa..6f2368424 100644 --- a/Libraries/SharpFont/Source/SharpFont/SharpFont.NetStandard.csproj +++ b/Libraries/SharpFont/Source/SharpFont/SharpFont.NetStandard.csproj @@ -1,45 +1,45 @@ - - - - netstandard2.1 - SharpFont - SharpFont - Cross-platform FreeType bindings for C# - Robmaister - SharpFont - - Copyright (c) Robert Rouhani 2012-2016 - AnyCPU;x64 - - - - TRACE;DEBUG;SHARPFONT_PORTABLE - true - - - - TRACE;DEBUG;SHARPFONT_PORTABLE - true - 1701;1702;3021 - - - - TRACE;SHARPFONT_PORTABLE - true - - - - TRACE;SHARPFONT_PORTABLE - true - 1701;1702;3021 - - - - - - - - - - - + + + + netstandard2.1 + SharpFont + SharpFont + Cross-platform FreeType bindings for C# + Robmaister + SharpFont + + Copyright (c) Robert Rouhani 2012-2016 + AnyCPU;x64 + + + + TRACE;DEBUG;SHARPFONT_PORTABLE + true + + + + TRACE;DEBUG;SHARPFONT_PORTABLE + true + 1701;1702;3021 + + + + TRACE;SHARPFONT_PORTABLE + true + + + + TRACE;SHARPFONT_PORTABLE + true + 1701;1702;3021 + + + + + + + + + + + diff --git a/Libraries/XNATypes/XNATypes.csproj b/Libraries/XNATypes/XNATypes.csproj index 57fd8b083..b3b90d794 100644 --- a/Libraries/XNATypes/XNATypes.csproj +++ b/Libraries/XNATypes/XNATypes.csproj @@ -1,10 +1,10 @@ - - - - netstandard2.1 - AnyCPU;x64 - - - - - + + + + netstandard2.1 + AnyCPU;x64 + + + + + diff --git a/Libraries/moonsharp b/Libraries/moonsharp index f67e9ee5a..b556e550e 160000 --- a/Libraries/moonsharp +++ b/Libraries/moonsharp @@ -1 +1 @@ -Subproject commit f67e9ee5a315ad0c1ba60199488df37f5ef09cf2 +Subproject commit b556e550eb20b950a7db3ef69006104af3f654da diff --git a/Libraries/webm_mem_playback/opus/win32/VS2015/common.props b/Libraries/webm_mem_playback/opus/win32/VS2015/common.props index 03cd45b0c..6c757d8b7 100644 --- a/Libraries/webm_mem_playback/opus/win32/VS2015/common.props +++ b/Libraries/webm_mem_playback/opus/win32/VS2015/common.props @@ -1,82 +1,82 @@ - - - - - - $(Platform)\$(Configuration)\ - $(Platform)\$(Configuration)\$(ProjectName)\ - Unicode - - - true - true - false - - - false - false - true - - - - Level3 - false - false - ..\..;..\..\include;..\..\silk;..\..\celt;..\..\win32;%(AdditionalIncludeDirectories) - HAVE_CONFIG_H;WIN32;_CRT_SECURE_NO_WARNINGS;%(PreprocessorDefinitions) - false - false - - - Console - - - true - Console - - - - - Guard - ProgramDatabase - NoExtensions - false - true - false - Disabled - false - false - Disabled - MultiThreadedDebug - MultiThreadedDebugDLL - true - false - - - true - - - - - false - None - true - true - false - Speed - Fast - Precise - true - true - true - MaxSpeed - MultiThreaded - MultiThreadedDLL - 16Bytes - - - false - - - + + + + + + $(Platform)\$(Configuration)\ + $(Platform)\$(Configuration)\$(ProjectName)\ + Unicode + + + true + true + false + + + false + false + true + + + + Level3 + false + false + ..\..;..\..\include;..\..\silk;..\..\celt;..\..\win32;%(AdditionalIncludeDirectories) + HAVE_CONFIG_H;WIN32;_CRT_SECURE_NO_WARNINGS;%(PreprocessorDefinitions) + false + false + + + Console + + + true + Console + + + + + Guard + ProgramDatabase + NoExtensions + false + true + false + Disabled + false + false + Disabled + MultiThreadedDebug + MultiThreadedDebugDLL + true + false + + + true + + + + + false + None + true + true + false + Speed + Fast + Precise + true + true + true + MaxSpeed + MultiThreaded + MultiThreadedDLL + 16Bytes + + + false + + + \ No newline at end of file diff --git a/Libraries/webm_mem_playback/opus/win32/VS2015/opus.vcxproj b/Libraries/webm_mem_playback/opus/win32/VS2015/opus.vcxproj index fc2241116..ae420d508 100644 --- a/Libraries/webm_mem_playback/opus/win32/VS2015/opus.vcxproj +++ b/Libraries/webm_mem_playback/opus/win32/VS2015/opus.vcxproj @@ -1,399 +1,399 @@ - - - - - DebugDLL_fixed - Win32 - - - DebugDLL_fixed - x64 - - - DebugDLL - Win32 - - - DebugDLL - x64 - - - Debug - Win32 - - - Debug - x64 - - - ReleaseDLL_fixed - Win32 - - - ReleaseDLL_fixed - x64 - - - ReleaseDLL - Win32 - - - ReleaseDLL - x64 - - - Release - Win32 - - - Release - x64 - - - - Win32Proj - opus - {219EC965-228A-1824-174D-96449D05F88A} - - - - StaticLibrary - v142 - - - DynamicLibrary - v142 - - - DynamicLibrary - v142 - - - StaticLibrary - v142 - - - DynamicLibrary - v142 - - - DynamicLibrary - v142 - - - StaticLibrary - v142 - - - DynamicLibrary - v142 - - - DynamicLibrary - v142 - - - StaticLibrary - v142 - - - DynamicLibrary - v142 - - - DynamicLibrary - v142 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ..\..\silk\fixed;..\..\silk\float;%(AdditionalIncludeDirectories) - DLL_EXPORT;%(PreprocessorDefinitions) - FIXED_POINT;%(PreprocessorDefinitions) - /arch:IA32 %(AdditionalOptions) - - - /ignore:4221 %(AdditionalOptions) - - - "$(ProjectDir)..\..\win32\genversion.bat" "$(ProjectDir)..\..\win32\version.h" PACKAGE_VERSION - Generating version.h - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 4244;%(DisableSpecificWarnings) - - - - - - - - - - - - - - - false - - - false - - - true - - - - - - - true - - - true - - - false - - - - - - - + + + + + DebugDLL_fixed + Win32 + + + DebugDLL_fixed + x64 + + + DebugDLL + Win32 + + + DebugDLL + x64 + + + Debug + Win32 + + + Debug + x64 + + + ReleaseDLL_fixed + Win32 + + + ReleaseDLL_fixed + x64 + + + ReleaseDLL + Win32 + + + ReleaseDLL + x64 + + + Release + Win32 + + + Release + x64 + + + + Win32Proj + opus + {219EC965-228A-1824-174D-96449D05F88A} + + + + StaticLibrary + v142 + + + DynamicLibrary + v142 + + + DynamicLibrary + v142 + + + StaticLibrary + v142 + + + DynamicLibrary + v142 + + + DynamicLibrary + v142 + + + StaticLibrary + v142 + + + DynamicLibrary + v142 + + + DynamicLibrary + v142 + + + StaticLibrary + v142 + + + DynamicLibrary + v142 + + + DynamicLibrary + v142 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ..\..\silk\fixed;..\..\silk\float;%(AdditionalIncludeDirectories) + DLL_EXPORT;%(PreprocessorDefinitions) + FIXED_POINT;%(PreprocessorDefinitions) + /arch:IA32 %(AdditionalOptions) + + + /ignore:4221 %(AdditionalOptions) + + + "$(ProjectDir)..\..\win32\genversion.bat" "$(ProjectDir)..\..\win32\version.h" PACKAGE_VERSION + Generating version.h + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 4244;%(DisableSpecificWarnings) + + + + + + + + + + + + + + + false + + + false + + + true + + + + + + + true + + + true + + + false + + + + + + + \ No newline at end of file diff --git a/Libraries/webm_mem_playback/opus/win32/VS2015/opus.vcxproj.filters b/Libraries/webm_mem_playback/opus/win32/VS2015/opus.vcxproj.filters index 47185c67d..97eb46551 100644 --- a/Libraries/webm_mem_playback/opus/win32/VS2015/opus.vcxproj.filters +++ b/Libraries/webm_mem_playback/opus/win32/VS2015/opus.vcxproj.filters @@ -1,744 +1,744 @@ - - - - - {4FC737F1-C7A5-4376-A066-2A32D752A2FF} - cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx - - - {93995380-89BD-4b04-88EB-625FBE52EBFB} - h;hpp;hxx;hm;inl;inc;xsd - - - {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} - rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav - - - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hpp;hxx;hm;inl;inc;xsd + + + {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} + rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav + + + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + \ No newline at end of file diff --git a/Libraries/webm_mem_playback/opus/win32/VS2015/opus_demo.vcxproj b/Libraries/webm_mem_playback/opus/win32/VS2015/opus_demo.vcxproj index fcd971bb6..7ad4b5e21 100644 --- a/Libraries/webm_mem_playback/opus/win32/VS2015/opus_demo.vcxproj +++ b/Libraries/webm_mem_playback/opus/win32/VS2015/opus_demo.vcxproj @@ -1,171 +1,171 @@ - - - - - DebugDLL_fixed - Win32 - - - DebugDLL_fixed - x64 - - - DebugDLL - Win32 - - - DebugDLL - x64 - - - Debug - Win32 - - - Debug - x64 - - - ReleaseDLL_fixed - Win32 - - - ReleaseDLL_fixed - x64 - - - ReleaseDLL - Win32 - - - ReleaseDLL - x64 - - - Release - Win32 - - - Release - x64 - - - - - {219ec965-228a-1824-174d-96449d05f88a} - - - - - - - {016C739D-6389-43BF-8D88-24B2BF6F620F} - Win32Proj - opus_demo - - - - Application - v142 - - - Application - v142 - - - Application - v142 - - - Application - v142 - - - Application - v142 - - - Application - v142 - - - Application - v142 - - - Application - v142 - - - Application - v142 - - - Application - v142 - - - Application - v142 - - - Application - v142 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + DebugDLL_fixed + Win32 + + + DebugDLL_fixed + x64 + + + DebugDLL + Win32 + + + DebugDLL + x64 + + + Debug + Win32 + + + Debug + x64 + + + ReleaseDLL_fixed + Win32 + + + ReleaseDLL_fixed + x64 + + + ReleaseDLL + Win32 + + + ReleaseDLL + x64 + + + Release + Win32 + + + Release + x64 + + + + + {219ec965-228a-1824-174d-96449d05f88a} + + + + + + + {016C739D-6389-43BF-8D88-24B2BF6F620F} + Win32Proj + opus_demo + + + + Application + v142 + + + Application + v142 + + + Application + v142 + + + Application + v142 + + + Application + v142 + + + Application + v142 + + + Application + v142 + + + Application + v142 + + + Application + v142 + + + Application + v142 + + + Application + v142 + + + Application + v142 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Libraries/webm_mem_playback/opus/win32/VS2015/opus_demo.vcxproj.filters b/Libraries/webm_mem_playback/opus/win32/VS2015/opus_demo.vcxproj.filters index dbcc8ae92..2eb113ac8 100644 --- a/Libraries/webm_mem_playback/opus/win32/VS2015/opus_demo.vcxproj.filters +++ b/Libraries/webm_mem_playback/opus/win32/VS2015/opus_demo.vcxproj.filters @@ -1,22 +1,22 @@ - - - - - {4FC737F1-C7A5-4376-A066-2A32D752A2FF} - cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx - - - {93995380-89BD-4b04-88EB-625FBE52EBFB} - h;hpp;hxx;hm;inl;inc;xsd - - - {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} - rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms - - - - - Source Files - - + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hpp;hxx;hm;inl;inc;xsd + + + {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} + rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms + + + + + Source Files + + \ No newline at end of file diff --git a/Libraries/webm_mem_playback/opus/win32/VS2015/test_opus_api.vcxproj b/Libraries/webm_mem_playback/opus/win32/VS2015/test_opus_api.vcxproj index e428bd3f7..4ba7c8ae5 100644 --- a/Libraries/webm_mem_playback/opus/win32/VS2015/test_opus_api.vcxproj +++ b/Libraries/webm_mem_playback/opus/win32/VS2015/test_opus_api.vcxproj @@ -1,171 +1,171 @@ - - - - - DebugDLL_fixed - Win32 - - - DebugDLL_fixed - x64 - - - DebugDLL - Win32 - - - DebugDLL - x64 - - - Debug - Win32 - - - Debug - x64 - - - ReleaseDLL_fixed - Win32 - - - ReleaseDLL_fixed - x64 - - - ReleaseDLL - Win32 - - - ReleaseDLL - x64 - - - Release - Win32 - - - Release - x64 - - - - - - - - {219ec965-228a-1824-174d-96449d05f88a} - - - - {1D257A17-D254-42E5-82D6-1C87A6EC775A} - Win32Proj - test_opus_api - - - - Application - v142 - - - Application - v142 - - - Application - v142 - - - Application - v142 - - - Application - v142 - - - Application - v142 - - - Application - v142 - - - Application - v142 - - - Application - v142 - - - Application - v142 - - - Application - v142 - - - Application - v142 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + DebugDLL_fixed + Win32 + + + DebugDLL_fixed + x64 + + + DebugDLL + Win32 + + + DebugDLL + x64 + + + Debug + Win32 + + + Debug + x64 + + + ReleaseDLL_fixed + Win32 + + + ReleaseDLL_fixed + x64 + + + ReleaseDLL + Win32 + + + ReleaseDLL + x64 + + + Release + Win32 + + + Release + x64 + + + + + + + + {219ec965-228a-1824-174d-96449d05f88a} + + + + {1D257A17-D254-42E5-82D6-1C87A6EC775A} + Win32Proj + test_opus_api + + + + Application + v142 + + + Application + v142 + + + Application + v142 + + + Application + v142 + + + Application + v142 + + + Application + v142 + + + Application + v142 + + + Application + v142 + + + Application + v142 + + + Application + v142 + + + Application + v142 + + + Application + v142 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Libraries/webm_mem_playback/opus/win32/VS2015/test_opus_api.vcxproj.filters b/Libraries/webm_mem_playback/opus/win32/VS2015/test_opus_api.vcxproj.filters index 070c8ab01..383d19f71 100644 --- a/Libraries/webm_mem_playback/opus/win32/VS2015/test_opus_api.vcxproj.filters +++ b/Libraries/webm_mem_playback/opus/win32/VS2015/test_opus_api.vcxproj.filters @@ -1,14 +1,14 @@ - - - - - {4FC737F1-C7A5-4376-A066-2A32D752A2FF} - cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx - - - - - Source Files - - + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx + + + + + Source Files + + \ No newline at end of file diff --git a/Libraries/webm_mem_playback/opus/win32/VS2015/test_opus_decode.vcxproj b/Libraries/webm_mem_playback/opus/win32/VS2015/test_opus_decode.vcxproj index cbf562183..8e4640094 100644 --- a/Libraries/webm_mem_playback/opus/win32/VS2015/test_opus_decode.vcxproj +++ b/Libraries/webm_mem_playback/opus/win32/VS2015/test_opus_decode.vcxproj @@ -1,171 +1,171 @@ - - - - - DebugDLL_fixed - Win32 - - - DebugDLL_fixed - x64 - - - DebugDLL - Win32 - - - DebugDLL - x64 - - - Debug - Win32 - - - Debug - x64 - - - ReleaseDLL_fixed - Win32 - - - ReleaseDLL_fixed - x64 - - - ReleaseDLL - Win32 - - - ReleaseDLL - x64 - - - Release - Win32 - - - Release - x64 - - - - - - - - {219ec965-228a-1824-174d-96449d05f88a} - - - - {8578322A-1883-486B-B6FA-E0094B65C9F2} - Win32Proj - test_opus_api - - - - Application - v142 - - - Application - v142 - - - Application - v142 - - - Application - v142 - - - Application - v142 - - - Application - v142 - - - Application - v142 - - - Application - v142 - - - Application - v142 - - - Application - v142 - - - Application - v142 - - - Application - v142 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + DebugDLL_fixed + Win32 + + + DebugDLL_fixed + x64 + + + DebugDLL + Win32 + + + DebugDLL + x64 + + + Debug + Win32 + + + Debug + x64 + + + ReleaseDLL_fixed + Win32 + + + ReleaseDLL_fixed + x64 + + + ReleaseDLL + Win32 + + + ReleaseDLL + x64 + + + Release + Win32 + + + Release + x64 + + + + + + + + {219ec965-228a-1824-174d-96449d05f88a} + + + + {8578322A-1883-486B-B6FA-E0094B65C9F2} + Win32Proj + test_opus_api + + + + Application + v142 + + + Application + v142 + + + Application + v142 + + + Application + v142 + + + Application + v142 + + + Application + v142 + + + Application + v142 + + + Application + v142 + + + Application + v142 + + + Application + v142 + + + Application + v142 + + + Application + v142 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Libraries/webm_mem_playback/opus/win32/VS2015/test_opus_decode.vcxproj.filters b/Libraries/webm_mem_playback/opus/win32/VS2015/test_opus_decode.vcxproj.filters index 588637e83..3036a4e70 100644 --- a/Libraries/webm_mem_playback/opus/win32/VS2015/test_opus_decode.vcxproj.filters +++ b/Libraries/webm_mem_playback/opus/win32/VS2015/test_opus_decode.vcxproj.filters @@ -1,14 +1,14 @@ - - - - - {4a0dd677-931f-4728-afe5-b761149fc7eb} - cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx - - - - - Source Files - - + + + + + {4a0dd677-931f-4728-afe5-b761149fc7eb} + cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx + + + + + Source Files + + \ No newline at end of file diff --git a/Libraries/webm_mem_playback/opus/win32/VS2015/test_opus_encode.vcxproj b/Libraries/webm_mem_playback/opus/win32/VS2015/test_opus_encode.vcxproj index 5a313c31d..6804918a3 100644 --- a/Libraries/webm_mem_playback/opus/win32/VS2015/test_opus_encode.vcxproj +++ b/Libraries/webm_mem_playback/opus/win32/VS2015/test_opus_encode.vcxproj @@ -1,172 +1,172 @@ - - - - - DebugDLL_fixed - Win32 - - - DebugDLL_fixed - x64 - - - DebugDLL - Win32 - - - DebugDLL - x64 - - - Debug - Win32 - - - Debug - x64 - - - ReleaseDLL_fixed - Win32 - - - ReleaseDLL_fixed - x64 - - - ReleaseDLL - Win32 - - - ReleaseDLL - x64 - - - Release - Win32 - - - Release - x64 - - - - - - - - - {219ec965-228a-1824-174d-96449d05f88a} - - - - {84DAA768-1A38-4312-BB61-4C78BB59E5B8} - Win32Proj - test_opus_api - - - - Application - v142 - - - Application - v142 - - - Application - v142 - - - Application - v142 - - - Application - v142 - - - Application - v142 - - - Application - v142 - - - Application - v142 - - - Application - v142 - - - Application - v142 - - - Application - v142 - - - Application - v142 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + DebugDLL_fixed + Win32 + + + DebugDLL_fixed + x64 + + + DebugDLL + Win32 + + + DebugDLL + x64 + + + Debug + Win32 + + + Debug + x64 + + + ReleaseDLL_fixed + Win32 + + + ReleaseDLL_fixed + x64 + + + ReleaseDLL + Win32 + + + ReleaseDLL + x64 + + + Release + Win32 + + + Release + x64 + + + + + + + + + {219ec965-228a-1824-174d-96449d05f88a} + + + + {84DAA768-1A38-4312-BB61-4C78BB59E5B8} + Win32Proj + test_opus_api + + + + Application + v142 + + + Application + v142 + + + Application + v142 + + + Application + v142 + + + Application + v142 + + + Application + v142 + + + Application + v142 + + + Application + v142 + + + Application + v142 + + + Application + v142 + + + Application + v142 + + + Application + v142 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Libraries/webm_mem_playback/opus/win32/VS2015/test_opus_encode.vcxproj.filters b/Libraries/webm_mem_playback/opus/win32/VS2015/test_opus_encode.vcxproj.filters index f04776380..4ed3bb9e7 100644 --- a/Libraries/webm_mem_playback/opus/win32/VS2015/test_opus_encode.vcxproj.filters +++ b/Libraries/webm_mem_playback/opus/win32/VS2015/test_opus_encode.vcxproj.filters @@ -1,17 +1,17 @@ - - - - - {546c8d9a-103e-4f78-972b-b44e8d3c8aba} - cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx - - - - - Source Files - - - Source Files - - + + + + + {546c8d9a-103e-4f78-972b-b44e8d3c8aba} + cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx + + + + + Source Files + + + Source Files + + \ No newline at end of file diff --git a/Libraries/webm_mem_playback/opus/win32/genversion.bat b/Libraries/webm_mem_playback/opus/win32/genversion.bat index aea557393..1def7460b 100644 --- a/Libraries/webm_mem_playback/opus/win32/genversion.bat +++ b/Libraries/webm_mem_playback/opus/win32/genversion.bat @@ -1,37 +1,37 @@ -@echo off - -setlocal enableextensions enabledelayedexpansion - -for /f %%v in ('cd "%~dp0.." ^&^& git status ^>NUL 2^>NUL ^&^& git describe --tags --match "v*" --dirty 2^>NUL') do set version=%%v - -if not "%version%"=="" set version=!version:~1! && goto :gotversion - -if exist "%~dp0..\package_version" goto :getversion - -echo Git cannot be found, nor can package_version. Generating unknown version. - -set version=unknown - -goto :gotversion - -:getversion - -for /f "delims== tokens=2" %%v in (%~dps0..\package_version) do set version=%%v -set version=!version:"=! - -:gotversion - -set version=!version: =! -set version_out=#define %~2 "%version%" - -echo %version_out%> "%~1_temp" - -echo n | comp "%~1_temp" "%~1" > NUL 2> NUL - -if not errorlevel 1 goto exit - -copy /y "%~1_temp" "%~1" - -:exit - -del "%~1_temp" +@echo off + +setlocal enableextensions enabledelayedexpansion + +for /f %%v in ('cd "%~dp0.." ^&^& git status ^>NUL 2^>NUL ^&^& git describe --tags --match "v*" --dirty 2^>NUL') do set version=%%v + +if not "%version%"=="" set version=!version:~1! && goto :gotversion + +if exist "%~dp0..\package_version" goto :getversion + +echo Git cannot be found, nor can package_version. Generating unknown version. + +set version=unknown + +goto :gotversion + +:getversion + +for /f "delims== tokens=2" %%v in (%~dps0..\package_version) do set version=%%v +set version=!version:"=! + +:gotversion + +set version=!version: =! +set version_out=#define %~2 "%version%" + +echo %version_out%> "%~1_temp" + +echo n | comp "%~1_temp" "%~1" > NUL 2> NUL + +if not errorlevel 1 goto exit + +copy /y "%~1_temp" "%~1" + +:exit + +del "%~1_temp" diff --git a/Libraries/webm_mem_playback/webm_mem_playback/webm-mem-playback.vcxproj b/Libraries/webm_mem_playback/webm_mem_playback/webm-mem-playback.vcxproj index 5a3253ee6..6e90faa56 100644 --- a/Libraries/webm_mem_playback/webm_mem_playback/webm-mem-playback.vcxproj +++ b/Libraries/webm_mem_playback/webm_mem_playback/webm-mem-playback.vcxproj @@ -1,148 +1,148 @@ - - - - - Debug - Win32 - - - Release - Win32 - - - Debug - x64 - - - Release - x64 - - - - 15.0 - {D0097438-DA4F-4E6D-87AC-7D99DDD276B2} - vpxmemplayback - 10.0 - webm_mem_playback - - - - DynamicLibrary - true - v142 - MultiByte - - - DynamicLibrary - false - v142 - true - MultiByte - - - DynamicLibrary - true - v142 - MultiByte - - - DynamicLibrary - false - v142 - true - MultiByte - - - - - - - - - - - - - - - - - - - - - $(ProjectName)_$(Platform) - - - - Level3 - Disabled - true - true - ..\libwebm_x86_64_vs15;..\libvpx_x86_64_vs15;%(AdditionalIncludeDirectories) - - - %(AdditionalDependencies) - - - - - Level3 - Disabled - true - true - %(AdditionalIncludeDirectories) - MultiThreadedDebug - - - %(AdditionalDependencies) - - - - - Level3 - MaxSpeed - true - true - true - true - ..\libwebm_x86_64_vs15;..\libvpx_x86_64_vs15;%(AdditionalIncludeDirectories) - - - true - true - %(AdditionalDependencies) - - - - - Level3 - MaxSpeed - true - true - true - true - ..\libwebm_x86_vs19;..\libvpx_x64_vs15;..\opus\include;%(AdditionalIncludeDirectories) - MultiThreaded - Speed - - - true - true - ../libvpx_x64_vs15/$(Platform)/$(Configuration)/vpxmt.lib;../libwebm_x64_vs19/Release/libwebm.lib;../opus/win32/VS2015/x64/Release/opus.lib;%(AdditionalDependencies) - - - - - - - - - - - - - - - + + + + + Debug + Win32 + + + Release + Win32 + + + Debug + x64 + + + Release + x64 + + + + 15.0 + {D0097438-DA4F-4E6D-87AC-7D99DDD276B2} + vpxmemplayback + 10.0 + webm_mem_playback + + + + DynamicLibrary + true + v142 + MultiByte + + + DynamicLibrary + false + v142 + true + MultiByte + + + DynamicLibrary + true + v142 + MultiByte + + + DynamicLibrary + false + v142 + true + MultiByte + + + + + + + + + + + + + + + + + + + + + $(ProjectName)_$(Platform) + + + + Level3 + Disabled + true + true + ..\libwebm_x86_64_vs15;..\libvpx_x86_64_vs15;%(AdditionalIncludeDirectories) + + + %(AdditionalDependencies) + + + + + Level3 + Disabled + true + true + %(AdditionalIncludeDirectories) + MultiThreadedDebug + + + %(AdditionalDependencies) + + + + + Level3 + MaxSpeed + true + true + true + true + ..\libwebm_x86_64_vs15;..\libvpx_x86_64_vs15;%(AdditionalIncludeDirectories) + + + true + true + %(AdditionalDependencies) + + + + + Level3 + MaxSpeed + true + true + true + true + ..\libwebm_x86_vs19;..\libvpx_x64_vs15;..\opus\include;%(AdditionalIncludeDirectories) + MultiThreaded + Speed + + + true + true + ../libvpx_x64_vs15/$(Platform)/$(Configuration)/vpxmt.lib;../libwebm_x64_vs19/Release/libwebm.lib;../opus/win32/VS2015/x64/Release/opus.lib;%(AdditionalDependencies) + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/README.md b/README.md index 0e6cfed9a..77f9f2b40 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ This is a LuaCsForBarotrauma modification that adds Multi-Thread and Multi-Core # Barotrauma -Copyright © FakeFish Ltd 2017-2024 +Copyright © FakeFish Ltd 2017-2026 Before downloading the source code, please read the [EULA](EULA.txt). @@ -44,7 +44,7 @@ If you're interested in working on the code, either to develop mods or to contri ### Windows - [Visual Studio](https://www.visualstudio.com/vs/community/) with C# 10 support (VS 2022 or later recommended) ### Linux -- [.NET 6 SDK](https://docs.microsoft.com/en-us/dotnet/core/install/linux) +- [.NET 8 SDK](https://docs.microsoft.com/en-us/dotnet/core/install/linux) ### macOS - [Visual Studio 2022 for Mac](https://visualstudio.microsoft.com/vs/mac/) diff --git a/WindowsSolution.sln b/WindowsSolution.sln index 37ce06f53..d6d248b21 100644 --- a/WindowsSolution.sln +++ b/WindowsSolution.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.0.32014.148 +# Visual Studio Version 18 +VisualStudioVersion = 18.3.11520.95 d18.3 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{D32A29D8-AC7B-4189-B734-8ED9EB4120D0}" ProjectSection(SolutionItems) = preProject @@ -58,228 +58,117 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EosInterface.Implementation EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU Debug|x64 = Debug|x64 - Release|Any CPU = Release|Any CPU Release|x64 = Release|x64 - Unstable|Any CPU = Unstable|Any CPU Unstable|x64 = Unstable|x64 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {E1BBC67C-DC2A-40E8-89F3-B57299D7B16C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {E1BBC67C-DC2A-40E8-89F3-B57299D7B16C}.Debug|Any CPU.Build.0 = Debug|Any CPU {E1BBC67C-DC2A-40E8-89F3-B57299D7B16C}.Debug|x64.ActiveCfg = Debug|x64 {E1BBC67C-DC2A-40E8-89F3-B57299D7B16C}.Debug|x64.Build.0 = Debug|x64 - {E1BBC67C-DC2A-40E8-89F3-B57299D7B16C}.Release|Any CPU.ActiveCfg = Release|Any CPU - {E1BBC67C-DC2A-40E8-89F3-B57299D7B16C}.Release|Any CPU.Build.0 = Release|Any CPU {E1BBC67C-DC2A-40E8-89F3-B57299D7B16C}.Release|x64.ActiveCfg = Release|x64 {E1BBC67C-DC2A-40E8-89F3-B57299D7B16C}.Release|x64.Build.0 = Release|x64 - {E1BBC67C-DC2A-40E8-89F3-B57299D7B16C}.Unstable|Any CPU.ActiveCfg = Debug|Any CPU - {E1BBC67C-DC2A-40E8-89F3-B57299D7B16C}.Unstable|Any CPU.Build.0 = Debug|Any CPU {E1BBC67C-DC2A-40E8-89F3-B57299D7B16C}.Unstable|x64.ActiveCfg = Release|x64 {E1BBC67C-DC2A-40E8-89F3-B57299D7B16C}.Unstable|x64.Build.0 = Release|x64 - {95C4D59D-9BE4-4278-B4F8-46C0BA1A3916}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {95C4D59D-9BE4-4278-B4F8-46C0BA1A3916}.Debug|Any CPU.Build.0 = Debug|Any CPU {95C4D59D-9BE4-4278-B4F8-46C0BA1A3916}.Debug|x64.ActiveCfg = Debug|x64 {95C4D59D-9BE4-4278-B4F8-46C0BA1A3916}.Debug|x64.Build.0 = Debug|x64 - {95C4D59D-9BE4-4278-B4F8-46C0BA1A3916}.Release|Any CPU.ActiveCfg = Release|Any CPU - {95C4D59D-9BE4-4278-B4F8-46C0BA1A3916}.Release|Any CPU.Build.0 = Release|Any CPU {95C4D59D-9BE4-4278-B4F8-46C0BA1A3916}.Release|x64.ActiveCfg = Release|x64 {95C4D59D-9BE4-4278-B4F8-46C0BA1A3916}.Release|x64.Build.0 = Release|x64 - {95C4D59D-9BE4-4278-B4F8-46C0BA1A3916}.Unstable|Any CPU.ActiveCfg = Debug|Any CPU - {95C4D59D-9BE4-4278-B4F8-46C0BA1A3916}.Unstable|Any CPU.Build.0 = Debug|Any CPU {95C4D59D-9BE4-4278-B4F8-46C0BA1A3916}.Unstable|x64.ActiveCfg = Release|x64 {95C4D59D-9BE4-4278-B4F8-46C0BA1A3916}.Unstable|x64.Build.0 = Release|x64 - {AD30AE95-7BF6-4CE5-AEED-B6C30A88F139}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {AD30AE95-7BF6-4CE5-AEED-B6C30A88F139}.Debug|Any CPU.Build.0 = Debug|Any CPU {AD30AE95-7BF6-4CE5-AEED-B6C30A88F139}.Debug|x64.ActiveCfg = Debug|x64 {AD30AE95-7BF6-4CE5-AEED-B6C30A88F139}.Debug|x64.Build.0 = Debug|x64 - {AD30AE95-7BF6-4CE5-AEED-B6C30A88F139}.Release|Any CPU.ActiveCfg = Release|Any CPU - {AD30AE95-7BF6-4CE5-AEED-B6C30A88F139}.Release|Any CPU.Build.0 = Release|Any CPU {AD30AE95-7BF6-4CE5-AEED-B6C30A88F139}.Release|x64.ActiveCfg = Release|x64 {AD30AE95-7BF6-4CE5-AEED-B6C30A88F139}.Release|x64.Build.0 = Release|x64 - {AD30AE95-7BF6-4CE5-AEED-B6C30A88F139}.Unstable|Any CPU.ActiveCfg = Debug|Any CPU - {AD30AE95-7BF6-4CE5-AEED-B6C30A88F139}.Unstable|Any CPU.Build.0 = Debug|Any CPU {AD30AE95-7BF6-4CE5-AEED-B6C30A88F139}.Unstable|x64.ActiveCfg = Release|x64 {AD30AE95-7BF6-4CE5-AEED-B6C30A88F139}.Unstable|x64.Build.0 = Release|x64 - {894D3518-A0E3-4B88-B9BF-9E1AFC3F9523}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {894D3518-A0E3-4B88-B9BF-9E1AFC3F9523}.Debug|Any CPU.Build.0 = Debug|Any CPU {894D3518-A0E3-4B88-B9BF-9E1AFC3F9523}.Debug|x64.ActiveCfg = Debug|x64 {894D3518-A0E3-4B88-B9BF-9E1AFC3F9523}.Debug|x64.Build.0 = Debug|x64 - {894D3518-A0E3-4B88-B9BF-9E1AFC3F9523}.Release|Any CPU.ActiveCfg = Release|Any CPU - {894D3518-A0E3-4B88-B9BF-9E1AFC3F9523}.Release|Any CPU.Build.0 = Release|Any CPU {894D3518-A0E3-4B88-B9BF-9E1AFC3F9523}.Release|x64.ActiveCfg = Release|x64 {894D3518-A0E3-4B88-B9BF-9E1AFC3F9523}.Release|x64.Build.0 = Release|x64 - {894D3518-A0E3-4B88-B9BF-9E1AFC3F9523}.Unstable|Any CPU.ActiveCfg = Debug|Any CPU - {894D3518-A0E3-4B88-B9BF-9E1AFC3F9523}.Unstable|Any CPU.Build.0 = Debug|Any CPU {894D3518-A0E3-4B88-B9BF-9E1AFC3F9523}.Unstable|x64.ActiveCfg = Release|x64 {894D3518-A0E3-4B88-B9BF-9E1AFC3F9523}.Unstable|x64.Build.0 = Release|x64 - {ED2873CA-C209-4CBC-ADD4-DAA753DFEEAF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {ED2873CA-C209-4CBC-ADD4-DAA753DFEEAF}.Debug|Any CPU.Build.0 = Debug|Any CPU {ED2873CA-C209-4CBC-ADD4-DAA753DFEEAF}.Debug|x64.ActiveCfg = Debug|x64 {ED2873CA-C209-4CBC-ADD4-DAA753DFEEAF}.Debug|x64.Build.0 = Debug|x64 - {ED2873CA-C209-4CBC-ADD4-DAA753DFEEAF}.Release|Any CPU.ActiveCfg = Release|Any CPU - {ED2873CA-C209-4CBC-ADD4-DAA753DFEEAF}.Release|Any CPU.Build.0 = Release|Any CPU {ED2873CA-C209-4CBC-ADD4-DAA753DFEEAF}.Release|x64.ActiveCfg = Release|x64 {ED2873CA-C209-4CBC-ADD4-DAA753DFEEAF}.Release|x64.Build.0 = Release|x64 - {ED2873CA-C209-4CBC-ADD4-DAA753DFEEAF}.Unstable|Any CPU.ActiveCfg = Debug|Any CPU - {ED2873CA-C209-4CBC-ADD4-DAA753DFEEAF}.Unstable|Any CPU.Build.0 = Debug|Any CPU {ED2873CA-C209-4CBC-ADD4-DAA753DFEEAF}.Unstable|x64.ActiveCfg = Release|x64 {ED2873CA-C209-4CBC-ADD4-DAA753DFEEAF}.Unstable|x64.Build.0 = Release|x64 - {978633A8-094A-4623-9B82-8533FC8BA1CC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {978633A8-094A-4623-9B82-8533FC8BA1CC}.Debug|Any CPU.Build.0 = Debug|Any CPU {978633A8-094A-4623-9B82-8533FC8BA1CC}.Debug|x64.ActiveCfg = Debug|x64 {978633A8-094A-4623-9B82-8533FC8BA1CC}.Debug|x64.Build.0 = Debug|x64 - {978633A8-094A-4623-9B82-8533FC8BA1CC}.Release|Any CPU.ActiveCfg = Release|Any CPU - {978633A8-094A-4623-9B82-8533FC8BA1CC}.Release|Any CPU.Build.0 = Release|Any CPU {978633A8-094A-4623-9B82-8533FC8BA1CC}.Release|x64.ActiveCfg = Release|x64 {978633A8-094A-4623-9B82-8533FC8BA1CC}.Release|x64.Build.0 = Release|x64 - {978633A8-094A-4623-9B82-8533FC8BA1CC}.Unstable|Any CPU.ActiveCfg = Unstable|Any CPU - {978633A8-094A-4623-9B82-8533FC8BA1CC}.Unstable|Any CPU.Build.0 = Unstable|Any CPU {978633A8-094A-4623-9B82-8533FC8BA1CC}.Unstable|x64.ActiveCfg = Unstable|x64 {978633A8-094A-4623-9B82-8533FC8BA1CC}.Unstable|x64.Build.0 = Unstable|x64 - {39E52316-D6C1-4D1F-95FF-37F41C9AB5A7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {39E52316-D6C1-4D1F-95FF-37F41C9AB5A7}.Debug|Any CPU.Build.0 = Debug|Any CPU {39E52316-D6C1-4D1F-95FF-37F41C9AB5A7}.Debug|x64.ActiveCfg = Debug|x64 {39E52316-D6C1-4D1F-95FF-37F41C9AB5A7}.Debug|x64.Build.0 = Debug|x64 - {39E52316-D6C1-4D1F-95FF-37F41C9AB5A7}.Release|Any CPU.ActiveCfg = Release|Any CPU - {39E52316-D6C1-4D1F-95FF-37F41C9AB5A7}.Release|Any CPU.Build.0 = Release|Any CPU {39E52316-D6C1-4D1F-95FF-37F41C9AB5A7}.Release|x64.ActiveCfg = Release|x64 {39E52316-D6C1-4D1F-95FF-37F41C9AB5A7}.Release|x64.Build.0 = Release|x64 - {39E52316-D6C1-4D1F-95FF-37F41C9AB5A7}.Unstable|Any CPU.ActiveCfg = Debug|Any CPU - {39E52316-D6C1-4D1F-95FF-37F41C9AB5A7}.Unstable|Any CPU.Build.0 = Debug|Any CPU {39E52316-D6C1-4D1F-95FF-37F41C9AB5A7}.Unstable|x64.ActiveCfg = Release|x64 {39E52316-D6C1-4D1F-95FF-37F41C9AB5A7}.Unstable|x64.Build.0 = Release|x64 - {D379BF8E-D696-4AB9-A27F-4D0C493BF484}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {D379BF8E-D696-4AB9-A27F-4D0C493BF484}.Debug|Any CPU.Build.0 = Debug|Any CPU {D379BF8E-D696-4AB9-A27F-4D0C493BF484}.Debug|x64.ActiveCfg = Debug|x64 {D379BF8E-D696-4AB9-A27F-4D0C493BF484}.Debug|x64.Build.0 = Debug|x64 - {D379BF8E-D696-4AB9-A27F-4D0C493BF484}.Release|Any CPU.ActiveCfg = Release|Any CPU - {D379BF8E-D696-4AB9-A27F-4D0C493BF484}.Release|Any CPU.Build.0 = Release|Any CPU {D379BF8E-D696-4AB9-A27F-4D0C493BF484}.Release|x64.ActiveCfg = Release|x64 {D379BF8E-D696-4AB9-A27F-4D0C493BF484}.Release|x64.Build.0 = Release|x64 - {D379BF8E-D696-4AB9-A27F-4D0C493BF484}.Unstable|Any CPU.ActiveCfg = Debug|Any CPU - {D379BF8E-D696-4AB9-A27F-4D0C493BF484}.Unstable|Any CPU.Build.0 = Debug|Any CPU {D379BF8E-D696-4AB9-A27F-4D0C493BF484}.Unstable|x64.ActiveCfg = Release|x64 {D379BF8E-D696-4AB9-A27F-4D0C493BF484}.Unstable|x64.Build.0 = Release|x64 - {47848C6E-C7A8-4EC3-96C2-3BC8A4234AFA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {47848C6E-C7A8-4EC3-96C2-3BC8A4234AFA}.Debug|Any CPU.Build.0 = Debug|Any CPU {47848C6E-C7A8-4EC3-96C2-3BC8A4234AFA}.Debug|x64.ActiveCfg = Debug|x64 {47848C6E-C7A8-4EC3-96C2-3BC8A4234AFA}.Debug|x64.Build.0 = Debug|x64 - {47848C6E-C7A8-4EC3-96C2-3BC8A4234AFA}.Release|Any CPU.ActiveCfg = Release|Any CPU - {47848C6E-C7A8-4EC3-96C2-3BC8A4234AFA}.Release|Any CPU.Build.0 = Release|Any CPU {47848C6E-C7A8-4EC3-96C2-3BC8A4234AFA}.Release|x64.ActiveCfg = Release|x64 {47848C6E-C7A8-4EC3-96C2-3BC8A4234AFA}.Release|x64.Build.0 = Release|x64 - {47848C6E-C7A8-4EC3-96C2-3BC8A4234AFA}.Unstable|Any CPU.ActiveCfg = Unstable|Any CPU - {47848C6E-C7A8-4EC3-96C2-3BC8A4234AFA}.Unstable|Any CPU.Build.0 = Unstable|Any CPU {47848C6E-C7A8-4EC3-96C2-3BC8A4234AFA}.Unstable|x64.ActiveCfg = Unstable|x64 {47848C6E-C7A8-4EC3-96C2-3BC8A4234AFA}.Unstable|x64.Build.0 = Unstable|x64 - {1F318AC4-F808-4130-867F-B98DF9AA8F95}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {1F318AC4-F808-4130-867F-B98DF9AA8F95}.Debug|Any CPU.Build.0 = Debug|Any CPU {1F318AC4-F808-4130-867F-B98DF9AA8F95}.Debug|x64.ActiveCfg = Debug|x64 {1F318AC4-F808-4130-867F-B98DF9AA8F95}.Debug|x64.Build.0 = Debug|x64 - {1F318AC4-F808-4130-867F-B98DF9AA8F95}.Release|Any CPU.ActiveCfg = Release|Any CPU - {1F318AC4-F808-4130-867F-B98DF9AA8F95}.Release|Any CPU.Build.0 = Release|Any CPU {1F318AC4-F808-4130-867F-B98DF9AA8F95}.Release|x64.ActiveCfg = Release|x64 {1F318AC4-F808-4130-867F-B98DF9AA8F95}.Release|x64.Build.0 = Release|x64 - {1F318AC4-F808-4130-867F-B98DF9AA8F95}.Unstable|Any CPU.ActiveCfg = Debug|Any CPU - {1F318AC4-F808-4130-867F-B98DF9AA8F95}.Unstable|Any CPU.Build.0 = Debug|Any CPU {1F318AC4-F808-4130-867F-B98DF9AA8F95}.Unstable|x64.ActiveCfg = Release|x64 {1F318AC4-F808-4130-867F-B98DF9AA8F95}.Unstable|x64.Build.0 = Release|x64 - {6911872D-40EF-400C-B0A1-9985A19ED488}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {6911872D-40EF-400C-B0A1-9985A19ED488}.Debug|Any CPU.Build.0 = Debug|Any CPU {6911872D-40EF-400C-B0A1-9985A19ED488}.Debug|x64.ActiveCfg = Debug|x64 {6911872D-40EF-400C-B0A1-9985A19ED488}.Debug|x64.Build.0 = Debug|x64 - {6911872D-40EF-400C-B0A1-9985A19ED488}.Release|Any CPU.ActiveCfg = Release|Any CPU - {6911872D-40EF-400C-B0A1-9985A19ED488}.Release|Any CPU.Build.0 = Release|Any CPU {6911872D-40EF-400C-B0A1-9985A19ED488}.Release|x64.ActiveCfg = Release|x64 {6911872D-40EF-400C-B0A1-9985A19ED488}.Release|x64.Build.0 = Release|x64 - {6911872D-40EF-400C-B0A1-9985A19ED488}.Unstable|Any CPU.ActiveCfg = Debug|Any CPU - {6911872D-40EF-400C-B0A1-9985A19ED488}.Unstable|Any CPU.Build.0 = Debug|Any CPU {6911872D-40EF-400C-B0A1-9985A19ED488}.Unstable|x64.ActiveCfg = Release|x64 {6911872D-40EF-400C-B0A1-9985A19ED488}.Unstable|x64.Build.0 = Release|x64 - {C7212AE2-A925-4225-A639-AE0653EF65B0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {C7212AE2-A925-4225-A639-AE0653EF65B0}.Debug|Any CPU.Build.0 = Debug|Any CPU {C7212AE2-A925-4225-A639-AE0653EF65B0}.Debug|x64.ActiveCfg = Debug|Any CPU {C7212AE2-A925-4225-A639-AE0653EF65B0}.Debug|x64.Build.0 = Debug|Any CPU - {C7212AE2-A925-4225-A639-AE0653EF65B0}.Release|Any CPU.ActiveCfg = Release|Any CPU - {C7212AE2-A925-4225-A639-AE0653EF65B0}.Release|Any CPU.Build.0 = Release|Any CPU {C7212AE2-A925-4225-A639-AE0653EF65B0}.Release|x64.ActiveCfg = Release|Any CPU {C7212AE2-A925-4225-A639-AE0653EF65B0}.Release|x64.Build.0 = Release|Any CPU - {C7212AE2-A925-4225-A639-AE0653EF65B0}.Unstable|Any CPU.ActiveCfg = Release|Any CPU - {C7212AE2-A925-4225-A639-AE0653EF65B0}.Unstable|Any CPU.Build.0 = Release|Any CPU {C7212AE2-A925-4225-A639-AE0653EF65B0}.Unstable|x64.ActiveCfg = Release|Any CPU {C7212AE2-A925-4225-A639-AE0653EF65B0}.Unstable|x64.Build.0 = Release|Any CPU - {2EEF2610-64A3-4E5D-95ED-0E181C1A34ED}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {2EEF2610-64A3-4E5D-95ED-0E181C1A34ED}.Debug|Any CPU.Build.0 = Debug|Any CPU {2EEF2610-64A3-4E5D-95ED-0E181C1A34ED}.Debug|x64.ActiveCfg = Debug|Any CPU {2EEF2610-64A3-4E5D-95ED-0E181C1A34ED}.Debug|x64.Build.0 = Debug|Any CPU - {2EEF2610-64A3-4E5D-95ED-0E181C1A34ED}.Release|Any CPU.ActiveCfg = Release|Any CPU - {2EEF2610-64A3-4E5D-95ED-0E181C1A34ED}.Release|Any CPU.Build.0 = Release|Any CPU {2EEF2610-64A3-4E5D-95ED-0E181C1A34ED}.Release|x64.ActiveCfg = Release|Any CPU {2EEF2610-64A3-4E5D-95ED-0E181C1A34ED}.Release|x64.Build.0 = Release|Any CPU - {2EEF2610-64A3-4E5D-95ED-0E181C1A34ED}.Unstable|Any CPU.ActiveCfg = Debug|Any CPU - {2EEF2610-64A3-4E5D-95ED-0E181C1A34ED}.Unstable|Any CPU.Build.0 = Debug|Any CPU {2EEF2610-64A3-4E5D-95ED-0E181C1A34ED}.Unstable|x64.ActiveCfg = Debug|Any CPU {2EEF2610-64A3-4E5D-95ED-0E181C1A34ED}.Unstable|x64.Build.0 = Debug|Any CPU - {C98FE0D0-BC7D-4806-B592-734B53016FD8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {C98FE0D0-BC7D-4806-B592-734B53016FD8}.Debug|Any CPU.Build.0 = Debug|Any CPU {C98FE0D0-BC7D-4806-B592-734B53016FD8}.Debug|x64.ActiveCfg = Debug|Any CPU {C98FE0D0-BC7D-4806-B592-734B53016FD8}.Debug|x64.Build.0 = Debug|Any CPU - {C98FE0D0-BC7D-4806-B592-734B53016FD8}.Release|Any CPU.ActiveCfg = Release|Any CPU - {C98FE0D0-BC7D-4806-B592-734B53016FD8}.Release|Any CPU.Build.0 = Release|Any CPU {C98FE0D0-BC7D-4806-B592-734B53016FD8}.Release|x64.ActiveCfg = Release|Any CPU {C98FE0D0-BC7D-4806-B592-734B53016FD8}.Release|x64.Build.0 = Release|Any CPU - {C98FE0D0-BC7D-4806-B592-734B53016FD8}.Unstable|Any CPU.ActiveCfg = Release|Any CPU - {C98FE0D0-BC7D-4806-B592-734B53016FD8}.Unstable|Any CPU.Build.0 = Release|Any CPU {C98FE0D0-BC7D-4806-B592-734B53016FD8}.Unstable|x64.ActiveCfg = Release|Any CPU {C98FE0D0-BC7D-4806-B592-734B53016FD8}.Unstable|x64.Build.0 = Release|Any CPU - {AF484604-D20F-4D87-B298-1A712052D0D9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {AF484604-D20F-4D87-B298-1A712052D0D9}.Debug|Any CPU.Build.0 = Debug|Any CPU {AF484604-D20F-4D87-B298-1A712052D0D9}.Debug|x64.ActiveCfg = Debug|Any CPU {AF484604-D20F-4D87-B298-1A712052D0D9}.Debug|x64.Build.0 = Debug|Any CPU - {AF484604-D20F-4D87-B298-1A712052D0D9}.Release|Any CPU.ActiveCfg = Release|Any CPU - {AF484604-D20F-4D87-B298-1A712052D0D9}.Release|Any CPU.Build.0 = Release|Any CPU {AF484604-D20F-4D87-B298-1A712052D0D9}.Release|x64.ActiveCfg = Release|Any CPU {AF484604-D20F-4D87-B298-1A712052D0D9}.Release|x64.Build.0 = Release|Any CPU - {AF484604-D20F-4D87-B298-1A712052D0D9}.Unstable|Any CPU.ActiveCfg = Debug|Any CPU - {AF484604-D20F-4D87-B298-1A712052D0D9}.Unstable|Any CPU.Build.0 = Debug|Any CPU {AF484604-D20F-4D87-B298-1A712052D0D9}.Unstable|x64.ActiveCfg = Debug|Any CPU {AF484604-D20F-4D87-B298-1A712052D0D9}.Unstable|x64.Build.0 = Debug|Any CPU - {FA273D62-455C-4BF7-B020-D0EBDE9EB565}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {FA273D62-455C-4BF7-B020-D0EBDE9EB565}.Debug|Any CPU.Build.0 = Debug|Any CPU {FA273D62-455C-4BF7-B020-D0EBDE9EB565}.Debug|x64.ActiveCfg = Debug|Any CPU {FA273D62-455C-4BF7-B020-D0EBDE9EB565}.Debug|x64.Build.0 = Debug|Any CPU - {FA273D62-455C-4BF7-B020-D0EBDE9EB565}.Release|Any CPU.ActiveCfg = Release|Any CPU - {FA273D62-455C-4BF7-B020-D0EBDE9EB565}.Release|Any CPU.Build.0 = Release|Any CPU {FA273D62-455C-4BF7-B020-D0EBDE9EB565}.Release|x64.ActiveCfg = Release|Any CPU {FA273D62-455C-4BF7-B020-D0EBDE9EB565}.Release|x64.Build.0 = Release|Any CPU - {FA273D62-455C-4BF7-B020-D0EBDE9EB565}.Unstable|Any CPU.ActiveCfg = Release|Any CPU - {FA273D62-455C-4BF7-B020-D0EBDE9EB565}.Unstable|Any CPU.Build.0 = Release|Any CPU {FA273D62-455C-4BF7-B020-D0EBDE9EB565}.Unstable|x64.ActiveCfg = Release|Any CPU {FA273D62-455C-4BF7-B020-D0EBDE9EB565}.Unstable|x64.Build.0 = Release|Any CPU - {38C5D23D-0858-4254-B7B7-145221A8AB75}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {38C5D23D-0858-4254-B7B7-145221A8AB75}.Debug|Any CPU.Build.0 = Debug|Any CPU {38C5D23D-0858-4254-B7B7-145221A8AB75}.Debug|x64.ActiveCfg = Debug|Any CPU {38C5D23D-0858-4254-B7B7-145221A8AB75}.Debug|x64.Build.0 = Debug|Any CPU - {38C5D23D-0858-4254-B7B7-145221A8AB75}.Release|Any CPU.ActiveCfg = Release|Any CPU - {38C5D23D-0858-4254-B7B7-145221A8AB75}.Release|Any CPU.Build.0 = Release|Any CPU {38C5D23D-0858-4254-B7B7-145221A8AB75}.Release|x64.ActiveCfg = Release|Any CPU {38C5D23D-0858-4254-B7B7-145221A8AB75}.Release|x64.Build.0 = Release|Any CPU - {38C5D23D-0858-4254-B7B7-145221A8AB75}.Unstable|Any CPU.ActiveCfg = Release|Any CPU - {38C5D23D-0858-4254-B7B7-145221A8AB75}.Unstable|Any CPU.Build.0 = Release|Any CPU {38C5D23D-0858-4254-B7B7-145221A8AB75}.Unstable|x64.ActiveCfg = Release|Any CPU {38C5D23D-0858-4254-B7B7-145221A8AB75}.Unstable|x64.Build.0 = Release|Any CPU - {B411A619-1643-4C5F-A95D-9427D59BE010}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {B411A619-1643-4C5F-A95D-9427D59BE010}.Debug|Any CPU.Build.0 = Debug|Any CPU {B411A619-1643-4C5F-A95D-9427D59BE010}.Debug|x64.ActiveCfg = Debug|Any CPU {B411A619-1643-4C5F-A95D-9427D59BE010}.Debug|x64.Build.0 = Debug|Any CPU - {B411A619-1643-4C5F-A95D-9427D59BE010}.Release|Any CPU.ActiveCfg = Release|Any CPU - {B411A619-1643-4C5F-A95D-9427D59BE010}.Release|Any CPU.Build.0 = Release|Any CPU {B411A619-1643-4C5F-A95D-9427D59BE010}.Release|x64.ActiveCfg = Release|Any CPU {B411A619-1643-4C5F-A95D-9427D59BE010}.Release|x64.Build.0 = Release|Any CPU - {B411A619-1643-4C5F-A95D-9427D59BE010}.Unstable|Any CPU.ActiveCfg = Release|Any CPU - {B411A619-1643-4C5F-A95D-9427D59BE010}.Unstable|Any CPU.Build.0 = Release|Any CPU {B411A619-1643-4C5F-A95D-9427D59BE010}.Unstable|x64.ActiveCfg = Release|Any CPU {B411A619-1643-4C5F-A95D-9427D59BE010}.Unstable|x64.Build.0 = Release|Any CPU EndGlobalSection