diff --git a/Uchu.StandardScripts/Base/BaseSurvivalGame.cs b/Uchu.StandardScripts/Base/BaseSurvivalGame.cs index fa0ed6e6..125fddb9 100644 --- a/Uchu.StandardScripts/Base/BaseSurvivalGame.cs +++ b/Uchu.StandardScripts/Base/BaseSurvivalGame.cs @@ -94,11 +94,13 @@ public BaseSurvivalGame(GameObject gameObject) : base(gameObject) }); Listen(destructibleComponent.OnResurrect, this.PlayerResurrected); }); - Listen(player.OnMessageBoxRespond, (buttonId, identifier, _) => - { - this.MessageBoxResponse(player, identifier, buttonId); - }); }); + + Listen(this.GameObject.OnMessageBoxRespond, (player, message) => + { + this.MessageBoxResponse(player, message.Identifier, message.Button); + }); + Listen(Zone.OnPlayerLeave, this.PlayerExit); } @@ -863,4 +865,4 @@ public override void OnActivityTimeDone(string name) } } } -} \ No newline at end of file +} diff --git a/Uchu.StandardScripts/Base/BaseWorldTeleporter.cs b/Uchu.StandardScripts/Base/BaseWorldTeleporter.cs new file mode 100644 index 00000000..874f053f --- /dev/null +++ b/Uchu.StandardScripts/Base/BaseWorldTeleporter.cs @@ -0,0 +1,109 @@ +using Uchu.Core; +using Uchu.World; +using Uchu.World.Scripting.Native; + +namespace Uchu.StandardScripts.Base +{ + public class BaseWorldTeleporter : ObjectScript + { + protected virtual string MessageBoxText { get; } + protected virtual int TargetZone { get; } + protected virtual string TargetSpawnLocation { get; } + protected virtual string Animation { get; } + protected virtual string AcceptIdentifier { get; } = "TransferBox"; + protected virtual string CancelIdentifier { get; } = "TransferBox"; + protected virtual bool CheckUserData { get; } = true; + + protected virtual void ShowTransferPopup(Player player) + { + player.Message(new DisplayMessageBoxMessage + { + Associate = player, + Identifier = this.AcceptIdentifier, + CallbackClient = GameObject, + Show = true, + UserData = this.TargetZone.ToString(), + Text = this.MessageBoxText, + }); + } + + protected BaseWorldTeleporter(GameObject gameObject) : base(gameObject) + { + Listen(gameObject.OnInteract, this.ShowTransferPopup); + + this.Listen(gameObject.OnMessageBoxRespond, (player, message) => + { + // Ensure player is answering the right message box + // Identifier differs for accept/cancel for LEGO® Club world teleporter interaction + if ((message.Identifier != this.AcceptIdentifier && message.Identifier != this.CancelIdentifier) + || (this.CheckUserData && message.UserData != this.TargetZone.ToString())) + return; + + // If user clicked no, terminate interaction + if (message.Button != 1) + { + player.Message(new TerminateInteractionMessage + { + Associate = player, + Terminator = gameObject, + Type = TerminateType.FromInteraction, + }); + return; + } + + // User clicked yes + // Stun user and transfer to zone + player.Message(new SetStunnedMessage + { + Associate = player, + CantMove = true, + CantAttack = true, + CantInteract = true, + CantTurn = true, + }); + player.Animate(this.Animation); + + if (this.TargetSpawnLocation != null) + player.GetComponent().SpawnLocationName = this.TargetSpawnLocation; + + // Wait a bit while animation shows. After 3 seconds, show zone summary. + // Transfer when that's complete (user clicked Next). + // Ideally we'd ensure an instance is available or start one _before_ this delay. + this.Zone.Schedule(() => + { + this.Listen(player.OnFireServerEvent, (arguments, _) => + { + if (arguments == "summaryComplete") + player.SendToWorldAsync((ZoneId) this.TargetZone); + }); + player.Message(new DisplayZoneSummaryMessage + { + Associate = player, + Sender = gameObject, + }); + }, 3000); + + // Green beam animation for NT<->AM transfer. + if (this.Animation == "nexus-teleport") + { + this.Zone.BroadcastMessage(new PlayFXEffectMessage + { + Associate = player, + EffectId = 6478, + EffectType = "teleportBeam", + Name = "crux_teleport_beam", + }); + + this.Zone.Schedule(() => + { + this.Zone.BroadcastMessage(new StopFXEffectMessage + { + Associate = player, + Name = "crux_teleport_beam", + }); + }, 2000); + } + }); + } + } +} diff --git a/Uchu.StandardScripts/General/ImaginationFountain.cs b/Uchu.StandardScripts/General/ImaginationFountain.cs index 536d5c99..c578e658 100644 --- a/Uchu.StandardScripts/General/ImaginationFountain.cs +++ b/Uchu.StandardScripts/General/ImaginationFountain.cs @@ -1,5 +1,3 @@ -using System.Linq; -using System.Numerics; using Uchu.World; using Uchu.World.Scripting.Native; @@ -8,17 +6,6 @@ namespace Uchu.StandardScripts.General [ScriptName("ScriptComponent_1419_script_name__removed")] public class ImaginationFountain : ObjectScript { - /// - /// Imagination drop LOT to total. - /// - private static readonly (Lot, int)[] ImaginationDrops = { - (Lot.Imagination, 1), - (Lot.TwoImagination, 2), - (Lot.ThreeImagination, 3), - (Lot.FiveImagination, 5), - (Lot.TenImagination, 10) - }; - /// /// Creates the object script. /// @@ -28,20 +15,17 @@ public ImaginationFountain(GameObject gameObject) : base(gameObject) // Listen to the fountain being interacted with. Listen(gameObject.OnInteract, player => { - if (!player.TryGetComponent(out var stats)) return; - var toGive = (int) stats.MaxImagination; - while (toGive > 0) + // Drop imagination and, if applicable, a water bottle + gameObject.GetComponent().GenerateYieldsAsync(player); + + // Terminate interaction + player.Message(new TerminateInteractionMessage { - // Get the imagination to drop. - var array = ImaginationDrops.Where((_, i) => i >= toGive).ToArray(); - var (lot, cost) = array.Length == 0 ? ImaginationDrops.Last() : array.Max(); - toGive -= cost; - - // Drop the loot. - var loot = InstancingUtilities.InstantiateLoot(lot, player, gameObject, gameObject.Transform.Position+ Vector3.UnitY * 3); - Start(loot); - } + Associate = player, + Terminator = gameObject, + Type = TerminateType.FromInteraction, + }); }); } } -} \ No newline at end of file +} diff --git a/Uchu.StandardScripts/General/WildAmbient.cs b/Uchu.StandardScripts/General/WildAmbient.cs new file mode 100644 index 00000000..2af8e0cc --- /dev/null +++ b/Uchu.StandardScripts/General/WildAmbient.cs @@ -0,0 +1,20 @@ +using Uchu.World; +using Uchu.World.Scripting.Native; + +namespace Uchu.StandardScripts.General +{ + /// + /// Native implementation of scripts/ai/wild/l_wild_ambients.lua + /// + [ScriptName("l_wild_ambients.lua")] + public class WildAmbient : ObjectScript + { + public WildAmbient(GameObject gameObject) : base(gameObject) + { + Listen(gameObject.OnInteract, player => + { + gameObject.Animate("interact"); + }); + } + } +} diff --git a/Uchu.StandardScripts/General/WildAmbientCrab.cs b/Uchu.StandardScripts/General/WildAmbientCrab.cs new file mode 100644 index 00000000..43197646 --- /dev/null +++ b/Uchu.StandardScripts/General/WildAmbientCrab.cs @@ -0,0 +1,52 @@ +using Uchu.World; +using Uchu.World.Scripting.Native; + +namespace Uchu.StandardScripts.General +{ + /// + /// Native implementation of scripts/ai/wild/l_wild_ambient_crab.lua + /// + [ScriptName("L_WILD_AMBIENT_CRAB.lua")] + public class WildAmbientCrab : ObjectScript + { + public WildAmbientCrab(GameObject gameObject) : base(gameObject) + { + this.SetVar("flipped", true); + gameObject.Animate("idle"); + Listen(gameObject.OnInteract, player => + { + if (this.GetVar("flipped")) + { + this.AddTimerWithCancel(0.6f, "Flipping"); + gameObject.Animate("flip-over"); + this.SetVar("flipped", false); + } + else + { + this.AddTimerWithCancel(0.8f, "Flipback"); + gameObject.Animate("flip-back"); + this.SetVar("flipped", true); + } + player.Message(new TerminateInteractionMessage + { + Associate = player, + Terminator = gameObject, + Type = TerminateType.FromInteraction, + }); + }); + } + + public override void OnTimerDone(string timerName) + { + switch (timerName) + { + case "Flipping": + this.GameObject.Animate("over-idle"); + break; + case "Flipback": + this.GameObject.Animate("idle"); + break; + } + } + } +} diff --git a/Uchu.StandardScripts/General/WishingWell.cs b/Uchu.StandardScripts/General/WishingWell.cs new file mode 100644 index 00000000..8734ed27 --- /dev/null +++ b/Uchu.StandardScripts/General/WishingWell.cs @@ -0,0 +1,63 @@ +using System; +using Uchu.StandardScripts.Base; +using Uchu.World; +using Uchu.World.Scripting.Native; + +namespace Uchu.StandardScripts.General +{ + /// + /// Wishing well script, for both the one in Nexus Tower + /// and the ones in other worlds. + /// + [ScriptName("ScriptComponent_1536_script_name__removed")] + [ScriptName("ScriptComponent_1195_script_name__removed")] + public class WishingWell : GenericActivityManager + { + private const int CooldownTime = 10000; + + /// + /// Creates the object script. + /// + /// Game object to control with the script. + public WishingWell(GameObject gameObject) : base(gameObject) + { + var random = new Random(); + Listen(gameObject.OnInteract, player => + { + // Register player as activity user + this.AddActivityUser(player); + // Charge the cost (1 red Imaginite) + this.ChargeActivityCost(player); + // Random value to determine loot matrix + this.SetActivityUserData(player, 1, random.Next(1, 999)); + // Drop rewards + this.DistributeActivityRewards(player); + + // Start the cooldown + player.Message(new NotifyClientObjectMessage + { + Associate = gameObject, + Name = "StartCooldown", + }); + + // Stop the cooldown when the time is up + this.Zone.Schedule(() => + { + player.Message(new NotifyClientObjectMessage + { + Associate = gameObject, + Name = "StopCooldown", + }); + }, CooldownTime); + + // Terminate interaction with player + player.Message(new TerminateInteractionMessage + { + Associate = player, + Terminator = gameObject, + Type = TerminateType.FromInteraction, + }); + }); + } + } +} diff --git a/Uchu.StandardScripts/NexusTower/ArmorSpawner.cs b/Uchu.StandardScripts/NexusTower/ArmorSpawner.cs new file mode 100644 index 00000000..49f9e131 --- /dev/null +++ b/Uchu.StandardScripts/NexusTower/ArmorSpawner.cs @@ -0,0 +1,37 @@ +using System.Numerics; +using Uchu.World; +using Uchu.World.Scripting.Native; + +namespace Uchu.StandardScripts.NexusTower +{ + [ScriptName("ScriptComponent_1535_script_name__removed")] + public class ArmorSpawner : ObjectScript + { + private const int ArmorCount = 3; + + /// + /// Creates the object script. + /// + /// Game object to control with the script. + public ArmorSpawner(GameObject gameObject) : base(gameObject) + { + Listen(gameObject.OnInteract, player => { + // Drop armor + for (var i = 0; i < ArmorCount; i++) + { + var loot = InstancingUtilities.InstantiateLoot(Lot.ThreeArmor, + player, gameObject, gameObject.Transform.Position + Vector3.UnitY * 3); + Start(loot); + } + + // Terminate interaction so the player can interact again. + player.Message(new TerminateInteractionMessage + { + Associate = player, + Terminator = gameObject, + Type = TerminateType.FromInteraction, + }); + }); + } + } +} diff --git a/Uchu.StandardScripts/NexusTower/AssemblyTube.cs b/Uchu.StandardScripts/NexusTower/AssemblyTube.cs new file mode 100644 index 00000000..2c340a16 --- /dev/null +++ b/Uchu.StandardScripts/NexusTower/AssemblyTube.cs @@ -0,0 +1,73 @@ +using System.Linq; +using System.Threading.Tasks; +using InfectedRose.Luz; +using Uchu.World; +using Uchu.World.Scripting.Native; + +namespace Uchu.StandardScripts.NexusTower +{ + [ScriptName("L_NT_ASSEMBLYTUBE_SERVER.lua")] + public class AssemblyTube : ObjectScript + { + /// + /// Creates the object script. + /// + /// Game object to control with the script. + public AssemblyTube(GameObject gameObject) : base(gameObject) + { + // Get info from object settings + var group = (string) gameObject.Settings["teleGroup"]; + var cinematic = (string) gameObject.Settings["Cinematic"]; + var animation = gameObject.Settings.ContainsKey("OverrideAnim") + ? (string) gameObject.Settings["OverrideAnim"] + : "tube-sucker"; + // nexus-tube-up also exists, don't know where it is used + + if (!gameObject.TryGetComponent(out var phys)) + return; + + Listen(phys.OnEnter, other => + { + // Should only teleport players + if (!(other.GameObject is Player player)) + return; + + // Find teleporter target location + var target = GetGroup(group).FirstOrDefault(); + if (target == null) + return; + + // Show camera path. Lock player so animation can't be canceled. + player.Message(new PlayCinematicMessage + { + Associate = player, + LockPlayer = true, + PathName = cinematic, + LeadIn = 0.5f, + }); + + player.Animate(animation); + + // Default time in case camera path can't be found in luz file + var time = 5f; + // Find total camera path (cinematic) duration + if (Zone.ZoneInfo.LuzFile.PathData.FirstOrDefault(p => + p is LuzCameraPath && p.PathName == cinematic) is LuzCameraPath cameraPath) + time = cameraPath.Waypoints.Sum(point => ((LuzCameraWaypoint) point).Time); + + // At the end of the cinematic, rebuild the player at the target location + Task.Run(async () => + { + await Task.Delay((int) (time * 1000) + 0); + player.Teleport(target.Transform.Position); + player.Animate("tube-resurrect"); + }); + + // Progress mission 1047 and 1331 + var missionInventory = player.GetComponent(); + missionInventory.ScriptAsync(1490, gameObject.Lot); + missionInventory.ScriptAsync(1861, gameObject.Lot); + }); + } + } +} diff --git a/Uchu.StandardScripts/NexusTower/CombatChallenge.cs b/Uchu.StandardScripts/NexusTower/CombatChallenge.cs new file mode 100644 index 00000000..24502ed4 --- /dev/null +++ b/Uchu.StandardScripts/NexusTower/CombatChallenge.cs @@ -0,0 +1,186 @@ +using System; +using System.Numerics; +using InfectedRose.Core; +using InfectedRose.Lvl; +using Uchu.StandardScripts.Base; +using Uchu.World; +using Uchu.World.Scripting.Native; +using Object = Uchu.World.Object; + +namespace Uchu.StandardScripts.NexusTower +{ + [ScriptName("ScriptComponent_1467_script_name__removed")] + public class CombatChallenge : GenericActivityManager + { + private Player _activePlayer; + + private static readonly int[] Targets = { + Lot.CombatChallengeTarget1, + Lot.CombatChallengeTarget2, + Lot.CombatChallengeTarget3, + Lot.CombatChallengeTarget4, + Lot.CombatChallengeTarget5, + Lot.CombatChallengeTarget6, + Lot.CombatChallengeTarget7, + Lot.CombatChallengeTarget8, + Lot.CombatChallengeTarget9, + Lot.CombatChallengeTarget10, + }; + + private const int TotalTime = 30; + + private int _targetsDestroyed; + + private GameObject _spawnedTarget; + + private bool _active = false; + + private readonly Quaternion _targetRotation; + + public CombatChallenge(GameObject gameObject) : base(gameObject) + { + // Calculate rotation quaternion for spawned objects. + this._targetRotation = Quaternion.Concatenate(gameObject.Transform.Rotation, + Quaternion.CreateFromAxisAngle(Vector3.UnitY, (float) Math.PI)); + + // Show the initial screen when player interacts (asks player to confirm they want to join) + Listen(gameObject.OnInteract, player => + { + player.Message(new NotifyClientObjectMessage + { + Associate = gameObject, + Name = "UI_Open", + ParamObj = player, + }); + }); + + // Start the activity when player presses Start button + Listen(gameObject.OnMessageBoxRespond, (player, message) => + { + var button = message.Button; + var identifier = message.Identifier; + // Check if player clicked the correct button + if (!(identifier == "PlayButton" && button == 1)) + return; + if (this._active) + return; + this._active = true; + this.Start(player); + }); + } + + private void Start(Player player) + { + // Set up activity + this.SetupActivity(1); + // Charge cost (1 green Imaginite) + this.ChargeActivityCost(player); + + // Remember current player + this._activePlayer = player; + + // Keep track of this to know which target to spawn + this._targetsDestroyed = 0;; + + // Hide interaction icon + this.SetNetworkVar("bInUse", true); + // Show UI + this.SetNetworkVar("toggle", true); + // Tell client the game takes 30 seconds + this.SetNetworkVar("totalTime", TotalTime); + // Tell client the remaining time every second + this.ActivityTimerStart("updateTime", 1, TotalTime); + // Add player and set initial score to 0 + this.MiniGameAddPlayer(_activePlayer); + this.SetActivityValue(player, 0, 0); + + + // Start spawning targets + this.SpawnTarget(player); + } + + private void SpawnTarget(Player player) + { + // Create new target + this._spawnedTarget = GameObject.Instantiate(new LevelObjectTemplate + { + Lot = Targets[Math.Min(this._targetsDestroyed / 2, Targets.Length - 1)], + Position = this.GameObject.Transform.Position, + Rotation = this._targetRotation, + Scale = 1, + LegoInfo = new LegoDataDictionary(), + }, this.Zone); + + Object.Start(this._spawnedTarget); + GameObject.Construct(this._spawnedTarget); + + var stats = this._spawnedTarget.GetComponent(); + + // Whenever the target takes damage, increase score by the amount of damage dealt. + // It also increases the score when other players damage the target. + // This is intentional; according to the LU wiki, this is how it worked in Live as well. + Listen(stats.OnHealthChanged, (u, i) => + { + if (i >= 0) // Health gets reset to max when object dies + return; + this.UpdateActivityValue(player, 0, -i); + this.SetNetworkVar("totalDmg", this.GetActivityValue(player, 0)); + }); + + // If the timer hasn't ended yet, spawn a new target when this one dies. + var destructibleComponent = this._spawnedTarget.GetComponent(); + Listen(destructibleComponent.OnSmashed, (smasher, lootOwner) => + { + Destroy(this._spawnedTarget); + this._targetsDestroyed++; + if (this._active) + this.SpawnTarget(player); + }); + } + + public override void OnActivityTimerUpdate(string name, float timeRemaining, float timeElapsed) + { + // Send the client the remaining time + if (name == "updateTime") + this.SetNetworkVar("update_time", Convert.ToInt32(timeRemaining)); + } + + public override void OnActivityTimeDone(string name) + { + // Time's up + if (name == "updateTime") + { + this._active = false; + + // Schedule UI to be hidden after 5 seconds + Zone.Schedule(() => + { + // Hide UI + this.SetNetworkVar("toggle", false); + // Allow interaction again + this.SetNetworkVar("bInUse", false); + }, 5000); + + // Destroy current target + this._spawnedTarget?.GetComponent()?.SmashAsync(this.GameObject); + + // Complete achievements + if (this._activePlayer != null && + this._activePlayer.TryGetComponent(out var missionInventory)) + { + var score = this.GetActivityValue(this._activePlayer, 0); + if (score >= 25) + missionInventory.ScriptAsync(1449, Lot.CombatChallengeActivator); + if (score >= 100) + missionInventory.ScriptAsync(1869, Lot.CombatChallengeActivator); + if (score >= 240) + missionInventory.ScriptAsync(1870, Lot.CombatChallengeActivator); + if (score >= 290) + missionInventory.ScriptAsync(1871, Lot.CombatChallengeActivator); + } + + this.StopActivity(this._activePlayer, this.GetActivityValue(this._activePlayer, 0)); + } + } + } +} diff --git a/Uchu.StandardScripts/NexusTower/DarkitectReveal.cs b/Uchu.StandardScripts/NexusTower/DarkitectReveal.cs new file mode 100644 index 00000000..dc0dffe3 --- /dev/null +++ b/Uchu.StandardScripts/NexusTower/DarkitectReveal.cs @@ -0,0 +1,41 @@ +using Uchu.World; +using Uchu.World.Scripting.Native; + +namespace Uchu.StandardScripts.NexusTower +{ + [ScriptName("ScriptComponent_1556_script_name__removed")] + public class DarkitectReveal : ObjectScript + { + /// + /// Creates the object script. + /// + /// Game object to control with the script. + public DarkitectReveal(GameObject gameObject) : base(gameObject) + { + // Listen for players interacting with this object + Listen(gameObject.OnInteract, player => + { + // The client script will handle everything after it receives this message. + player.Message(new NotifyClientObjectMessage + { + Name = "reveal", + ParamObj = player, + Associate = gameObject, + }); + + Zone.Schedule(() => + { + // Set player's health to 1, as shown in the cinematic. + // Wait 2 seconds before doing this to ensure it won't be visible before the cinematic starts. + var stats = player.GetComponent(); + stats.Health = 1; + stats.Armor = 0; + + // Set flag for https://lu-explorer.web.app/missions/1295 + var character = player.GetComponent(); + character.SetFlagAsync(1911, true); + }, 2000); + }); + } + } +} diff --git a/Uchu.StandardScripts/NexusTower/HealthSpawner.cs b/Uchu.StandardScripts/NexusTower/HealthSpawner.cs new file mode 100644 index 00000000..615a1984 --- /dev/null +++ b/Uchu.StandardScripts/NexusTower/HealthSpawner.cs @@ -0,0 +1,37 @@ +using System.Numerics; +using Uchu.World; +using Uchu.World.Scripting.Native; + +namespace Uchu.StandardScripts.NexusTower +{ + [ScriptName("ScriptComponent_1534_script_name__removed")] + public class HealthSpawner : ObjectScript + { + private const int HealthCount = 3; + + /// + /// Creates the object script. + /// + /// Game object to control with the script. + public HealthSpawner(GameObject gameObject) : base(gameObject) + { + Listen(gameObject.OnInteract, player => { + // Drop health + for (var i = 0; i < HealthCount; i++) + { + var loot = InstancingUtilities.InstantiateLoot(Lot.ThreeHealth, + player, gameObject, gameObject.Transform.Position + Vector3.UnitY * 3); + Start(loot); + } + + // Terminate interaction so the player can interact again. + player.Message(new TerminateInteractionMessage + { + Associate = player, + Terminator = gameObject, + Type = TerminateType.FromInteraction, + }); + }); + } + } +} diff --git a/Uchu.StandardScripts/NexusTower/MaelstromConverter.cs b/Uchu.StandardScripts/NexusTower/MaelstromConverter.cs new file mode 100644 index 00000000..a6c164f7 --- /dev/null +++ b/Uchu.StandardScripts/NexusTower/MaelstromConverter.cs @@ -0,0 +1,40 @@ +using Uchu.Core; +using Uchu.World; +using Uchu.World.Scripting.Native; + +namespace Uchu.StandardScripts.NexusTower +{ + [ScriptName("ScriptComponent_1537_script_name__removed")] + public class MaelstromConverter : ObjectScript + { + private const int BrickCount = 25; + + private const int FactionTokenCount = 5; + + /// + /// Creates the object script. + /// + /// Game object to control with the script. + public MaelstromConverter(GameObject gameObject) : base(gameObject) + { + Listen(gameObject.OnInteract, player => + { + // Let the player trade 25 Maelstrom Infected Bricks for 5 faction tokens + var inventory = player.GetComponent(); + // Ensure player has at least 25 Maelstrom Infected Bricks + if (inventory.FindItem(Lot.MaelstromInfectedBrick, InventoryType.Items, BrickCount) == default) + return; + inventory.RemoveLotAsync(Lot.MaelstromInfectedBrick, BrickCount); + inventory.AddLotAsync(Lot.FactionTokenProxy, FactionTokenCount); + + // Terminate interaction so the player can interact again. + player.Message(new TerminateInteractionMessage + { + Associate = player, + Terminator = gameObject, + Type = TerminateType.FromInteraction, + }); + }); + } + } +} diff --git a/Uchu.StandardScripts/NexusTower/ParadoxTeleporter.cs b/Uchu.StandardScripts/NexusTower/ParadoxTeleporter.cs new file mode 100644 index 00000000..2e9aec2b --- /dev/null +++ b/Uchu.StandardScripts/NexusTower/ParadoxTeleporter.cs @@ -0,0 +1,92 @@ +using System.Linq; +using System.Threading.Tasks; +using Uchu.World; +using Uchu.World.Scripting.Native; + +namespace Uchu.StandardScripts.NexusTower +{ + [ScriptName("L_NT_PARADOXTELE_SERVER.lua")] + public class ParadoxTeleporter : ObjectScript + { + /// + /// Creates the object script. + /// + /// Game object to control with the script. + public ParadoxTeleporter(GameObject gameObject) : base(gameObject) + { + // Get info from object settings + var group = (string) gameObject.Settings["teleGroup"]; + var cinematic = (string) gameObject.Settings["Cinematic"]; + + // All these must have a physics component to detect the player entering. + if (!gameObject.TryGetComponent(out var phys)) + return; + + Listen(phys.OnEnter, other => + { + // Should only teleport players. + if (!(other.GameObject is Player player)) + return; + + // Find teleporter target location. + var target = GetGroup(group).FirstOrDefault(); + if (target == null) + return; + + // The player shouldn't be able to move during the animation and the teleportation. + player.Message(new SetStunnedMessage + { + Associate = player, + Originator = gameObject, + StateChangeType = StunState.Push, + CantAttack = true, + CantJump = true, + CantMove = true, + CantTurn = true, + }); + + // Show teleport animation. + player.Animate("paradoxdeath"); + // paradox-teleport-in and nexusteleport also exist, don't know if they're used somewhere else. + + // At the end of the cinematic, rebuild the player at the target location. + Task.Run(async () => + { + // Wait for the animation to complete. + await Task.Delay(2000); + + // Teleport player to target location. + player.Teleport(target.Transform.Position); + + // Show camera path. + player.Message(new PlayCinematicMessage + { + Associate = player, + LockPlayer = true, + PathName = cinematic, + }); + + // Un-stun player. + player.Message(new SetStunnedMessage + { + Associate = player, + Originator = gameObject, + StateChangeType = StunState.Pop, + CantAttack = true, + CantJump = true, + CantMove = true, + CantTurn = true, + }); + + // Show rebuilding animation. + player.Animate("paradox-teleport-in"); + }); + + // Progress mission 1047 and 1331 + var missionInventory = player.GetComponent(); + missionInventory.ScriptAsync(1491, gameObject.Lot); + missionInventory.ScriptAsync(1861, gameObject.Lot); + }); + } + } +} diff --git a/Uchu.StandardScripts/NexusTower/PowerPanel.cs b/Uchu.StandardScripts/NexusTower/PowerPanel.cs new file mode 100644 index 00000000..4836ee02 --- /dev/null +++ b/Uchu.StandardScripts/NexusTower/PowerPanel.cs @@ -0,0 +1,98 @@ +using System.Linq; +using System.Numerics; +using Uchu.Core.Client; +using Uchu.Core.Resources; +using Uchu.World; +using Uchu.World.Scripting.Native; + +namespace Uchu.StandardScripts.NexusTower +{ + [ScriptName("ScriptComponent_1483_script_name__removed")] + public class PowerPanel : ObjectScript + { + /// + /// Creates the object script. + /// + /// Game object to control with the script. + public PowerPanel(GameObject gameObject) : base(gameObject) + { + // Find animation duration from cdclient. + var cdClientContext = new CdClientContext(); + var animation = cdClientContext.AnimationsTable.FirstOrDefault(e => e.Animationtype == "nexus-powerpanel"); + if (animation?.Animationlength == null) return; + + Listen(gameObject.OnInteract, player => + { + // If the player has this mission active, they should fix the panel. Don't display explosion/sparks animation. + if (player.GetComponent().HasActive(MissionId.PowerintheTower)) + { + // Show animation of player fixing the panel. + player.Animate("nexus-powerpanel"); + + // When the animation is done, update the effects and set the flag to progress the mission. + Zone.Schedule(() => + { + // Set flag so sparks will be hidden in the future. + player.GetComponent() + .SetFlagAsync((int) gameObject.Settings["flag"], true); + + // Stop sparks effect. + player.Message(new NotifyClientObjectMessage + { + Associate = gameObject, + Name = "SparkStop", + }); + + // Show rebuild celebrate animation. + player.Animate("rebuild-celebrate", true); + }, 1000 * (float) animation.Animationlength); + } + else + { + // Orient player to object. + player.Message(new OrientToObjectMessage + { + Associate = player, + ObjId = gameObject, + }); + + // Apply the knockback effect, in the direction away from the object. + player.Message(new KnockbackMessage + { + Associate = player, + Caster = gameObject, + Originator = gameObject, + KnockbackTime = 200, + Vector = Vector3.Transform(new Vector3(15, 5, 0), gameObject.Transform.Rotation), + }); + player.Animate("knockback-recovery", true, 2f); + + // Start and set a timer to stop the paradox_panel_burst effect (explosion/sparks). + this.PlayFXEffect("paradox_panel_burst", "create", 6432); + this.AddTimerWithCancel(1, "FXTime"); + + // Wait 2 seconds before making the interaction available again. + Zone.Schedule(() => + { + // Terminate the interaction. + player.Message(new TerminateInteractionMessage + { + Associate = player, + Terminator = gameObject, + Type = TerminateType.FromInteraction, + }); + }, 2000); + } + }); + } + + /// + /// Callback for the timer completing. + /// + /// Timer that was completed. + public override void OnTimerDone(string timerName) + { + this.StopFXEffect("paradox_panel_burst"); + } + } +} diff --git a/Uchu.StandardScripts/NexusTower/SentinelSpeedPad.cs b/Uchu.StandardScripts/NexusTower/SentinelSpeedPad.cs new file mode 100644 index 00000000..0d308a07 --- /dev/null +++ b/Uchu.StandardScripts/NexusTower/SentinelSpeedPad.cs @@ -0,0 +1,48 @@ +using System.Numerics; +using Uchu.World; +using Uchu.World.Scripting.Native; + +namespace Uchu.StandardScripts.NexusTower +{ + [ScriptName("L_NT_SENTINELWALKWAY_SERVER.lua")] + public class SentinelSpeedPad : ObjectScript + { + /// + /// Creates the object script. + /// + /// Game object to control with the script. + public SentinelSpeedPad(GameObject gameObject) : base(gameObject) + { + // https://lu.lcdruniverse.org/explorer/objects/12041 + // They all have a Flag and a Name on the LDF + // Flag between 1917 and 1945, no repeats + // Name between Walk1 and Walk9, with repeats + // These values don't seem to be relevant for the server + + if (!gameObject.TryGetComponent(out var phantomPhysicsComponent)) + return; + + // Create physics effect to accelerate player + phantomPhysicsComponent.IsEffectActive = true; + phantomPhysicsComponent.EffectType = PhantomPhysicsEffectType.Push; + phantomPhysicsComponent.EffectAmount = 115f; // This is accurate, found in captures + + // Pointing in the direction of the arrows + var direction = Vector3.Transform(new Vector3(-1, 0, 0), + GameObject.Transform.Rotation); + phantomPhysicsComponent.EffectDirection = direction; + + var physics = gameObject.GetComponent(); + Listen(physics.OnEnter, other => + { + if (!(other.GameObject is Player player)) + return; + + // Progress mission 1047 and 1331 + var missionInventory = player.GetComponent(); + missionInventory.ScriptAsync(1492, gameObject.Lot); + missionInventory.ScriptAsync(1861, gameObject.Lot); + }); + } + } +} diff --git a/Uchu.StandardScripts/NexusTower/VentureCannon.cs b/Uchu.StandardScripts/NexusTower/VentureCannon.cs new file mode 100644 index 00000000..548448df --- /dev/null +++ b/Uchu.StandardScripts/NexusTower/VentureCannon.cs @@ -0,0 +1,56 @@ +using System.Linq; +using System.Threading.Tasks; +using Uchu.World; +using Uchu.World.Scripting.Native; + +namespace Uchu.StandardScripts.NexusTower +{ + [ScriptName("ScriptComponent_1519_script_name__removed")] + public class VentureCannon : ObjectScript + { + /// + /// Creates the object script. + /// + /// Game object to control with the script. + public VentureCannon(GameObject gameObject) : base(gameObject) + { + // Get info from object settings + var group = (string) gameObject.Settings["teleGroup"]; + var enterCinematic = (string) gameObject.Settings["EnterCinematic"]; + + // Values from scripts/02_client/map/nt/l_nt_venture_cannon_client.lua + const string enterAnimation = "scale-down"; + const string exitAnimation = "venture-cannon-out"; + + // Listen for interactions + Listen(gameObject.OnInteract, player => + { + // Find target location + var target = Zone.GameObjects.FirstOrDefault(obj => + obj.Settings.TryGetValue("groupID", out var group2) + && ((string) group2).Split(";").Contains(group)); + + if (target == null) + return; + + // Show camera path. Lock player so animation can't be canceled. + player.Message(new PlayCinematicMessage + { + Associate = player, + LockPlayer = true, + PathName = enterCinematic, + }); + + player.Animate(enterAnimation); + + // Teleport player and show animation after 1 second + Task.Run(async () => + { + await Task.Delay(1000); + player.Teleport(target.Transform.Position, target.Transform.Rotation); + player.Animate(exitAnimation); + }); + }); + } + } +} diff --git a/Uchu.StandardScripts/NexusTower/VentureSpeedPad.cs b/Uchu.StandardScripts/NexusTower/VentureSpeedPad.cs new file mode 100644 index 00000000..4da80885 --- /dev/null +++ b/Uchu.StandardScripts/NexusTower/VentureSpeedPad.cs @@ -0,0 +1,35 @@ +using Uchu.World; +using Uchu.World.Scripting.Native; + +namespace Uchu.StandardScripts.NexusTower +{ + [ScriptName("ScriptComponent_1541_script_name__removed")] + public class VentureSpeedPad : ObjectScript + { + /// + /// Creates the object script. + /// + /// Game object to control with the script. + public VentureSpeedPad(GameObject gameObject) : base(gameObject) + { + if (!gameObject.TryGetComponent(out var physicsComponent)) + return; + + Listen(physicsComponent.OnEnter, async other => + { + if (!(other.GameObject is Player player)) + return; + + var skill = player.GetComponent(); + + // This skill gives a speed boost for 4 seconds + await skill.CalculateSkillAsync(927, player); + + // Progress mission 1047 and 1331 + var missionInventory = player.GetComponent(); + await missionInventory.ScriptAsync(1489, gameObject.Lot); + await missionInventory.ScriptAsync(1861, gameObject.Lot); + }); + } + } +} diff --git a/Uchu.StandardScripts/NexusTower/XRay.cs b/Uchu.StandardScripts/NexusTower/XRay.cs new file mode 100644 index 00000000..945d5d9c --- /dev/null +++ b/Uchu.StandardScripts/NexusTower/XRay.cs @@ -0,0 +1,29 @@ +using Uchu.World; +using Uchu.World.Scripting.Native; + +namespace Uchu.StandardScripts.NexusTower +{ + [ScriptName("L_NT_XRAY_SERVER.lua")] + public class XRay : ObjectScript + { + /// + /// Creates the object script. + /// + /// Game object to control with the script. + public XRay(GameObject gameObject) : base(gameObject) + { + if (!gameObject.TryGetComponent(out var phys)) + return; + + // Listen for players leaving the trigger area. + Listen(phys.OnLeave, other => + { + if (!(other.GameObject is Player player)) + return; + + // Show green glowing effect on player. + player.GetComponent().CalculateSkillAsync(1220, player); + }); + } + } +} diff --git a/Uchu.StandardScripts/WorldTeleporters/CruxPrimeToNexusTower.cs b/Uchu.StandardScripts/WorldTeleporters/CruxPrimeToNexusTower.cs new file mode 100644 index 00000000..34f3d4a5 --- /dev/null +++ b/Uchu.StandardScripts/WorldTeleporters/CruxPrimeToNexusTower.cs @@ -0,0 +1,20 @@ +using Uchu.StandardScripts.Base; +using Uchu.World; +using Uchu.World.Scripting.Native; + +namespace Uchu.StandardScripts.WorldTeleporters +{ + [ScriptName("ScriptComponent_1486_script_name__removed")] + public class CruxPrimeToNexusTower : BaseWorldTeleporter + { + protected override string MessageBoxText => "%[UI_TRAVEL_TO_NEXUS_TOWER]"; + protected override int TargetZone => 1900; + protected override string TargetSpawnLocation => "cp_door"; + protected override string Animation => "nexus-teleport"; + + public CruxPrimeToNexusTower(GameObject gameObject) : base(gameObject) + { + + } + } +} diff --git a/Uchu.StandardScripts/WorldTeleporters/LegoClubPortal.cs b/Uchu.StandardScripts/WorldTeleporters/LegoClubPortal.cs new file mode 100644 index 00000000..d9a33521 --- /dev/null +++ b/Uchu.StandardScripts/WorldTeleporters/LegoClubPortal.cs @@ -0,0 +1,55 @@ +using System.Collections.Generic; +using Uchu.StandardScripts.Base; +using Uchu.World; +using Uchu.World.Scripting.Native; +using Uchu.World.Social; + +namespace Uchu.StandardScripts.WorldTeleporters +{ + [ScriptName("ScriptComponent_1239_script_name__removed")] + [ScriptName("ScriptComponent_1485_script_name__removed")] + public class LegoClubPortal : BaseWorldTeleporter + { + protected override string MessageBoxText => this.TargetZone switch + { + 1200 => "%[UI_TRAVEL_TO_NS]", + 1900 => "%[UI_TRAVEL_TO_NEXUS_TOWER]", + _ => null, + }; + protected override int TargetZone => int.Parse((string) this.GameObject.Settings["transferZoneID"]); + protected override string Animation => "lup-teleport"; + protected override string AcceptIdentifier => "PlayButton"; + protected override string CancelIdentifier => "CloseButton"; + protected override string TargetSpawnLocation => this.TargetZone switch + { + 1200 => "NS_LEGO_Club", + 1900 => "NS_LEGO_Club", + _ => null, + }; + protected override bool CheckUserData => false; + + protected override void ShowTransferPopup(Player player) + { + // Show special popup for LEGO® Club + if (this.TargetZone == 1700) + player.MessageGuiAsync("pushGameState", new Dictionary + { + {"context", new Dictionary + { + {"HelpVisible", "show"}, + {"callbackObj", this.GameObject.Id.ToString()}, + {"type", "Lego_Club_Valid"}, + {"user", player.Id.ToString()}, + }}, + {"state", "Lobby"}, + }); + // Show default confirmation message box + else + base.ShowTransferPopup(player); + } + + public LegoClubPortal(GameObject gameObject) : base(gameObject) + { + } + } +} diff --git a/Uchu.StandardScripts/WorldTeleporters/NexusTowerToCruxPrime.cs b/Uchu.StandardScripts/WorldTeleporters/NexusTowerToCruxPrime.cs new file mode 100644 index 00000000..f4f200b8 --- /dev/null +++ b/Uchu.StandardScripts/WorldTeleporters/NexusTowerToCruxPrime.cs @@ -0,0 +1,19 @@ +using Uchu.StandardScripts.Base; +using Uchu.World; +using Uchu.World.Scripting.Native; + +namespace Uchu.StandardScripts.WorldTeleporters +{ + [ScriptName("ScriptComponent_1527_script_name__removed")] + public class NexusTowerToCruxPrime : BaseWorldTeleporter + { + protected override string MessageBoxText => "%[UI_TRAVEL_TO_CRUX_PRIME]"; + protected override int TargetZone => 1800; + protected override string Animation => "nexus-teleport"; + + public NexusTowerToCruxPrime(GameObject gameObject) : base(gameObject) + { + + } + } +} diff --git a/Uchu.StandardScripts/WorldTeleporters/ToStarbase3001.cs b/Uchu.StandardScripts/WorldTeleporters/ToStarbase3001.cs new file mode 100644 index 00000000..027b97ea --- /dev/null +++ b/Uchu.StandardScripts/WorldTeleporters/ToStarbase3001.cs @@ -0,0 +1,20 @@ +using Uchu.StandardScripts.Base; +using Uchu.World; +using Uchu.World.Scripting.Native; + +namespace Uchu.StandardScripts.WorldTeleporters +{ + [ScriptName("ScriptComponent_1484_script_name__removed")] + [ScriptName("ScriptComponent_1276_script_name__removed")] + public class ToStarbase3001 : BaseWorldTeleporter + { + protected override string MessageBoxText => "%[UI_TRAVEL_TO_LUP_STATION]"; + protected override int TargetZone => 1600; + protected override string Animation => "lup-teleport"; + + public ToStarbase3001(GameObject gameObject) : base(gameObject) + { + + } + } +} diff --git a/Uchu.World/Extensions/BitWriterExtensions.cs b/Uchu.World/Extensions/BitWriterExtensions.cs index f22634c9..3d587ca6 100644 --- a/Uchu.World/Extensions/BitWriterExtensions.cs +++ b/Uchu.World/Extensions/BitWriterExtensions.cs @@ -17,8 +17,8 @@ public static void WriteLdfCompressed(this BitWriter @this, LegoDataDictionary d { using var stream = new MemoryStream(); using var temp = new BitWriter(stream); - - Core.BitWriterExtensions.Write(temp, dict); + + dict.Serialize(temp, false); var buffer = stream.ToArray(); diff --git a/Uchu.World/Handlers/GameMessages/GeneralHandler.cs b/Uchu.World/Handlers/GameMessages/GeneralHandler.cs index 4544cebe..85a3d15d 100644 --- a/Uchu.World/Handlers/GameMessages/GeneralHandler.cs +++ b/Uchu.World/Handlers/GameMessages/GeneralHandler.cs @@ -176,7 +176,7 @@ public void RequestPlatformResyncHandler(RequestPlatformResyncMessage message, P [PacketHandler] public void MessageBoxRespondMessageHandler(MessageBoxRespondMessage message, Player player) { - player.OnMessageBoxRespond.Invoke(message.Button, message.Identifier, message.UserData); + message.Associate?.OnMessageBoxRespond.Invoke(player, message); } } -} \ No newline at end of file +} diff --git a/Uchu.World/Objects/Components/ReplicaComponents/DestructibleComponent.cs b/Uchu.World/Objects/Components/ReplicaComponents/DestructibleComponent.cs index 0268fae9..88e40047 100644 --- a/Uchu.World/Objects/Components/ReplicaComponents/DestructibleComponent.cs +++ b/Uchu.World/Objects/Components/ReplicaComponents/DestructibleComponent.cs @@ -155,7 +155,7 @@ public async Task SmashAsync(GameObject smasher, Player owner = default, string await OnSmashed.InvokeAsync(smasher, owner); } - private async Task GenerateYieldsAsync(Player owner) + public async Task GenerateYieldsAsync(Player owner) { var container = GameObject.GetComponent(); @@ -241,4 +241,4 @@ public async Task ResurrectAsync() await OnResurrect.InvokeAsync(); } } -} \ No newline at end of file +} diff --git a/Uchu.World/Objects/Components/Server/PhysicsComponent.cs b/Uchu.World/Objects/Components/Server/PhysicsComponent.cs index cd56f7c2..555b3f77 100644 --- a/Uchu.World/Objects/Components/Server/PhysicsComponent.cs +++ b/Uchu.World/Objects/Components/Server/PhysicsComponent.cs @@ -143,6 +143,12 @@ public void SetPhysicsByPath(string path) // We can't read HKX so this is basica finalObject = BoxBody.Create(Zone.Simulation, pos, Transform.Rotation, size); } + // approx. 20 x 5 x 7, used for https://lu.lcdruniverse.org/explorer/objects/12041 + else if (path.Contains("fx_nt_sentinal_ground_arrows.hkx")) + { + size = new Vector3(20, 5, 7) * Math.Abs(GameObject.Transform.Scale); + finalObject = BoxBody.Create(Zone.Simulation, Transform.Position, Transform.Rotation, size); + } // default is a 5x5x5 cube else { diff --git a/Uchu.World/Objects/GameObjects/GameObject.cs b/Uchu.World/Objects/GameObjects/GameObject.cs index c3f2905f..6167a8b6 100644 --- a/Uchu.World/Objects/GameObjects/GameObject.cs +++ b/Uchu.World/Objects/GameObjects/GameObject.cs @@ -106,6 +106,8 @@ public int GameMasterLevel public Event OnRailMovementReady { get; } + public Event OnMessageBoxRespond { get; } + #endregion #region Macro @@ -138,6 +140,8 @@ protected GameObject() OnCancelRailMovement = new Event(); + OnMessageBoxRespond = new Event(); + Listen(OnStart, () => { foreach (var component in Components.ToArray()) Start(component); diff --git a/Uchu.World/Objects/GameObjects/Player.cs b/Uchu.World/Objects/GameObjects/Player.cs index 1e972e0b..0d89a75f 100644 --- a/Uchu.World/Objects/GameObjects/Player.cs +++ b/Uchu.World/Objects/GameObjects/Player.cs @@ -53,7 +53,6 @@ internal Player() OnFireServerEvent = new Event(); OnReadyForUpdatesEvent = new Event(); OnPositionUpdate = new Event(); - OnMessageBoxRespond = new Event(); OnPetTamingTryBuild = new Event(); OnNotifyTamingBuildSuccessMessage = new Event(); OnLootPickup = new Event(); @@ -246,8 +245,6 @@ public async Task LoadAsync(IRakConnection connection, CancellationToken cancell public Event OnWorldLoad { get; } public Event OnPositionUpdate { get; } - - public Event OnMessageBoxRespond { get; } public IRakConnection Connection { get; private set; } @@ -374,12 +371,14 @@ private void UpdatePhysics(Vector3 position, Quaternion rotation) /// Teleports the player to a different position /// /// The position to teleport the player to - public void Teleport(Vector3 position, bool ignore = false) + public void Teleport(Vector3 position, Quaternion? rotation = null, bool ignore = false) { Message(new TeleportMessage { Associate = this, + SetRotation = rotation != null, Position = position, + Rotation = rotation ?? Quaternion.Identity, IgnoreY = ignore }); } diff --git a/Uchu.World/Scripting/Native/Attributes/ScriptNameAttribute.cs b/Uchu.World/Scripting/Native/Attributes/ScriptNameAttribute.cs index 134da268..762c3f14 100644 --- a/Uchu.World/Scripting/Native/Attributes/ScriptNameAttribute.cs +++ b/Uchu.World/Scripting/Native/Attributes/ScriptNameAttribute.cs @@ -2,7 +2,7 @@ namespace Uchu.World.Scripting.Native { - [AttributeUsage(AttributeTargets.Class)] + [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] public class ScriptName : Attribute { /// @@ -19,4 +19,4 @@ public ScriptName(string scriptName) this.Name = scriptName; } } -} \ No newline at end of file +} diff --git a/Uchu.World/Scripting/Native/Extensions/GameObjectScriptExtensions.cs b/Uchu.World/Scripting/Native/Extensions/GameObjectScriptExtensions.cs index 2fe56db0..e1a6491c 100644 --- a/Uchu.World/Scripting/Native/Extensions/GameObjectScriptExtensions.cs +++ b/Uchu.World/Scripting/Native/Extensions/GameObjectScriptExtensions.cs @@ -2,14 +2,14 @@ namespace Uchu.World.Scripting.Native { public static class GameObjectScriptExtensions { - public static void Animate(this GameObject @this, string animation, bool playImmediate = false) + public static void Animate(this GameObject @this, string animation, bool playImmediate = false, float priority = 0.4f) { @this.Zone.BroadcastMessage(new PlayAnimationMessage { Associate = @this, AnimationId = animation, PlayImmediate = playImmediate, - Priority = 0.4f, + Priority = priority, Scale = 1, }); } @@ -49,4 +49,4 @@ public static string[] GetGroups(this GameObject @this) return groups; } } -} \ No newline at end of file +} diff --git a/Uchu.World/Scripting/Native/NativeScriptPack.cs b/Uchu.World/Scripting/Native/NativeScriptPack.cs index e4b60e5b..44ba42bc 100644 --- a/Uchu.World/Scripting/Native/NativeScriptPack.cs +++ b/Uchu.World/Scripting/Native/NativeScriptPack.cs @@ -59,11 +59,12 @@ internal void ReadAssembly() { // Ignore non-object scripts. if (!type.IsAssignableTo(typeof(ObjectScript))) continue; - var scriptName = type.GetCustomAttribute(); - if (scriptName == null) continue; - - // Add the object script. - this.ObjectScriptTypes.Add(scriptName.Name.ToLower(), type); + var scriptNames = type.GetCustomAttributes(); + foreach (var scriptName in scriptNames) + { + // Add the object script. + this.ObjectScriptTypes.Add(scriptName.Name.ToLower(), type); + } } } @@ -103,4 +104,4 @@ public override async Task ReloadAsync() await LoadAsync(); } } -} \ No newline at end of file +} diff --git a/Uchu.World/Social/Amf3Helper.cs b/Uchu.World/Social/Amf3Helper.cs index 677f011d..15028e05 100644 --- a/Uchu.World/Social/Amf3Helper.cs +++ b/Uchu.World/Social/Amf3Helper.cs @@ -1,141 +1,75 @@ +using System.Collections.Generic; using RakDotNet.IO; namespace Uchu.World.Social { - // Stolen from https://github.com/LUNIServer/UniverseServer/blob/master/Source/WorldServer.cpp - // TODO: Replace with sane code + // Stolen from https://github.com/lcdr/utils/blob/master/utils/amf3.py + // (Python code for WriteNumber translated to C#. Python code is + // available under the AGPLv3 license, of which you can find a copy + // in the top-level directory of Uchu.) public static class Amf3Helper { public static void WriteNumber(BitWriter writer, uint n) { - if (n < 128) + if (n < 0x80) { writer.Write((byte) n); } - else + else if (n < 0x4000) { - writer.WriteBit(true); - writer.WriteBit(true); - if (n < 2048) - { - writer.WriteBit(false); - } - else - { - writer.WriteBit(true); - if (n < 65536) - { - writer.WriteBit(false); - } - else - { - if (n > 2097151) n = 2097151; - - writer.WriteBit(true); - writer.WriteBit(false); - writer.WriteBit((n & 1048576) > 0); - writer.WriteBit((n & 524288) > 0); - writer.WriteBit((n & 262144) > 0); - - writer.WriteBit(true); - writer.WriteBit(false); - writer.WriteBit((n & 131072) > 0); - writer.WriteBit((n & 65536) > 0); - } - - writer.WriteBit((n & 32768) > 0); - writer.WriteBit((n & 16384) > 0); - writer.WriteBit((n & 8192) > 0); - writer.WriteBit((n & 4096) > 0); - - writer.WriteBit(true); - writer.WriteBit(false); - writer.WriteBit((n & 2048) > 0); - } - - writer.WriteBit((n & 1024) > 0); - writer.WriteBit((n & 512) > 0); - writer.WriteBit((n & 256) > 0); - writer.WriteBit((n & 128) > 0); - writer.WriteBit((n & 64) > 0); - - writer.WriteBit(true); - writer.WriteBit(false); - writer.WriteBit((n & 32) > 0); - writer.WriteBit((n & 16) > 0); - writer.WriteBit((n & 8) > 0); - writer.WriteBit((n & 4) > 0); - writer.WriteBit((n & 2) > 0); - writer.WriteBit((n & 1) > 0); + writer.Write((byte) ((n >> 7) | 0x80)); + writer.Write((byte) (n & 0x7f)); + } + else if (n < 0x200000) + { + writer.Write((byte) ((n >> 14) | 0x80)); + writer.Write((byte) ((n >> 7) | 0x80)); + writer.Write((byte) (n & 0x7f)); + } + else if (n < 0x20000000) + { + writer.Write((byte) ((n >> 22) | 0x80)); + writer.Write((byte) ((n >> 15) | 0x80)); + writer.Write((byte) ((n >> 7) | 0x80)); + writer.Write((byte) (n & 0xff)); } } - public static void WriteNumber2(BitWriter writer, uint n) + public static void Write(BitWriter writer, object value) { - if (n < 128) + switch (value) { - writer.Write((byte) n); + case string str: + writer.Write((byte) Amf3Type.String); + WriteText(writer, str); + break; + case int integer: + writer.Write((byte) Amf3Type.Integer); + WriteNumber(writer, (uint) integer); + break; + case uint unsigned: + writer.Write((byte) Amf3Type.Integer); + WriteNumber(writer, unsigned); + break; + case bool boolean: + writer.Write((byte) (boolean ? Amf3Type.True : Amf3Type.False)); + break; + case IDictionary dict: + writer.Write((byte) Amf3Type.Array); + WriteArray(writer, dict); + break; + case null: + writer.Write((byte) Amf3Type.Undefined); + break; } - else - { - writer.WriteBit(true); - - var flag = n > 2097151; - - if (n < 16383) - { - if (flag) - { - writer.WriteBit((n & 268435456) > 0); - writer.WriteBit((n & 134217728) > 0); - writer.WriteBit((n & 67108864) > 0); - writer.WriteBit((n & 33554432) > 0); - writer.WriteBit((n & 16777216) > 0); - writer.WriteBit((n & 8388608) > 0); - writer.WriteBit((n & 4194304) > 0); - writer.WriteBit(true); - writer.WriteBit((n & 2097152) > 0); - } - - writer.WriteBit((n & 1048576) > 0); - writer.WriteBit((n & 524288) > 0); - writer.WriteBit((n & 262144) > 0); - writer.WriteBit((n & 131072) > 0); - writer.WriteBit((n & 65536) > 0); - writer.WriteBit((n & 32768) > 0); - - if (flag) writer.WriteBit(true); - - writer.WriteBit((n & 16384) > 0); - - if (!flag) writer.WriteBit(true); - } - - writer.WriteBit((n & 8192) > 0); - writer.WriteBit((n & 4096) > 0); - writer.WriteBit((n & 2048) > 0); - writer.WriteBit((n & 1024) > 0); - writer.WriteBit((n & 512) > 0); - writer.WriteBit((n & 256) > 0); - writer.WriteBit((n & 128) > 0); - if (!flag) writer.WriteBit(false); - - writer.WriteBit((n & 64) > 0); - writer.WriteBit((n & 32) > 0); - writer.WriteBit((n & 16) > 0); - writer.WriteBit((n & 8) > 0); - writer.WriteBit((n & 4) > 0); - writer.WriteBit((n & 2) > 0); - writer.WriteBit((n & 1) > 0); - } } public static void WriteText(BitWriter writer, string value) { var size = (value.Length << 1) + 1; - WriteNumber2(writer, (uint) size); + WriteNumber(writer, (uint) size); foreach (var character in value) { @@ -143,13 +77,15 @@ public static void WriteText(BitWriter writer, string value) } } - public static void Array(BitWriter writer, int length) + public static void WriteArray(BitWriter writer, IDictionary dict) { - writer.Write((byte) Amf3Type.Array); - - var size = (length << 1) | 1; - - WriteNumber2(writer, (uint) size); + WriteNumber(writer, 0x01); + foreach (var (key, value) in dict) + { + WriteText(writer, key); + Write(writer, value); + } + writer.Write((byte) Amf3Type.Null); } } -} \ No newline at end of file +} diff --git a/Uchu.World/Social/UiHelper.cs b/Uchu.World/Social/UiHelper.cs index ca8cb672..c3cc73c9 100644 --- a/Uchu.World/Social/UiHelper.cs +++ b/Uchu.World/Social/UiHelper.cs @@ -84,7 +84,7 @@ public static async Task AddGuildMemberAsync(Player player, int index, GuildMemb Amf3Helper.WriteText(writer, "index"); writer.Write((byte) Amf3Type.Integer); - Amf3Helper.WriteNumber2(writer, (uint) index); + Amf3Helper.WriteNumber(writer, (uint) index); writer.Write((byte) Amf3Type.Null); @@ -170,37 +170,7 @@ public static async Task MessageGuiAsync(this Player @this, string name, IDictio await using var stream = new MemoryStream(); using var writer = new BitWriter(stream); - writer.Write((byte) Amf3Type.Array); - writer.Write(1); - - foreach (var (key, value) in arguments) - { - Amf3Helper.WriteText(writer, key); - - switch (value) - { - case string str: - writer.Write((byte) Amf3Type.String); - Amf3Helper.WriteText(writer, str); - break; - case int integer: - writer.Write((byte) Amf3Type.Integer); - Amf3Helper.WriteNumber2(writer, (uint) integer); - break; - case uint unsigned: - writer.Write((byte) Amf3Type.Integer); - Amf3Helper.WriteNumber2(writer, unsigned); - break; - case bool boolean: - writer.Write((byte) (boolean ? Amf3Type.True : Amf3Type.False)); - break; - case null: - writer.Write((byte) Amf3Type.Undefined); - break; - } - } - - writer.Write((byte) Amf3Type.Null); + Amf3Helper.Write(writer, arguments); @this.Message(new UIMessageServerToSingleClientMessage { @@ -278,4 +248,4 @@ public static async Task StoryBoxGuiAsync(this Player @this, string text) public static Task OpenMailboxGuiAsync(this Player @this) => StateAsync(@this, "Mail"); } -} \ No newline at end of file +} diff --git a/Uchu.World/Structs/Lot.cs b/Uchu.World/Structs/Lot.cs index e52aefa5..e9fef5ab 100644 --- a/Uchu.World/Structs/Lot.cs +++ b/Uchu.World/Structs/Lot.cs @@ -137,6 +137,19 @@ public int[] GetComponentIds(int componentType) public const int Krazi = 16049; public const int Bonezai = 16050; + public const int MaelstromInfectedBrick = 6194; + + public const int CombatChallengeActivator = 12045; + public const int CombatChallengeTarget1 = 13556; + public const int CombatChallengeTarget2 = 13764; + public const int CombatChallengeTarget3 = 13765; + public const int CombatChallengeTarget4 = 13766; + public const int CombatChallengeTarget5 = 13767; + public const int CombatChallengeTarget6 = 13768; + public const int CombatChallengeTarget7 = 13769; + public const int CombatChallengeTarget8 = 13770; + public const int CombatChallengeTarget9 = 13771; + public const int CombatChallengeTarget10 = 13772; #endregion }