diff --git a/.github/DISCUSSION_TEMPLATE/bug-reports.yml b/.github/DISCUSSION_TEMPLATE/bug-reports.yml index cdefa37533..71271780cf 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 338b28ed43..3b7949d126 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -78,16 +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 - Farseer.NetStandard.dll + LocalMods/LuaCsForBarotrauma jobs: build: diff --git a/.gitignore b/.gitignore index 287132dc02..b4b5f19019 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 49c4b43098..1e7554604a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/EnemyAIController.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/EnemyAIController.cs @@ -29,7 +29,7 @@ public override void DebugDraw(SpriteBatch spriteBatch) } } } - 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 @@ public override void DebugDraw(SpriteBatch spriteBatch) 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 4e628af8b3..9e3ac4669e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/HumanAIController.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/HumanAIController.cs @@ -23,6 +23,8 @@ public override void DebugDraw(Microsoft.Xna.Framework.Graphics.SpriteBatch spri //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 @@ public override void DebugDraw(Microsoft.Xna.Framework.Graphics.SpriteBatch spri 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 @@ public override void DebugDraw(Microsoft.Xna.Framework.Graphics.SpriteBatch spri 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 @@ public override void DebugDraw(Microsoft.Xna.Framework.Graphics.SpriteBatch spri } } - 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 6e2ac141ba..ce45568d22 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Animation/Ragdoll.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Animation/Ragdoll.cs @@ -547,7 +547,7 @@ partial void SeverLimbJointProjSpecific(LimbJoint limbJoint, bool playSound) } } - public void Draw(SpriteBatch spriteBatch, Camera cam) + public void Draw(SpriteBatch spriteBatch, Camera cam, bool onlyDrawSeveredLimbs) { if (simplePhysicsEnabled) { return; } @@ -573,8 +573,12 @@ public void Draw(SpriteBatch spriteBatch, Camera cam) { 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 0180efb8c6..c388782684 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs @@ -938,8 +938,8 @@ public void DoVisibilityCheck(Camera cam) 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 8976379503..cd9ba6820f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/InteractionLabelManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/InteractionLabelManager.cs @@ -4,6 +4,7 @@ using Microsoft.Xna.Framework.Graphics; using System.Collections.Generic; using Barotrauma.Items.Components; +using System.Linq; namespace Barotrauma; @@ -106,12 +107,17 @@ private static void RecalculateLabelPositions(Camera cam, Character character) } 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 @@ private static bool IsLooseItem(Item item) 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 47e90a6cb6..0d9c45be1f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs @@ -340,10 +340,6 @@ partial void InitProjSpecific(ContentXElement element) 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 @@ partial void AddDamageProjSpecific(bool playSound, AttackResult result) 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 @@ public void Draw(SpriteBatch spriteBatch, Camera cam, Color? overrideColor = nul 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 @@ public void Draw(SpriteBatch spriteBatch, Camera cam, Color? overrideColor = nul } } 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 d9658086f6..7e0a3b5476 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxInputOutputNode.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxInputOutputNode.cs @@ -31,7 +31,7 @@ public void PromptEdit(GUIComponent parent) 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 9bd0a595bf..9deffae478 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs @@ -16,6 +16,7 @@ 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 @@ public static void Toggle() 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 @@ async Task gameOwnershipTokenTest() 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() })); @@ -4236,51 +4227,8 @@ void getTextsFromElement(XElement element, List list, string parentName) 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 3a7988c212..e7538dc03e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/ConversationAction.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/ConversationAction.cs @@ -277,6 +277,14 @@ void AssignActionsToButtons(List optionButtons, GUIMessageBox target) 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 @@ private static List CreateConversation(GUIListBox parentBox, Localize 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 @@ private static List CreateConversation(GUIListBox parentBox, Localize } 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 e3a607d75e..d8644061b8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/AbandonedOutpostMission.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/AbandonedOutpostMission.cs @@ -61,17 +61,9 @@ public override void ClientReadInitial(IReadMessage msg) { 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 0000000000..601d4ec095 --- /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 8b9d8c492e..fbf3e7a16a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/SalvageMission.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/SalvageMission.cs @@ -14,7 +14,7 @@ partial class SalvageMission : Mission 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 deb4201bf1..5514fa1a09 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 @@ public void AddMessage(ChatMessage message) { 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 918b67d16f..af674c6960 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs @@ -1435,8 +1435,15 @@ private static GUIImage LoadGUIImage(ContentXElement element, RectTransform pare 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 3116cfd817..2b0041c131 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIDropDown.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIDropDown.cs @@ -26,7 +26,9 @@ public class GUIDropDown : GUIComponent, IKeyboardSubscriber 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 928ae3519e..ea17ff7b57 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/HRManagerUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/HRManagerUI.cs @@ -710,19 +710,24 @@ public GUIComponent CreateCharacterFrame(CharacterInfo characterInfo, GUIListBox 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 953062de9e..b8ddf8f72f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs @@ -18,12 +18,12 @@ 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 @@ public GameMain(string[] args) 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 @@ public GameMain(string[] args) MainThread = Thread.CurrentThread; Window.FileDropped += OnFileDropped; + + LuaCsSetup.Instance.GetType(); } public static void ExecuteAfterContentFinishedLoading(Action action) @@ -636,9 +636,6 @@ static void log(string str) HasLoaded = true; log("LOADING COROUTINE FINISHED"); -#if CLIENT - LuaCsInstaller.CheckUpdate(); -#endif contentLoaded = true; while (postContentLoadActions.TryDequeue(out Action action)) @@ -986,8 +983,6 @@ static bool itemHudActive() Screen.Selected.AddToGUIUpdateList(); - LuaCsLogger.AddToGUIUpdateList(); - Client?.AddToGUIUpdateList(); SubmarinePreview.AddToGUIUpdateList(); @@ -1054,8 +1049,6 @@ static bool itemHudActive() SoundManager?.Update(); - GameMain.LuaCs.Update(); - Timing.Accumulator -= Timing.Step; updateCount++; @@ -1237,8 +1230,6 @@ public static void QuitToMainMenu(bool save) GUIMessageBox.CloseAll(); MainMenuScreen.Select(); GameSession = null; - - GameMain.LuaCs.Stop(); } public void ShowBugReporter() @@ -1301,6 +1292,18 @@ protected override void OnExiting(object sender, EventArgs args) { 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 4dfea9e86a..3b6ac94881 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/Tutorial.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/Tutorial.cs @@ -179,8 +179,6 @@ private IEnumerable Loading() 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 0452c45e70..e336609548 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs @@ -487,7 +487,21 @@ private float GetSoundVolume(ItemSound sound) 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 c0a6ece275..31af8751a8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs @@ -552,9 +552,9 @@ public void DrawContainedItems(SpriteBatch spriteBatch, float itemDepth, Color? 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 4e637d1dce..82183702e8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs @@ -52,10 +52,12 @@ partial void SetLightSourceState(bool enabled, float brightness) 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 @@ public void Draw(SpriteBatch spriteBatch, bool editing = false, float itemDepth 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 @@ public void Draw(SpriteBatch spriteBatch, bool editing = false, float itemDepth { color = new Color(lightColor, Light.OverrideLightSpriteAlpha.Value); } + Light.LightSprite.Draw(spriteBatch, new Vector2(drawPos.X, -drawPos.Y), color * lightBrightness, @@ -128,8 +134,16 @@ public override void FlipX(bool relativeToSub) { 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 710dcb9f64..bee454c79c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Controller.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Controller.cs @@ -8,6 +8,30 @@ partial class Controller : ItemComponent { 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 @@ public void ClientEventRead(IReadMessage msg, float sendingTime) 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 b791fcf876..5dca29d13f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs @@ -434,18 +434,13 @@ partial void CreateRecipes() 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 @@ partial void CreateRecipes() itemIcon, scaleToFit: true) { Color = itemIcon == fi.TargetItem.Sprite ? fi.TargetItem.SpriteColor : fi.TargetItem.InventoryIconColor, - ToolTip = recipeTooltip + CanBeFocused = false }; } @@ -466,7 +461,7 @@ partial void CreateRecipes() { 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 @@ partial void CreateRecipes() } } + 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 @@ private bool FilterEntities(MapEntityCategory? category, string filter) } } - 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 @@ private void CreateSelectedItemUI(SelectedRecipe recipe) 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 3c08f7fe70..f76b9506a2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs @@ -443,6 +443,7 @@ private bool VisibleOnItemFinder(Item targetItem) 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 872b9be0c4..adb44aa8e9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs @@ -575,6 +575,22 @@ void AddIfValid(Level.ClusterLocation c) 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 @@ void AddIfValid(Level.ClusterLocation c) 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 @@ private void DrawSonar(SpriteBatch spriteBatch, Rectangle rect) 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 e86923ef70..ad7305e89d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs @@ -58,7 +58,7 @@ public Vector2 DrawSize 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 9d8ba42141..cf4f068131 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CircuitBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CircuitBox.cs @@ -78,7 +78,7 @@ partial void InitProjSpecific(ContentXElement element) } } - 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 50ee85985b..d7d63d74d7 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/ConnectionPanel.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/ConnectionPanel.cs @@ -97,7 +97,7 @@ public override void Move(Vector2 amount, bool ignoreContacts = false) 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 9b999dbae2..c6a7231fbe 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/StatusHUD.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/StatusHUD.cs @@ -287,20 +287,24 @@ private void DrawCharacterInfo(SpriteBatch spriteBatch, Character target, float 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 21f681e5e5..3f7bfdfcdb 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs @@ -1597,7 +1597,8 @@ void DrawDragRelated() { 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 @@ public static void DrawSlot(SpriteBatch spriteBatch, Inventory inventory, Visual 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 eb371af3f9..78be7c33ad 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs @@ -419,7 +419,7 @@ public void Draw(SpriteBatch spriteBatch, bool editing, bool back = true, Color? 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 @@ public void Draw(SpriteBatch spriteBatch, bool editing, bool back = true, Color? 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 @@ public GUIComponent CreateEditingHUD(bool inGame = false) 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 @@ public GUIComponent CreateEditingHUD(bool inGame = false) 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 @@ public GUIComponent CreateEditingHUD(bool inGame = false) 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 @@ public void UpdateHUD(Camera cam, Character character, float deltaTime) 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 @@ bool DrawHud(ItemComponent ic) 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 @@ public void DrawHUD(SpriteBatch spriteBatch, Camera cam, Character character) } } + 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 7a7701b9e0..6f5041c4f6 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/ItemPrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/ItemPrefab.cs @@ -166,6 +166,14 @@ private void ParseSubElementsClient(ContentXElement element, ItemPrefab variantO 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 0000000000..5a902c7537 --- /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 0000000000..6d690ec50b --- /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 0000000000..e61ad2e1d9 --- /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 0000000000..c0202b2e8c --- /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 0000000000..0314c6c5de --- /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 0000000000..a56d111122 --- /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 0000000000..f9f56ed67c --- /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 c487bdeea2..68fa823c7a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/LuaCs/LuaCsInstaller.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/LuaCs/LuaCsInstaller.cs @@ -8,115 +8,7 @@ static partial class LuaCsInstaller { 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 ProjectEP?", 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 32c830bb46..0000000000 --- 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 96570ba821..0000000000 --- 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 ProjectEP", style: "GUIButtonSmall") - { - ToolTip = "Remove Client-Side ProjectEP.", - 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 b85175ded7..fea455064c 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; } - } - - public void CheckInitialize() - { - List csharpMods = new List(); - foreach (ContentPackage cp in ContentPackageManager.EnabledPackages.All) + else if (_csRunPolicy?.Value is "Disabled") { - if (Directory.Exists(cp.Dir + "/CSharp") || Directory.Exists(cp.Dir + "/bin")) - { - csharpMods.Add(cp); - } + IsCsEnabledForSession = false; + onSelection(false); + return; } - if (csharpMods.Count == 0 || ShouldRunCs) + if (contentPackages.None()) { - Initialize(); + onSelection(true); return; } - StringBuilder sb = new StringBuilder(); + GUIMessageBox messageBox = new GUIMessageBox( + TextManager.Get("warning"), + relativeSize: new Vector2(0.3f, 0.55f), + minSize: new Point(400, 500), + text: string.Empty, + buttons: []); - foreach (ContentPackage cp in csharpMods) + GUILayoutGroup msgBoxLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.75f), messageBox.Content.RectTransform), isHorizontal: false, childAnchor: Anchor.TopCenter) { - if (cp.UgcId.TryUnwrap(out ContentPackageId id)) - { - sb.AppendLine($"- {cp.Name} ({id})"); - } - else + 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") { - sb.AppendLine($"- {cp.Name} (Not On Workshop)"); - } + OnClicked = (GUIButton button, object obj) => + { + string directory = package.Dir; + if (string.IsNullOrEmpty(directory)) { return false; } + + ToolBox.OpenFileWithShell(directory); + return true; + } + }; } - if (GameMain.Client == null || GameMain.Client.IsServerOwner) + 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) { - 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; - } + Wrap = true + }; - 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" }); + GUILayoutGroup buttonLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.25f), messageBox.Content.RectTransform, Anchor.BottomCenter), isHorizontal: false, childAnchor: Anchor.TopCenter); - msg.Buttons[0].OnClicked = (GUIButton button, object obj) => + new GUIButton(new RectTransform(new Vector2(0.8f, 0.0f), buttonLayout.RectTransform), "Enable C# for this session") { - Initialize(true); - msg.Close(); - return true; + TextBlock = { AutoScaleHorizontal = true }, + OnClicked = (btn, userdata) => + { + IsCsEnabledForSession = true; + onSelection(true); + messageBox.Close(); + return true; + } }; - msg.Buttons[1].OnClicked = (GUIButton button, object obj) => + new GUIButton(new RectTransform(new Vector2(0.8f, 0.0f), buttonLayout.RectTransform), "Cancel") { - Initialize(); - msg.Close(); - return true; + OnClicked = (btn, userdata) => + { + IsCsEnabledForSession = false; + onSelection(false); + messageBox.Close(); + return true; + } }; } + + private void SetupServicesProviderClient(IServicesProvider serviceProvider) + { + 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(() => + { + switch (screen) + { + // 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; + } + }, 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 0000000000..16a67e301b --- /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 0000000000..bb1af88a41 --- /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 0000000000..ade7f43055 --- /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 0000000000..fa3b2905ef --- /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 0000000000..6bf134d8ce --- /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 0000000000..2508d73761 --- /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 0000000000..5c1ff7e0d7 --- /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 0000000000..98a23dd6d0 --- /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 0000000000..906fa79715 --- /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 0000000000..3a8ec99d85 --- /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 0000000000..4d09f12848 --- /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 0000000000..14b6f58d4c --- /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 0000000000..9cfa2667e5 --- /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 0000000000..95c626c8fa --- /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 0000000000..1d5cb2d4d6 --- /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 129ef09584..49ab79cca6 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs @@ -16,15 +16,17 @@ private class RemoteDecal { 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 @@ public void ClientEventWrite(IWriteMessage msg, NetEntityEvent.IData extraData = 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 @@ public void ClientEventRead(IReadMessage msg, float sendingTime) 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 @@ private void ApplyRemoteState() 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 8202240aa2..9c145eca06 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs @@ -296,13 +296,9 @@ public void RenderLightMap(GraphicsDevice graphics, SpriteBatch spriteBatch, Cam 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 @@ float lightPriority(float range, LightSource light) 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 e54f37f21a..2912d88f2d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs @@ -50,8 +50,14 @@ public float Range [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 @@ public Vector2 Position 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 @@ public float Rotation 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 @@ public LightSource(ContentXElement element, ISerializableEntity conditionalTarge break; } } + //make sure the rotation defined in the parameters is taken into account + RefreshDirection(); + NeedsRecalculation = true; } public LightSource(LightSourceParams lightSourceParams) @@ -497,6 +510,9 @@ public LightSource(LightSourceParams lightSourceParams) { 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 @@ public LightSource(Vector2 position, float range, Color color, Submarine submari 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 @@ public void RayCastTask(Vector2 drawPos, float rotation) 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 @@ public void RayCastTask(Vector2 drawPos, float rotation) 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 @@ public void DrawLightVolume(SpriteBatch spriteBatch, BasicEffect lightEffect, Ma 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 e15d5bd484..4db180177b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs @@ -193,7 +193,12 @@ public GUIComponent CreateEditingHUD(bool inGame = false) { 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 c416d7a108..5e64723bf4 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 @@ private void ReadDataMessage(IReadMessage inc) { ServerPacketHeader header = (ServerPacketHeader)inc.ReadByte(); - GameMain.LuaCs.Networking.NetMessageReceived(inc, header); - if (roundInitStatus == RoundInitStatus.WaitingForStartGameFinalize && header is not ( ServerPacketHeader.STARTGAMEFINALIZE @@ -2905,8 +2904,6 @@ public bool HasConsoleCommandPermission(Identifier commandName) public void Quit() { - GameMain.LuaCs.Stop(); - ClientPeer?.Close(PeerDisconnectPacket.WithReason(DisconnectReason.Disconnected)); GUIMessageBox.MessageBoxes.RemoveAll(c => c?.UserData is RoundSummary); @@ -3006,7 +3003,8 @@ public void ShowMoneyTransferVoteInterface(Client starter, Client from, int amou 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 c0656420b9..96f56708a5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/LidgrenClientPeer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/LidgrenClientPeer.cs @@ -30,6 +30,13 @@ public LidgrenClientPeer(LidgrenEndpoint endpoint, Callbacks callbacks, Option - { - 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 ProjectEP 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 @@ public static void UpdateInstanceTutorialButtons() #region Selection public override void Select() { - GameMain.LuaCs.Stop(); - ResetModUpdateButton(); if (WorkshopItemsToUpdate.Any()) @@ -1044,7 +1028,7 @@ private void TryStartServer() else { StartServer(); - } + } } private IEnumerable WaitForSubmarineHashCalculations(GUIMessageBox messageBox) @@ -1095,7 +1079,7 @@ private void StartServer() "-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 @@ private void StartServer() 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 @@ private void StartGame(SubmarineInfo selectedSub, string savePath, string mapSee 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 @@ private void LoadGame(string path, Option backupIndex) { if (string.IsNullOrWhiteSpace(path)) return; - GameMain.LuaCs.CheckInitialize(); - try { CampaignDataPath dataPath = @@ -1392,7 +1379,7 @@ private void CreateHostServerFields() } 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 @@ private void CreateHostServerFields() 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 @@ private void FetchRemoteContent() 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 @@ private void RemoteContentReceived(Task t) 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 e3d878e817..3af215c5e6 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/ModDownloadScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/ModDownloadScreen.cs @@ -5,6 +5,7 @@ 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 @@ void mainLayoutSpacing() ContentPackageManager.EnabledPackages.SetRegular(regularPackages); } GameMain.NetLobbyScreen.Select(); - GameMain.LuaCs.CheckInitialize(); return; } @@ -366,7 +366,7 @@ CorePackage corePackage 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 @@ CorePackage corePackage } 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 @@ public void CurrentDownloadFinished(FileReceiver.FileTransferIn transfer) 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 37b5d6e5cd..72ccad2185 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs @@ -12,6 +12,7 @@ using System.Linq; using System.Threading; using System.Xml.Linq; +using Barotrauma.LuaCs.Events; using Barotrauma.Sounds; namespace Barotrauma @@ -733,6 +734,8 @@ private void CreateUI() 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 @@ private void CreateEntityElement(MapEntityPrefab ep, int entitiesPerRow, GUIComp public override void Select() { Select(enableAutoSave: true); - - GameMain.LuaCs.CheckInitialize(); } public void Select(bool enableAutoSave = true) @@ -3255,6 +3256,20 @@ var packToSaveInFilter = 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 @@ GUILayoutGroup addItemToPackageToSaveList(LocalizedString itemText, ContentPacka 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 @@ private void CreateLoadScreen() 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 @@ private void RenameLayer(string original, string newName) public void ReconstructLayers() { + Dictionary previousLayers = Layers.ToDictionary(); ClearLayers(); foreach (MapEntity entity in MapEntity.MapEntityList) { @@ -4496,6 +4514,13 @@ public void ReconstructLayers() 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 668b1bde84..22b885e50e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs @@ -26,6 +26,8 @@ sealed class SerializableEntityEditor : GUIComponent public static DateTime NextCommandPush; public static Tuple CommandBuffer; + private bool dimOutDefaultValues; + private bool isReadonly; public bool Readonly { @@ -316,16 +318,17 @@ public void UpdateValue(SerializableProperty property, object newValue, bool fla } } - 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 @@ public GUIComponent CreateNewField(SerializableProperty property, ISerializableE { 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 @@ public GUIComponent CreateBoolField(ISerializableEntity entity, SerializableProp tickBox.Selected = propertyValue; tickBox.Flash(Color.Red); } + UpdateTextColors(property, entity, tickBox); return true; } }; @@ -611,6 +673,7 @@ public GUIComponent CreateIntField(ISerializableEntity entity, SerializablePrope { TrySendNetworkUpdate(entity, property); } + UpdateTextColors(property, entity, frame); }; refresh += () => { @@ -654,6 +717,7 @@ public GUIComponent CreateFloatField(ISerializableEntity entity, SerializablePro { TrySendNetworkUpdate(entity, property); } + UpdateTextColors(property, entity, frame); }; HandleSetterValueTampering(numberInput, () => property.GetFloatValue(entity)); @@ -711,6 +775,7 @@ public GUIComponent CreateEnumField(ISerializableEntity entity, SerializableProp { TrySendNetworkUpdate(entity, property); } + UpdateTextColors(property, entity, frame); return true; }; refresh += () => @@ -829,6 +894,7 @@ bool OnApply(GUITextBox textBox) 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 @@ public GUIComponent CreatePointField(ISerializableEntity entity, SerializablePro { TrySendNetworkUpdate(entity, property); } + UpdateTextColors(property, entity, frame); }; fields[i] = numberInput; } @@ -1046,6 +1113,7 @@ public GUIComponent CreateVector2Field(ISerializableEntity entity, SerializableP { TrySendNetworkUpdate(entity, property); } + UpdateTextColors(property, entity, frame); }; HandleSetterValueTampering(numberInput, () => { @@ -1126,6 +1194,7 @@ public GUIComponent CreateVector3Field(ISerializableEntity entity, SerializableP { TrySendNetworkUpdate(entity, property); } + UpdateTextColors(property, entity, frame); }; fields[i] = numberInput; } @@ -1206,6 +1275,7 @@ public GUIComponent CreateVector4Field(ISerializableEntity entity, SerializableP { TrySendNetworkUpdate(entity, property); } + UpdateTextColors(property, entity, frame); }; fields[i] = numberInput; } @@ -1299,6 +1369,7 @@ public GUIComponent CreateColorField(ISerializableEntity entity, SerializablePro 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 @@ public GUIComponent CreateRectangleField(ISerializableEntity entity, Serializabl { TrySendNetworkUpdate(entity, property); } + UpdateTextColors(property, entity, frame); }; fields[i] = numberInput; } @@ -1437,6 +1509,7 @@ bool OnApply(GUITextBox textBox) 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 34ebe4aca6..537ad5b602 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Settings/SettingsMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Settings/SettingsMenu.cs @@ -33,10 +33,10 @@ public enum Tab 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 @@ private void SwitchContent(GUIFrame newContent) newContent.Visible = true; } - private readonly Dictionary tabContents; + public readonly Dictionary tabContents; public void SelectTab(Tab tab) { @@ -149,7 +149,7 @@ private GUIFrame CreateNewContentFrame(Tab tab) 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 @@ private static (GUILayoutGroup Left, GUILayoutGroup Right) CreateSidebars(GUIFra 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 @@ private static GUIDropDown Dropdown(GUILayoutGroup parent, 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 @@ private static (GUIScrollBar slider, GUITextBlock label) Slider(GUILayoutGroup p 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 @@ private static GUITickBox Tickbox(GUILayoutGroup parent, LocalizedString label, }; } - 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 @@ static void audioDeviceElement( 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 @@ public void Close() GUI.SettingsMenuOpen = false; } } -} \ No newline at end of file +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/SpamServerFilter.cs b/Barotrauma/BarotraumaClient/ClientSource/SpamServerFilter.cs index 2214848d48..47d3d1b580 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/SpamServerFilter.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/SpamServerFilter.cs @@ -387,13 +387,11 @@ public static void RequestGlobalSpamFilter() 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 @@ static void RemoteContentReceived(Task t) 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 cc1ef9c795..dc31dc47cf 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/Workshop.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/Workshop.cs @@ -18,18 +18,36 @@ public static partial class Workshop { 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 @@ public void Dispose() 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 91038dab50..3c7ec45b2d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/ItemList.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/ItemList.cs @@ -535,9 +535,9 @@ var tagsList = 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 a74a963923..20e0c832f1 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 67009978a0..cc877ab15a 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 43f2ab06c9..9bf9ca415f 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 0000000000..051269cc3c --- /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 18a89a450a..411d2e5ee1 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 fe5683a688..fb79c6495f 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 e173c7a456..5ab01aeee5 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Characters/Character.cs @@ -65,7 +65,7 @@ partial void KillProjSpecific(CauseOfDeathType causeOfDeath, Affliction causeOfD var owner = GameMain.Server.ConnectedClients.Find(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 59c91a4a5d..e176183439 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs @@ -320,11 +320,7 @@ public virtual void ServerEventRead(IReadMessage msg, Client c) 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); } } @@ -815,7 +811,7 @@ void TryWriteStatus(IWriteMessage msg) 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 901aef8b75..dff16fe001 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs @@ -10,6 +10,7 @@ using System.Text; using Barotrauma.Steam; using Barotrauma.Extensions; +using Barotrauma.LuaCs.Events; namespace Barotrauma { @@ -1287,45 +1288,6 @@ void CreateTraitorList(Action createMessage) 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_ep", "Installs Client-Side ProjectEP into your client.", (string[] args) => - { - LuaCsInstaller.Install(); - })); - */ - // Removed due to critical partical issues - // TODO: Partical manager requires a refactor to solve race condition - commands.Add(new Command("randomizeseed", "randomizeseed: Toggles level seed randomization on/off.", (string[] args) => { GameMain.Server.ServerSettings.RandomizeSeed = !GameMain.Server.ServerSettings.RandomizeSeed; diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/EventManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/EventManager.cs index 6850a51748..50e04783c4 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Events/EventManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/EventManager.cs @@ -79,6 +79,15 @@ public void ServerRead(IReadMessage inc, Client sender) 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 736c985424..836dc9f1cc 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/CombatMission.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/CombatMission.cs @@ -133,7 +133,7 @@ private void CheckWinCondition(float deltaTime) 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 516bbba8f6..d6ba7e9808 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs @@ -14,6 +14,7 @@ using System.Net; using Barotrauma.Extensions; using System.Threading.Tasks; +using Barotrauma.LuaCs.Events; namespace Barotrauma { @@ -35,8 +36,6 @@ public static World World set { world = value; } } - public static LuaCsSetup LuaCs; - public static GameServer Server; public static NetworkMember NetworkMember { @@ -113,6 +112,8 @@ public GameMain(string[] args) GameScreen = new GameScreen(); MainThread = Thread.CurrentThread; + + LuaCsSetup.Instance.GetType(); } public void Init() @@ -131,8 +132,6 @@ public void Init() NetLobbyScreen = new NetLobbyScreen(); CheckContentPackage(); - - LuaCs = new LuaCsSetup(); } @@ -244,6 +243,9 @@ public void StartServer() //handled in TryStartChildServerRelay i += 2; break; + case "-lenienthandshake": + NetConfig.UseLenientHandshake = true; + break; } } @@ -367,12 +369,16 @@ public void Run() TaskPool.Update(); CoroutineManager.Update(paused: false, (float)Timing.Step); - GameMain.LuaCs.Update(); performanceCounterTimer.Stop(); - if (GameMain.LuaCs.PerformanceCounter.EnablePerformanceCounter) + if (LuaCsSetup.Instance.PerformanceCounterService.EnablePerformanceCounter) + { + LuaCsSetup.Instance.PerformanceCounterService.AddElapsedTicks(new SimplePerformanceData("Update", performanceCounterTimer.ElapsedTicks)); + } + if (LuaCsSetup.Instance.PerformanceCounter.EnablePerformanceCounter) { - GameMain.LuaCs.PerformanceCounter.UpdateElapsedTime = (double)performanceCounterTimer.ElapsedTicks / Stopwatch.Frequency; + LuaCsSetup.Instance.PerformanceCounter.UpdateElapsedTime = (double)performanceCounterTimer.ElapsedTicks / Stopwatch.Frequency; } + performanceCounterTimer.Reset(); Timing.Accumulator -= Timing.Step; @@ -459,7 +465,17 @@ public CoroutineHandle ShowLoading(IEnumerable loader, bool wai 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 8d8fe2933a..5a6f1708c3 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Controller.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Controller.cs @@ -7,7 +7,7 @@ partial class Controller : ItemComponent, IServerSerializable 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 089fc03df5..f372957909 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Repairable.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Repairable.cs @@ -16,8 +16,11 @@ public override void OnMapLoaded() 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 3d85e08adf..38e09b975e 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Inventory.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Inventory.cs @@ -121,9 +121,9 @@ void HandleRemovedItems() 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 @@ void HandleRemovedItems() 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 @@ void HandleAddedItems() 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 0a66bbd082..216aeafa82 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs @@ -12,8 +12,6 @@ partial class Item : MapEntity, IDamageable, ISerializableEntity, IServerSeriali { 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 62289624fd..0000000000 --- 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 2e3a2fa0af..2ed47b6602 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,7 +10,7 @@ static partial class LuaCsInstaller { public static void Install() { - ContentPackage luaPackage = LuaCsSetup.GetPackage(LuaCsSetup.LuaForBarotraumaId); + ContentPackage luaPackage = LuaCsSetup.GetLuaCsPackage(); if (luaPackage == null) { @@ -45,8 +46,9 @@ public static void Install() 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) { diff --git a/Barotrauma/BarotraumaServer/ServerSource/LuaCs/LuaCsNetworking.cs b/Barotrauma/BarotraumaServer/ServerSource/LuaCs/LuaCsNetworking.cs deleted file mode 100644 index 62810ca569..0000000000 --- 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 0000000000..7c33a0f648 --- /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 0000000000..5bd32a8a17 --- /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 cfb0592cdd..705c06fa15 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Map/Hull.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Map/Hull.cs @@ -112,6 +112,7 @@ public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData e 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: @@ -251,7 +252,7 @@ public void ServerEventRead(IReadMessage msg, Client c) 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 c76385bf7f..0142259468 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 @@ public static void ServerRead(IReadMessage msg, Client c) 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 @@ public static void ServerRead(IReadMessage msg, Client c) 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 e1e7a5ba21..1600352947 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 @@ public void StartServer(bool registerToServerList) VoipServer = new VoipServer(serverPeer); - GameMain.LuaCs.Initialize(); Log("Server started", ServerLog.MessageType.ServerMessage); GameMain.NetLobbyScreen.Select(); @@ -339,8 +339,6 @@ private void OnInitializationComplete(NetworkConnection connection, string clien 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 @@ public void Update(float deltaTime) (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); @@ -837,8 +835,6 @@ private void ReadDataMessage(NetworkConnection sender, IReadMessage inc) using var _ = dosProtection.Start(connectedClient); ClientPacketHeader header = (ClientPacketHeader)inc.ReadByte(); - - GameMain.LuaCs.Networking.NetMessageReceived(inc, header, connectedClient); switch (header) { @@ -2314,7 +2310,6 @@ private void WriteClientList(in SegmentTableWriter segmentTabl segmentTable.StartNewSegment(ServerNetSegment.ClientList); outmsg.WriteUInt16(LastClientListUpdateID); - GameMain.LuaCs.Hook.Call("writeClientList", c, outmsg); outmsg.WriteByte((byte)Team1Count); outmsg.WriteByte((byte)Team2Count); @@ -2340,13 +2335,6 @@ private void WriteClientList(in SegmentTableWriter segmentTabl 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(); @@ -3199,7 +3187,7 @@ void AddCharacterToList(CharacterTeamType team, Character character) } TraitorManager.Initialize(GameMain.GameSession.EventManager, Level.Loaded); - if (GameMain.LuaCs.Game.overrideTraitors) + if (LuaCsSetup.Instance.Game.overrideTraitors) { TraitorManager.Enabled = false; } @@ -3228,8 +3216,6 @@ void AddCharacterToList(CharacterTeamType team, Character character) roundStartTime = DateTime.Now; - GameMain.LuaCs.Hook.Call("roundStart"); - startGameCoroutine = null; yield return CoroutineStatus.Success; } @@ -3402,15 +3388,6 @@ public void EndGame(CampaignMode.TransitionType transitionType = CampaignMode.Tr 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; @@ -3543,7 +3520,8 @@ private bool ReadClientNameChange(Client c, IReadMessage inc) 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) { @@ -3747,8 +3725,6 @@ public void DisconnectClient(Client client, PeerDisconnectPacket peerDisconnectP { if (client == null) return; - GameMain.LuaCs.Hook.Call("client.disconnected", client); - if (client.Character != null) { client.Character.ClientDisconnected = true; @@ -3997,21 +3973,29 @@ public void SendChatMessage(string message, ChatMessageType? type = null, Client 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) { @@ -4682,8 +4666,6 @@ void AssignJob(Client client, JobPrefab jobPrefab) $"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) @@ -4814,7 +4796,7 @@ public static void Log(string line, ServerLog.MessageType messageType) { 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); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/KarmaManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/KarmaManager.cs index 6bab9e4ecd..eb04424536 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/KarmaManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/KarmaManager.cs @@ -244,6 +244,8 @@ public void OnItemTakenFromPlayer(CharacterInventory inventory, Client yoinker, 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 @@ public void OnItemTakenFromPlayer(CharacterInventory inventory, Client yoinker, } Item foundItem = null; - if (isValid(item)) + if (IsValid(item)) { foundItem = item; } @@ -269,7 +271,7 @@ public void OnItemTakenFromPlayer(CharacterInventory inventory, Client yoinker, { foreach (Item containedItem in item.ContainedItems) { - if (isValid(containedItem)) + if (IsValid(containedItem)) { foundItem = containedItem; break; @@ -277,16 +279,19 @@ public void OnItemTakenFromPlayer(CharacterInventory inventory, Client yoinker, } } - static bool isValid(Item item) + static bool IsValid(Item item) + { + return item.GetComponent() != null || IsWeapon(item); + } + static bool IsWeapon(Item item) { - return item.GetComponent() != null || item.GetComponent() != null || item.GetComponent() != null; + //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 @@ static bool isValid(Item item) 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 e5cf2c9752..f43919d12b 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 @@ public LidgrenServerPeer(Option ownKey, ServerSettings settings, Callbacks 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 @@ private void HandleConnection(NetIncomingMessage inc) { 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 3e1a11c335..0003f1ef8c 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 @@ bool isAccountIdBanned(AccountId accountId, out string? banReason) 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 b83376d66b..907d39d742 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs @@ -18,7 +18,7 @@ private IEnumerable GetClientsToRespawn(CharacterTeamType teamId) 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 @@ private static int GetMinCharactersToRespawn() private bool ShouldStartRespawnCountdown(int characterToRespawnCount) { - if (GameMain.LuaCs.Game.overrideRespawnSub) + if (LuaCsSetup.Instance.Game.overrideRespawnSub) { characterToRespawnCount = 0; } @@ -187,7 +187,7 @@ partial void UpdateWaiting(TeamSpecificState teamSpecificState) 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 @@ public void DispatchShuttle(TeamSpecificState teamSpecificState) if (RespawnShuttles.Any()) { ResetShuttle(teamSpecificState); - if (GameMain.LuaCs.Game.overrideRespawnSub) + if (LuaCsSetup.Instance.Game.overrideRespawnSub) { teamSpecificState.CurrentState = State.Waiting; } @@ -596,6 +596,23 @@ private void RespawnCharacters(TeamSpecificState teamSpecificState) 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 4645a7a533..1e41951a58 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs @@ -46,7 +46,7 @@ public NetFlags UnsentFlags() .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 5349d2ea4e..5970af2bfc 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 @@ private static bool CanReceive(Client sender, Client recipient, out float distan 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 @@ private static bool CanReceive(Client sender, Client recipient, out float distan } } - 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 442b913156..f67ce80edf 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/PerformenceMonitor.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/PerformenceMonitor.cs @@ -39,7 +39,7 @@ public int PhysicsBodyCount } public int ConnectClients { - get { return Client.ClientList.Count; } + get { return GameMain.Server.ConnectedClients.Count; } } public double RealTickRate diff --git a/Barotrauma/BarotraumaServer/WindowsServer.csproj b/Barotrauma/BarotraumaServer/WindowsServer.csproj index 857c49007e..edf236deb8 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 0000000000..99fdf1f3f7 --- /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 524818c5f5..6ca382f632 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 eb94d18f57..a6a6a15626 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 68bfe97efe..1076e90f6d 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 1fde8456b5..db84691586 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 4e9608f602..564d39bc76 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 0000000000..08e139173c --- /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 0000000000..434b6f2f28 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 0000000000..25afd73df6 --- /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 0000000000..a364a50f3d --- /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 0000000000..2108b69ad4 --- /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 0000000000..4b149c0029 --- /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 0000000000..679f8d4c87 --- /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 f90685787b..bb24f0573b 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 0000000000..282a30b375 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 0000000000..f897a3c2bb --- /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 0000000000..76d1aaf546 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 0000000000..82378a936a --- /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 0a09be029d..10d816db39 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 0000000000..4b58a87c84 --- /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 fb1fd016ed..be951b04dc 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 0000000000..0c6c821fcd --- /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 0000000000..b5f96babda --- /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 0000000000..5c7e4458fe --- /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 0000000000..902b2a77be --- /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 0000000000..3e04dae6b3 --- /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 0000000000..a6c96ff2fc --- /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 0000000000..ba75a446c2 --- /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 0000000000..1fc37166a6 --- /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 0000000000..fecc60223a --- /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 0000000000..b1620c4198 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 0000000000..bc9dfb0f63 --- /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 bd4a5a0563..0000000000 --- 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 237e6445ec..0000000000 --- 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 6f074747be..0000000000 --- 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 0de67a7f46..0000000000 --- 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 fc5ee9b2c6..0000000000 --- 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 fc970c3b67..0000000000 --- a/Barotrauma/BarotraumaShared/Lua/LuaSetup.lua +++ /dev/null @@ -1,47 +0,0 @@ -LuaSetup = {} - -local path = table.pack(...)[1] - -package.path = {path .. "/?.lua"} - -setmodulepaths(package.path) - --- Setup Libraries -LuaSetup.LuaUserData = 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") - -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/ModLoader.lua b/Barotrauma/BarotraumaShared/Lua/ModLoader.lua deleted file mode 100644 index 828b562052..0000000000 --- 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 d18dbc685e..0000000000 --- a/Barotrauma/BarotraumaShared/Lua/PostSetup.lua +++ /dev/null @@ -1,13 +0,0 @@ -if not CSActive then - LuaUserDataIUUD = LuaUserData.RegisterType("Barotrauma.LuaSafeUserData") - LuaUserData = LuaUserData.CreateStatic("Barotrauma.LuaSafeUserData"); - - for k, v in pairs(debug) do - if k ~= "getmetatable" and k ~= "setmetatable" and k ~= "traceback" then - debug[k] = nil - end - end -end - -Descriptors = LuaUserData.__new() -LuaUserDataIUUD = nil \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/Luatrauma.props b/Barotrauma/BarotraumaShared/Luatrauma.props index cf9a48490a..cbcdb4bccb 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 0000000000..54a01bdadc --- /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 6ba44b3f5b..0000000000 --- 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 a4feacedde..2e03653179 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/AchievementManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/AchievementManager.cs @@ -551,9 +551,14 @@ GameMain.GameSession.Campaign is MultiPlayerCampaign && 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 6501c13339..8e005243bc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AITarget.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AITarget.cs @@ -49,6 +49,13 @@ public float SoundRange } } + + /// + /// 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; } @@ -206,6 +213,7 @@ public AITarget(Entity e, XElement element) : this(e) 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 49389c008c..eeab5c9b6b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs @@ -242,16 +242,44 @@ public override bool CanFlip } /// - /// 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 @@ public override void Update(float deltaTime) //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 @@ bool ShouldRetaliate(Character.Attacker a) } } + //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 @@ private void UpdateAttack(float deltaTime) return; } + if (Character.IsAttachedToController()) { return; } + attackWorldPos = SelectedAiTarget.WorldPosition; attackSimPos = SelectedAiTarget.SimPosition; @@ -1751,6 +1825,7 @@ bool IsBlocked(Vector2 targetPosition) { SelectTarget(door.Item.AiTarget, currentTargetMemory.Priority); State = AIState.Attack; + AttackLimb = null; return; } } @@ -1761,12 +1836,20 @@ bool IsBlocked(Vector2 targetPosition) 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 @@ float CalculatePriority(Limb limb, Vector2 attackPos) { 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 @@ private bool UpdateLimbAttack(float deltaTime, Vector2 attackSimPos, IDamageable { SelectTarget(aiTarget, GetTargetMemory(SelectedAiTarget, addIfNotFound: true).Priority); State = AIState.Attack; + AttackLimb = null; return true; } } @@ -2555,14 +2670,10 @@ private bool UpdateLimbAttack(float deltaTime, Vector2 attackSimPos, IDamageable 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 @@ private bool UpdateLimbAttack(float deltaTime, Vector2 attackSimPos, IDamageable } 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 @@ private bool Aim(float deltaTime, ISpatialEntity target, Item weapon) { 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 @@ private void UpdateEating(float deltaTime) } } 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 @@ public void UpdateTargets() } 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 @@ public void UpdateTargets() { 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 @@ public override bool Escape(float deltaTime) { 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 e84df22f73..320db7e273 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs @@ -1380,7 +1380,7 @@ private void RespondToAttack(Character attacker, AttackResult attackResult) } 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 60ad0799d1..5c74475559 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs @@ -4,6 +4,7 @@ using System; using System.Linq; using FarseerPhysics; +using System.Diagnostics; namespace Barotrauma { @@ -50,7 +51,7 @@ public bool IsPathDirty } /// - /// 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 @@ void SkipCurrentPathNodes() } } - 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 @@ private Vector2 DiffToCurrentNode() 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 @@ private Vector2 DiffToCurrentNode() { 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 @@ private Vector2 DiffToCurrentNode() 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 @@ private Vector2 DiffToCurrentNode() { if (nextLadder.Item.TryInteract(character, forceSelectKey: true)) { - NextNode(!doorsChecked); + return NextNode(!doorsChecked); } } } @@ -432,9 +445,9 @@ private Vector2 DiffToCurrentNode() } 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 @@ private Vector2 DiffToCurrentNode() } } } - 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 @@ private Vector2 DiffToCurrentNode() 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 @@ private Vector2 DiffToCurrentNode() { // 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 @@ private Vector2 DiffToCurrentNode() 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) + { + 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) { - // 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. + 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 @@ public bool CanAccessDoor(Door door, Func buttonFilter = null) } } - private Vector2 GetColliderSize() => ConvertUnits.ToDisplayUnits(character.AnimController.Collider.GetSize()); - private float GetColliderLength() { Vector2 colliderSize = character.AnimController.Collider.GetSize(); @@ -676,7 +750,7 @@ private void CheckDoorsInPath() 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 @@ private void CheckDoorsInPath() 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 @@ private void CheckDoorsInPath() } } - 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 @@ public void Wander(float deltaTime, float wallAvoidDistance = 150, bool stayStil //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 e27bf7a045..664813c08b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs @@ -103,9 +103,19 @@ public bool ForceHighestPriority } } - // 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 @@ protected virtual float GetPriority() /// 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 b5304e13c2..728645fa61 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItems.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItems.cs @@ -40,7 +40,7 @@ protected override float GetTargetPriority() 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 42e35c1b29..093cf34a25 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs @@ -258,13 +258,14 @@ public override void Update(float deltaTime) 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 @@ protected override bool CheckObjectiveState() { 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 @@ private void Move(float deltaTime) HumanAIController.AutoFaceMovement = false; if (!gotoObjective.ShouldRun(true)) { - ForceWalk = true; + ForceWalkTemporarily = true; } } } @@ -468,7 +470,7 @@ void BackOff() isMoving = true; if (!IsEnemyClose(MeleeDistance)) { - ForceWalk = true; + ForceWalkTemporarily = true; } HumanAIController.FaceTarget(Enemy); HumanAIController.AutoFaceMovement = false; @@ -1234,7 +1236,7 @@ private void Engage(float deltaTime) } 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 2f0f8ec712..c3fd166689 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveDeconstructItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveDeconstructItem.cs @@ -95,7 +95,11 @@ private Deconstructor FindDeconstructor() 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 781ed47465..391bbc4dd0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveDeconstructItems.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveDeconstructItems.cs @@ -64,7 +64,11 @@ protected override bool IsValidTarget(Item target) 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 53ec388806..0a57628a2c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFire.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFire.cs @@ -148,7 +148,7 @@ protected override void Act(float deltaTime) 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 af532273c5..c5790c08d8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFires.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFires.cs @@ -11,6 +11,8 @@ class AIObjectiveExtinguishFires : AIObjectiveLoop 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 94a876ca73..b2f0ae419d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs @@ -49,6 +49,13 @@ class AIObjectiveGetItem : AIObjective 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 @@ protected override void Act(float deltaTime) { if (isDoneSeeking) { - HandlePotentialItems(); + HandlePotentialItems(deltaTime); } if (objectiveManager.CurrentOrder is not AIObjectiveGoTo) { @@ -389,6 +396,8 @@ protected override void Act(float deltaTime) // 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 @@ private void FindTargetItem() } } - private void HandlePotentialItems() + private void HandlePotentialItems(float deltaTime) { Debug.Assert(isDoneSeeking); if (itemCandidates.Any()) @@ -652,10 +661,14 @@ private void HandlePotentialItems() } 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 @@ protected override bool CheckObjectiveState() 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 fd5ade4947..79786bceac 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs @@ -958,6 +958,7 @@ public override void Reset() 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 efa495d6cd..1a2d140486 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs @@ -267,7 +267,10 @@ protected override void Act(float deltaTime) 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 b8639dd08f..6439708bc5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveInspectNoises.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveInspectNoises.cs @@ -78,9 +78,10 @@ private void CheckEnemyNoises() 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 894f27e602..65d0e110a6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoadItems.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoadItems.cs @@ -112,7 +112,7 @@ protected override float GetTargetPriority() 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 83e475c370..23aea4234f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs @@ -103,10 +103,6 @@ public void AddObjective(AIObjective objective) 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 ad38f8cee9..04a9bafa45 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveOperateItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveOperateItem.cs @@ -257,6 +257,7 @@ protected override void Act(float deltaTime) { 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 6c1a7a37bb..7aa3d1e872 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePumpWater.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePumpWater.cs @@ -29,7 +29,7 @@ protected override bool IsValidTarget(Pump pump) { 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 42cbd4de65..8963eeb34e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescueAll.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescueAll.cs @@ -136,7 +136,7 @@ protected override void OnObjectiveCompleted(AIObjective objective, Character ta 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 879c4197f2..669bc3a0a1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AICharacter.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AICharacter.cs @@ -42,7 +42,9 @@ public override void Update(float deltaTime, Camera cam) { 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 316d3db5d3..db9322c490 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs @@ -685,7 +685,7 @@ void UpdateWalkAnim(float deltaTime) { 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 fb8754e550..3c3bd5c48e 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 @@ private void UpdateCPR(float deltaTime) //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 059844e6dc..3e11c6e7f8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs @@ -1,17 +1,18 @@ -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.Generic; using System.Linq; 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 { @@ -31,6 +32,7 @@ struct Impact { public Fixture F1, F2; public Vector2 LocalNormal; + public Vector2 WorldNormal; public Vector2 Velocity; public Vector2 ImpactPos; @@ -40,7 +42,7 @@ public Impact(Fixture f1, Fixture f2, Contact contact, Vector2 velocity) 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]; } } @@ -827,7 +829,7 @@ LimbStairCollisionResponse getStairCollisionResponse() 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; } @@ -839,7 +841,7 @@ private void ApplyImpact(Fixture f1, Fixture f2, Vector2 localNormal, Vector2 im return; } - Vector2 normal = localNormal; + Vector2 normal = worldNormal; float impact = Vector2.Dot(velocity, -normal); if (f1.Body == Collider.FarseerBody || !Collider.Enabled) { @@ -857,7 +859,8 @@ private void ApplyImpact(Fixture f1, Fixture f2, Vector2 localNormal, Vector2 im 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) { @@ -1077,9 +1080,12 @@ public void FindHull(Vector2? worldPosition = null, bool setSubmarine = true, bo } 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; } @@ -1122,7 +1128,10 @@ public void FindHull(Vector2? worldPosition = null, bool setSubmarine = true, bo { //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); } @@ -1259,6 +1268,9 @@ private void UpdateCollisionCategories() private float BodyInRestDelay = 1.0f; + /// + /// Controls the sleeping state of this character + /// public bool BodyInRest { get { return bodyInRestTimer > BodyInRestDelay; } @@ -1282,7 +1294,7 @@ public void UpdateRagdoll(float deltaTime, Camera cam) while (impactQueue.Count > 0) { var impact = impactQueue.Dequeue(); - ApplyImpact(impact.F1, impact.F2, impact.LocalNormal, impact.ImpactPos, impact.Velocity); + ApplyImpact(impact.F1, impact.F2, impact.WorldNormal, impact.ImpactPos, impact.Velocity); } CheckValidity(); @@ -1325,9 +1337,18 @@ public void UpdateRagdoll(float deltaTime, Camera cam) } 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) { @@ -1381,9 +1402,19 @@ public void UpdateRagdoll(float deltaTime, Camera cam) 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(); } @@ -1473,10 +1504,10 @@ public void UpdateRagdoll(float deltaTime, Camera cam) 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); @@ -1836,7 +1867,13 @@ private float GetFloorY(Vector2 simPosition, bool ignoreStairs = false) { 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) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs index 4312cd07c4..14a0327179 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs @@ -190,6 +190,11 @@ public bool DoesBleed 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 { @@ -686,6 +691,11 @@ public float Mass 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; } /// @@ -791,7 +801,24 @@ public Character SelectedCharacter 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 @@ -1433,8 +1460,6 @@ public static Character Create(CharacterPrefab prefab, Vector2 position, string } #endif - GameMain.LuaCs.Hook.Call("character.created", new object[] { newCharacter }); - return newCharacter; } @@ -1648,8 +1673,10 @@ protected Character(CharacterPrefab prefab, Vector2 position, string seed, Chara 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) @@ -1889,7 +1916,6 @@ public void GiveJobItems(bool isPvPMode, WayPoint spawnPoint = null) } } info.Job?.GiveJobItems(this, isPvPMode, spawnPoint); - GameMain.LuaCs.Hook.Call("character.giveJobItems", this, spawnPoint, isPvPMode); } public void GiveIdCardTags(WayPoint spawnPoint, bool createNetworkEvent = false) @@ -2275,6 +2301,12 @@ public void Control(float deltaTime, Camera cam) } } + // 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) @@ -2323,7 +2355,7 @@ public void Control(float deltaTime, Camera cam) { 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 @@ -2850,14 +2882,14 @@ public bool CanInteractWith(Item item, out float distanceToItem, bool checkLinke #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); @@ -2979,7 +3011,9 @@ public bool CanInteractWith(Item item, out float distanceToItem, bool checkLinke } } - 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); @@ -3008,7 +3042,6 @@ public bool CanInteractWith(Item item, out float distanceToItem, bool checkLinke { return itemCenterVisible; } - } return true; @@ -3098,7 +3131,11 @@ public void DoInteractionUpdate(float deltaTime, Vector2 mouseSimPos) if (!CanInteract) { - SelectedItem = SelectedSecondaryItem = null; + if (!IsAttachedToController()) + { + SelectedItem = null; + } + SelectedSecondaryItem = null; focusedItem = null; if (!AllowInput) { @@ -3117,8 +3154,16 @@ public void DoInteractionUpdate(float deltaTime, Vector2 mouseSimPos) { 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)) { @@ -3443,7 +3488,7 @@ public virtual void Update(float deltaTime, Camera cam) 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 @@ -3452,6 +3497,7 @@ public virtual void Update(float deltaTime, Camera cam) if (item.body == null || item.body.Enabled) { continue; } item.SetTransform(SimPosition, 0.0f, forceSubmarine: Submarine); } + lastInventoryItemSetTransformPosition = Position; } HideFace = false; @@ -3578,7 +3624,7 @@ public virtual void Update(float deltaTime, Camera cam) { 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); } @@ -3630,7 +3676,19 @@ bool bodyMovingTooFast(PhysicsBody body) 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; } @@ -3659,6 +3717,13 @@ bool bodyMovingTooFast(PhysicsBody body) 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) @@ -4384,6 +4449,41 @@ private void UpdateAIChatMessages(float deltaTime) } } + 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); @@ -4596,12 +4696,6 @@ public void RecordKill(Character target) 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); @@ -4774,6 +4868,10 @@ public void SetStun(float newStun, bool allowStunDecrease = false, bool isNetwor { 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; @@ -4801,7 +4899,12 @@ public void SetStun(float newStun, bool allowStunDecrease = false, bool isNetwor CharacterHealth.Stun = newStun; if (newStun > 0.0f) { - SelectedItem = SelectedSecondaryItem = null; + if (!IsAttachedToController()) + { + SelectedItem = null; + } + + SelectedSecondaryItem = null; if (SelectedCharacter != null) { DeselectCharacter(); } } HealthUpdateInterval = 0.0f; @@ -4990,6 +5093,37 @@ public void TurnIntoHusk(AfflictionPrefabHusk huskInfection = null, bool? playDe } } + 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; } @@ -5117,7 +5251,6 @@ static string GetCharacterType(Character character) AchievementManager.OnCharacterKilled(this, CauseOfDeath); } - GameMain.LuaCs.Hook.Call("character.death", this, causeOfDeathAffliction); KillProjSpecific(causeOfDeath, causeOfDeathAffliction, log); if (info != null) @@ -5128,7 +5261,7 @@ static string GetCharacterType(Character character) 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 93e4b1bf6c..68110740bf 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 2dbe048f56..1202d80f45 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs @@ -482,7 +482,6 @@ public virtual void Update(CharacterHealth characterHealth, Limb targetLimb, flo { 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 31abf6789d..37b2f35f5f 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; using Barotrauma.Extensions; using Microsoft.Xna.Framework; +using Barotrauma.LuaCs.Events; namespace Barotrauma { @@ -337,13 +338,13 @@ private IEnumerable CreateAIHusk() 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 cb36b11967..8008179fb6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs @@ -629,7 +629,7 @@ public PeriodicEffect(ContentXElement element, string parentDebugName) 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 13f91414ab..36b87f4bc9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs @@ -1,17 +1,19 @@ 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.Generic; using System.Globalization; +using System.Globalization; using System.Linq; using System.Xml.Linq; -using Barotrauma.Networking; -using Barotrauma.Extensions; -using System.Globalization; -using MoonSharp.Interpreter; -using Barotrauma.Abilities; +using static OneOf.Types.TrueFalseOrNull; namespace Barotrauma { @@ -657,7 +659,8 @@ public void ApplyDamage(Limb hitLimb, AttackResult attackResult, bool allowStack 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) @@ -828,10 +831,9 @@ private void AddLimbAffliction(LimbHealth limbHealth, Limb limb, Affliction newA 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) @@ -843,9 +845,21 @@ private void AddLimbAffliction(LimbHealth limbHealth, Limb limb, Affliction newA } } + 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 @@ -867,7 +881,7 @@ private void AddLimbAffliction(LimbHealth limbHealth, Limb limb, Affliction newA //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.Add(copyAffliction, limbHealth); AchievementManager.OnAfflictionReceived(copyAffliction, Character); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs index 680d292794..d3e69b401f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs @@ -190,7 +190,7 @@ public void InitializeCharacter(Character npc, ISpatialEntity positionToStayIn = 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 d9226ce751..e9f1d2f0a9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs @@ -1293,7 +1293,7 @@ public void ApplyStatusEffects(ActionType actionType, float deltaTime) 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 14c8e83c8f..675faa7771 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs @@ -728,7 +728,9 @@ public class AIParams : SubParam [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 e589401bc1..99b619886c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/CircuitBox/CircuitBoxInputOutputNode.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/CircuitBox/CircuitBoxInputOutputNode.cs @@ -77,8 +77,8 @@ private void InitSize(IReadOnlyList conns) } 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 e608d06bae..0e67a57ef5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/CharacterFile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/CharacterFile.cs @@ -106,9 +106,10 @@ public override void Preload(Action addPreloadedSprite) 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 2bee7b3c14..12967f1252 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackage.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackage.cs @@ -199,9 +199,16 @@ public static Result TryLoad(string path) 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 79ce7be3ff..50cf39e5bb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackageManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackageManager.cs @@ -9,8 +9,10 @@ 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 @@ public static class BackupPackages 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 @@ public static void EnableRegular(RegularPackage p) } public static void SetRegular(IReadOnlyList newRegular) - => SetRegularEnumerable(newRegular).Consume(); + { + SetRegularEnumerable(newRegular).Consume(); + } public static IEnumerable SetRegularEnumerable(IReadOnlyList inNewRegular) { @@ -583,6 +590,11 @@ public static void CheckMissingDependencies() 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 @@ public static void LogEnabledRegularPackageErrors() } } } -} \ No newline at end of file +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPath.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPath.cs index 0d9a4a1128..2661aef1dd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPath.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPath.cs @@ -99,12 +99,13 @@ public static ContentPath FromRaw(string? rawValue) 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 @@ public override int GetHashCode() 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 05444468b0..17b09b461f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs @@ -2311,6 +2311,8 @@ IEnumerable TestLevels(string fixedSeed = null, int? amount = n 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 @@ void ParseOptionalArgs(out Vector2 spawnPosition, out WayPoint spawnPoint, out C 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 b42f1131c8..2810223159 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Decals/Decal.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Decals/Decal.cs @@ -34,11 +34,12 @@ public float LifeTime 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 79a25913ae..3f2cb74c60 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Enums.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Enums.cs @@ -131,6 +131,14 @@ public enum ActionType /// 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 08abda9ed4..1c2cc3a871 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Event.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Event.cs @@ -12,7 +12,11 @@ class Event 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 04d517c2ff..b1391bd64c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckConditionalAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckConditionalAction.cs @@ -29,6 +29,9 @@ class CheckConditionalAction : BinaryOptionAction [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 @@ static bool IsConditionalAttribute(XAttribute attribute) 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 f6ff09a033..bc40eab162 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 @@ public enum DialogTypes [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 @@ public Character Speaker 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 @@ public ConversationAction(ScriptedEvent parentEvent, ContentXElement element) : { 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 @@ public override void Reset() interrupt = false; dialogOpened = false; Speaker = null; +#if CLIENT + dialogBox?.Close(); + dialogBox = null; +#endif } /// @@ -292,6 +335,7 @@ public override void Update(float deltaTime) if (dialogOpened) { lastActiveTime = Timing.TotalTime; + #if CLIENT if (GUIMessageBox.MessageBoxes.Any(mb => mb.UserData as string == "ConversationAction")) { @@ -350,7 +394,7 @@ public override void Update(float deltaTime) } else { - TryStartConversation(null); + TryStartConversation(Speaker); } } else @@ -467,11 +511,26 @@ private void TryStartConversation(Character speaker, Character targetCharacter = 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 13d3b68597..ba10a1f7e2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CountTargetsAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CountTargetsAction.cs @@ -99,6 +99,7 @@ public CountTargetsAction(ScriptedEvent parentEvent, ContentXElement element) : 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 f1a54e7425..bf96cb2865 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/EventAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/EventAction.cs @@ -9,14 +9,7 @@ abstract class EventAction { 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 @@ public EventAction CurrentSubAction } } - 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 0000000000..1e5b933a23 --- /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 a6dd612bef..92ddc1ee3b 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 a specific active mission. The way the states are used depends on the type of mission. - /// - class MissionStateAction : EventAction +/// Changes the state of missions. The way the states are used depends on the type of mission. +internal sealed 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; } - - public enum OperationType - { - Set, - Add - } + /// Sets the missions' states to . + Set, + /// Adds to the missions' states. + Add + } - [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; } + [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(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; } + [Serialize(OperationType.Set, IsPropertySaveable.Yes, "The operation to perform on missions' states.")] + public OperationType Operation { get; set; } - [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; } + [Serialize(0, IsPropertySaveable.Yes, "The value to apply to missions' states.")] + public int State { get; set; } - private bool isFinished; + [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) + public MissionStateAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) + { + State = element.GetAttributeInt("value", State); + if (Operation == OperationType.Add && State == 0 && !ForceFailure) { - 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); - } + 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); } + } - public override bool IsFinished(ref string goTo) - { - return isFinished; - } - public override void Reset() - { - isFinished = false; - } + private bool isFinished; + public override bool IsFinished(ref string goTo) => isFinished; + public override void Reset() => isFinished = false; - public override void Update(float deltaTime) - { - if (isFinished) { return; } + public override void Update(float deltaTime) + { + if (isFinished) { return; } + if (!MissionIdentifier.IsEmpty) + { 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); + } + + isFinished = true; + } - public override string ToDebugString() + 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 968e8c9889..ed8fa1590e 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 @@ class NPCFollowAction : EventAction [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 @@ public override void Update(float deltaTime) 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 3b28cd1863..6accd385e5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs @@ -271,6 +271,10 @@ HumanPrefab TryFindHumanPrefab(Faction faction) 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 01009f1dba..195ff9fe6b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs @@ -240,42 +240,45 @@ void AddSet(EventSet eventSet) 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) + { + 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) { - //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) + foreach (var eventTag in outpost.Info.TriggerOutpostMissionEvents) { - var unlockPathEventPrefab = EventPrefab.GetUnlockPathEvent(level.LevelData.Biome.Identifier, level.StartLocation.Faction); - if (unlockPathEventPrefab != null) + EventPrefab eventPrefab = EventPrefab.FindEventPrefab(identifier: Identifier.Empty, tag: eventTag, outpost.ContentPackage); + if (eventPrefab == null) { - var newEvent = unlockPathEventPrefab.CreateInstance(RandomSeed); - activeEvents.Add(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 09703cf055..16d635b698 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AbandonedOutpostMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AbandonedOutpostMission.cs @@ -233,7 +233,7 @@ protected override void UpdateMissionSpecific(float deltaTime) } - 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 f9b102a877..70fc86b007 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/BeaconMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/BeaconMission.cs @@ -171,7 +171,7 @@ protected override void UpdateMissionSpecific(float deltaTime) #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 f62efa2eff..4768696f58 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CargoMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CargoMission.cs @@ -331,7 +331,7 @@ protected override void StartMissionSpecific(Level level) } } - 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 64bf201c7f..6b45f0ef97 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CombatMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CombatMission.cs @@ -204,7 +204,7 @@ protected override void StartMissionSpecific(Level level) } } - 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 0000000000..d18349dbd1 --- /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 cc7855701c..1bfeaea1f7 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 @@ private bool AllTargetsEliminated() 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 1ffc09b93d..e68af0349c 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 @@ protected override void UpdateMissionSpecific(float deltaTime) 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 473006b60b..3a770fffaa 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/EscortMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/EscortMission.cs @@ -343,7 +343,7 @@ private static bool IsAlive(Character character) 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 a1924db58d..c35e7b789e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/GoToMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/GoToMission.cs @@ -17,7 +17,7 @@ protected override void UpdateMissionSpecific(float deltaTime) } } - protected override bool DetermineCompleted() + protected override bool DetermineCompleted(CampaignMode.TransitionType transitionType) { if (Level.Loaded?.Type == LevelData.LevelType.Outpost) { @@ -25,7 +25,7 @@ protected override bool DetermineCompleted() } 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 10d1741702..d75e61fa62 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 @@ partial class MineralMission : Mission } } + /// + /// 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 @@ protected override void UpdateMissionSpecific(float deltaTime) } } - 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 e6629022be..6dcfd314fc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs @@ -401,14 +401,9 @@ protected virtual Character LoadMonster(CharacterPrefab monsterPrefab, XElement { 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 @@ private void TriggerEvent(MissionPrefab.TriggerEvent trigger) 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 @@ private void TriggerEvent(MissionPrefab.TriggerEvent trigger) /// /// 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 @@ public void End() } } - 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 cd32310eb5..28c5e4a26f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs @@ -30,7 +30,8 @@ internal sealed partial class MissionPrefab : PrefabWithUintIdentifier, IImpleme { "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 @@ public ReputationReward(XElement element) 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 @@ LocalizedString GetText(string textTag, string textTagPrefix) 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 @@ public override void Dispose() } /// - /// 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 @@ public static IEnumerable GetAllMultiplayerSelectableMissionTypes() 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 941b7c4dd9..656764b14f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MonsterMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MonsterMission.cs @@ -242,7 +242,7 @@ protected override void UpdateMissionSpecific(float deltaTime) } } - 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 3ad3effe18..41dbb356a8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/NestMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/NestMission.cs @@ -337,7 +337,7 @@ private bool AllItemsDestroyedOrRetrieved() 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 6331311af2..6d7f895ff9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/PirateMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/PirateMission.cs @@ -547,7 +547,7 @@ private static bool DeadOrCaptured(Character character) 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 602e4dd0d8..e0a7aa4af4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs @@ -715,7 +715,7 @@ void TrySetRetrievalState(Target.RetrievalState retrievalState) } } - 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 e388b29087..a16c556fd5 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 @@ partial class ScanMission : Mission 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 @@ partial class ScanMission : Mission 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 @@ protected override void StartMissionSpecific(Level level) 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 @@ protected override void UpdateMissionSpecific(float deltaTime) 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 129fa3d25e..6edea36730 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameAnalytics/GameAnalyticsConsent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameAnalytics/GameAnalyticsConsent.cs @@ -104,7 +104,7 @@ private static async Task GetSteamAuthTicket() 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 @@ private static async Task SendAnswerToRemoteDatabase(Consent consent) 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 @@ static void error(string reason, Exception? exception) RestClient client; try { - client = new RestClient(consentServerUrl); + client = RestFactory.CreateClient(consentServerUrl); } catch (Exception e) { @@ -329,7 +328,7 @@ static void error(string reason, Exception? exception) 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 daa20659aa..ae18a5a210 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs @@ -1017,7 +1017,7 @@ public void HandleSaveAndQuit() 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 e00bf540be..d19250e5e6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MissionMode.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MissionMode.cs @@ -53,6 +53,7 @@ protected static IEnumerable ValidateMissionPrefabs(IEnumerable ValidateMissionTypes(IEnumerable 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 f3782f134a..bd31f59c36 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 @@ public static bool ShouldApplyDisembarkPoints(GameModePreset? preset) public void LoadPreviousSave() { - GameMain.LuaCs.Hook.Call("roundEnd"); AchievementManager.OnRoundEnded(this, roundInterrupted: true); Submarine.Unload(); SaveUtil.LoadGame(DataPath); @@ -761,7 +761,6 @@ public void StartRound(LevelData? levelData, bool mirrorLevel = false, Submarine GUI.PreventPauseMenuToggle = false; HintManager.OnRoundStarted(); - GameMain.LuaCs.Hook.Call("roundStart"); EnableEventLogNotificationIcon(enabled: false); LogStartRoundStats(); @@ -955,14 +954,6 @@ public static void PlaceSubAtInitialPosition(Submarine? sub, Level? level, bool 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 @@ public static void PlaceSubAtInitialPosition(Submarine? sub, Level? level, bool 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 @@ public void EnforceMissionOrder(List missionIdentifiers) /// 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 @@ public void EndRound(string endMessage, CampaignMode.TransitionType transitionTy { RoundEnding = true; -#if CLIENT - GameMain.LuaCs.Hook.Call("roundEnd"); -#endif //Clear the grids to allow for garbage collection Powered.Grids.Clear(); Powered.ChangedConnections.Clear(); @@ -1103,15 +1098,13 @@ public void EndRound(string endMessage, CampaignMode.TransitionType transitionTy 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) { @@ -1208,12 +1201,12 @@ int GetAmountOfMoney(IEnumerable crew) } } - 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 98b9bd60a7..146f6b603d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs @@ -43,8 +43,12 @@ public enum AccessLevel 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 @@ public CharacterInventory(ContentXElement element, Character character, bool spa 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 @@ public int FindLimbSlot(InvSlotType limbSlot) 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 @@ public bool TryPutItemWithAutoEquipCheck(Item item, Character user, IEnumerable< /// /// 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 @@ public override bool TryPutItem(Item item, Character user, IEnumerable -1; } - - public bool IsAnySlotAvailable(Item item) => GetFreeAnySlot(item, inWrongSlot: false) > -1; private int GetFreeAnySlot(Item item, bool inWrongSlot) @@ -542,7 +571,7 @@ private int GetFreeAnySlot(Item item, bool inWrongSlot) 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 @@ public override bool TryPutItem(Item item, int index, bool allowSwapping, bool a 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 dacf4103a5..75155b00cb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs @@ -739,6 +739,12 @@ public override bool OnPicked(Character picker) { 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 d4d45f1206..84a2a1b192 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 @@ private bool OnCollision(Fixture f1, Fixture f2, Contact contact) } 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 @@ private void HandleImpact(Fixture targetFixture) 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 8e156bb549..5ef03bfe30 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs @@ -380,7 +380,7 @@ public ItemComponent(Item item, ContentXElement element) break; case "requireditem": case "requireditems": - SetRequiredItems(subElement); + SetRequiredItems(subElement, allowEmpty: true); break; case "requiredskill": case "requiredskills": @@ -1102,6 +1102,9 @@ public virtual XElement Save(XElement parentElement) 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 ae7016391f..c105aee808 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs @@ -425,7 +425,7 @@ public int GetMaxStackSize(int slotIndex) 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 @@ public void OnItemContained(Item containedItem) 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; + } } } } @@ -1119,6 +1125,16 @@ private Vector2 GetContainedPosition(bool drawPosition, } 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 0000000000..6faa6db36b --- /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 899258e4fd..9e2d344fe4 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; namespace Barotrauma.Items.Components @@ -45,6 +48,39 @@ partial class Controller : ItemComponent, IServerSerializable 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; @@ -55,11 +91,6 @@ public Vector2 UserPos 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)] @@ -123,6 +154,13 @@ public bool HideHUD 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 @@ -152,6 +190,49 @@ public bool AllowSelectingWhenSelectedByBot 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; } @@ -204,6 +285,23 @@ public bool ForceUserToStayAttached 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) { @@ -211,6 +309,18 @@ public Controller(Item item, ContentXElement element) 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"); + } } /// @@ -236,58 +346,77 @@ public override void Update(float deltaTime, Camera cam) 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; } } @@ -295,10 +424,10 @@ public override void Update(float deltaTime, Camera cam) { // 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) { @@ -308,48 +437,48 @@ public override void Update(float deltaTime, Camera cam) 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) @@ -357,12 +486,12 @@ public override void Update(float deltaTime, Camera cam) 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; } } @@ -374,15 +503,77 @@ public override void Update(float deltaTime, Camera cam) } } + 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 @@ -394,13 +585,13 @@ private bool CheckUserCanInteract() 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; } @@ -419,7 +610,7 @@ public override bool Use(float deltaTime, Character activator = null) } 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; @@ -428,13 +619,13 @@ public override bool Use(float deltaTime, Character activator = null) 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) @@ -495,7 +686,7 @@ public Item GetFocusTarget() 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); for (int i = item.LastSentSignalRecipients.Count - 1; i >= 0; i--) { @@ -547,8 +738,20 @@ public override bool Pick(Character picker) 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); @@ -588,31 +791,80 @@ public override bool Select(Character activator) 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 }) + { + return false; + } + + // 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; + CancelUsing(User); + User = null; return false; } - else if (user.IsBot && !activator.IsBot) + 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) { @@ -621,15 +873,12 @@ public override bool Select(Character activator) } } - //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; } @@ -639,7 +888,7 @@ public override bool Select(Character activator) /// public bool IsAttachedUser(Character character) { - return character != null && character == user && ForceUserToStayAttached; + return character != null && character == User && ForceUserToStayAttached; } public override void FlipX(bool relativeToSub) @@ -668,12 +917,87 @@ public override void FlipY(bool relativeToSub) } } + 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 cd08a541a5..c9ab5d0ad8 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 @@ public override void Update(float deltaTime, Camera cam) 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 @@ public override void Update(float deltaTime, Camera cam) 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 @@ private void ProcessItem(Item targetItem, IEnumerable inputItems, List("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 @@ void OnCombinedOrRefined() } } } + inputContainer.Inventory.RemoveItem(targetItem); Entity.Spawner.AddItemToRemoveQueue(targetItem); MoveInputQueue(); @@ -384,6 +393,34 @@ void tryPutInOutputSlots(Item item) } } + 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 @@ private void PutItemsToLinkedContainer() } } + 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 52df296964..6676545aed 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs @@ -388,6 +388,13 @@ private void Launch(Character user, Vector2 simPosition, float rotation, float d { 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. @@ -460,6 +467,7 @@ public bool Use(Character character = null, float launchImpulseModifier = 0f) { initialRotation -= MathHelper.Pi; } + Submarine initialSubmarine = item.Submarine; for (int i = 0; i < HitScanCount; i++) { float launchAngle; @@ -476,6 +484,8 @@ public bool Use(Character character = null, float launchImpulseModifier = 0f) 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 03b727bc38..758b29585e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Quality.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Quality.cs @@ -25,7 +25,7 @@ public enum StatType 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 1aebf8187c..6ec4fd5a2f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs @@ -448,9 +448,10 @@ public override void Update(float deltaTime, Camera cam) 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 e149298046..58b1152e68 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Connection.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Connection.cs @@ -27,6 +27,11 @@ public LocalizedString DisplayName private init => _displayName = value; } + /// + /// Display name ignoring + /// + public LocalizedString DefaultDisplayName => _displayName; + public LocalizedString DisplayNameOverride; private readonly HashSet wires; @@ -350,15 +355,11 @@ public void SendSignal(Signal signal) 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) { 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) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs index e0104a2131..bccb65f42e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs @@ -296,6 +296,7 @@ public override void OnMapLoaded() } #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 e59ab65999..9d2104d73f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WifiComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WifiComponent.cs @@ -1,10 +1,13 @@ -using Barotrauma.Networking; +using Barotrauma.LuaCs.Events; +using Barotrauma.Networking; using Microsoft.Xna.Framework; using System; 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 { @@ -228,8 +231,8 @@ public void SetChannelMemory(int index, int value) 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 d5d7678c59..9da0bcfba3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs @@ -42,6 +42,10 @@ partial class Turret : Powered, IDrawableComponent, IServerSerializable 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 @@ public void UpdateAutoOperate(float deltaTime, bool ignorePower, Identifier frie 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 @@ public void UpdateAutoOperate(float deltaTime, bool ignorePower, Identifier frie } 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 @@ void CheckRemainingAmmo() // 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 d1be5145e8..54c9a5f90d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs @@ -569,16 +569,16 @@ public virtual int HowManyCanBePut(ItemPrefab itemPrefab, int i, float? conditio /// /// 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)) { @@ -609,14 +609,14 @@ public virtual bool TryPutItem(Item item, int i, bool allowSwapping, bool allowC //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) @@ -642,7 +642,7 @@ public virtual bool TryPutItem(Item item, int i, bool allowSwapping, bool allowC } } - 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)) { @@ -651,11 +651,6 @@ protected virtual void PutItem(Item item, int i, Character user, bool removeItem 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; @@ -765,11 +760,6 @@ protected bool TrySwapping(int index, Item item, Character user, bool createNetw 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; @@ -951,7 +941,8 @@ void TryPutAndForce(IEnumerable items, Inventory inventory, int slotIndex) { 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 83b869ba41..bb3149d6bc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs @@ -1,20 +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 Barotrauma.Extensions; -using Barotrauma.MapCreatures.Behavior; -using MoonSharp.Interpreter; -using System.Collections.Immutable; -using Barotrauma.Abilities; +using static Barotrauma.CharacterHealth; +using static Barotrauma.MedicalClinic; #if CLIENT using Microsoft.Xna.Framework.Graphics; @@ -263,6 +266,8 @@ bool HasInGameEditableProperties /// public Character Equipper; + public Inventory PreviousParentInventory { get; set; } + //the inventory in which the item is contained in public Inventory ParentInventory { @@ -278,9 +283,7 @@ public Inventory ParentInventory Container = parentInventory.Owner as Item; RemoveFromDroppedStack(allowClientExecute: false); } -#if SERVER PreviousParentInventory = value; -#endif } } @@ -561,6 +564,8 @@ public float PositionUpdateInterval 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 @@ -1103,7 +1108,7 @@ private set public override string ToString() { - return Name + " (ID: " + ID + ")"; + return (Name.IsNullOrEmpty() ? Prefab.Identifier : Name) + " (ID: " + ID + ")"; } private readonly List allPropertyObjects = new List(); @@ -1411,8 +1416,6 @@ public Item(Rectangle newRect, ItemPrefab itemPrefab, Submarine submarine, bool 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() @@ -1793,7 +1796,8 @@ public override void Move(Vector2 amount, bool ignoreContacts = true) 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) @@ -2073,6 +2077,12 @@ public bool ConditionalMatches(PropertyConditional conditional, bool checkContai } } + 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. /// @@ -3258,7 +3268,9 @@ public bool TryInteract(Character user, bool ignoreRequiredItems = false, bool f 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)) || @@ -3364,10 +3376,6 @@ public void Use(float deltaTime, Character user = null, Limb targetLimb = null, } 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; @@ -3400,11 +3408,6 @@ public void SecondaryUse(float deltaTime, Character character = null) { 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) @@ -3902,9 +3905,9 @@ private void ReadPropertyChange(IReadMessage msg, bool inGameEditableOnly, Clien } } - 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 = ""; @@ -4370,6 +4373,9 @@ private void Replace(ItemPrefab replacement, Option newId, bool createEn Rotation = Rotation }; + if (FlippedX) { newItem.FlipX(relativeToSub: false); } + if (FlippedY) { newItem.FlipY(relativeToSub: false); } + float scaleRelativeToPrefab = Scale / Prefab.Scale; newItem.Scale *= scaleRelativeToPrefab; @@ -4621,8 +4627,6 @@ public override void ShallowRemove() body.Remove(); body = null; } - - GameMain.LuaCs.Hook.Call("item.removed", this); } public override void Remove() @@ -4707,8 +4711,6 @@ public override void Remove() } 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 1a8cf3cc1e..29b914960c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemInventory.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemInventory.cs @@ -88,9 +88,9 @@ public override bool IsFull(bool takeStacksIntoAccount = false) 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 @@ public override bool TryPutItem(Item item, Character user, IEnumerable 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 @@ private void ParseConfigElement(ItemPrefab variantOf) var storePrices = new Dictionary(); var preferredContainers = new List(); DeconstructTime = 1.0f; + DeconstructTimeInOutposts = DeconstructTime; if (ConfigElement.GetAttribute("allowasextracargo") != null) { @@ -1174,6 +1177,7 @@ private void ParseConfigElement(ItemPrefab variantOf) 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 2bb9cb5872..cad423406a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/RelatedItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/RelatedItem.cs @@ -398,7 +398,7 @@ private bool CheckContained(Item parentItem) } 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 0000000000..074b04c2ed --- /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 0000000000..00349d44fb --- /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 0000000000..dbe6c4916a --- /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 0000000000..bcf5a3ebcb --- /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 0000000000..22b7414880 --- /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 0000000000..8f119b106e --- /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 0000000000..9fe824d0e2 --- /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 0000000000..3ab6fafed3 --- /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 0000000000..06598ab2d4 --- /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 77c18a8876..807b30ecca 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 @@ public void SetHookElapsedTicks(string eventName, string hookName, long ticks) 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 0000000000..b6186231d4 --- /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 0000000000..56d192dd45 --- /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 0000000000..2326ecce24 --- /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 0000000000..696683af9b --- /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 0000000000..28e9a6e1ab --- /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 0000000000..652ddd9675 --- /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 0000000000..c8740e51d6 --- /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 0000000000..ba315464f0 --- /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 0000000000..a0da6263b7 --- /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 0000000000..d82349eea3 --- /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 0000000000..2601e1826b --- /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 0000000000..f9f9826dee --- /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 0000000000..5b1cacd232 --- /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 0000000000..5c080f5bab --- /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 0000000000..f2c1909b9b --- /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 0000000000..8e705c4d69 --- /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 0000000000..ca52dab778 --- /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 357c02bb2a..0000000000 --- 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/LuaSafeUserData.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Lua/LuaClasses/LuaSafeUserData.cs deleted file mode 100644 index e4d91d5c11..0000000000 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Lua/LuaClasses/LuaSafeUserData.cs +++ /dev/null @@ -1,198 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Linq; -using System.Reflection; -using MoonSharp.Interpreter; -using MoonSharp.Interpreter.Interop; - -namespace Barotrauma -{ - partial class LuaSafeUserData - { - public IUserDataDescriptor this[string index] - { - get => LuaUserData.Descriptors.GetValueOrDefault(index); - } - - private static 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.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 static 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; - } - - private static bool IsAllowed(string typeName) - { - if (!CanBeReRegistered(typeName) && LuaUserData.IsRegistered(typeName)) - { - return false; - } - - if (!CanBeRegistered(typeName) && !LuaUserData.IsRegistered(typeName)) - { - return false; - } - - return true; - } - - private static void CheckAllowed(string typeName) - { - if (!IsAllowed(typeName)) - { - throw new ScriptRuntimeException($"Type {typeName} can't be registered"); - } - } - - public static Type GetType(string typeName) - { - CheckAllowed(typeName); - - return LuaUserData.GetType(typeName); - } - - public static IUserDataDescriptor RegisterType(string typeName) - { - CheckAllowed(typeName); - - return LuaUserData.RegisterType(typeName); - } - - public static IUserDataDescriptor RegisterTypeBarotrauma(string typeName) - { - return RegisterType($"Barotrauma.{typeName}"); - } - - public static void RegisterExtensionType(string typeName) - { - CheckAllowed(typeName); - LuaUserData.RegisterExtensionType(typeName); - } - - public static bool IsRegistered(string typeName) - { - return LuaUserData.IsRegistered(typeName); - } - - public static void UnregisterType(string typeName, bool deleteHistory = false) - { - LuaUserData.UnregisterType(typeName, deleteHistory); - } - public static IUserDataDescriptor RegisterGenericType(string typeName, params string[] typeNameArguements) - { - CheckAllowed(typeName); - return LuaUserData.RegisterGenericType(typeName, typeNameArguements); - } - - public static void UnregisterGenericType(string typeName, params string[] typeNameArguements) - { - LuaUserData.UnregisterGenericType(typeName, typeNameArguements); - } - - public static bool IsTargetType(object obj, string typeName) - { - return LuaUserData.IsTargetType(obj, typeName); - } - - public static string TypeOf(object obj) - { - return LuaUserData.TypeOf(obj); - } - - public static object CreateStatic(string typeName) - { - CheckAllowed(typeName); - return LuaUserData.CreateStatic(typeName); - } - - public static object CreateEnumTable(string typeName) - { - return LuaUserData.CreateEnumTable(typeName); - } - - public static void MakeFieldAccessible(IUserDataDescriptor IUUD, string fieldName) - { - LuaUserData.MakeFieldAccessible(IUUD, fieldName); - } - - public static void MakeMethodAccessible(IUserDataDescriptor IUUD, string methodName, string[] parameters = null) - { - LuaUserData.MakeMethodAccessible(IUUD, methodName, parameters); - } - - public static void MakePropertyAccessible(IUserDataDescriptor IUUD, string propertyName) - { - LuaUserData.MakePropertyAccessible(IUUD, propertyName); - } - - public static void AddMethod(IUserDataDescriptor IUUD, string methodName, object function) - { - LuaUserData.AddMethod(IUUD, methodName, function); - } - - public static void AddField(IUserDataDescriptor IUUD, string fieldName, DynValue value) - { - LuaUserData.AddField(IUUD, fieldName, value); - } - - public static void RemoveMember(IUserDataDescriptor IUUD, string memberName) - { - LuaUserData.RemoveMember(IUUD, memberName); - } - - public static bool HasMember(object obj, string memberName) - { - return LuaUserData.HasMember(obj, memberName); - } - - public static void AddCallMetaTable(object userdata) { } - - public static DynValue CreateUserDataFromDescriptor(DynValue scriptObject, IUserDataDescriptor desiredTypeDescriptor) - { - return LuaUserData.CreateUserDataFromDescriptor(scriptObject, desiredTypeDescriptor); - } - - public static DynValue CreateUserDataFromType(DynValue scriptObject, Type desiredType) - { - return LuaUserData.CreateUserDataFromType(scriptObject, desiredType); - } - } -} 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 959ee31a9f..0000000000 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Lua/LuaClasses/LuaUserData.cs +++ /dev/null @@ -1,391 +0,0 @@ -using MoonSharp.Interpreter; -using MoonSharp.Interpreter.Interop; -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Linq; -using System.Reflection; - -namespace Barotrauma -{ - partial class LuaUserData - { - public static ReadOnlyDictionary Descriptors => new ReadOnlyDictionary(descriptors); - private static ConcurrentDictionary descriptors = new ConcurrentDictionary(); - - public IUserDataDescriptor this[string index] - { - get => Descriptors.GetValueOrDefault(index); - } - - 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}."); - } - - var descriptor = UserData.RegisterType(type); - descriptors.TryAdd(typeName, descriptor); - - return descriptor; - } - - public static IUserDataDescriptor RegisterTypeBarotrauma(string typeName) - { - return RegisterType($"Barotrauma.{typeName}"); - } - - 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); - var result = generic.Invoke(null, null); - - return result; - } - - 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); - } - - public static void AddCallMetaTable(object userdata) { } - - - public static void Clear() - { - descriptors.Clear(); - } - } -} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Lua/LuaScriptLoader.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Lua/LuaScriptLoader.cs deleted file mode 100644 index 212c70ae56..0000000000 --- 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 ee5c43e83a..002b059b32 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/LuaCsInstaller.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/LuaCsInstaller.cs @@ -9,10 +9,19 @@ static partial class LuaCsInstaller { 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 @@ static partial class LuaCsInstaller "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 a8816c5959..0000000000 --- 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 cdd77e17dd..0000000000 --- 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 845f905597..0000000000 --- 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 05ea25af16..03c2501b92 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/LuaCsSetup.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/LuaCsSetup.cs @@ -1,549 +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(3629459376); -#elif LINUX - public static ContentPackageId LuaForBarotraumaId = new SteamWorkshopId(3632586273); -#elif OSX - public static ContentPackageId LuaForBarotraumaId = new SteamWorkshopId(3672543756); -#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 + #endregion + + #region Services_CVars + + /* + * === 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 { - get + get => _isCsEnabledForSession?.Value ?? false; + internal set { -#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 + _isCsEnabledForSession?.TrySetValue(value); + if (_isCsEnabledForSession != null) + { + if (_isCsEnabledForSession.GetConfigInfo() == null) + { + Logger.LogError($"Config info was nil while trying to save {IsCsEnabledForSession}"); + return; + } + ConfigService.SaveConfigValue(_isCsEnabledForSession); + } } } - private static int executionNumber = 0; - + /// + /// 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 + } - public Script Lua { get; private set; } - public LuaScriptLoader LuaScriptLoader { get; private set; } + private ISettingList _csRunPolicy; - 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; } + public string CsRunPolicyValue => _csRunPolicy?.Value ?? "Prompt"; - // must be available at anytime - private static AssemblyManager _assemblyManager; - public static AssemblyManager AssemblyManager => _assemblyManager ??= new AssemblyManager(); - - private CsPackageManager _pluginPackageManager; - public CsPackageManager PluginPackageManager => _pluginPackageManager ??= new CsPackageManager(AssemblyManager, this); + /// + /// Whether usernames are anonymized or show in logs. + /// + public bool HideUserNamesInLogs + { + get => _hideUserNamesInLogs?.Value ?? false; + internal set => _hideUserNamesInLogs?.TrySetValue(value); + } + private ISettingBase _hideUserNamesInLogs; - 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; } + public bool UseCaching + { + get => _useCaching?.Value ?? true; + } + private ISettingBase _useCaching; - private bool ShouldRunCs + 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() { - get + 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)) { -#if SERVER - if (GetPackage(CsForBarotraumaId, false, false) != null && GameMain.Server.ServerPeer is LidgrenServerPeer) { return true; } -#endif - - return Config.EnableCsScripting; + // sorry perfidius (not sorry) + luaCsPackage.UnloadFilesOfType(); + luaCsPackage.LoadFilesOfType(); } } - - public LuaCsSetup() + + private IServicesProvider SetupServicesProvider() { - Script.GlobalOptions.Platform = new LuaPlatformAccessor(); - - Hook = new LuaCsHook(this); - ModStore = new LuaCsModStore(); - - Game = new LuaGame(); - Networking = new LuaCsNetworking(); - DebugServer = new MoonSharpVsCodeDebugServer(); + 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); - ReadSettings(); +#if CLIENT + SetupServicesProviderClient(servicesProvider); +#endif + + // gen IL + servicesProvider.CompileAndRun(); + return servicesProvider; } - [Obsolete("Use AssemblyManager::GetTypesByName()")] - public static Type GetType(string typeName, bool throwOnError = false, bool ignoreCase = false) + #endregion + + #region StateMachine + + private RunState _runState; + /// + /// The current run state of all services managed by LuaCs. + /// + public RunState CurrentRunState { - return AssemblyManager.GetTypesByName(typeName).FirstOrDefault((Type)null); + get => _runState; + private set => _runState = value; } + + private readonly StateMachine _runStateMachine; - public void ToggleDebugger(int port = 41912) + public void OnEnabledPackageListChanged(CorePackage package, IEnumerable regularPackages) { - if (!GameMain.LuaCs.DebugServer.IsStarted) - { - DebugServer.Start(); - AttachDebugger(); - - LuaCsLogger.Log($"Lua Debug Server started on port {port}."); - } - else - { - DetachDebugger(); - DebugServer.Stop(); - - LuaCsLogger.Log($"Lua Debug Server stopped."); - } + ProcessEnabledPackageChanges(new []{ package }.Concat(regularPackages).ToImmutableArray()); } - public void AttachDebugger() + public void OnReloadAllPackages() { - DebugServer.AttachToScript(Lua, "Script", s => + CoroutineManager.Invoke(() => { - if (s.Name.StartsWith("LocalMods") || s.Name.StartsWith("Lua")) + SetRunState(RunState.Unloaded); + CoroutineManager.Invoke(() => { - return Environment.CurrentDirectory + "/" + s.Name; - } - return s.Name; + 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; + } + + var state = CurrentRunState; + if (CurrentRunState > RunState.LoadedNoExec) + { + SetRunState(RunState.LoadedNoExec); } + + this.Logger.LogResults(PackageManagementService.SyncLoadedPackagesList(GetLuaCsEnabledPackagesList(packages))); + ConfigService.LoadSavedConfigsValues(); + SetRunState(state); // restore } - - public void WriteSettings() + + public void SetRunState(RunState targetRunState) { - 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); + if (CurrentRunState == targetRunState) + { + return; + } + _runStateMachine.GotoState(targetRunState); } - public static ContentPackage GetPackage(ContentPackageId id, bool fallbackToAll = true, bool useBackup = false) + private ImmutableArray GetEnabledPackagesList() + => GetLuaCsEnabledPackagesList(ContentPackageManager.EnabledPackages.Regular + .ToImmutableArray()); + + private ImmutableArray GetLuaCsEnabledPackagesList(ImmutableArray enabledRegular) { - foreach (ContentPackage package in ContentPackageManager.EnabledPackages.All) + if (!enabledRegular.Any(p => p.Name.Equals(PackageName, StringComparison.InvariantCultureIgnoreCase))) { - if (package.UgcId.ValueEquals(id)) + var luaCs = ContentPackageManager.AllPackages.FirstOrDefault(p => p.Name.Equals(PackageName, StringComparison.InvariantCultureIgnoreCase)); + if (luaCs is null) { - return package; + 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); - if (fallbackToAll) + // ReSharper disable InconsistentNaming + void RunStateUnloaded_OnEnter(State currentState) { - foreach (ContentPackage package in ContentPackageManager.LocalPackages) - { - if (package.UgcId.ValueEquals(id)) - { - return package; - } - } + Logger.LogMessage("LuaCs unloaded state entered"); - foreach (ContentPackage package in ContentPackageManager.AllPackages) + if (PackageManagementService.IsAnyPackageRunning()) { - if (package.UgcId.ValueEquals(id)) - { - return package; - } + Logger.LogResults(PackageManagementService.StopRunningPackages()); } - } - if (useBackup && ContentPackageManager.EnabledPackages.BackupPackages.Regular != null) - { - foreach (ContentPackage package in ContentPackageManager.EnabledPackages.BackupPackages.Regular.Value) + if (PackageManagementService.IsAnyPackageLoaded()) { - if (package.UgcId.ValueEquals(id)) - { - return package; - } + DisposeLuaCsConfig(); + Logger.LogResults(PackageManagementService.UnloadAllPackages()); } - } - return null; - } + EventService.Reset(); + ConfigService.Reset(); + LuaScriptManagementService.Reset(); + PackageManagementService.Reset(); + NetworkingService.Reset(); + Game.Reset(); + _servicesProvider.GetService().Reset(); - 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."); - } + Logger.LogMessage("Services have been reset"); - if (!LuaCsFile.Exists(file)) - { - throw new ScriptRuntimeException($"dofile: File {file} not found."); - } - - return Lua.DoFile(file, globalContext, codeStringFriendly); - } + SubscribeToLuaCsEvents(); - 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."); + CurrentRunState = RunState.Unloaded; } - if (!LuaCsFile.Exists(file)) + void RunStateLoadedNoExec_OnEnter(State currentState) { - throw new ScriptRuntimeException($"loadfile: File {file} not found."); - } - - return Lua.LoadFile(file, globalContext, codeStringFriendly); - } + Logger.LogMessage("LuaCs no execution state entered"); - 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 + if (PackageManagementService.IsAnyPackageRunning()) { - return Lua.Call(function, args); + Logger.LogResults(PackageManagementService.StopRunningPackages()); } - catch (Exception e) + + if (!PackageManagementService.IsAnyPackageLoaded()) { - LuaCsLogger.HandleException(e, LuaCsMessageOrigin.LuaMod); + foreach (var registrationProvider in _servicesProvider.GetAllServices()) + { + registrationProvider.RegisterTypeProviders(ConfigService, null); + } + Logger.LogResults(PackageManagementService.LoadPackagesInfo(GetEnabledPackagesList())); + Logger.LogResults(ConfigService.LoadSavedConfigsValues()); + LoadLuaCsConfig(); } - return null; + + CurrentRunState = RunState.LoadedNoExec; } - } + + void RunStateRunning_OnEnter(State currentState) + { + if (!PackageManagementService.IsAnyPackageLoaded()) + { + foreach (var registrationProvider in _servicesProvider.GetAllServices()) + { + registrationProvider.RegisterTypeProviders(ConfigService, null); + } + Logger.LogResults(PackageManagementService.LoadPackagesInfo(GetEnabledPackagesList())); + Logger.LogResults(ConfigService.LoadSavedConfigsValues()); + LoadLuaCsConfig(); + } - private void SetModulePaths(string[] str) - { - LuaScriptLoader.ModulePaths = str; - } + string csEnabled = IsCsEnabled ? "enabled" : "disabled"; + Logger.LogMessage($"LuaCs running state entered. Running under commit {AssemblyInfo.GitRevision}, CSharp is {csEnabled}"); - public void Update() - { - Timer?.Update(); - Steam?.Update(); + if (!PackageManagementService.IsAnyPackageRunning()) + { + Logger.LogResults(PackageManagementService.ExecuteLoadedPackages(GetEnabledPackagesList(), IsCsEnabled)); + } #if CLIENT - Stopwatch luaSw = new Stopwatch(); - luaSw.Start(); -#endif - Hook?.Call("think"); -#if CLIENT - luaSw.Stop(); - GameMain.PerformanceCounter.AddElapsedTicks("Think Hook", luaSw.ElapsedTicks); + // 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 - } - public void Stop() - { - PluginPackageManager.UnloadPlugins(); - - // unregister types - foreach (Type type in AssemblyManager.GetAllLoadedACLs().SelectMany( - acl => acl.AssembliesTypes.Select(kvp => kvp.Value))) - { - UserData.UnregisterType(type, true); - } +#if SERVER + GameMain.Server.ServerSettings.LoadClientPermissions(); +#endif - if (Lua?.Globals is not null) - { - Lua.Globals.Remove("CsPackageManager"); - Lua.Globals.Remove("AssemblyManager"); + CurrentRunState = RunState.Running; } - if (Thread.CurrentThread == GameMain.MainThread) - { - Hook?.Call("stop"); - } - if (Lua != null && DebugServer.IsStarted) + void RunStateRunning_OnExit(State currentState) { - DebugServer.Detach(Lua); + EventService.Call("stop"); + Logger.LogResults(PackageManagementService.StopRunningPackages()); + Logger.LogMessage("LuaCs running state exited"); } - - LuaUserData.Clear(); - - 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; + // ReSharper restore InconsistentNaming } - public void Initialize(bool forceEnableCs = false) + + + + + #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() { - if (IsInitialized) + try { - Stop(); + SetRunState(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(); - var uuid = 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["LuaUserDataIUUD"] = uuid; - 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) + catch (Exception e) { - AttachDebugger(); + Logger.LogError(e.Message); } - if (csActive) + try { - LuaCsLogger.LogMessage("Cs! Version " + AssemblyInfo.GitRevision); - - UserData.RegisterType(); - UserData.RegisterType(); - UserData.RegisterType(); - - Lua.Globals["PluginPackageManager"] = PluginPackageManager; - Lua.Globals["AssemblyManager"] = AssemblyManager; + DisposeLuaCsConfig(); - try - { - Stopwatch taskTimer = new(); - taskTimer.Start(); - ModStore.Clear(); - - var state = PluginPackageManager.LoadAssemblyPackages(); - if (state is AssemblyLoadingSuccessState.Success or AssemblyLoadingSuccessState.AlreadyLoaded) - { - 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(); - } - } - catch (Exception e) - { - ModUtils.Logging.PrintError($"{nameof(LuaCsSetup)}::{nameof(Initialize)}() | Error while loading assemblies! Details: {e.Message} | {e.StackTrace}"); - } - } - - - 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(); } + 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(); } - else + catch (Exception e) { - if (File.Exists(LuaSetupFile)) { RunLocal(); } - else if (luaPackage != null) { RunWorkshop(); } - else { RunNone(); } + Console.WriteLine(e); + throw; } -#if SERVER - GameMain.Server.ServerSettings.LoadClientPermissions(); -#endif + _luaCsSetup = null; + + GC.SuppressFinalize(this); + } - executionNumber++; + /// + /// 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 3354449e37..0000000000 --- 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 089cea715c..9e667cf09f 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 + { + 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 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 + 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 - LuaCsLogger.LogMessage($"[Server] {s}"); + return GameMain.Server.ConnectedClients; #else - LuaCsLogger.LogMessage($"[Client] {s}"); + return GameMain.Client.ConnectedClients; #endif + } + } } - public static void PrintWarning(string s) + public static class Definitions { -#if SERVER - LuaCsLogger.Log($"[Server] {s}", Color.Yellow); + public const string LuaCsForBarotrauma = nameof(LuaCsForBarotrauma); + } + + public static class Environment + { + 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 + Platform.Linux; +#endif + + public static readonly Target CurrentTarget = +#if CLIENT + Target.Client; +#elif SERVER + Target.Server; #else - LuaCsLogger.Log($"[Client] {s}", Color.Yellow); + Target.Server; #endif + } - public static void PrintError(string s) + #region LOGGING + + public static class Logging { + public static void PrintMessage(string s) + { #if SERVER - LuaCsLogger.LogError($"[Server] {s}"); + LuaCsSetup.Instance.Logger.LogMessage($"{s}"); #else - LuaCsLogger.LogError($"[Client] {s}"); + LuaCsSetup.Instance.Logger.LogMessage($"{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 + public static void PrintWarning(string s) { - return Directory.GetFiles(folder, pattern, option); +#if SERVER + LuaCsSetup.Instance.Logger.Log($"{s}", Color.Yellow); +#else + LuaCsSetup.Instance.Logger.Log($"{s}", Color.Yellow); +#endif } - catch (DirectoryNotFoundException e) + + public static void PrintError(string s) { - return new string[] { }; +#if SERVER + LuaCsSetup.Instance.Logger.LogError($"{s}"); +#else + LuaCsSetup.Instance.Logger.LogError($"{s}"); +#endif } } - public static string PrepareFilePathString(string filePath) => - PrepareFilePathString(Path.GetDirectoryName(filePath)!, Path.GetFileName(filePath)); + #endregion - public static string PrepareFilePathString(string path, string fileName) => - Path.Combine(SanitizePath(path), SanitizeFileName(fileName)); + #region FILE_IO - public static string SanitizeFileName(string fileName) + // ReSharper disable once InconsistentNaming + public static class IO { - foreach (char c in Barotrauma.IO.Path.GetInvalidFileNameCharsCrossPlatform()) - fileName = fileName.Replace(c, '_'); - return fileName; - } + public static IEnumerable FindAllFilesInDirectory(string folder, string pattern, + SearchOption option) + { + try + { + return Directory.GetFiles(folder, pattern, option); + } + catch (DirectoryNotFoundException e) + { + return new string[] { }; + } + } - /// - /// 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 PrepareFilePathString(string filePath) => + PrepareFilePathString(Path.GetDirectoryName(filePath)!, Path.GetFileName(filePath)); - public static string SanitizePath(string path) - { - foreach (char c in Path.GetInvalidPathChars()) - path = path.Replace(c.ToString(), "_"); - return path.CleanUpPath(); - } + public static string PrepareFilePathString(string path, string fileName) => + Path.Combine(SanitizePath(path), SanitizeFileName(fileName)); - public static IOActionResultState GetOrCreateFileText(string filePath, out string fileText, Func fileDataFactory = null, bool createFile = true) - { - fileText = null; - string fp = Path.GetFullPath(SanitizePath(filePath)); + public static string SanitizeFileName(string fileName) + { + foreach (char c in Barotrauma.IO.Path.GetInvalidFileNameCharsCrossPlatform()) + fileName = fileName.Replace(c, '_'); + return fileName; + } - IOActionResultState ioActionResultState = IOActionResultState.Success; - if (createFile) + /// + /// Gets the sanitized path for the top-level directory for a given content package. + /// + /// + /// + public static string GetContentPackageDir(ContentPackage package) { - ioActionResultState = CreateFilePath(SanitizePath(filePath), out fp, fileDataFactory); + return SanitizePath(Path.GetFullPath(package.Dir)); } - else if (!File.Exists(fp)) + + public static string SanitizePath(string path) { - return IOActionResultState.FileNotFound; + 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; } - if (ioActionResultState == IOActionResultState.Success) + 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 { - fileText = File.ReadAllText(fp!); + 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: {fp ?? "null"} | Exception Details: {ane.Message}"); + 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: {fp ?? "null"} | Exception Details: {ae.Message}"); + 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: {fp ?? "null"} | Exception Details: {dnfe.Message}"); + 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: {fp ?? "null"} | Exception Details: {ptle.Message}"); + 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: {fp ?? "null"} | Exception Details: {nse.Message}"); + 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: {fp ?? "null"} | Exception Details: {ioe.Message}"); + 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: {fp ?? "null"} | ExceptionMessage: {e.Message}"); + ModUtils.Logging.PrintError( + $"ModUtils::CreateFilePath() | Exception: Unknown/Other Exception. path: {path ?? "null"} | Exception Details: {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) + public static IOActionResultState WriteFileText(string filePath, string fileText) { - ModUtils.Logging.PrintError($"ModUtils::CreateFilePath() | Exception: An argument is null. path: {formattedFilePath ?? "null"} | Exception Details: {ane.Message}"); - return IOActionResultState.FilePathNull; + 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; } - catch (ArgumentException ae) + + /// + /// + /// + /// + /// + /// + /// + /// + /// + public static bool LoadOrCreateTypeXml(out T instance, + string filepath, Func typeFactory = null, bool createFile = true) where T : class, new() { - ModUtils.Logging.PrintError($"ModUtils::CreateFilePath() | Exception: An argument is invalid. path: {formattedFilePath ?? "null"} | Exception Details: {ae.Message}"); - return IOActionResultState.FilePathInvalid; + 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; } - catch (DirectoryNotFoundException dnfe) + + public enum IOActionResultState { - ModUtils.Logging.PrintError($"ModUtils::CreateFilePath() | Exception: Cannot find directory. path: {path ?? "null"} | Exception Details: {dnfe.Message}"); - return IOActionResultState.DirectoryMissing; + Success, + FileNotFound, + FilePathNull, + FilePathInvalid, + DirectoryMissing, + PathTooLong, + InvalidOperation, + IOFailure, + UnknownError } - catch (PathTooLongException ptle) + } + + #endregion + + #region GAME + + public static class Game + { + /// + /// Returns whether or not there is a round running. + /// + /// + public static bool IsRoundInProgress() { - ModUtils.Logging.PrintError($"ModUtils::CreateFilePath() | Exception: path length is over 200 characters. path: {formattedFilePath ?? "null"} | Exception Details: {ptle.Message}"); - return IOActionResultState.PathTooLong; +#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; } - catch (NotSupportedException nse) + + } + + #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) { - ModUtils.Logging.PrintError($"ModUtils::CreateFilePath() | Exception: Operation not supported on your platform/environment (permissions?). path: {formattedFilePath ?? "null"} | Exception Details: {nse.Message}"); - return IOActionResultState.InvalidOperation; + if (value) + { + Interlocked.CompareExchange(ref var, 1, 0); + } + else + { + Interlocked.CompareExchange(ref var, 0, 1); + } } - catch (IOException ioe) + + /// + /// 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) { - ModUtils.Logging.PrintError($"ModUtils::CreateFilePath() | Exception: IO tasks failed (Operation not supported). path: {formattedFilePath ?? "null"} | Exception Details: {ioe.Message}"); - return IOActionResultState.IOFailure; + return Interlocked.CompareExchange(ref var, 1, 0) < 1; } - catch (Exception e) + + /// + /// 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) { - ModUtils.Logging.PrintError($"ModUtils::CreateFilePath() | Exception: Unknown/Other Exception. path: {path ?? "null"} | Exception Details: {e.Message}"); - return IOActionResultState.UnknownError; + return Interlocked.CompareExchange(ref var, 0, 1) > 0; } } - - public static IOActionResultState WriteFileText(string filePath, string fileText) + + #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) { - IOActionResultState ioActionResultState = CreateFilePath(filePath, out var fp); - if (ioActionResultState == IOActionResultState.Success) + // Based on https://github.com/Qkrisi/ktanemodkit/blob/master/Assets/Scripts/ReflectionHelper.cs#L53-L67 + + try + { + return assembly.GetTypes(); + } + catch (ReflectionTypeLoadException re) { 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; + return re.Types.Where(x => x != null)!; } - catch (Exception e) + catch (InvalidOperationException) { - ModUtils.Logging.PrintError($"ModUtils::WriteFileText() | Exception: Unknown/Other Exception. path: {fp ?? "null"} | ExceptionMessage: {e.Message}"); - return IOActionResultState.UnknownError; + return new List(); } } - - return ioActionResultState; + catch (Exception) + { + return new List(); + } } + } + public static class CollectionExtensions + { /// - /// + /// Executes a series of asynchronous tasks with limited parallelism to maintain execution efficiency. /// - /// - /// - /// - /// + /// + /// + /// /// /// - 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 +#region ExceptionData - public static class Game +namespace FluentResults.LuaCs +{ + public static class MetadataType { + public static string ExceptionDetails = nameof(ExceptionDetails); /// - /// Returns whether or not there is a round running. + /// The object that threw the exception. /// - /// - 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; - } - + 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 } + +#endregion diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Plugins/ApplicationMode.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Plugins/ApplicationMode.cs deleted file mode 100644 index 6e60184bb7..0000000000 --- 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 e55821eb3d..0000000000 --- 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 c7f582395a..0000000000 --- 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 8ba1f8921c..0000000000 --- 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 5a450ba744..0000000000 --- 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 dd61c0108f..0000000000 --- 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 0000000000..81143ffce1 --- /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 0000000000..809a2f9c1b --- /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 0000000000..62ce88f267 --- /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 0000000000..441f6e8bc4 --- /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 0000000000..a13e0a1909 --- /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 0000000000..a3b2157c26 --- /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 76dfac73f2..1a189e31b1 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 @@ public static string GetStoreFolder() where T : ACsMod 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 @@ public virtual void Dispose() } 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 0000000000..9b964cf650 --- /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 0000000000..f02f5c89bd --- /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 0000000000..7667c86cdf --- /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 64bc660061..d5283156a0 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 0000000000..b6a4732ec5 --- /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 0000000000..242938a16f --- /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 0000000000..22eec4a693 --- /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 0000000000..fe54d9096d --- /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 0000000000..1c09be0e19 --- /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 0000000000..25e5ab0b2e --- /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 0000000000..99032700db --- /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 0000000000..798a9a2492 --- /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 0000000000..e1dc53c7a4 --- /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 0000000000..d7278c72b6 --- /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 0000000000..42511ed019 --- /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 0000000000..14b0d141f7 --- /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 0000000000..30210fc6fd --- /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 0000000000..1c42ebcd49 --- /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 0000000000..983a13b32b --- /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 0000000000..8bfad23f21 --- /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 0000000000..8ef819f267 --- /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 0000000000..a55fd85065 --- /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 0000000000..dc5d8ab19b --- /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 0000000000..09d5719625 --- /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 0000000000..2d4bad4680 --- /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 0000000000..867742321e --- /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 0000000000..23e623fc28 --- /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 0000000000..7f2c3ed6ef --- /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 0000000000..57a8a02513 --- /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 0000000000..b5ee2cfc58 --- /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 0000000000..847c610206 --- /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 0000000000..007cd6ffe5 --- /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 0000000000..568e660786 --- /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 0000000000..a26c49ff63 --- /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 0000000000..35cf1dfc47 --- /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 0000000000..e3e4428cc5 --- /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 0000000000..78fd7c8ba6 --- /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 0000000000..29ad120453 --- /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 0000000000..3a50e3dc26 --- /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 0000000000..44e35bbdc0 --- /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 0000000000..29b851fc2a --- /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 0000000000..4e3b49a83a --- /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 0000000000..3e6ed307ce --- /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 0000000000..0a7447fe1b --- /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 0000000000..c0b11ad49a --- /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 0000000000..f2c1b91628 --- /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 0000000000..3d385a9d30 --- /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 0000000000..cca1d482d0 --- /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 0000000000..becb543330 --- /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 87% rename from Barotrauma/BarotraumaShared/SharedSource/LuaCs/Lua/LuaConverters.cs rename to Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/_Lua/LuaClasses/LuaConverters.cs index e2c038ebeb..ab53a5538d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Lua/LuaConverters.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/_Lua/LuaClasses/LuaConverters.cs @@ -5,12 +5,22 @@ 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(); @@ -24,41 +34,40 @@ private void RegisterLuaConverters() 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))); @@ -125,6 +134,22 @@ private void RegisterLuaConverters() 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 => @@ -228,7 +253,7 @@ private void RegisterLuaConverters() RegisterImmutableArray(); } - private void RegisterImmutableArray() + private static void RegisterImmutableArray() { Script.GlobalOptions.CustomConverters.SetScriptToClrCustomConversion(DataType.Table, typeof(ImmutableArray), v => { @@ -236,7 +261,7 @@ private void RegisterImmutableArray() }); } - private void RegisterEither() + private static void RegisterEither() { DynValue convertEitherIntoDynValue(Either either) { @@ -274,7 +299,7 @@ DynValue convertEitherIntoDynValue(Either either) }); } - private void RegisterOption(DataType dataType) + private static void RegisterOption(DataType dataType) { Script.GlobalOptions.CustomConverters.SetClrToScriptCustomConversion(typeof(Option), (Script v, object obj) => { @@ -305,13 +330,13 @@ private void RegisterAction() 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)); }); } @@ -320,13 +345,13 @@ private void RegisterAction() 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)); }); } @@ -335,13 +360,13 @@ private void RegisterAction() 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 0000000000..d46b6ec27b --- /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 0000000000..8d900e4600 --- /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 a88f7c5b5a..d888c45971 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 @@ public TimedAction(LuaCsAction action, int delayMs) 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,7 +86,24 @@ private void AddTimer(TimedAction timedAction) } } - public void Update() + public void Clear() + { + timedActions = new List(); + } + + public void Wait(LuaCsAction action, int millisecondDelay) + { + TimedAction timedAction = new TimedAction(action, millisecondDelay); + AddTimer(timedAction); + } + + public void NextFrame(LuaCsAction action) + { + TimedAction timedAction = new TimedAction(action, 0); + AddTimer(timedAction); + } + + public void OnUpdate(double fixedDeltaTime) { lock (timedActions) { @@ -89,7 +119,7 @@ public void Update() } catch (Exception e) { - LuaCsLogger.HandleException(e, LuaCsMessageOrigin.CSharpMod); + _loggerService.HandleException(e); } timedActions.Remove(timedAction); @@ -102,21 +132,22 @@ public void Update() } } - public void Clear() + private void SubscribeToEvents() { - timedActions = new List(); + _eventService.Subscribe(this); } - public void Wait(LuaCsAction action, int millisecondDelay) + public FluentResults.Result Reset() { - TimedAction timedAction = new TimedAction(action, millisecondDelay); - AddTimer(timedAction); + SubscribeToEvents(); + return FluentResults.Result.Ok(); } - public void NextFrame(LuaCsAction action) + public void Dispose() { - TimedAction timedAction = new TimedAction(action, 0); - AddTimer(timedAction); + _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 0000000000..dc7b0b8351 --- /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 fc515c8bd9..75bc2a3dca 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.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 @@ public RespawnManager RespawnManager } } + public List Commands => DebugConsole.Commands; + public bool? ForceVoice = null; public bool? ForceLocalVoice = null; @@ -146,7 +149,6 @@ public RespawnManager RespawnManager public bool disableSpamFilter = false; public bool disableDisconnectCharacter = false; public bool enableControlHusk = false; - public int MapEntityUpdateInterval { get { return MapEntity.MapEntityUpdateInterval; } @@ -269,10 +271,13 @@ public ClientPeer Peer } #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 @@ public static Signal CreateSignal(string value, int stepsTaken = 1, Character se 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 @@ public void RemoveCommand(string name) 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 @@ public static void EndGame() 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 @@ public void Stop() 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 957f77ba96..c3f56a2729 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 @@ private static void _hookLuaCsPatch(MethodBase __originalMethod, object[] __args 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) - { - outOfSocpe.Add(tuple); - } - else + var _result = tuple.Item2(__instance, args); + if (_result != null) { - 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 @@ private static void _hookLuaCsPatch(MethodBase __originalMethod, object[] __args 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 @@ private static bool HookLuaCsPatchRetPrefix(MethodBase __originalMethod, object[ 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 @@ public void HookMethod(string identifier, MethodBase method, LuaCsCompatPatchFun 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 @@ public void HookMethod(string identifier, MethodBase method, LuaCsCompatPatchFun } } - 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 @@ public void HookMethod(string identifier, MethodBase method, LuaCsCompatPatchFun } } - 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 @@ protected void HookMethod(string identifier, string className, string methodName } 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 cce9de4190..0bee7c96cd 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.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); - - 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); - } - } + public delegate DynValue LuaCsPatchFunc(object instance, LuaPatcherService.ParameterTable ptable); +} - public partial class LuaCsHook +namespace Barotrauma.LuaCs +{ + public partial class LuaPatcherService : ILuaPatcher { - public enum HookMethodType - { - Before, After - } - private class LuaCsHookCallback { public string name; @@ -512,38 +122,6 @@ public object ReturnValue 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 @@ public override int GetHashCode() }; } - internal LuaCsHook(LuaCsSetup luaCs) + private static readonly string[] prohibitedHooks = { - instance = this; - this.luaCs = luaCs; - } + "Barotrauma.Lua", + "Barotrauma.Cs", + "Barotrauma.ContentPackageManager", + }; + - public void Initialize() + private Harmony harmony; + private Lazy patchModuleBuilder; + private readonly Dictionary registeredPatches = new Dictionary(); + + public LuaPatcherService() { + instance = this; + 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 @@ public void Initialize() 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 @@ private ModuleBuilder CreateModuleBuilder() 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 @@ private static MethodBase ResolveMethod(string className, string methodName, str 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 @@ public DynamicParameterMapping(string name, Type originalMethodParamType, Type h 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 @@ private MethodInfo CreateDynamicHarmonyPatch(string identifier, MethodBase origi 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 @@ private MethodInfo CreateDynamicHarmonyPatch(string identifier, MethodBase origi // 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 @@ private MethodInfo CreateDynamicHarmonyPatch(string identifier, MethodBase origi 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 @@ private MethodInfo CreateDynamicHarmonyPatch(string identifier, MethodBase origi 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 @@ private MethodInfo CreateDynamicHarmonyPatch(string identifier, MethodBase origi il.EndExceptionBlock(exceptionBlock); // Only prefixes return a bool - if (hookType == HookMethodType.Before) + if (hookType == LuaCsHook.HookMethodType.Before) { il.LoadLocal(harmonyReturnValue); } @@ -1214,11 +670,11 @@ private MethodInfo CreateDynamicHarmonyPatch(string identifier, MethodBase origi } 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 @@ private string Patch(string identifier, MethodBase method, LuaCsPatchFunc patch, 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 @@ private string Patch(string identifier, MethodBase method, LuaCsPatchFunc patch, PatchFunc = patch, }); } - else if (hookType == HookMethodType.After) + else if (hookType == LuaCsHook.HookMethodType.After) { if (methodPatches.Postfixes.Remove(identifier)) { @@ -1266,31 +722,31 @@ private string Patch(string identifier, MethodBase method, LuaCsPatchFunc patch, 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 @@ private bool RemovePatch(string identifier, MethodBase method, HookMethodType ho 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 0000000000..d1b751d23f --- /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 0000000000..366249f21f --- /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 0000000000..a31869e785 --- /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 8a6cb6c30a..0dbb7f23a5 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; @@ -888,8 +890,8 @@ private void UpdateOxygen(Hull hull1, Hull hull2, float deltaTime) 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; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs index 1c193d9839..7c4391e1fb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs @@ -123,6 +123,8 @@ partial class Hull : MapEntity, ISerializableEntity, IServerSerializable 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; @@ -913,7 +915,7 @@ public override void Update(float deltaTime, Camera cam) 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 @@ -1159,7 +1161,10 @@ public float GetApproximateDistance(Vector2 startPos, Vector2 endPos, Hull targe 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) @@ -1167,7 +1172,7 @@ public float GetApproximateDistance(Vector2 startPos, Vector2 endPos, Hull targe 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) @@ -1643,9 +1648,18 @@ public void CleanSection(BackgroundSection section, float cleanVal, bool updateR 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 c5f0d161f8..71f4b07521 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs @@ -7,6 +7,7 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Diagnostics; using System.Globalization; using System.Linq; @@ -327,6 +328,7 @@ public float RealWorldCrushDepth 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. @@ -4398,6 +4400,13 @@ private void CreateWrecks() 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)"); } @@ -4782,6 +4791,7 @@ private void CreateBeaconStation() return; } beaconSonar = sonarItem.GetComponent(); + beaconTransducers = sonarItem.GetConnectedComponents().ToImmutableArray(); } public void PrepareBeaconStation() @@ -4908,9 +4918,20 @@ public void DamageBeaconStationWalls(float damageWallProbability) 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()) @@ -5149,6 +5170,7 @@ public override void Remove() 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 03e37859cd..b7460e61c1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs @@ -413,8 +413,7 @@ public void Save(XElement parentElement) 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 261a217064..dbf04847d5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelTrigger.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelTrigger.cs @@ -816,7 +816,7 @@ private void ApplyForce(PhysicsBody body) 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/Structure.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs index 8339e6c659..b96b7f1a76 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs @@ -651,7 +651,7 @@ private void CreateStairBodies() 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/WayPoint.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs index 1dc9d991c8..10c1728ae3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs @@ -583,7 +583,11 @@ public static bool GenerateSubWaypoints(Submarine submarine) { 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 0c60db650b..bdaeb91dce 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/NetConfig.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetConfig.cs @@ -39,6 +39,12 @@ static class NetConfig 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 e752020860..54f6715214 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/NetIdUtils.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetIdUtils.cs @@ -33,13 +33,7 @@ public static bool IdMoreRecentOrMatches(ushort newId, ushort oldId) /// 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 c33e87c57d..1b4402d9e2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs @@ -46,7 +46,6 @@ public enum ClientPacketHeader TOGGLE_RESERVE_BENCH, REQUEST_BACKUP_INDICES, // client wants a list of available backups for a save file - LUA_NET_MESSAGE } enum ClientNetSegment @@ -105,8 +104,6 @@ public enum ServerPacketHeader 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 5ef184891e..1c9ff1907f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/AccountInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/AccountInfo.cs @@ -5,7 +5,7 @@ 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 @@ public override int GetHashCode() 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 ff27881580..aa2d461432 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Auth/SteamAuthTicketForEosHostAuthenticator.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Auth/SteamAuthTicketForEosHostAuthenticator.cs @@ -15,13 +15,18 @@ public override async Task VerifyTicket(AuthenticationTicket ticket { 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 f1599e6540..ad6ef6e006 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 2bfc7eec41..6259e7678e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Message/IReadMessage.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Message/IReadMessage.cs @@ -4,7 +4,7 @@ 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 b5721153d3..47580b4bb8 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 040321b59e..2189a166ac 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkConnection/NetworkConnection.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkConnection/NetworkConnection.cs @@ -8,14 +8,14 @@ public enum NetworkConnectionStatus 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 8f67b919d6..a9d9ab0d62 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/RespawnManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/RespawnManager.cs @@ -213,9 +213,6 @@ private bool OnShuttleCollision(Fixture sender, Fixture other, Contact contact) 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 8918f9252b..b6a2008ec3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs @@ -458,7 +458,7 @@ public bool UseRespawnShuttle 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 58f0f5280a..5434b25a59 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs @@ -534,8 +534,10 @@ public Vector2 GetLocalFront(float? spritesheetRotation = null) 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 47b575be56..f3a0a4842d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Prefabs/PrefabCollection.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Prefabs/PrefabCollection.cs @@ -410,7 +410,7 @@ public void Add(T prefab, bool isOverride) && 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/Serialization/XMLExtensions.cs b/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs index b04914057a..a3a19cddb3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs @@ -36,6 +36,27 @@ private readonly static ImmutableDictionary> { 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 eb44f9d6b2..8f22a4da0a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs @@ -72,6 +72,7 @@ public static Config GetDefault() EnableSplashScreen = true, PauseOnFocusLost = true, RemoteMainMenuContentUrl = "https://www.barotraumagame.com/gamedata/", + RemoteContentTimeoutSeconds = 15f, AimAssistAmount = DefaultAimAssist, ShowEnemyHealthBars = EnemyHealthBarMode.ShowAll, ChatSpeechBubbles = true, @@ -167,6 +168,17 @@ public static Config FromElement(XElement element, in Config? fallback = null) 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/StatusEffects/StatusEffect.cs b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs index a0cd1be2a2..e53b6f79d4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs @@ -843,6 +843,12 @@ public float Range /// 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. @@ -903,6 +909,7 @@ protected StatusEffect(ContentXElement element, string parentDebugName) 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) @@ -1731,6 +1738,7 @@ private Hull GetHull(Entity entity) 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) @@ -1767,9 +1775,22 @@ protected Vector2 GetPosition(Entity entity, IReadOnlyList } } } + } + + 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 += offset; position += Rand.Vector(Rand.Range(0.0f, RandomOffset)); return position; } @@ -1787,14 +1808,14 @@ protected void Apply(float deltaTime, Entity entity, IReadOnlyList("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; } } @@ -1804,7 +1825,7 @@ protected void Apply(float deltaTime, Entity entity, IReadOnlyList(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; } } @@ -2093,24 +2114,9 @@ protected void Apply(float deltaTime, Entity entity, IReadOnlyList 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 92c987b9e4..4eca59969f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/TrimLString.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/TrimLString.cs @@ -9,19 +9,21 @@ public class TrimLString : LocalizedString 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 19ee41a2ff..5ba27c155b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Text/TextPack.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Text/TextPack.cs @@ -242,7 +242,10 @@ public void WriteToCSV(int index) } - 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 0000000000..1c3c38f97c --- /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 a963760478..f8c13c1778 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs @@ -11,6 +11,7 @@ using System.Reflection; using System.Security.Cryptography; using System.Text; +using System.Collections.Concurrent; namespace Barotrauma { @@ -75,7 +76,7 @@ public static bool IsProperFilenameCase(string filename) return !corrected; } - private static readonly Dictionary cachedFileNames = new Dictionary(); + private static readonly ConcurrentDictionary cachedFileNames = new ConcurrentDictionary(); public static string CorrectFilenameCase(string filename, out bool corrected, string directory = "") { @@ -153,7 +154,7 @@ public static string CorrectFilenameCase(string filename, out bool corrected, st if (i < subDirs.Length - 1) { filename += "/"; } } - cachedFileNames.Add(originalFilename, filename); + cachedFileNames.TryAdd(originalFilename, filename); return filename; } diff --git a/Barotrauma/BarotraumaShared/changelog.txt b/Barotrauma/BarotraumaShared/changelog.txt index fcd1b57307..1e029d35c0 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 cdc4ed6f18..854f011400 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 @@ private static DynValue DoHookPatch( 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 @@ private static DynValue DoHookRemovePatch( 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 @@ public static PatchHandle AddPrefix(this LuaCsSetup luaCs, string body, strin 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 @@ public static PatchHandle AddPostfix(this LuaCsSetup luaCs, string body, stri 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 @@ public static PatchHandle AddPostfix(this LuaCsSetup luaCs, string body, stri 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 @@ public static bool RemovePrefix(this LuaCsSetup luaCs, string patchId, string 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 b9366b239a..9608b804a5 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 Xunit; using Xunit.Abstractions; +// TODO: Rewrite all of this. + namespace TestProject.LuaCs { [Collection("LuaCs")] @@ -16,6 +19,7 @@ public class HookPatchTests : IClassFixture 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 @@ public HookPatchTests(LuaCsFixture luaCsFixture, ITestOutputHelper output) 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 @@ public void TestCastDouble() } } } +*/ diff --git a/Barotrauma/BarotraumaTest/LuaCs/LuaCsFixture.cs b/Barotrauma/BarotraumaTest/LuaCs/LuaCsFixture.cs index 70c2d9c1e6..61fdc58a63 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 @@ public LuaCsFixture() void IDisposable.Dispose() => LuaCs.Stop(); } } +*/ diff --git a/Libraries/BarotraumaLibs/BarotraumaCore/BarotraumaCore.csproj b/Libraries/BarotraumaLibs/BarotraumaCore/BarotraumaCore.csproj index 43147b2b5c..7cc2b89a13 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 d4c7f54f2d..e221774501 100644 --- a/Libraries/BarotraumaLibs/BarotraumaCore/Utils/ToolBoxCore.cs +++ b/Libraries/BarotraumaLibs/BarotraumaCore/Utils/ToolBoxCore.cs @@ -47,11 +47,17 @@ public static UInt32 StringToUInt32Hash(string str, MD5 md5) 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 e366bb52f3..1976acfcaf 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 629fe11000..fcd9a1be97 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 725dda808c..99073a079e 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/Farseer.NetStandard.csproj b/Libraries/Farseer Physics Engine 3.5/Farseer.NetStandard.csproj index 96e8ca92cd..69fbafa2fd 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 6b99d030ad..1a989ea0fb 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 a4477adaa4..6f2368424d 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 57fd8b083f..b3b90d7946 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 e3c2270e82..b556e550eb 160000 --- a/Libraries/moonsharp +++ b/Libraries/moonsharp @@ -1 +1 @@ -Subproject commit e3c2270e8277de98b0ec2b42b42909e6e6c8afd9 +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 03cd45b0c4..6c757d8b71 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 fc2241116d..ae420d5086 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 47185c67d8..97eb465514 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 fcd971bb6d..7ad4b5e212 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 dbcc8ae92e..2eb113ac8a 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 e428bd3f74..4ba7c8ae5c 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 070c8ab015..383d19f71a 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 cbf5621836..8e46400948 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 588637e836..3036a4e706 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 5a313c31d0..6804918a3a 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 f047763804..4ed3bb9e71 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 aea5573934..1def7460b5 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 5a3253ee66..6e90faa561 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 eba4ae19a6..4216a9c6b7 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 37ce06f53d..d6d248b216 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