From e6ee0d00107ab4ccd6c056d746fa3085ab724dcd Mon Sep 17 00:00:00 2001 From: Justin Baugh Date: Thu, 1 Dec 2016 22:00:35 -0500 Subject: [PATCH 01/26] Fix PlayerCondition enum (it's a bitfield) and upgrade to int --- hybrasyl/Enums.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/hybrasyl/Enums.cs b/hybrasyl/Enums.cs index 86f7f22c..2e07071a 100644 --- a/hybrasyl/Enums.cs +++ b/hybrasyl/Enums.cs @@ -338,7 +338,7 @@ public enum WeaponObjectType #endregion [Flags] - public enum PlayerCondition : byte + public enum PlayerCondition : int { Alive = 0x01, Frozen = 0x02, @@ -348,7 +348,7 @@ public enum PlayerCondition : byte InExchange = 0x20, InDialog = 0x40, InComa = 0x80, - Casting = 0xCA, + Casting = 0x100, AliveExchange = (Alive | InExchange) } From 93aa003936573945ac1986d4638422bf8a9d44f3 Mon Sep 17 00:00:00 2001 From: Justin Baugh Date: Thu, 1 Dec 2016 22:00:57 -0500 Subject: [PATCH 02/26] Bump SDK usage to 0.5.5.9 --- hybrasyl/Hybrasyl.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hybrasyl/Hybrasyl.csproj b/hybrasyl/Hybrasyl.csproj index 45435dea..88ddb461 100644 --- a/hybrasyl/Hybrasyl.csproj +++ b/hybrasyl/Hybrasyl.csproj @@ -68,7 +68,7 @@ packages\FastMember.1.0.0.11\lib\net40\FastMember.dll - packages\Hybrasyl.XML.0.5.5.8\lib\net452\Hybrasyl.XML.dll + packages\Hybrasyl.XML.0.5.5.9\lib\net452\Hybrasyl.XML.dll True From eafaacb6be33534b25eacf0c47a9d5864a60509f Mon Sep 17 00:00:00 2001 From: Justin Baugh Date: Thu, 1 Dec 2016 22:01:20 -0500 Subject: [PATCH 03/26] Ensure we're correctly cleaning up gold when it goes away --- hybrasyl/Map.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/hybrasyl/Map.cs b/hybrasyl/Map.cs index a82542a5..bbd0751c 100755 --- a/hybrasyl/Map.cs +++ b/hybrasyl/Map.cs @@ -633,6 +633,7 @@ public void RemoveGold(Gold gold) NotifyNearbyAoiDeparture(gold); EntityTree.Remove(gold); Objects.Remove(gold); + World.Remove(gold); } public void RemoveItem(ItemObject itemObject) From c75cddfad8594c0174e41c7869d2c2de9376981e Mon Sep 17 00:00:00 2001 From: Justin Baugh Date: Thu, 1 Dec 2016 22:03:02 -0500 Subject: [PATCH 04/26] Fix various death issues (can't properly pickup gold; ghosts don't return to deathmap); castables should not kill npcs --- hybrasyl/Objects/User.cs | 13 +++++++------ hybrasyl/World.cs | 14 +++++++++----- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/hybrasyl/Objects/User.cs b/hybrasyl/Objects/User.cs index f34dcef0..2ccaede6 100644 --- a/hybrasyl/Objects/User.cs +++ b/hybrasyl/Objects/User.cs @@ -102,9 +102,10 @@ public static string GetStorageKey(string name) [JsonProperty] public bool IsMaster { get; set; } public UserGroup Group { get; set; } + [JsonProperty] - public bool Dead { get; set; } - public bool IsCasting { get; set; } + public bool Dead => !Status.HasFlag(PlayerCondition.Alive); + public bool IsCasting => Status.HasFlag(PlayerCondition.Casting); public Mailbox Mailbox => World.GetMailbox(Name); public bool UnreadMail => Mailbox.HasUnreadMessages; @@ -558,6 +559,7 @@ public override void OnDeath() DeathPileOwner = Name, DeathPileTime = timeofdeath }; + World.Insert(newGold); Map.AddGold(X,Y, newGold); Gold = 0; } @@ -1269,8 +1271,8 @@ internal void UseSpell(byte slot, uint target = 0) var castable = SpellBook[slot]; Creature targetCreature = Map.EntityTree.OfType().SingleOrDefault(x => x.Id == target) ?? null; Direction playerFacing = this.Direction; - - if(targetCreature != null) Attack(castable, targetCreature); + if (targetCreature is Merchant) return; + if (targetCreature != null) Attack(castable, targetCreature); else Attack(castable); } @@ -2239,7 +2241,6 @@ public override void Attack(Castable castObject, Creature target = null) public override void Attack(Castable castObject) { - var direction = this.Direction; var damage = castObject.Effects.Damage; if (damage != null) { @@ -2266,7 +2267,7 @@ public override void Attack(Castable castObject) foreach (var target in actualTargets) { - if (target != null) + if (target != null && target is Monster) { Random rand = new Random(); diff --git a/hybrasyl/World.cs b/hybrasyl/World.cs index f79abfc0..491e6fe7 100644 --- a/hybrasyl/World.cs +++ b/hybrasyl/World.cs @@ -1309,6 +1309,7 @@ private void PacketHandler_0x0E_Talk(Object obj, ClientPacket packet) case "/clearstatus": { user.RemoveAllStatuses(); + user.Status = PlayerCondition.Alive; user.SendSystemMessage("All statuses cleared."); } break; @@ -2217,7 +2218,7 @@ where searchTerm.IsMatch(amap.Key) } else { - if (!user.Status.HasFlag(PlayerCondition.Alive)) + if (user.Dead) { user.SendSystemMessage("Your voice is carried away by a sudden wind."); return; @@ -2312,27 +2313,30 @@ private void PacketHandler_0x10_ClientJoin(Object obj, ClientPacket packet) loginUser.SendSpells(); loginUser.SetCitizenship(); + Insert(loginUser); Logger.DebugFormat("Elapsed time since login: {0}", loginUser.SinceLastLogin); - if (loginUser.Nation.SpawnPoints.Count != 0 && + if (loginUser.Dead) + { + loginUser.Teleport("Chaotic Threshold", 10, 10); + } + else if(loginUser.Nation.SpawnPoints.Count != 0 && loginUser.SinceLastLogin > Hybrasyl.Constants.NATION_SPAWN_TIMEOUT) { - Insert(loginUser); var spawnpoint = loginUser.Nation.SpawnPoints.First(); loginUser.Teleport(spawnpoint.MapName, spawnpoint.X, spawnpoint.Y); } else if (Maps.ContainsKey(loginUser.Location.MapId)) { - Insert(loginUser); loginUser.Teleport(loginUser.Location.MapId, (byte)loginUser.Location.X, (byte)loginUser.Location.Y); } else { // Handle any weird cases where a map someone exited on was deleted, etc // This "default" of Mileth should be set somewhere else - Insert(loginUser); loginUser.Teleport((ushort)500, (byte)50, (byte)50); } + Logger.DebugFormat("Adding {0} to hash", loginUser.Name); AddUser(loginUser); ActiveUsers[connectionId] = loginUser; From 49ee0f5f34374290cbeaababb9faf5959701775b Mon Sep 17 00:00:00 2001 From: Justin Baugh Date: Thu, 1 Dec 2016 22:03:13 -0500 Subject: [PATCH 05/26] sdk bump 0.5.5.9 --- hybrasyl/packages.config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hybrasyl/packages.config b/hybrasyl/packages.config index a96ea7d5..29b68fb6 100644 --- a/hybrasyl/packages.config +++ b/hybrasyl/packages.config @@ -5,7 +5,7 @@ - + From 8ac136e9a797d6a66ca170c9c089d9263b80705c Mon Sep 17 00:00:00 2001 From: Justin Baugh Date: Thu, 1 Dec 2016 22:33:48 -0500 Subject: [PATCH 06/26] Use different icons for all status effects (fix multiples being applied) --- hybrasyl/PlayerStatus.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/hybrasyl/PlayerStatus.cs b/hybrasyl/PlayerStatus.cs index 6015e26d..b76d3095 100644 --- a/hybrasyl/PlayerStatus.cs +++ b/hybrasyl/PlayerStatus.cs @@ -149,7 +149,7 @@ public override void OnEnd() internal class PoisonStatus : PlayerStatus { - private new static ushort Icon = 36; + private new static ushort Icon = 97; public new static string Name = "poison"; public static ushort OnTickEffect = 25; @@ -215,7 +215,7 @@ public override void OnEnd() internal class FreezeStatus : PlayerStatus { - public new static ushort Icon = 36; + public new static ushort Icon = 50; public new static string Name = "freeze"; public static ushort OnTickEffect = 40; @@ -243,7 +243,7 @@ public override void OnEnd() internal class SleepStatus : PlayerStatus { - public new static ushort Icon = 36; + public new static ushort Icon = 2; public new static string Name = "sleep"; public static ushort OnTickEffect = 28; From 0542f62c29f2a8e99ddfcdb241e07c613ba39e06 Mon Sep 17 00:00:00 2001 From: Justin Baugh Date: Thu, 1 Dec 2016 22:34:02 -0500 Subject: [PATCH 07/26] bump assembly version to 0.5.5 --- hybrasyl/Properties/AssemblyInfo.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/hybrasyl/Properties/AssemblyInfo.cs b/hybrasyl/Properties/AssemblyInfo.cs index ce78abb8..0077ab90 100644 --- a/hybrasyl/Properties/AssemblyInfo.cs +++ b/hybrasyl/Properties/AssemblyInfo.cs @@ -34,5 +34,5 @@ // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("0.5.0.*")] -[assembly: AssemblyFileVersion("0.5.0.0")] +[assembly: AssemblyVersion("0.5.5.*")] +[assembly: AssemblyFileVersion("0.5.5.0")] From 91ef3e070bf45c2eb6d8915b1516f691d06bd9f7 Mon Sep 17 00:00:00 2001 From: Justin Baugh Date: Thu, 1 Dec 2016 22:43:17 -0500 Subject: [PATCH 08/26] Add status restrictions for casting spells/skills, reorder packet handlers a bit --- hybrasyl/World.cs | 85 +++++++++++++++++++++++++---------------------- 1 file changed, 45 insertions(+), 40 deletions(-) diff --git a/hybrasyl/World.cs b/hybrasyl/World.cs index 491e6fe7..d97dce62 100644 --- a/hybrasyl/World.cs +++ b/hybrasyl/World.cs @@ -786,30 +786,6 @@ public void SetPacketHandlers() PacketHandlers[0x7B] = PacketHandler_0x7B_RequestMetafile; } - private void PacketHandler_0x3E_UseSkill(object obj, ClientPacket packet) - { - var user = (User)obj; - var slot = packet.ReadByte(); - - user.UseSkill(slot); - } - - private void PacketHandler_0x0F_UseSpell(object obj, ClientPacket packet) - { - var user = (User)obj; - var slot = packet.ReadByte(); - var target = packet.ReadUInt32(); - - user.UseSpell(slot, target); - user.Status ^= PlayerCondition.Casting; - } - - private void PacketHandler_0x13_Attack(object obj, ClientPacket packet) - { - var user = (User)obj; - user.AssailAttack(user.Direction); - } - public void SetMerchantMenuHandlers() { merchantMenuHandlers = new Dictionary() @@ -1252,19 +1228,6 @@ private void PacketHandler_0x08_DropItem(Object obj, ClientPacket packet) user.Map.AddItem(x, y, toDrop); } - [ProhibitedCondition(PlayerCondition.Frozen)] - private void PacketHandler_0x11_Turn(Object obj, ClientPacket packet) - { - var user = (User)obj; - var direction = packet.ReadByte(); - if (direction > 3) return; - user.Turn((Direction)direction); - } - - private void ProcessSlashCommands(Client client, ClientPacket packet) - { - } - private void PacketHandler_0x0E_Talk(Object obj, ClientPacket packet) { var user = (User)obj; @@ -2240,9 +2203,19 @@ where searchTerm.IsMatch(amap.Key) } } - private void CheckCommandPrivileges() + [ProhibitedCondition(PlayerCondition.InComa)] + [ProhibitedCondition(PlayerCondition.Asleep)] + [ProhibitedCondition(PlayerCondition.Frozen)] + [ProhibitedCondition(PlayerCondition.Paralyzed)] + [RequiredCondition(PlayerCondition.Alive)] + private void PacketHandler_0x0F_UseSpell(object obj, ClientPacket packet) { - throw new NotImplementedException(); + var user = (User)obj; + var slot = packet.ReadByte(); + var target = packet.ReadUInt32(); + + user.UseSpell(slot, target); + user.Status ^= PlayerCondition.Casting; } private void PacketHandler_0x0B_ClientExit(Object obj, ClientPacket packet) @@ -2352,6 +2325,25 @@ private void PacketHandler_0x10_ClientJoin(Object obj, ClientPacket packet) loginUser.Reindex(); } + [ProhibitedCondition(PlayerCondition.Frozen)] + private void PacketHandler_0x11_Turn(Object obj, ClientPacket packet) + { + var user = (User)obj; + var direction = packet.ReadByte(); + if (direction > 3) return; + user.Turn((Direction)direction); + } + + [ProhibitedCondition(PlayerCondition.InComa)] + [ProhibitedCondition(PlayerCondition.Asleep)] + [ProhibitedCondition(PlayerCondition.Frozen)] + [ProhibitedCondition(PlayerCondition.Paralyzed)] + private void PacketHandler_0x13_Attack(object obj, ClientPacket packet) + { + var user = (User)obj; + user.AssailAttack(user.Direction); + } + private void PacketHandler_0x18_ShowPlayerList(Object obj, ClientPacket packet) { var me = (User)obj; @@ -2648,7 +2640,6 @@ private void PacketHandler_0x2D_PlayerInfo(Object obj, ClientPacket packet) * 5) Send them a dialog and have them explicitly accept. * 6) If accepted, join group (see stage 0x03). */ - [ProhibitedCondition(PlayerCondition.InComa)] [RequiredCondition(PlayerCondition.Alive)] private void PacketHandler_0x2E_GroupRequest(Object obj, ClientPacket packet) @@ -3302,6 +3293,20 @@ private void PacketHandler_0x3B_AccessMessages(Object obj, ClientPacket packet) user.Enqueue(response); } + + [ProhibitedCondition(PlayerCondition.InComa)] + [ProhibitedCondition(PlayerCondition.Asleep)] + [ProhibitedCondition(PlayerCondition.Frozen)] + [ProhibitedCondition(PlayerCondition.Paralyzed)] + [RequiredCondition(PlayerCondition.Alive)] + private void PacketHandler_0x3E_UseSkill(object obj, ClientPacket packet) + { + var user = (User)obj; + var slot = packet.ReadByte(); + + user.UseSkill(slot); + } + [ProhibitedCondition(PlayerCondition.InComa)] [ProhibitedCondition(PlayerCondition.Asleep)] [ProhibitedCondition(PlayerCondition.Frozen)] From eaaca4d370abaa071da43f6749cbfa2bf4d42b73 Mon Sep 17 00:00:00 2001 From: Justin Baugh Date: Thu, 1 Dec 2016 23:20:41 -0500 Subject: [PATCH 09/26] DRY up statuses a bit; display OnTickEffect every tick; add CanCast to tell if, uh, a player can cast a spell --- hybrasyl/Objects/User.cs | 7 ++++++- hybrasyl/PlayerStatus.cs | 40 ++++++++++++---------------------------- hybrasyl/World.cs | 2 +- 3 files changed, 19 insertions(+), 30 deletions(-) diff --git a/hybrasyl/Objects/User.cs b/hybrasyl/Objects/User.cs index 2ccaede6..d9110351 100644 --- a/hybrasyl/Objects/User.cs +++ b/hybrasyl/Objects/User.cs @@ -103,10 +103,15 @@ public static string GetStorageKey(string name) public bool IsMaster { get; set; } public UserGroup Group { get; set; } - [JsonProperty] public bool Dead => !Status.HasFlag(PlayerCondition.Alive); public bool IsCasting => Status.HasFlag(PlayerCondition.Casting); + public bool CanCast + => + !(Status.HasFlag(PlayerCondition.Asleep) || + Status.HasFlag(PlayerCondition.Frozen) || Status.HasFlag(PlayerCondition.Paralyzed)); + + public Mailbox Mailbox => World.GetMailbox(Name); public bool UnreadMail => Mailbox.HasUnreadMessages; diff --git a/hybrasyl/PlayerStatus.cs b/hybrasyl/PlayerStatus.cs index b76d3095..e54f059b 100644 --- a/hybrasyl/PlayerStatus.cs +++ b/hybrasyl/PlayerStatus.cs @@ -76,12 +76,20 @@ public abstract class PlayerStatus : IPlayerStatus public virtual void OnStart() { if (OnStartMessage != string.Empty) User.SendSystemMessage(OnStartMessage); + var tickEffect = (ushort?)GetType().GetField("OnTickEffect").GetValue(null); + if (tickEffect == null) return; + if (!User.Status.HasFlag(PlayerCondition.InComa)) + User.Effect((ushort)tickEffect, 120); } public virtual void OnTick() { LastTick = DateTime.Now; if (OnTickMessage != string.Empty) User.SendSystemMessage(OnTickMessage); + var tickEffect = (ushort?) GetType().GetField("OnTickEffect").GetValue(null); + if (tickEffect == null) return; + if (!User.Status.HasFlag(PlayerCondition.InComa)) + User.Effect((ushort)tickEffect, 120); } public virtual void OnEnd() @@ -131,12 +139,9 @@ public BlindStatus(User user, int duration, int tick) : base(user, duration, tic public override void OnStart() { - base.OnStart(); + base.OnEnd(); User.ToggleBlind(); - } - public override void OnTick() - { } public override void OnEnd() @@ -165,18 +170,9 @@ public PoisonStatus(User user, int duration, int tick, double damagePerTick) : b _damagePerTick = damagePerTick; } - public override void OnStart() - { - base.OnStart(); - if (!User.Status.HasFlag(PlayerCondition.InComa)) - User.Effect(OnTickEffect, 120); - } - public override void OnTick() { base.OnTick(); - if (!User.Status.HasFlag(PlayerCondition.InComa)) - User.Effect(OnTickEffect, 120); if (_damagePerTick >= User.Hp) User.Damage(User.Hp - 1); else @@ -194,15 +190,13 @@ internal class ParalyzeStatus : PlayerStatus public ParalyzeStatus(User user, int duration, int tick) : base(user, duration, tick, Icon, Name) { - OnStartMessage = "You are in hibernation."; - OnEndMessage = "Your body thaws."; + OnStartMessage = "Stunned!"; + OnEndMessage = "You can move again."; } public override void OnStart() { base.OnStart(); - if (!User.Status.HasFlag(PlayerCondition.InComa)) - User.Effect(OnTickEffect, 120); User.ToggleParalyzed(); } public override void OnEnd() @@ -230,8 +224,6 @@ public FreezeStatus(User user, int duration, int tick) : base(user, duration, ti public override void OnStart() { base.OnStart(); - if (!User.Status.HasFlag(PlayerCondition.InComa)) - User.Effect(OnTickEffect, 120); User.ToggleFreeze(); } public override void OnEnd() @@ -258,16 +250,9 @@ public SleepStatus(User user, int duration, int tick) : base(user, duration, tic public override void OnStart() { base.OnStart(); - if (!User.Status.HasFlag(PlayerCondition.InComa)) - User.Effect(OnTickEffect, 120); User.ToggleAsleep(); } - - public override void OnTick() - { - base.OnTick(); - User.Effect(OnTickEffect, 120); - } + public override void OnEnd() { base.OnEnd(); @@ -293,7 +278,6 @@ public NearDeathStatus(User user, int duration, int tick) : base(user, duration, public override void OnStart() { base.OnStart(); - User.Effect(OnTickEffect, 120); User.ToggleNearDeath(); User.Group?.SendMessage($"{User.Name} is dying!"); } diff --git a/hybrasyl/World.cs b/hybrasyl/World.cs index d97dce62..8f541103 100644 --- a/hybrasyl/World.cs +++ b/hybrasyl/World.cs @@ -3807,7 +3807,7 @@ private void PacketHandler_0x4E_CastLine(object obj, ClientPacket packet) var user = (User) obj; var textLength = packet.ReadByte(); var text = packet.Read(textLength); - + if (!user.CanCast) return; var x0D = new ServerPacketStructures.CastLine() {ChatType = 2, LineLength = textLength, LineText = Encoding.UTF8.GetString(text), TargetId = user.Id}; var enqueue = x0D.Packet(); user.Enqueue(enqueue); From a0bc9bcbedc6d0f166166409bef9674f82d8ddba Mon Sep 17 00:00:00 2001 From: Justin Baugh Date: Sat, 3 Dec 2016 22:53:59 -0500 Subject: [PATCH 10/26] Reorganize Scripting into separate files; add headers; remove unused junk --- hybrasyl/Dialogs.cs | 1 + hybrasyl/Hybrasyl.csproj | 9 + hybrasyl/Map.cs | 8 - hybrasyl/Objects/ItemObject.cs | 1 + hybrasyl/Objects/Merchant.cs | 1 + hybrasyl/Objects/Reactor.cs | 2 + hybrasyl/Objects/WorldObject.cs | 1 + hybrasyl/Scripting.cs | 896 ------------------ hybrasyl/Scripting/HybrasylDialog.cs | 55 ++ hybrasyl/Scripting/HybrasylDialogSequence.cs | 47 + .../{Pursuit.cs => Scripting/HybrasylMap.cs} | 71 +- hybrasyl/Scripting/HybrasylUser.cs | 298 ++++++ hybrasyl/Scripting/HybrasylWorld.cs | 113 +++ hybrasyl/Scripting/HybrasylWorldObject.cs | 93 ++ hybrasyl/Scripting/Script.cs | 278 ++++++ hybrasyl/Scripting/ScriptInvocation.cs | 50 + hybrasyl/Scripting/ScriptProcessor.cs | 125 +++ hybrasyl/World.cs | 5 +- 18 files changed, 1119 insertions(+), 935 deletions(-) delete mode 100644 hybrasyl/Scripting.cs create mode 100644 hybrasyl/Scripting/HybrasylDialog.cs create mode 100644 hybrasyl/Scripting/HybrasylDialogSequence.cs rename hybrasyl/{Pursuit.cs => Scripting/HybrasylMap.cs} (62%) create mode 100644 hybrasyl/Scripting/HybrasylUser.cs create mode 100644 hybrasyl/Scripting/HybrasylWorld.cs create mode 100644 hybrasyl/Scripting/HybrasylWorldObject.cs create mode 100644 hybrasyl/Scripting/Script.cs create mode 100644 hybrasyl/Scripting/ScriptInvocation.cs create mode 100644 hybrasyl/Scripting/ScriptProcessor.cs diff --git a/hybrasyl/Dialogs.cs b/hybrasyl/Dialogs.cs index 32c805ae..98f3a8d1 100755 --- a/hybrasyl/Dialogs.cs +++ b/hybrasyl/Dialogs.cs @@ -25,6 +25,7 @@ using System; using System.Collections.Generic; using System.Linq; +using Hybrasyl.Scripting; namespace Hybrasyl { diff --git a/hybrasyl/Hybrasyl.csproj b/hybrasyl/Hybrasyl.csproj index 88ddb461..3dfee6ee 100644 --- a/hybrasyl/Hybrasyl.csproj +++ b/hybrasyl/Hybrasyl.csproj @@ -146,6 +146,15 @@ + + + + + + + + + diff --git a/hybrasyl/Map.cs b/hybrasyl/Map.cs index bbd0751c..d65d87e1 100755 --- a/hybrasyl/Map.cs +++ b/hybrasyl/Map.cs @@ -36,14 +36,6 @@ using System.Xml; using log4net.Appender; -namespace Hybrasyl.Properties -{ - public partial class Door - { - public bool Open { get; set; } - } -} - namespace Hybrasyl { diff --git a/hybrasyl/Objects/ItemObject.cs b/hybrasyl/Objects/ItemObject.cs index 9573f04a..86189859 100644 --- a/hybrasyl/Objects/ItemObject.cs +++ b/hybrasyl/Objects/ItemObject.cs @@ -23,6 +23,7 @@ using Hybrasyl.Enums; using Hybrasyl.Items; +using Hybrasyl.Scripting; using System; namespace Hybrasyl.Objects diff --git a/hybrasyl/Objects/Merchant.cs b/hybrasyl/Objects/Merchant.cs index 3a1a66e4..b2a93d2e 100755 --- a/hybrasyl/Objects/Merchant.cs +++ b/hybrasyl/Objects/Merchant.cs @@ -22,6 +22,7 @@ using Hybrasyl.Items; +using Hybrasyl.Scripting; using System; using System.Collections.Generic; diff --git a/hybrasyl/Objects/Reactor.cs b/hybrasyl/Objects/Reactor.cs index c0afcf7e..f4eac057 100644 --- a/hybrasyl/Objects/Reactor.cs +++ b/hybrasyl/Objects/Reactor.cs @@ -20,6 +20,8 @@ * */ + using Hybrasyl.Scripting; + namespace Hybrasyl.Objects { public class Reactor : VisibleObject diff --git a/hybrasyl/Objects/WorldObject.cs b/hybrasyl/Objects/WorldObject.cs index 2c204212..e3ab589e 100644 --- a/hybrasyl/Objects/WorldObject.cs +++ b/hybrasyl/Objects/WorldObject.cs @@ -26,6 +26,7 @@ using System.Drawing; using C3; using Hybrasyl.Dialogs; +using Hybrasyl.Scripting; using log4net; using Newtonsoft.Json; diff --git a/hybrasyl/Scripting.cs b/hybrasyl/Scripting.cs deleted file mode 100644 index 4db3d2cb..00000000 --- a/hybrasyl/Scripting.cs +++ /dev/null @@ -1,896 +0,0 @@ -/* - * This file is part of Project Hybrasyl. - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the Affero General Public License as published by - * the Free Software Foundation, version 3. - * - * This program is distributed in the hope that it will be useful, but - * without ANY WARRANTY; without even the implied warranty of MERCHANTABILITY - * or FITNESS FOR A PARTICULAR PURPOSE. See the Affero General Public License - * for more details. - * - * You should have received a copy of the Affero General Public License along - * with this program. If not, see . - * - * (C) 2013 Justin Baugh (baughj@hybrasyl.com) - * (C) 2015 Project Hybrasyl (info@hybrasyl.com) - * - * Authors: Justin Baugh - * Kyle Speck - */ - -using Hybrasyl.Dialogs; -using Hybrasyl.Enums; -using Hybrasyl.Objects; -using Hybrasyl.Properties; -using IronPython.Hosting; -using IronPython.Runtime; -using IronPython.Runtime.Operations; -using log4net; -using Microsoft.Scripting.Hosting; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Linq.Expressions; -using System.Reflection; -using Hybrasyl.Items; - -namespace Hybrasyl -{ - - public struct ScriptInvocation - { - public dynamic Function; - public Script Script; - public WorldObject Associate; - public WorldObject Invoker; - - public ScriptInvocation(dynamic function = null, WorldObject associate = null, WorldObject invoker = null, Script script = null) - { - Function = function; - Associate = associate; - Invoker = invoker; - Script = null; - } - - public bool Execute(params object[] parameters) - { - if (Script != null) - return Script.ExecuteFunction(this, parameters); - else - return Associate.Script.ExecuteFunction(this, parameters); - } - } - - public delegate void UpdateHandler(); - - public class Script - { - public static readonly ILog Logger = LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType); - public ScriptSource Source { get; set; } - public string Name { get; set; } - public string Path { get; private set; } - - public HybrasylScriptProcessor Processor { get; set; } - public CompiledCode Compiled { get; private set; } - public ScriptScope Scope { get; set; } - public dynamic Instance { get; set; } - public HybrasylWorldObject Associate { get; private set; } - - public bool Disabled { get; set; } - public string CompilationError { get; private set; } - public string LastRuntimeError { get; private set; } - - public Script Clone() - { - var clone = new Script(Path, Processor); - // Reload and reinstantiate the script with a new ScriptScope - Scope = Processor.Engine.CreateScope(); - clone.Load(); - clone.InstantiateScriptable(); - return clone; - } - - public Script(string path, HybrasylScriptProcessor processor) - { - Path = path; - Compiled = null; - Source = null; - Processor = processor; - Disabled = false; - CompilationError = string.Empty; - LastRuntimeError = string.Empty; - } - - public void AssociateScriptWithObject(WorldObject obj) - { - Associate = new HybrasylWorldObject(obj); - obj.Script = this; - } - - public dynamic GetObjectWrapper(WorldObject obj) - { - if (obj is User) - return new HybrasylUser(obj as User); - return new HybrasylWorldObject(obj); - } - /// - /// Load the script from disk, recompile it into bytecode, and execute it. - /// - /// boolean indicating whether the script was reloaded or not - public bool Load() - { - string scriptText; - try - { - scriptText = File.ReadAllText(Path); - } - catch (Exception e) - { - Logger.ErrorFormat("Couldn't open script {0}: {1}", Path, e.ToString()); - Disabled = true; - CompilationError = e.ToString(); - return false; - } - - scriptText = //HybrasylScriptProcessor.RestrictStdlib - HybrasylScriptProcessor.HybrasylImports + scriptText; - - Source = - Processor.Engine.CreateScriptSourceFromString(scriptText); - - Name = System.IO.Path.GetFileName(Path).ToLower(); - - try - { - Compile(); - Compiled.Execute(Scope); - Disabled = false; - } - catch (Exception e) - { - var pythonFrames = PythonOps.GetDynamicStackFrames(e); - var exceptionString = Processor.Engine.GetService().FormatException(e); - Logger.ErrorFormat("script {0} encountered error, Python stack follows", Path); - Logger.ErrorFormat("{0}", exceptionString); - Disabled = true; - CompilationError = exceptionString; - return false; - } - return true; - } - - /// - /// Compile the script, using the global Hybrasyl engine. - /// - /// boolean indicating success or failure (might raise exception in the future) - public bool Compile() - { - if (Source == null) return false; - Compiled = Source.Compile(); - return true; - } - - /// - /// If the script has a Scriptable class (used for WorldObject hooks), instantiate it. - /// - public bool InstantiateScriptable() - { - // First, disable the script, then if we have an instance, delete it. - Disabled = true; - - if (Instance != null) - Instance = null; - - try - { - var klass = Scope.GetVariable("Scriptable"); - Scope.SetVariable("world", Processor.World); - if (Associate != null) - { - Scope.SetVariable("npc", Associate); - Associate.Obj.ResetPursuits(); - } - Instance = Processor.Engine.Operations.CreateInstance(klass); - Disabled = false; - } - catch (Exception e) - { - var pythonFrames = PythonOps.GetDynamicStackFrames(e); - var exceptionString = Processor.Engine.GetService().FormatException(e); - Logger.ErrorFormat("script {0} encountered error, Python stack follows", Path); - Logger.ErrorFormat("{0}", exceptionString); - Logger.ErrorFormat("script {0} now disabled", Path); - Disabled = true; - CompilationError = exceptionString; - return false; - } - return true; - } - - public bool ExecuteFunction(ScriptInvocation invocation, params object[] parameters) - { - if (Disabled) - return false; - - if (!Processor.Engine.Operations.IsCallable(invocation.Function)) return false; - if (invocation.Invoker is User) - { - Scope.SetVariable("invoker", new HybrasylUser(invocation.Invoker as User)); - } - else - { - Scope.SetVariable("invoker", new HybrasylWorldObject(invocation.Invoker as WorldObject)); - } - if (invocation.Associate is WorldObject) - { - Scope.SetVariable("npc", new HybrasylWorldObject(invocation.Associate as WorldObject)); - } - try - { - var ret = Processor.Engine.Operations.Invoke(invocation.Function, parameters); - if (ret is bool) - return (bool)ret; - } - catch (Exception e) - { - var pythonFrames = PythonOps.GetDynamicStackFrames(e); - var exceptionString = Processor.Engine.GetService().FormatException(e); - Logger.ErrorFormat("script {0} encountered error, Python stack follows", Path); - Logger.ErrorFormat("{0}", exceptionString); - Logger.ErrorFormat("script {0} now disabled", Path); - LastRuntimeError = exceptionString; - return false; - } - return true; - } - - /// - /// Attach a Scriptable to an in game NPC. - /// - /// - public bool AttachScriptable(WorldObject obj) - { - Associate = new HybrasylWorldObject(obj); - Logger.InfoFormat("Scriptable name: {0}", Instance.name); - return true; - } - - /// - /// If the script has a Scriptable class and the given function exists, execute it. - /// - /// - /// The parameters to pass to the function. - public void ExecuteScriptableFunction(string name, params object[] parameters) - { - if (Disabled) - return; - - Scope.SetVariable("world", Processor.World); - Scope.SetVariable("npc", Associate); - - try - { - Processor.Engine.Operations.InvokeMember(Instance, name, parameters); - } - catch (System.NotImplementedException) - { - Logger.DebugFormat("script {0}: missing member {1}", Path, name); - } - catch (Exception e) - { - var pythonFrames = PythonOps.GetDynamicStackFrames(e); - var exceptionString = Processor.Engine.GetService().FormatException(e); - Logger.ErrorFormat("script {0} encountered error, Python stack follows", Path); - Logger.ErrorFormat("{0}", exceptionString); - Logger.ErrorFormat("script {0} now disabled"); - LastRuntimeError = exceptionString; - } - } - - /// - /// Execute the script in the passed scope. - /// - /// The ScriptScope the script will execute in. - public void ExecuteScript(WorldObject caller = null) - { - dynamic resolvedCaller; - - if (caller != null) - { - if (caller is User) - resolvedCaller = new HybrasylUser(caller as User); - else - resolvedCaller = new HybrasylWorldObject(caller); - Scope.SetVariable("npc", resolvedCaller); - } - - Scope.SetVariable("world", Processor.World); - Compiled.Execute(Scope); - } - - } - - public class HybrasylScriptProcessor - { - public static readonly ILog Logger = LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType); - - public ScriptEngine Engine { get; private set; } - public Dictionary Scripts { get; private set; } - public HybrasylWorld World { get; private set; } - - // We make an attempt to limit Hybrasyl scripts to stdlib, - // excluding "dangerous" functions (imports are disallowed outright, - // along with file i/o or eval/exec/ - public static readonly string RestrictStdlib = - @"__builtins__.__import__ = None -__builtins__.reload = None -__builtins__.open = None -__builtins__.eval = None -__builtins__.compile = None -__builtins__.execfile = None -__builtins__.file = None -__builtins__.memoryview = None -__builtins__.raw_input = None - -"; - - public static readonly string HybrasylImports = - @"import clr -clr.AddReference('Hybrasyl') -from Hybrasyl.Enums import * -from System import DateTime - -"; - - public HybrasylScriptProcessor(World world) - { - Engine = Python.CreateEngine(); - var paths = Engine.GetSearchPaths(); - // FIXME: obvious - paths.Add(@"C:\Program Files (x86)\IronPython 2.7\Lib"); - paths.Add(@"C:\Python27\Lib"); - Engine.SetSearchPaths(paths); - Engine.ImportModule("random"); - - Scripts = new Dictionary(); - World = new HybrasylWorld(world); - } - - public bool TryGetScript(string scriptName, out Script script) - { - // Try to find "name.py" or "name" - if (Scripts.TryGetValue($"{scriptName.ToLower()}.py", out script)) - { - return true; - } - return Scripts.TryGetValue(scriptName.ToLower(), out script); - } - - public Script GetScript(string scriptName) - { - Script script; - // Try to find "name.py" or "name" - var exists = Scripts.TryGetValue($"{scriptName.ToLower()}.py", out script); - if (!exists) - { - if (Scripts.TryGetValue($"{scriptName.ToLower()}", out script)) - return script; - } - else - return script; - return null; - } - - public bool RegisterScript(Script script) - { - script.Scope = Engine.CreateScope(); - script.Load(); - Scripts[script.Name] = script; - - if (script.Disabled) - { - Logger.ErrorFormat("{0}: error loading script", script.Name); - return false; - } - else - { - Logger.InfoFormat("{0}: loaded successfully", script.Name); - return true; - } - } - - public bool DeregisterScript(string scriptname) - { - Scripts[scriptname] = null; - return true; - } - } - - // We define some wrapper classes here, which we actually use to expose our scripting API. - // In theory this is faster than isolation with an AppDomain, since we aren't serializing anything, - // and adding / changing to the exposed API is as simple as modifying these classes. I'm not entirely - // sure this is the best approach but it's what we're going with originally as it's the simplest (and I - // believe, fastest) approach. - - public class HybrasylDialog - { - internal Dialog Dialog { get; set; } - internal DialogSequence Sequence { get; set; } - - public HybrasylDialog(Dialog dialog) - { - Dialog = dialog; - } - - public void SetNpcDisplaySprite(int displaySprite) - { - Dialog.DisplaySprite = (ushort)(0x4000 + displaySprite); - } - - public void SetItemDisplaySprite(int displaySprite) - { - Dialog.DisplaySprite = (ushort)(0x8000 + displaySprite); - } - - public void AssociateDialogWithSequence(DialogSequence sequence) - { - Sequence = sequence; - sequence.AddDialog(Dialog); - } - - } - - public class HybrasylDialogSequence - { - internal DialogSequence Sequence { get; private set; } - - public HybrasylDialogSequence(string sequenceName) - { - Sequence = new DialogSequence(sequenceName); - } - - public void AddDialog(HybrasylDialog scriptDialog) - { - scriptDialog.AssociateDialogWithSequence(Sequence); - } - - public void AddCheck(dynamic check) - { - Sequence.AddPreDisplayCallback(check); - } - } - - public class HybrasylWorld - { - public static readonly ILog Logger = LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType); - - internal World World { get; set; } - - public HybrasylWorld(World world) - { - World = world; - } - - public HybrasylDialogSequence NewDialogSequence(string sequenceName, params object[] list) - { - var dialogSequence = new HybrasylDialogSequence(sequenceName); - foreach (var entry in list) - { - Logger.InfoFormat("Type is {0}", entry.GetType().ToString()); - if (entry is HybrasylDialog) - { - var newdialog = entry as HybrasylDialog; - dialogSequence.AddDialog(newdialog); - } - else if (entry is PythonFunction) - { - var action = entry as PythonFunction; - } - - } - return dialogSequence; - } - - public HybrasylDialog NewDialog(string displayText, dynamic callback = null) - { - var dialog = new SimpleDialog(displayText); - dialog.SetCallbackHandler(callback); - return new HybrasylDialog(dialog); - } - - public HybrasylDialog NewTextDialog(string displayText, string topCaption, string bottomCaption, int inputLength = 254, dynamic handler = null, dynamic callback = null) - { - var dialog = new TextDialog(displayText, topCaption, bottomCaption, inputLength); - dialog.setInputHandler(handler); - dialog.SetCallbackHandler(callback); - return new HybrasylDialog(dialog); - } - - public HybrasylDialog NewOptionsDialog(string displayText, dynamic optionsStructure, dynamic handler = null, dynamic callback = null) - { - var dialog = new OptionsDialog(displayText); - dialog.SetCallbackHandler(callback); - - if (optionsStructure is IronPython.Runtime.List) - { - // A simple options dialog with a callback handler for the response - var optionlist = optionsStructure as IronPython.Runtime.List; - foreach (var option in optionsStructure) - { - if (option is string) - { - dialog.AddDialogOption(option as string); - } - } - if (handler != null) - { - dialog.setInputHandler(handler); - Logger.InfoFormat("Input handler associated with dialog"); - } - } - else if (optionsStructure is IronPython.Runtime.PythonDictionary) - { - var hash = optionsStructure as IronPython.Runtime.PythonDictionary; - foreach (var key in hash.Keys) - { - if (key is string) - { - dialog.AddDialogOption(key as string, hash[key]); - } - } - - } - return new HybrasylDialog(dialog); - } - } - - public class HybrasylMap - { - private Map Map { get; set; } - - public HybrasylMap(Map map) - { - Map = map; - } - - public bool DropItem(string name, int x = -1, int y = -1) - { - return false; - } - - } - - public class HybrasylUser - { - public static readonly ILog Logger = LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType); - - internal User User { get; set; } - internal HybrasylWorld World { get; set; } - internal HybrasylMap Map { get; set; } - public string Name => User.Name; - - public HybrasylUser(User user) - { - User = user; - World = new HybrasylWorld(user.World); - Map = new HybrasylMap(user.Map); - } - - public List GetViewportObjects() - { - return new List(); - } - - public List GetViewportPlayers() - { - return new List(); - } - - public void Resurrect() - { - User.Resurrect(); - } - - public HybrasylUser GetFacingUser() - { - var facing = User.GetFacingUser(); - return facing != null ? new HybrasylUser(facing) : null; - } - - public List GetFacingObjects() - { - return User.GetFacingObjects().Select(item => new HybrasylWorldObject(item)).ToList(); - } - - public void EndComa() - { - User.EndComa(); - } - - public dynamic GetLegendMark(string prefix) - { - LegendMark mark; - return User.Legend.TryGetMark(prefix, out mark) ? mark : (object)null; - } - - public Legend GetLegend() - { - return User.Legend; - } - - public bool AddLegendMark(LegendIcon icon, LegendColor color, string text, DateTime created, string prefix=default(string), bool isPublic = true, int quantity = 0) - { - try - { - return User.Legend.AddMark(icon, color, text, created, prefix, isPublic, quantity); - } - catch (ArgumentException) - { - Logger.ErrorFormat("Legend mark: {0}: duplicate prefix {1}", User.Name, prefix); - } - return false; - } - - public bool RemoveLegendMark(string prefix) - { - return User.Legend.RemoveMark(prefix); - } - - public bool ModifyLegendMark(string prefix, int quantity, bool isPublic) - { - LegendMark mark; - if (!User.Legend.TryGetMark(prefix, out mark)) return false; - mark.Quantity = quantity; - mark.Public = isPublic; - return true; - } - - public void SetSessionFlag(string flag, dynamic value) - { - try - { - User.SetSessionFlag(flag, value.ToString()); - Logger.DebugFormat("{0} - set session flag {1} to {2}", User.Name, flag, value.toString()); - } - catch (Exception e) - { - Logger.WarnFormat("{0}: value could not be converted to string? {1}", User.Name, e.ToString()); - } - } - - public void SetFlag(string flag, dynamic value) - { - try - { - User.SetFlag(flag, value.ToString()); - } - catch (Exception e) - { - Logger.WarnFormat("{0}: value could not be converted to string? {1}", User.Name, e.ToString()); - } - - } - - public string GetSessionFlag(string flag) - { - return User.GetSessionFlag(flag); - } - - public string GetFlag(string flag) - { - return User.GetFlag(flag); - } - - public void DisplayEffect(ushort effect, short speed = 100, bool global = true) - { - if (!global) - User.SendEffect(User.Id, effect, speed); - else - User.Effect(effect, speed); - } - - public void DisplayEffectAtCoords(short x, short y, ushort effect, short speed = 100, bool global = true) - { - if (!global) - User.SendEffect(x, y, effect, speed); - else - User.Effect(x, y, effect, speed); - } - - public void Teleport(String location, int x, int y) - { - User.Teleport(location, (byte) x, (byte) y); - } - - public void SoundEffect(byte sound) - { - User.SendSound(sound); - } - - public void HealToFull() - { - User.Heal(User.MaximumHp); - } - - public void Heal(int heal) - { - User.Heal((double)heal); - } - - public void Damage(int damage, Enums.Element element = Enums.Element.None, - Enums.DamageType damageType = Enums.DamageType.Direct) - { - User.Damage((double) damage, element, damageType); - } - - public bool GiveItem(string name) - { - // Does the item exist? - Item theitem; - if (Game.World.ItemCatalog.TryGetValue(new Tuple(User.Sex, name), out theitem) || - Game.World.ItemCatalog.TryGetValue(new Tuple(Sex.Neutral, name), out theitem)) - { - Logger.DebugFormat("giving item {0} to {1}", name, User.Name); - var itemobj = Game.World.CreateItem(theitem.Id); - Game.World.Insert(itemobj); - User.AddItem(itemobj); - return true; - } - else - { - Logger.DebugFormat("item {0} cannot be found", name); - } - return false; - } - - public bool TakeItem(string name) - { - return false; - } - - public bool GiveExperience(int exp) - { - SystemMessage($"{exp} experience!"); - User.GiveExperience((uint)exp); - return true; - } - - public bool TakeExperience(int exp) - { - User.Experience -= (uint)exp; - SystemMessage($"Your world spins as your insight leaves you ((-{exp} experience!))"); - User.UpdateAttributes(StatUpdateFlags.Experience); - return true; - } - - public void SystemMessage(string message) - { - // This is a typical client "orange message" - User.SendMessage(message, Hybrasyl.MessageTypes.SYSTEM_WITH_OVERHEAD); - } - - - public void Whisper(string name, string message) - { - User.SendWhisper(name, message); - } - - public void Mail(string name, string message) - { - } - - public void StartDialogSequence(string sequenceName, HybrasylWorldObject associate) - { - DialogSequence newSequence; - if (User.World.GlobalSequencesCatalog.TryGetValue(sequenceName, out newSequence)) - { - newSequence.ShowTo(User, (VisibleObject)associate.Obj); - // End previous sequence - User.DialogState.EndDialog(); - User.DialogState.StartDialog(associate.Obj as VisibleObject, newSequence); - } - - } - - public void StartSequence(string sequenceName, HybrasylWorldObject associateOverride = null) - { - DialogSequence sequence; - VisibleObject associate; - Logger.DebugFormat("{0} starting sequence {1}", User.Name, sequenceName); - - // If we're using a new associate, we will consult that to find our sequence - associate = associateOverride == null ? User.DialogState.Associate as VisibleObject : associateOverride.Obj as VisibleObject; - - // Use the local catalog for sequences first, then consult the global catalog - - if (!associate.SequenceCatalog.TryGetValue(sequenceName, out sequence)) - { - if (!User.World.GlobalSequencesCatalog.TryGetValue(sequenceName, out sequence)) - { - Logger.ErrorFormat("called from {0}: sequence name {1} cannot be found!", - associate.Name, sequenceName); - // To be safe, end all dialogs and basically abort - User.DialogState.EndDialog(); - return; - } - } - - // sequence should now be our target sequence, let's end the current state and start a new one - - User.DialogState.EndDialog(); - User.DialogState.StartDialog(associate, sequence); - User.DialogState.ActiveDialog.ShowTo(User, associate); - } - - } - - public class HybrasylWorldObject - { - internal WorldObject Obj { get; set; } - - public HybrasylWorldObject(WorldObject obj) - { - Obj = obj; - } - - public void DisplayPursuits(dynamic invoker) - { - if (Obj is Merchant) - { - var merchant = Obj as Merchant; - if (invoker is HybrasylUser) - { - var hybUser = (HybrasylUser) invoker; - merchant.DisplayPursuits(hybUser.User); - } - } - - } - - public void Destroy() - { - if (Obj is ItemObject || Obj is Gold) - { - Game.World.Remove(Obj); - } - } - - public void AddPursuit(HybrasylDialogSequence hybrasylSequence) - { - if (Obj is VisibleObject && !(Obj is User)) - { - var vobj = Obj as VisibleObject; - vobj.AddPursuit(hybrasylSequence.Sequence); - } - - } - - public void RegisterSequence(HybrasylDialogSequence hybrasylSequence) - { - if (Obj is VisibleObject && !(Obj is User)) - { - var vobj = Obj as VisibleObject; - vobj.RegisterDialogSequence(hybrasylSequence.Sequence); - } - } - - public void RegisterGlobalSequence(HybrasylDialogSequence globalSequence) - { - Game.World.RegisterGlobalSequence(globalSequence.Sequence); - } - - public void Say(string message) - { - if (Obj is Creature) - { - var creature = Obj as Creature; - creature.Say(message); - } - } - - } - -} diff --git a/hybrasyl/Scripting/HybrasylDialog.cs b/hybrasyl/Scripting/HybrasylDialog.cs new file mode 100644 index 00000000..d0102061 --- /dev/null +++ b/hybrasyl/Scripting/HybrasylDialog.cs @@ -0,0 +1,55 @@ +/* + * This file is part of Project Hybrasyl. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the Affero General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but + * without ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the Affero General Public License + * for more details. + * + * You should have received a copy of the Affero General Public License along + * with this program. If not, see . + * + * (C) 2013 Justin Baugh (baughj@hybrasyl.com) + * (C) 2015-2016 Project Hybrasyl (info@hybrasyl.com) + * + * For contributors and individual authors please refer to CONTRIBUTORS.MD. + * + */ + +using Hybrasyl.Dialogs; + +namespace Hybrasyl.Scripting +{ + + public class HybrasylDialog + { + internal Dialog Dialog { get; set; } + internal DialogSequence Sequence { get; set; } + + public HybrasylDialog(Dialog dialog) + { + Dialog = dialog; + } + + public void SetNpcDisplaySprite(int displaySprite) + { + Dialog.DisplaySprite = (ushort)(0x4000 + displaySprite); + } + + public void SetItemDisplaySprite(int displaySprite) + { + Dialog.DisplaySprite = (ushort)(0x8000 + displaySprite); + } + + public void AssociateDialogWithSequence(DialogSequence sequence) + { + Sequence = sequence; + sequence.AddDialog(Dialog); + } + + } +} diff --git a/hybrasyl/Scripting/HybrasylDialogSequence.cs b/hybrasyl/Scripting/HybrasylDialogSequence.cs new file mode 100644 index 00000000..74cff400 --- /dev/null +++ b/hybrasyl/Scripting/HybrasylDialogSequence.cs @@ -0,0 +1,47 @@ +/* + * This file is part of Project Hybrasyl. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the Affero General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but + * without ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the Affero General Public License + * for more details. + * + * You should have received a copy of the Affero General Public License along + * with this program. If not, see . + * + * (C) 2013 Justin Baugh (baughj@hybrasyl.com) + * (C) 2015-2016 Project Hybrasyl (info@hybrasyl.com) + * + * For contributors and individual authors please refer to CONTRIBUTORS.MD. + * + */ + + using Hybrasyl.Dialogs; + +namespace Hybrasyl.Scripting +{ + + public class HybrasylDialogSequence + { + internal DialogSequence Sequence { get; private set; } + + public HybrasylDialogSequence(string sequenceName) + { + Sequence = new DialogSequence(sequenceName); + } + + public void AddDialog(HybrasylDialog scriptDialog) + { + scriptDialog.AssociateDialogWithSequence(Sequence); + } + + public void AddCheck(dynamic check) + { + Sequence.AddPreDisplayCallback(check); + } + } +} diff --git a/hybrasyl/Pursuit.cs b/hybrasyl/Scripting/HybrasylMap.cs similarity index 62% rename from hybrasyl/Pursuit.cs rename to hybrasyl/Scripting/HybrasylMap.cs index f3257581..73fc1d46 100644 --- a/hybrasyl/Pursuit.cs +++ b/hybrasyl/Scripting/HybrasylMap.cs @@ -1,29 +1,42 @@ -/* - * This file is part of Project Hybrasyl. - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the Affero General Public License as published by - * the Free Software Foundation, version 3. - * - * This program is distributed in the hope that it will be useful, but - * without ANY WARRANTY; without even the implied warranty of MERCHANTABILITY - * or FITNESS FOR A PARTICULAR PURPOSE. See the Affero General Public License - * for more details. - * - * You should have received a copy of the Affero General Public License along - * with this program. If not, see . - * - * (C) 2013 Justin Baugh (baughj@hybrasyl.com) - * (C) 2015 Project Hybrasyl (info@hybrasyl.com) - * - * Authors: Justin Baugh - * Kyle Speck - */ - -namespace Hybrasyl -{ - public class Pursuit - { - public string Name { get; set; } - } -} +/* + * This file is part of Project Hybrasyl. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the Affero General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but + * without ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the Affero General Public License + * for more details. + * + * You should have received a copy of the Affero General Public License along + * with this program. If not, see . + * + * (C) 2013 Justin Baugh (baughj@hybrasyl.com) + * (C) 2015-2016 Project Hybrasyl (info@hybrasyl.com) + * + * For contributors and individual authors please refer to CONTRIBUTORS.MD. + * + */ + +namespace Hybrasyl.Scripting +{ + + public class HybrasylMap + { + private Map Map { get; set; } + + public HybrasylMap(Map map) + { + Map = map; + } + + public bool DropItem(string name, int x = -1, int y = -1) + { + return false; + } + + } + +} diff --git a/hybrasyl/Scripting/HybrasylUser.cs b/hybrasyl/Scripting/HybrasylUser.cs new file mode 100644 index 00000000..582be14b --- /dev/null +++ b/hybrasyl/Scripting/HybrasylUser.cs @@ -0,0 +1,298 @@ +/* + * This file is part of Project Hybrasyl. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the Affero General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but + * without ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the Affero General Public License + * for more details. + * + * You should have received a copy of the Affero General Public License along + * with this program. If not, see . + * + * (C) 2013 Justin Baugh (baughj@hybrasyl.com) + * (C) 2015-2016 Project Hybrasyl (info@hybrasyl.com) + * + * For contributors and individual authors please refer to CONTRIBUTORS.MD. + * + */ + +using System; +using System.Collections.Generic; +using System.Linq; +using Hybrasyl.Dialogs; +using Hybrasyl.Enums; +using Hybrasyl.Items; +using Hybrasyl.Objects; +using log4net; + +namespace Hybrasyl.Scripting +{ + + public class HybrasylUser + { + public static readonly ILog Logger = LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType); + + internal User User { get; set; } + internal HybrasylWorld World { get; set; } + internal HybrasylMap Map { get; set; } + public string Name => User.Name; + + public HybrasylUser(User user) + { + User = user; + World = new HybrasylWorld(user.World); + Map = new HybrasylMap(user.Map); + } + + public List GetViewportObjects() + { + return new List(); + } + + public List GetViewportPlayers() + { + return new List(); + } + + public void Resurrect() + { + User.Resurrect(); + } + + public HybrasylUser GetFacingUser() + { + var facing = User.GetFacingUser(); + return facing != null ? new HybrasylUser(facing) : null; + } + + public List GetFacingObjects() + { + return User.GetFacingObjects().Select(item => new HybrasylWorldObject(item)).ToList(); + } + + public void EndComa() + { + User.EndComa(); + } + + public dynamic GetLegendMark(string prefix) + { + LegendMark mark; + return User.Legend.TryGetMark(prefix, out mark) ? mark : (object)null; + } + + public Legend GetLegend() + { + return User.Legend; + } + + public bool AddLegendMark(LegendIcon icon, LegendColor color, string text, DateTime created, string prefix = default(string), bool isPublic = true, int quantity = 0) + { + try + { + return User.Legend.AddMark(icon, color, text, created, prefix, isPublic, quantity); + } + catch (ArgumentException) + { + Logger.ErrorFormat("Legend mark: {0}: duplicate prefix {1}", User.Name, prefix); + } + return false; + } + + public bool RemoveLegendMark(string prefix) + { + return User.Legend.RemoveMark(prefix); + } + + public bool ModifyLegendMark(string prefix, int quantity, bool isPublic) + { + LegendMark mark; + if (!User.Legend.TryGetMark(prefix, out mark)) return false; + mark.Quantity = quantity; + mark.Public = isPublic; + return true; + } + + public void SetSessionFlag(string flag, dynamic value) + { + try + { + User.SetSessionFlag(flag, value.ToString()); + Logger.DebugFormat("{0} - set session flag {1} to {2}", User.Name, flag, value.toString()); + } + catch (Exception e) + { + Logger.WarnFormat("{0}: value could not be converted to string? {1}", User.Name, e.ToString()); + } + } + + public void SetFlag(string flag, dynamic value) + { + try + { + User.SetFlag(flag, value.ToString()); + } + catch (Exception e) + { + Logger.WarnFormat("{0}: value could not be converted to string? {1}", User.Name, e.ToString()); + } + + } + + public string GetSessionFlag(string flag) + { + return User.GetSessionFlag(flag); + } + + public string GetFlag(string flag) + { + return User.GetFlag(flag); + } + + public void DisplayEffect(ushort effect, short speed = 100, bool global = true) + { + if (!global) + User.SendEffect(User.Id, effect, speed); + else + User.Effect(effect, speed); + } + + public void DisplayEffectAtCoords(short x, short y, ushort effect, short speed = 100, bool global = true) + { + if (!global) + User.SendEffect(x, y, effect, speed); + else + User.Effect(x, y, effect, speed); + } + + public void Teleport(String location, int x, int y) + { + User.Teleport(location, (byte)x, (byte)y); + } + + public void SoundEffect(byte sound) + { + User.SendSound(sound); + } + + public void HealToFull() + { + User.Heal(User.MaximumHp); + } + + public void Heal(int heal) + { + User.Heal((double)heal); + } + + public void Damage(int damage, Enums.Element element = Enums.Element.None, + Enums.DamageType damageType = Enums.DamageType.Direct) + { + User.Damage((double)damage, element, damageType); + } + + public bool GiveItem(string name) + { + // Does the item exist? + Item theitem; + if (Game.World.ItemCatalog.TryGetValue(new Tuple(User.Sex, name), out theitem) || + Game.World.ItemCatalog.TryGetValue(new Tuple(Sex.Neutral, name), out theitem)) + { + Logger.DebugFormat("giving item {0} to {1}", name, User.Name); + var itemobj = Game.World.CreateItem(theitem.Id); + Game.World.Insert(itemobj); + User.AddItem(itemobj); + return true; + } + else + { + Logger.DebugFormat("item {0} cannot be found", name); + } + return false; + } + + public bool TakeItem(string name) + { + return false; + } + + public bool GiveExperience(int exp) + { + SystemMessage($"{exp} experience!"); + User.GiveExperience((uint)exp); + return true; + } + + public bool TakeExperience(int exp) + { + User.Experience -= (uint)exp; + SystemMessage($"Your world spins as your insight leaves you ((-{exp} experience!))"); + User.UpdateAttributes(StatUpdateFlags.Experience); + return true; + } + + public void SystemMessage(string message) + { + // This is a typical client "orange message" + User.SendMessage(message, Hybrasyl.MessageTypes.SYSTEM_WITH_OVERHEAD); + } + + + public void Whisper(string name, string message) + { + User.SendWhisper(name, message); + } + + public void Mail(string name, string message) + { + } + + public void StartDialogSequence(string sequenceName, HybrasylWorldObject associate) + { + DialogSequence newSequence; + if (User.World.GlobalSequencesCatalog.TryGetValue(sequenceName, out newSequence)) + { + newSequence.ShowTo(User, (VisibleObject)associate.Obj); + // End previous sequence + User.DialogState.EndDialog(); + User.DialogState.StartDialog(associate.Obj as VisibleObject, newSequence); + } + + } + + public void StartSequence(string sequenceName, HybrasylWorldObject associateOverride = null) + { + DialogSequence sequence; + VisibleObject associate; + Logger.DebugFormat("{0} starting sequence {1}", User.Name, sequenceName); + + // If we're using a new associate, we will consult that to find our sequence + associate = associateOverride == null ? User.DialogState.Associate as VisibleObject : associateOverride.Obj as VisibleObject; + + // Use the local catalog for sequences first, then consult the global catalog + + if (!associate.SequenceCatalog.TryGetValue(sequenceName, out sequence)) + { + if (!User.World.GlobalSequencesCatalog.TryGetValue(sequenceName, out sequence)) + { + Logger.ErrorFormat("called from {0}: sequence name {1} cannot be found!", + associate.Name, sequenceName); + // To be safe, end all dialogs and basically abort + User.DialogState.EndDialog(); + return; + } + } + + // sequence should now be our target sequence, let's end the current state and start a new one + + User.DialogState.EndDialog(); + User.DialogState.StartDialog(associate, sequence); + User.DialogState.ActiveDialog.ShowTo(User, associate); + } + + } +} diff --git a/hybrasyl/Scripting/HybrasylWorld.cs b/hybrasyl/Scripting/HybrasylWorld.cs new file mode 100644 index 00000000..45b2ddf5 --- /dev/null +++ b/hybrasyl/Scripting/HybrasylWorld.cs @@ -0,0 +1,113 @@ +/* + * This file is part of Project Hybrasyl. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the Affero General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but + * without ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the Affero General Public License + * for more details. + * + * You should have received a copy of the Affero General Public License along + * with this program. If not, see . + * + * (C) 2013 Justin Baugh (baughj@hybrasyl.com) + * (C) 2015-2016 Project Hybrasyl (info@hybrasyl.com) + * + * For contributors and individual authors please refer to CONTRIBUTORS.MD. + * + */ + +using Hybrasyl.Dialogs; +using IronPython.Runtime; +using log4net; + +namespace Hybrasyl.Scripting +{ + + public class HybrasylWorld + { + public static readonly ILog Logger = LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType); + + internal World World { get; set; } + + public HybrasylWorld(World world) + { + World = world; + } + + public HybrasylDialogSequence NewDialogSequence(string sequenceName, params object[] list) + { + var dialogSequence = new HybrasylDialogSequence(sequenceName); + foreach (var entry in list) + { + Logger.InfoFormat("Type is {0}", entry.GetType().ToString()); + if (entry is HybrasylDialog) + { + var newdialog = entry as HybrasylDialog; + dialogSequence.AddDialog(newdialog); + } + else if (entry is PythonFunction) + { + var action = entry as PythonFunction; + } + + } + return dialogSequence; + } + + public HybrasylDialog NewDialog(string displayText, dynamic callback = null) + { + var dialog = new SimpleDialog(displayText); + dialog.SetCallbackHandler(callback); + return new HybrasylDialog(dialog); + } + + public HybrasylDialog NewTextDialog(string displayText, string topCaption, string bottomCaption, int inputLength = 254, dynamic handler = null, dynamic callback = null) + { + var dialog = new TextDialog(displayText, topCaption, bottomCaption, inputLength); + dialog.setInputHandler(handler); + dialog.SetCallbackHandler(callback); + return new HybrasylDialog(dialog); + } + + public HybrasylDialog NewOptionsDialog(string displayText, dynamic optionsStructure, dynamic handler = null, dynamic callback = null) + { + var dialog = new OptionsDialog(displayText); + dialog.SetCallbackHandler(callback); + + if (optionsStructure is IronPython.Runtime.List) + { + // A simple options dialog with a callback handler for the response + var optionlist = optionsStructure as IronPython.Runtime.List; + foreach (var option in optionsStructure) + { + if (option is string) + { + dialog.AddDialogOption(option as string); + } + } + if (handler != null) + { + dialog.setInputHandler(handler); + Logger.InfoFormat("Input handler associated with dialog"); + } + } + else if (optionsStructure is IronPython.Runtime.PythonDictionary) + { + var hash = optionsStructure as IronPython.Runtime.PythonDictionary; + foreach (var key in hash.Keys) + { + if (key is string) + { + dialog.AddDialogOption(key as string, hash[key]); + } + } + + } + return new HybrasylDialog(dialog); + } + } +} diff --git a/hybrasyl/Scripting/HybrasylWorldObject.cs b/hybrasyl/Scripting/HybrasylWorldObject.cs new file mode 100644 index 00000000..50aae3a7 --- /dev/null +++ b/hybrasyl/Scripting/HybrasylWorldObject.cs @@ -0,0 +1,93 @@ +/* + * This file is part of Project Hybrasyl. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the Affero General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but + * without ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the Affero General Public License + * for more details. + * + * You should have received a copy of the Affero General Public License along + * with this program. If not, see . + * + * (C) 2013 Justin Baugh (baughj@hybrasyl.com) + * (C) 2015-2016 Project Hybrasyl (info@hybrasyl.com) + * + * For contributors and individual authors please refer to CONTRIBUTORS.MD. + * + */ + + +using Hybrasyl.Objects; + +namespace Hybrasyl.Scripting +{ + public class HybrasylWorldObject + { + internal WorldObject Obj { get; set; } + + public HybrasylWorldObject(WorldObject obj) + { + Obj = obj; + } + + public void DisplayPursuits(dynamic invoker) + { + if (Obj is Merchant) + { + var merchant = Obj as Merchant; + if (invoker is HybrasylUser) + { + var hybUser = (HybrasylUser)invoker; + merchant.DisplayPursuits(hybUser.User); + } + } + + } + + public void Destroy() + { + if (Obj is ItemObject || Obj is Gold) + { + Game.World.Remove(Obj); + } + } + + public void AddPursuit(HybrasylDialogSequence hybrasylSequence) + { + if (Obj is VisibleObject && !(Obj is User)) + { + var vobj = Obj as VisibleObject; + vobj.AddPursuit(hybrasylSequence.Sequence); + } + + } + + public void RegisterSequence(HybrasylDialogSequence hybrasylSequence) + { + if (Obj is VisibleObject && !(Obj is User)) + { + var vobj = Obj as VisibleObject; + vobj.RegisterDialogSequence(hybrasylSequence.Sequence); + } + } + + public void RegisterGlobalSequence(HybrasylDialogSequence globalSequence) + { + Game.World.RegisterGlobalSequence(globalSequence.Sequence); + } + + public void Say(string message) + { + if (Obj is Creature) + { + var creature = Obj as Creature; + creature.Say(message); + } + } + + } +} diff --git a/hybrasyl/Scripting/Script.cs b/hybrasyl/Scripting/Script.cs new file mode 100644 index 00000000..35f231b7 --- /dev/null +++ b/hybrasyl/Scripting/Script.cs @@ -0,0 +1,278 @@ +/* + * This file is part of Project Hybrasyl. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the Affero General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but + * without ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the Affero General Public License + * for more details. + * + * You should have received a copy of the Affero General Public License along + * with this program. If not, see . + * + * (C) 2013 Justin Baugh (baughj@hybrasyl.com) + * (C) 2015-2016 Project Hybrasyl (info@hybrasyl.com) + * + * For contributors and individual authors please refer to CONTRIBUTORS.MD. + * + */ + +using IronPython.Runtime.Operations; +using System; +using System.IO; +using Hybrasyl.Objects; +using log4net; +using Microsoft.Scripting.Hosting; + +namespace Hybrasyl.Scripting +{ + public class Script + { + public static readonly ILog Logger = LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType); + public ScriptSource Source { get; set; } + public string Name { get; set; } + public string Path { get; private set; } + + public ScriptProcessor Processor { get; set; } + public CompiledCode Compiled { get; private set; } + public ScriptScope Scope { get; set; } + public dynamic Instance { get; set; } + public HybrasylWorldObject Associate { get; private set; } + + public bool Disabled { get; set; } + public string CompilationError { get; private set; } + public string LastRuntimeError { get; private set; } + + public Script Clone() + { + var clone = new Script(Path, Processor); + // Reload and reinstantiate the script with a new ScriptScope + Scope = Processor.Engine.CreateScope(); + clone.Load(); + clone.InstantiateScriptable(); + return clone; + } + + public Script(string path, ScriptProcessor processor) + { + Path = path; + Compiled = null; + Source = null; + Processor = processor; + Disabled = false; + CompilationError = string.Empty; + LastRuntimeError = string.Empty; + } + + public void AssociateScriptWithObject(WorldObject obj) + { + Associate = new HybrasylWorldObject(obj); + obj.Script = this; + } + + public dynamic GetObjectWrapper(WorldObject obj) + { + if (obj is User) + return new HybrasylUser(obj as User); + return new HybrasylWorldObject(obj); + } + /// + /// Load the script from disk, recompile it into bytecode, and execute it. + /// + /// boolean indicating whether the script was reloaded or not + public bool Load() + { + string scriptText; + try + { + scriptText = File.ReadAllText(Path); + } + catch (Exception e) + { + Logger.ErrorFormat("Couldn't open script {0}: {1}", Path, e.ToString()); + Disabled = true; + CompilationError = e.ToString(); + return false; + } + + scriptText = //ScriptProcessor.RestrictStdlib + ScriptProcessor.HybrasylImports + scriptText; + + Source = + Processor.Engine.CreateScriptSourceFromString(scriptText); + + Name = System.IO.Path.GetFileName(Path).ToLower(); + + try + { + Compile(); + Compiled.Execute(Scope); + Disabled = false; + } + catch (Exception e) + { + var pythonFrames = PythonOps.GetDynamicStackFrames(e); + var exceptionString = Processor.Engine.GetService().FormatException(e); + Logger.ErrorFormat("script {0} encountered error, Python stack follows", Path); + Logger.ErrorFormat("{0}", exceptionString); + Disabled = true; + CompilationError = exceptionString; + return false; + } + return true; + } + + /// + /// Compile the script, using the global Hybrasyl engine. + /// + /// boolean indicating success or failure (might raise exception in the future) + public bool Compile() + { + if (Source == null) return false; + Compiled = Source.Compile(); + return true; + } + + /// + /// If the script has a Scriptable class (used for WorldObject hooks), instantiate it. + /// + public bool InstantiateScriptable() + { + // First, disable the script, then if we have an instance, delete it. + Disabled = true; + + if (Instance != null) + Instance = null; + + try + { + var klass = Scope.GetVariable("Scriptable"); + Scope.SetVariable("world", Processor.World); + if (Associate != null) + { + Scope.SetVariable("npc", Associate); + Associate.Obj.ResetPursuits(); + } + Instance = Processor.Engine.Operations.CreateInstance(klass); + Disabled = false; + } + catch (Exception e) + { + var pythonFrames = PythonOps.GetDynamicStackFrames(e); + var exceptionString = Processor.Engine.GetService().FormatException(e); + Logger.ErrorFormat("script {0} encountered error, Python stack follows", Path); + Logger.ErrorFormat("{0}", exceptionString); + Logger.ErrorFormat("script {0} now disabled", Path); + Disabled = true; + CompilationError = exceptionString; + return false; + } + return true; + } + + public bool ExecuteFunction(ScriptInvocation invocation, params object[] parameters) + { + if (Disabled) + return false; + + if (!Processor.Engine.Operations.IsCallable(invocation.Function)) return false; + if (invocation.Invoker is User) + { + Scope.SetVariable("invoker", new HybrasylUser(invocation.Invoker as User)); + } + else + { + Scope.SetVariable("invoker", new HybrasylWorldObject(invocation.Invoker as WorldObject)); + } + if (invocation.Associate is WorldObject) + { + Scope.SetVariable("npc", new HybrasylWorldObject(invocation.Associate as WorldObject)); + } + try + { + var ret = Processor.Engine.Operations.Invoke(invocation.Function, parameters); + if (ret is bool) + return (bool)ret; + } + catch (Exception e) + { + var pythonFrames = PythonOps.GetDynamicStackFrames(e); + var exceptionString = Processor.Engine.GetService().FormatException(e); + Logger.ErrorFormat("script {0} encountered error, Python stack follows", Path); + Logger.ErrorFormat("{0}", exceptionString); + Logger.ErrorFormat("script {0} now disabled", Path); + LastRuntimeError = exceptionString; + return false; + } + return true; + } + + /// + /// Attach a Scriptable to an in game NPC. + /// + /// + public bool AttachScriptable(WorldObject obj) + { + Associate = new HybrasylWorldObject(obj); + Logger.InfoFormat("Scriptable name: {0}", Instance.name); + return true; + } + + /// + /// If the script has a Scriptable class and the given function exists, execute it. + /// + /// + /// The parameters to pass to the function. + public void ExecuteScriptableFunction(string name, params object[] parameters) + { + if (Disabled) + return; + + Scope.SetVariable("world", Processor.World); + Scope.SetVariable("npc", Associate); + + try + { + Processor.Engine.Operations.InvokeMember(Instance, name, parameters); + } + catch (System.NotImplementedException) + { + Logger.DebugFormat("script {0}: missing member {1}", Path, name); + } + catch (Exception e) + { + var pythonFrames = PythonOps.GetDynamicStackFrames(e); + var exceptionString = Processor.Engine.GetService().FormatException(e); + Logger.ErrorFormat("script {0} encountered error, Python stack follows", Path); + Logger.ErrorFormat("{0}", exceptionString); + Logger.ErrorFormat("script {0} now disabled"); + LastRuntimeError = exceptionString; + } + } + + /// + /// Execute the script in the passed scope. + /// + /// The ScriptScope the script will execute in. + public void ExecuteScript(WorldObject caller = null) + { + dynamic resolvedCaller; + + if (caller != null) + { + if (caller is User) + resolvedCaller = new HybrasylUser(caller as User); + else + resolvedCaller = new HybrasylWorldObject(caller); + Scope.SetVariable("npc", resolvedCaller); + } + + Scope.SetVariable("world", Processor.World); + Compiled.Execute(Scope); + } + + } +} diff --git a/hybrasyl/Scripting/ScriptInvocation.cs b/hybrasyl/Scripting/ScriptInvocation.cs new file mode 100644 index 00000000..4e050ecb --- /dev/null +++ b/hybrasyl/Scripting/ScriptInvocation.cs @@ -0,0 +1,50 @@ +/* + * This file is part of Project Hybrasyl. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the Affero General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but + * without ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the Affero General Public License + * for more details. + * + * You should have received a copy of the Affero General Public License along + * with this program. If not, see . + * + * (C) 2013 Justin Baugh (baughj@hybrasyl.com) + * (C) 2015-2016 Project Hybrasyl (info@hybrasyl.com) + * + * For contributors and individual authors please refer to CONTRIBUTORS.MD. + * + */ + + using Hybrasyl.Objects; + +namespace Hybrasyl.Scripting +{ + public struct ScriptInvocation + { + public dynamic Function; + public Script Script; + public WorldObject Associate; + public WorldObject Invoker; + + public ScriptInvocation(dynamic function = null, WorldObject associate = null, WorldObject invoker = null, Script script = null) + { + Function = function; + Associate = associate; + Invoker = invoker; + Script = null; + } + + public bool Execute(params object[] parameters) + { + if (Script != null) + return Script.ExecuteFunction(this, parameters); + else + return Associate.Script.ExecuteFunction(this, parameters); + } + } +} diff --git a/hybrasyl/Scripting/ScriptProcessor.cs b/hybrasyl/Scripting/ScriptProcessor.cs new file mode 100644 index 00000000..8bba5fb6 --- /dev/null +++ b/hybrasyl/Scripting/ScriptProcessor.cs @@ -0,0 +1,125 @@ +/* + * This file is part of Project Hybrasyl. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the Affero General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but + * without ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the Affero General Public License + * for more details. + * + * You should have received a copy of the Affero General Public License along + * with this program. If not, see . + * + * (C) 2013 Justin Baugh (baughj@hybrasyl.com) + * (C) 2015-2016 Project Hybrasyl (info@hybrasyl.com) + * + * For contributors and individual authors please refer to CONTRIBUTORS.MD. + * + */ + + using System.Collections.Generic; +using IronPython.Hosting; +using log4net; +using Microsoft.Scripting.Hosting; + +namespace Hybrasyl.Scripting +{ + public class ScriptProcessor + { + public static readonly ILog Logger = LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType); + + public ScriptEngine Engine { get; private set; } + public Dictionary Scripts { get; private set; } + public HybrasylWorld World { get; private set; } + + // We make an attempt to limit Hybrasyl scripts to stdlib, + // excluding "dangerous" functions (imports are disallowed outright, + // along with file i/o or eval/exec/ + public static readonly string RestrictStdlib = + @"__builtins__.__import__ = None +__builtins__.reload = None +__builtins__.open = None +__builtins__.eval = None +__builtins__.compile = None +__builtins__.execfile = None +__builtins__.file = None +__builtins__.memoryview = None +__builtins__.raw_input = None + +"; + + public static readonly string HybrasylImports = + @"import clr +clr.AddReference('Hybrasyl') +from Hybrasyl.Enums import * +from System import DateTime + +"; + + public ScriptProcessor(World world) + { + Engine = Python.CreateEngine(); + var paths = Engine.GetSearchPaths(); + // FIXME: obvious + paths.Add(@"C:\Program Files (x86)\IronPython 2.7\Lib"); + paths.Add(@"C:\Python27\Lib"); + Engine.SetSearchPaths(paths); + Engine.ImportModule("random"); + + Scripts = new Dictionary(); + World = new HybrasylWorld(world); + } + + public bool TryGetScript(string scriptName, out Script script) + { + // Try to find "name.py" or "name" + if (Scripts.TryGetValue($"{scriptName.ToLower()}.py", out script)) + { + return true; + } + return Scripts.TryGetValue(scriptName.ToLower(), out script); + } + + public Script GetScript(string scriptName) + { + Script script; + // Try to find "name.py" or "name" + var exists = Scripts.TryGetValue($"{scriptName.ToLower()}.py", out script); + if (!exists) + { + if (Scripts.TryGetValue($"{scriptName.ToLower()}", out script)) + return script; + } + else + return script; + return null; + } + + public bool RegisterScript(Script script) + { + script.Scope = Engine.CreateScope(); + script.Load(); + Scripts[script.Name] = script; + + if (script.Disabled) + { + Logger.ErrorFormat("{0}: error loading script", script.Name); + return false; + } + else + { + Logger.InfoFormat("{0}: loaded successfully", script.Name); + return true; + } + } + + public bool DeregisterScript(string scriptname) + { + Scripts[scriptname] = null; + return true; + } + } +} diff --git a/hybrasyl/World.cs b/hybrasyl/World.cs index 8f541103..7c2e0596 100644 --- a/hybrasyl/World.cs +++ b/hybrasyl/World.cs @@ -49,6 +49,7 @@ using System.Timers; using System.Xml; using System.Xml.Schema; +using Hybrasyl.Scripting; using Castable = Hybrasyl.Castables.Castable; using Creature = Hybrasyl.Objects.Creature; @@ -153,7 +154,7 @@ public Nation DefaultNation public Dictionary, Item> ItemCatalog { get; set; } public Dictionary MapCatalog { get; set; } - public HybrasylScriptProcessor ScriptProcessor { get; set; } + public ScriptProcessor ScriptProcessor { get; set; } public static BlockingCollection MessageQueue; public static ConcurrentDictionary ActiveUsers { get; private set; } @@ -235,7 +236,7 @@ public World(int port, DataStore store) ItemCatalog = new Dictionary, Item>(); MapCatalog = new Dictionary(); - ScriptProcessor = new HybrasylScriptProcessor(this); + ScriptProcessor = new ScriptProcessor(this); MessageQueue = new BlockingCollection(new ConcurrentQueue()); ActiveUsers = new ConcurrentDictionary(); ActiveUsersByName = new ConcurrentDictionary(); From 451cceb25753d4de16b244dd321008e1155dda7a Mon Sep 17 00:00:00 2001 From: Justin Baugh Date: Wed, 7 Dec 2016 23:55:34 -0500 Subject: [PATCH 11/26] Replace our overusage of dictionaries with WorldDataStore --- hybrasyl/Board.cs | 3 - hybrasyl/Book.cs | 7 +- hybrasyl/Client.cs | 3 - hybrasyl/FormulaParser.cs | 2 - hybrasyl/Hybrasyl.csproj | 9 +- hybrasyl/Inventory.cs | 5 +- hybrasyl/Jobs.cs | 4 +- hybrasyl/Lobby.cs | 1 - hybrasyl/Login.cs | 5 - hybrasyl/Map.cs | 32 +-- hybrasyl/Monolith.cs | 31 +-- hybrasyl/Objects/ItemObject.cs | 2 +- hybrasyl/Objects/Monster.cs | 2 +- hybrasyl/Objects/User.cs | 2 +- hybrasyl/Objects/VisibleObject.cs | 8 +- hybrasyl/PlayerStatus.cs | 3 - hybrasyl/Server.cs | 4 - hybrasyl/ServerPacketStructures.cs | 5 - hybrasyl/Utility.cs | 2 - hybrasyl/World.cs | 322 ++++++++++++++++------------- hybrasyl/WorldDataStore.cs | 127 ++++++++++++ hybrasyl/packages.config | 2 +- 22 files changed, 362 insertions(+), 219 deletions(-) create mode 100644 hybrasyl/WorldDataStore.cs diff --git a/hybrasyl/Board.cs b/hybrasyl/Board.cs index 9b529343..8414669c 100644 --- a/hybrasyl/Board.cs +++ b/hybrasyl/Board.cs @@ -1,11 +1,8 @@ using System; using System.Collections; using System.Collections.Generic; -using System.Data.SqlTypes; using System.Linq; -using System.Runtime.CompilerServices; using System.Threading; -using Microsoft.Scripting.Interpreter; using Newtonsoft.Json; namespace Hybrasyl diff --git a/hybrasyl/Book.cs b/hybrasyl/Book.cs index ae1df17d..52bc34fa 100644 --- a/hybrasyl/Book.cs +++ b/hybrasyl/Book.cs @@ -1,14 +1,11 @@ using Hybrasyl.Castables; using log4net; -using Hybrasyl.Castables; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using System; using System.Collections; using System.Collections.Generic; using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace Hybrasyl { @@ -45,7 +42,7 @@ public override object ReadJson(JsonReader reader, Type objectType, object exist string[] item; if (TryGetValue(jArray[i], out item)) { - book[i] = Game.World.Skills.SingleOrDefault(x => x.Value.Name.ToLower() == item[i]).Value; + book[i] = Game.World.WorldData.Values().SingleOrDefault(x => x.Name.ToLower() == item[i]); } } return book; @@ -60,7 +57,7 @@ public override object ReadJson(JsonReader reader, Type objectType, object exist if (TryGetValue(jArray[i], out item)) { book[i] = - Game.World.Spells.SingleOrDefault(x => x.Value.Name.ToLower() == item[i]).Value; + Game.World.WorldData.Values().SingleOrDefault(x => x.Name.ToLower() == item[i]); } } return book; diff --git a/hybrasyl/Client.cs b/hybrasyl/Client.cs index db6cd9a7..452dd7b7 100755 --- a/hybrasyl/Client.cs +++ b/hybrasyl/Client.cs @@ -20,8 +20,6 @@ * Kyle Speck */ -using System.Runtime.Serialization; -using IronPython.Modules; using log4net; using System; using System.Collections.Concurrent; @@ -31,7 +29,6 @@ using System.Net.Sockets; using System.Text; using System.Threading; -using IronPython.Compiler; using Microsoft.Scripting.Utils; namespace Hybrasyl diff --git a/hybrasyl/FormulaParser.cs b/hybrasyl/FormulaParser.cs index fd777743..abaded73 100644 --- a/hybrasyl/FormulaParser.cs +++ b/hybrasyl/FormulaParser.cs @@ -1,8 +1,6 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Text; -using System.Threading.Tasks; using Hybrasyl.Castables; using Hybrasyl.Objects; diff --git a/hybrasyl/Hybrasyl.csproj b/hybrasyl/Hybrasyl.csproj index 3dfee6ee..5b88af21 100644 --- a/hybrasyl/Hybrasyl.csproj +++ b/hybrasyl/Hybrasyl.csproj @@ -42,7 +42,7 @@ prompt 4 false - true + false x64 @@ -67,8 +67,8 @@ packages\FastMember.1.0.0.11\lib\net40\FastMember.dll - - packages\Hybrasyl.XML.0.5.5.9\lib\net452\Hybrasyl.XML.dll + + packages\Hybrasyl.XML.0.5.5.10\lib\net452\Hybrasyl.XML.dll True @@ -178,9 +178,7 @@ True Resources.resx - - @@ -188,6 +186,7 @@ + diff --git a/hybrasyl/Inventory.cs b/hybrasyl/Inventory.cs index 5629bac7..abe0b68f 100644 --- a/hybrasyl/Inventory.cs +++ b/hybrasyl/Inventory.cs @@ -379,8 +379,9 @@ public override object ReadJson(JsonReader reader, Type objectType, object exist Item itmType = null; Dictionary item; if (TryGetValue(jArray[i], out item)) - { - itmType = World.Items.Where(x => x.Value.Name == (string)item.FirstOrDefault().Value).FirstOrDefault().Value; + { + itmType = Game.World.WorldData.Get(item.FirstOrDefault().Value); + //itmType = Game.World.WorldData.Values().Where(x => x.Name == (string)item.FirstOrDefault().Value).FirstOrDefault().Name; if (itmType != null) { inv[i] = new ItemObject(itmType.Id, Game.World) diff --git a/hybrasyl/Jobs.cs b/hybrasyl/Jobs.cs index 9162cd91..86cb40f7 100755 --- a/hybrasyl/Jobs.cs +++ b/hybrasyl/Jobs.cs @@ -112,7 +112,7 @@ public static void Execute(object obj, ElapsedEventArgs args) Logger.Debug("Job starting"); var now = DateTime.Now.Ticks; - foreach (var mailbox in Game.World.Mailboxes.Values.Where(mb => mb.Full)) + foreach (var mailbox in Game.World.WorldData.Values().Where(mb => mb.Full)) { try { @@ -125,7 +125,7 @@ public static void Execute(object obj, ElapsedEventArgs args) Logger.ErrorFormat("{0}: mailbox locked during cleanup...?", mailbox.Name); } } - foreach (var board in Game.World.Messageboards.Values.Where(mb => mb.Full)) + foreach (var board in Game.World.WorldData.Values().Where(mb => mb.Full)) { try { diff --git a/hybrasyl/Lobby.cs b/hybrasyl/Lobby.cs index 107bf6cf..36266f1b 100644 --- a/hybrasyl/Lobby.cs +++ b/hybrasyl/Lobby.cs @@ -21,7 +21,6 @@ */ using System; -using System.Threading; namespace Hybrasyl { diff --git a/hybrasyl/Login.cs b/hybrasyl/Login.cs index 52dbee7a..c40f94e9 100755 --- a/hybrasyl/Login.cs +++ b/hybrasyl/Login.cs @@ -20,18 +20,13 @@ * Kyle Speck */ -using System.IO; using System.Net; -using System.Runtime.CompilerServices; -using System.Runtime.Serialization.Formatters.Binary; using Hybrasyl.Enums; using Hybrasyl.Objects; -using Hybrasyl.Properties; using System; using System.Collections; using System.Linq; using System.Text.RegularExpressions; -using IronPython.Modules; using Newtonsoft.Json; using StackExchange.Redis; diff --git a/hybrasyl/Map.cs b/hybrasyl/Map.cs index d65d87e1..e89c8f76 100755 --- a/hybrasyl/Map.cs +++ b/hybrasyl/Map.cs @@ -22,19 +22,14 @@ using C3; using Hybrasyl.Objects; -using Hybrasyl.Maps; -using Hybrasyl.Properties; -using Hybrasyl.XML; using log4net; using System; using System.Collections.Generic; using System.Drawing; using System.IO; -using System.Linq; using System.Text; using System.Threading; -using System.Xml; -using log4net.Appender; +using Microsoft.Scripting.Runtime; namespace Hybrasyl { @@ -235,17 +230,26 @@ public Map(Maps.Map newMap, World theWorld) foreach (var npcElement in newMap.Npcs) { + var npcTemplate = World.WorldData.Get(npcElement.Name); + if (npcTemplate == null) + { + Logger.Error("map ${Name}: NPC ${npcElement.Name} is missing, will not be loaded"); + continue; + } var merchant = new Merchant { X = npcElement.X, Y = npcElement.Y, Name = npcElement.Name, - Sprite = npcElement.Appearance.Sprite, - Direction = (Enums.Direction) npcElement.Appearance.Direction, - Portrait = npcElement.Appearance.Portrait, - // Wow this is terrible - Jobs = ((MerchantJob) (int) npcElement.Jobs) + Sprite = npcTemplate.Appearance.Sprite, + Direction = (Enums.Direction) npcElement.Direction, + Portrait = npcTemplate.Appearance.Portrait, }; + if (npcTemplate.Roles.Post != null) { merchant.Jobs ^= MerchantJob.Post; } + if (npcTemplate.Roles.Bank != null) { merchant.Jobs ^= MerchantJob.Bank; } + if (npcTemplate.Roles.Repair != null) { merchant.Jobs ^= MerchantJob.Repair; } + if (npcTemplate.Roles.Train != null) { merchant.Jobs ^= MerchantJob.Train; } + if (npcTemplate.Roles.Vend != null) { merchant.Jobs ^= MerchantJob.Vend; } InsertNpc(merchant); } @@ -728,7 +732,7 @@ public bool Use(User target) { case WarpType.Map: Map map; - if (SourceMap.World.MapCatalog.TryGetValue(DestinationMapName, out map)) + if (SourceMap.World.WorldData.TryGetValueByIndex(DestinationMapName, out map)) { Thread.Sleep(250); target.Teleport(map.Id, DestinationX, DestinationY); @@ -739,11 +743,11 @@ public bool Use(User target) break; case WarpType.WorldMap: WorldMap wmap; - if (SourceMap.World.WorldMaps.TryGetValue(DestinationMapName, out wmap)) + if (SourceMap.World.WorldData.TryGetValueByIndex(DestinationMapName, out wmap)) { SourceMap.Remove(target); target.SendWorldMap(wmap); - SourceMap.World.Maps[Hybrasyl.Constants.LAG_MAP].Insert(target, 5, 5, false); + SourceMap.World.WorldData.Get(Hybrasyl.Constants.LAG_MAP).Insert(target, 5, 5, false); return true; } Logger.ErrorFormat("User {0} tried to warp to nonexistent worldmap {1} from {2}: {3},{4}", diff --git a/hybrasyl/Monolith.cs b/hybrasyl/Monolith.cs index d4e82b60..3571d577 100644 --- a/hybrasyl/Monolith.cs +++ b/hybrasyl/Monolith.cs @@ -41,34 +41,37 @@ internal class Monolith LogManager.GetLogger( System.Reflection.MethodBase.GetCurrentMethod().DeclaringType); - private readonly Dictionary _spawnGroups; - private readonly Dictionary _maps; - private readonly Dictionary _creatures; - + private IEnumerable _spawnGroups => Game.World.WorldData.Values(); + private IEnumerable _maps => Game.World.WorldData.Values(); + private IEnumerable _creatures => Game.World.WorldData.Values(); + // private readonly Dictionary _spawnGroups; + // private readonly Dictionary _maps; + // private readonly Dictionary _creatures; + internal Monolith() { - _spawnGroups = Game.World.SpawnGroups; - _maps = Game.World.MapCatalog; - _creatures = Game.World.Creatures; + // _spawnGroups = Game.World.WorldData.GetDictionary(); + // _maps = Game.World.MapCatalog; + // _creatures = Game.World.WorldData.Values(); _random = new Random(); } public void Start() { - try +/* try { - foreach (var map in _spawnGroups.Values.SelectMany(spawnGroup => spawnGroup.Maps)) + foreach (var map in _spawnGroups.SelectMany(spawnGroup => spawnGroup.Maps)) { //set extension properties on startup - map.Id = _maps.Values.Single(x => x.Name == map.Name).Id; + map.Id = Game.World.WorldData.Values().Single(x => x.Name == map.Name).Id; map.LastSpawn = DateTime.Now; } while (true) { - foreach (var spawnGroup in _spawnGroups.Values) + foreach (var spawnGroup in _spawnGroups) { Spawn(spawnGroup); Thread.Sleep(100); @@ -79,14 +82,14 @@ public void Start() { throw; - } + }*/ } public void Spawn(SpawnGroup spawnGroup) { foreach (var map in spawnGroup.Maps) { - var spawnMap = Game.World.Maps[(ushort) map.Id]; + var spawnMap = Game.World.WorldData.Get(map.Id); var monsterList = spawnMap.Objects.OfType().ToList(); var monsterCount = monsterList.Count; @@ -102,7 +105,7 @@ public void Spawn(SpawnGroup spawnGroup) { var idx = _random.Next(0, spawnGroup.Spawns.Count - 1); var spawn = spawnGroup.Spawns[idx]; - var creature = _creatures.Values.Single(x => x.Name == spawn.Base); + var creature = _creatures.Single(x => x.Name == spawn.Base); var baseMob = new Monster(creature, spawn, map.Id); var mob = (Monster)baseMob.Clone(); diff --git a/hybrasyl/Objects/ItemObject.cs b/hybrasyl/Objects/ItemObject.cs index 86189859..b4d7db9c 100644 --- a/hybrasyl/Objects/ItemObject.cs +++ b/hybrasyl/Objects/ItemObject.cs @@ -109,7 +109,7 @@ public bool CheckRequirements(User userobj, out String message) return true; } - private Item Template => World.Items[TemplateId]; + private Item Template => World.WorldData.Get(TemplateId); public new string Name => Template.Name; diff --git a/hybrasyl/Objects/Monster.cs b/hybrasyl/Objects/Monster.cs index d26487f8..62ee4d91 100644 --- a/hybrasyl/Objects/Monster.cs +++ b/hybrasyl/Objects/Monster.cs @@ -62,7 +62,7 @@ public Monster(Hybrasyl.Creatures.Creature creature, Spawn spawn, int map) Name = creature.Name; Sprite = creature.Sprite; World = Game.World; - Map = Game.World.Maps[(ushort)map]; + Map = Game.World.WorldData.Get(map); Level = spawn.Stats.Level; BaseHp = spawn.Stats.Hp; Hp = spawn.Stats.Hp; diff --git a/hybrasyl/Objects/User.cs b/hybrasyl/Objects/User.cs index d9110351..08eb44da 100644 --- a/hybrasyl/Objects/User.cs +++ b/hybrasyl/Objects/User.cs @@ -237,7 +237,7 @@ public void SetCitizenship() if (Citizenship != null) { Nation theNation; - Nation = World.Nations.TryGetValue(Citizenship, out theNation) ? theNation : World.DefaultNation; + Nation = World.WorldData.TryGetValue(Citizenship, out theNation) ? theNation : World.DefaultNation; } } diff --git a/hybrasyl/Objects/VisibleObject.cs b/hybrasyl/Objects/VisibleObject.cs index 2a022bd0..e194fdfa 100644 --- a/hybrasyl/Objects/VisibleObject.cs +++ b/hybrasyl/Objects/VisibleObject.cs @@ -136,16 +136,16 @@ public virtual void Remove() public virtual void Teleport(ushort mapid, byte x, byte y) { - if (!World.Maps.ContainsKey(mapid)) return; + if (!World.WorldData.ContainsKey(mapid)) return; Map?.Remove(this); - Logger.DebugFormat("Teleporting {0} to {1}.", Name, World.Maps[mapid].Name); - World.Maps[mapid].Insert(this, x, y); + Logger.DebugFormat("Teleporting {0} to {1}.", Name, World.WorldData.Get(mapid).Name); + World.WorldData.Get(mapid).Insert(this, x, y); } public virtual void Teleport(string name, byte x, byte y) { Map targetMap; - if (!World.MapCatalog.TryGetValue(name, out targetMap)) return; + if (!World.WorldData.TryGetValueByIndex(name, out targetMap)) return; Map?.Remove(this); Logger.DebugFormat("Teleporting {0} to {1}.", Name, targetMap.Name); targetMap.Insert(this, x, y); diff --git a/hybrasyl/PlayerStatus.cs b/hybrasyl/PlayerStatus.cs index e54f059b..66734ad6 100644 --- a/hybrasyl/PlayerStatus.cs +++ b/hybrasyl/PlayerStatus.cs @@ -1,8 +1,5 @@  using System; -using System.Data.SqlTypes; -using System.Windows.Forms; -using Community.CsharpSqlite; using Hybrasyl.Enums; using Hybrasyl.Objects; diff --git a/hybrasyl/Server.cs b/hybrasyl/Server.cs index 3fe018e7..b98b8caa 100755 --- a/hybrasyl/Server.cs +++ b/hybrasyl/Server.cs @@ -22,16 +22,12 @@ using log4net; using System; -using System.CodeDom; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Net; using System.Net.Sockets; -using System.Security.Policy; using System.Threading; -using IronPython.Modules; -using Microsoft.Scripting.Utils; namespace Hybrasyl { diff --git a/hybrasyl/ServerPacketStructures.cs b/hybrasyl/ServerPacketStructures.cs index 664ef73a..185b6d12 100644 --- a/hybrasyl/ServerPacketStructures.cs +++ b/hybrasyl/ServerPacketStructures.cs @@ -1,9 +1,4 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Net.Sockets; -using System.Text; -using System.Threading.Tasks; using Hybrasyl.Enums; using log4net; diff --git a/hybrasyl/Utility.cs b/hybrasyl/Utility.cs index 0718b368..ddffdf70 100755 --- a/hybrasyl/Utility.cs +++ b/hybrasyl/Utility.cs @@ -20,13 +20,11 @@ * Kyle Speck */ -using System.IO; using log4net; using System; using System.Collections.Generic; using System.ComponentModel; using System.Reflection; -using System.Security.Cryptography.X509Certificates; using System.Text.RegularExpressions; using Hybrasyl.Enums; diff --git a/hybrasyl/World.cs b/hybrasyl/World.cs index 7c2e0596..3375e7e7 100644 --- a/hybrasyl/World.cs +++ b/hybrasyl/World.cs @@ -115,35 +115,38 @@ public static DateTime StartDate LogManager.GetLogger( System.Reflection.MethodBase.GetCurrentMethod().DeclaringType); + public Dictionary Objects { get; set; } - public Dictionary Maps { get; set; } - public Dictionary WorldMaps { get; set; } - public static Dictionary Items { get; set; } - public Dictionary ItemVariants { get; set; } - public Dictionary Skills { get; set; } - public Dictionary Spells { get; set; } - public Dictionary Monsters { get; set; } - public Dictionary Merchants { get; set; } - public Dictionary Reactors { get; set; } + //public Dictionary Maps { get; set; } + //public Dictionary WorldMaps { get; set; } + //public static Dictionary Items { get; set; } + //public Dictionary ItemVariants { get; set; } + //public Dictionary Skills { get; set; } + //public Dictionary Spells { get; set; } + //public Dictionary Monsters { get; set; } + //public Dictionary Merchants { get; set; } + //public Dictionary Reactors { get; set; } public Dictionary Portraits { get; set; } - public Dictionary Methods { get; set; } - public Dictionary Users { get; set; } - public Dictionary MapPoints { get; set; } - public Dictionary Metafiles { get; set; } - public Dictionary Nations { get; set; } - public Dictionary Mailboxes { get; set; } - public Dictionary MessageboardIndex { get; set; } - public Dictionary Messageboards { get; set; } - public Dictionary Creatures { get; set; } - public Dictionary SpawnGroups { get; set; } - + //public Dictionary Methods { get; set; } + //public Dictionary Users { get; set; } + //public Dictionary MapPoints { get; set; } + //public Dictionary Metafiles { get; set; } + //public Dictionary Nations { get; set; } + //public Dictionary Mailboxes { get; set; } + //public Dictionary MessageboardIndex { get; set; } + //public Dictionary Messageboards { get; set; } + //public Dictionary Creatures { get; set; } + //public Dictionary SpawnGroups { get; set; } + + public WorldDataStore WorldData { set; get; } + public Nation DefaultNation { get { - var nation = Nations.Values.FirstOrDefault(n => n.Default); - return nation ?? Nations.Values.First(); + var nation = WorldData.Values().FirstOrDefault(n => n.Default); + return nation ?? WorldData.Values().First(); } } @@ -152,7 +155,7 @@ public Nation DefaultNation private Dictionary merchantMenuHandlers; public Dictionary, Item> ItemCatalog { get; set; } - public Dictionary MapCatalog { get; set; } + // public Dictionary MapCatalog { get; set; } public ScriptProcessor ScriptProcessor { get; set; } @@ -190,6 +193,9 @@ public Nation DefaultNation public static string ItemVariantDirectory => Path.Combine(DataDirectory, "world", "xml", "itemvariants"); + public static string NpcsDirectory => Path.Combine(DataDirectory, "world", "xml", "npcs"); + + public static bool TryGetUser(string name, out User userobj) { var jsonString = (string)DatastoreConnection.GetDatabase().Get(User.GetStorageKey(name)); @@ -211,36 +217,39 @@ public static bool TryGetUser(string name, out User userobj) public World(int port, DataStore store) : base(port) { - Maps = new Dictionary(); - WorldMaps = new Dictionary(); - Items = new Dictionary(); - Skills = new Dictionary(); - Spells = new Dictionary(); - Creatures = new Dictionary(); - SpawnGroups = new Dictionary(); - Merchants = new Dictionary(); - Methods = new Dictionary(); + //Maps = new Dictionary(); + /* WorldMaps = new Dictionary(); + Items = new Dictionary(); + Skills = new Dictionary(); + Spells = new Dictionary(); + Creatures = new Dictionary(); + SpawnGroups = new Dictionary(); + Merchants = new Dictionary(); + Methods = new Dictionary(); + Users = new Dictionary(StringComparer.CurrentCultureIgnoreCase); + MapPoints = new Dictionary(); + Metafiles = new Dictionary(); + Nations = new Dictionary(); + GlobalSequences = new List(); + ItemVariants = new Dictionary(); + Mailboxes = new Dictionary(); + Messageboards = new Dictionary(); + MessageboardIndex = new Dictionary(); + */ Objects = new Dictionary(); - Users = new Dictionary(StringComparer.CurrentCultureIgnoreCase); - MapPoints = new Dictionary(); - Metafiles = new Dictionary(); - Nations = new Dictionary(); Portraits = new Dictionary(); - GlobalSequences = new List(); - ItemVariants = new Dictionary(); - Mailboxes = new Dictionary(); - Messageboards = new Dictionary(); - MessageboardIndex = new Dictionary(); GlobalSequencesCatalog = new Dictionary(); ItemCatalog = new Dictionary, Item>(); - MapCatalog = new Dictionary(); + //MapCatalog = new Dictionary(); ScriptProcessor = new ScriptProcessor(this); MessageQueue = new BlockingCollection(new ConcurrentQueue()); ActiveUsers = new ConcurrentDictionary(); ActiveUsersByName = new ConcurrentDictionary(); + WorldData = new WorldDataStore(); + var datastoreConfig = new ConfigurationOptions() { EndPoints = @@ -291,6 +300,21 @@ private bool LoadData() // native XML classes for Hybrasyl objects. This is unfortunate and should be // refactored later, but it is way too much work to do now (e.g. maps, etc). + //Load NPCs + foreach (var xml in Directory.GetFiles(NpcsDirectory)) + { + try + { + var npc = Serializer.Deserialize(XmlReader.Create(xml), new Creatures.Npc()); + Logger.Debug($"NPCs: loaded {npc.Name}"); + WorldData.Set(npc.Name, npc); + } + catch (Exception e) + { + Logger.Error($"Error parsing {xml}: {e}"); + } + } + // Load maps foreach (var xml in Directory.GetFiles(MapDirectory)) { @@ -298,8 +322,9 @@ private bool LoadData() { Maps.Map newMap = Serializer.Deserialize(XmlReader.Create(xml), new Maps.Map()); var map = new Map(newMap, this); - Maps.Add(map.Id, map); - MapCatalog.Add(map.Name, map); + //Maps.Add(map.Id, map); + //MapCatalog.Add(map.Name, map); + WorldData.SetWithIndex(map.Id, map, map.Name); Logger.DebugFormat("Maps: Loaded {0}", map.Name); } catch (Exception e) @@ -308,7 +333,7 @@ private bool LoadData() } } - Logger.InfoFormat("Maps: {0} maps loaded", Maps.Count); + Logger.InfoFormat("Maps: {0} maps loaded", WorldData.Count()); // Load nations foreach (var xml in Directory.GetFiles(NationDirectory)) @@ -317,7 +342,8 @@ private bool LoadData() { Nation newNation = Serializer.Deserialize(XmlReader.Create(xml), new Nation()); Logger.DebugFormat("Nations: Loaded {0}", newNation.Name); - Nations.Add(newNation.Name, newNation); + //Nations.Add(newNation.Name, newNation); + WorldData.Set(newNation.Name, newNation); } catch (Exception e) { @@ -326,19 +352,19 @@ private bool LoadData() } // Ensure at least one nation and one map exist. Otherwise, things get a little weird - if (Nations.Count == 0) + if (WorldData.Count() == 0) { Logger.FatalFormat("National data: at least one well-formed nation file must exist!"); return false; } - if (Maps.Count == 0) + if (WorldData.Count() == 0) { Logger.FatalFormat("Map data: at least one well-formed map file must exist!"); return false; } - Logger.InfoFormat("National data: {0} nations loaded", Nations.Count); + Logger.InfoFormat("National data: {0} nations loaded", WorldData.Count()); //Load Creatures foreach (var xml in Directory.GetFiles(CreatureDirectory)) @@ -347,13 +373,17 @@ private bool LoadData() { var creature = Serializer.Deserialize(XmlReader.Create(xml), new Creatures.Creature()); Logger.DebugFormat("Creatures: loaded {0}", creature.Name); - Creatures.Add(creature.Name, creature); + //Creatures.Add(creature.Name, creature); + WorldData.Set(creature.Name, creature); } catch (Exception e) { Logger.ErrorFormat("Error parsing {0}: {1}", xml, e); } } + + + //Load SpawnGroups foreach (var xml in Directory.GetFiles(SpawnGroupDirectory)) { @@ -361,7 +391,10 @@ private bool LoadData() { var spawnGroup = Serializer.Deserialize(XmlReader.Create(xml), new SpawnGroup()); Logger.DebugFormat("SpawnGroup: loaded {0}", spawnGroup.GetHashCode()); - SpawnGroups.Add(spawnGroup.GetHashCode(), spawnGroup); + //SpawnGroups.Add(spawnGroup.GetHashCode(), spawnGroup); + WorldData.Set(spawnGroup.GetHashCode(), spawnGroup); + + } catch (Exception e) { @@ -376,9 +409,13 @@ private bool LoadData() { Maps.WorldMap newWorldMap = Serializer.Deserialize(XmlReader.Create(xml), new Maps.WorldMap()); var worldmap = new WorldMap(newWorldMap); - WorldMaps.Add(worldmap.Name, worldmap); + //WorldMaps.Add(worldmap.Name, worldmap); + WorldData.Set(worldmap.Name, worldmap); foreach (var point in worldmap.Points) - MapPoints.Add(point.Id, point); + { + //MapPoints.Add(point.Id, point); + WorldData.Set(point.Id, point); + } Logger.DebugFormat("World Maps: Loaded {0}", worldmap.Name); } catch (Exception e) @@ -387,7 +424,7 @@ private bool LoadData() } } - Logger.InfoFormat("World Maps: {0} world maps loaded", WorldMaps.Count); + Logger.InfoFormat("World Maps: {0} world maps loaded", WorldData.Count()); // Load item variants foreach (var xml in Directory.GetFiles(ItemVariantDirectory)) @@ -396,7 +433,9 @@ private bool LoadData() { Items.VariantGroup newGroup = Serializer.Deserialize(XmlReader.Create(xml), new Items.VariantGroup()); Logger.DebugFormat("Item variants: loaded {0}", newGroup.Name); - ItemVariants.Add(newGroup.Name, newGroup); + //ItemVariants.Add(newGroup.Name, newGroup); + WorldData.Set(newGroup.Name, newGroup); + } catch (Exception e) { @@ -404,7 +443,7 @@ private bool LoadData() } } - Logger.InfoFormat("ItemObject variants: {0} variant sets loaded", ItemVariants.Count); + Logger.InfoFormat("ItemObject variants: {0} variant sets loaded", WorldData.Values().Count()); // Load items foreach (var xml in Directory.GetFiles(ItemDirectory)) @@ -413,18 +452,21 @@ private bool LoadData() { Item newItem = Serializer.Deserialize(XmlReader.Create(xml), new Item()); Logger.DebugFormat("Items: loaded {0}, id {1}", newItem.Name, newItem.Id); - Items.Add(newItem.Id, newItem); - ItemCatalog.Add(new Tuple(Sex.Neutral, newItem.Name), newItem); + //Items.Add(newItem.Id, newItem); + WorldData.SetWithIndex(newItem.Id, newItem, new Tuple(Sex.Neutral, newItem.Name)); + //ItemCatalog.Add(new Tuple(Sex.Neutral, newItem.Name), newItem); foreach (var targetGroup in newItem.Properties.Variants.Group) { - foreach (var variant in ItemVariants[targetGroup].Variant) + foreach (var variant in WorldData.Get(targetGroup).Variant) { var variantItem = ResolveVariant(newItem, variant, targetGroup); //variantItem.Name = $"{variant.Name} {newItem.Name}"; Logger.DebugFormat("ItemObject {0}: variantgroup {1}, subvariant {2}", variantItem.Name, targetGroup, variant.Name); - if (Items.ContainsKey(variantItem.Id)) Logger.ErrorFormat("Item already exists with Key {0} : {1}. Cannot add {2}", variantItem.Id, Items[variantItem.Id].Name, variantItem.Name); - Items.Add(variantItem.Id, variantItem); - ItemCatalog.Add(new Tuple(Sex.Neutral, variantItem.Name), variantItem); + if (WorldData.ContainsKey(variantItem.Id)) Logger.ErrorFormat("Item already exists with Key {0} : {1}. Cannot add {2}", variantItem.Id, WorldData.Get(variantItem.Id).Name, variantItem.Name); + //Items.Add(variantItem.Id, variantItem); + WorldData.SetWithIndex(variantItem.Id, variantItem, + new Tuple(Sex.Neutral, variantItem.Name)); + //ItemCatalog.Add(new Tuple(Sex.Neutral, variantItem.Name), variantItem); } } } @@ -440,14 +482,18 @@ private bool LoadData() { string name = string.Empty; Castables.Castable newCastable = Serializer.Deserialize(XmlReader.Create(xml), new Castables.Castable()); - if (newCastable.Book == Castables.Book.PrimarySkill || newCastable.Book == Castables.Book.SecondarySkill || + if (newCastable.Book == Castables.Book.PrimarySkill || + newCastable.Book == Castables.Book.SecondarySkill || newCastable.Book == Castables.Book.UtilitySkill) { - Skills.Add(newCastable.Id, newCastable); + //Skills.Add(newCastable.Id, newCastable); + WorldData.Set(newCastable.Id, newCastable); } else - Spells.Add(newCastable.Id, newCastable); - + { + //Spells.Add(newCastable.Id, newCastable); + WorldData.Set(newCastable.Id, newCastable); + } Logger.DebugFormat("Castables: loaded {0}, id {1}", newCastable.Name, newCastable.Id); } catch (Exception e) @@ -470,7 +516,8 @@ private bool LoadData() Logger.Warn("Potentially corrupt mailbox data in Redis; ignoring"); continue; } - Mailboxes.Add(name, mailbox); + //Mailboxes.Add(name, mailbox); + WorldData.Set(name, mailbox); } // Load all boards @@ -486,9 +533,10 @@ private bool LoadData() continue; } // Messageboard IDs are fairly irrelevant and only matter to the client - messageboard.Id = Messageboards.Count + 1; - Messageboards.Add(messageboard.Name, messageboard); - MessageboardIndex.Add(messageboard.Id, messageboard); + messageboard.Id = WorldData.Count() + 1; + //Messageboards.Add(messageboard.Name, messageboard); + WorldData.SetWithIndex(messageboard.Name, messageboard, messageboard.Id); + //MessageboardIndex.Add(messageboard.Id, messageboard); } // Ensure global boards exist and are up to date with anything specified in the config @@ -590,22 +638,21 @@ public Item ResolveVariant(Item item, Items.Variant variant, string variantGroup public Mailbox GetMailbox(string name) { var mailboxName = name.ToLower(); - if (Mailboxes.ContainsKey(mailboxName)) return Mailboxes[mailboxName]; - Mailboxes.Add(mailboxName, new Mailbox(mailboxName)); - Mailboxes[mailboxName].Save(); + if (WorldData.ContainsKey(mailboxName)) return WorldData.Get(mailboxName); + WorldData.Set(mailboxName, new Mailbox(mailboxName)); + WorldData.Get(mailboxName).Save(); Logger.InfoFormat("Mailbox: Creating mailbox for {0}", name); - return Mailboxes[mailboxName]; + return WorldData.Get(mailboxName); } public Board GetBoard(string name) { - if (Messageboards.ContainsKey(name)) return Messageboards[name]; - var newBoard = new Board(name) { Id = MessageboardIndex.Count + 1 }; - Messageboards.Add(name, new Board(name)); - MessageboardIndex.Add(newBoard.Id, newBoard); - Messageboards[name].Save(); + if (WorldData.ContainsKey(name)) return WorldData.Get(name); + var newBoard = new Board(name) { Id = WorldData.Values().Count() + 1 }; + WorldData.SetWithIndex(name, newBoard, newBoard.Id); + newBoard.Save(); Logger.InfoFormat("Board: Creating {0}", name); - return Messageboards[name]; + return WorldData.Get(name); } private static void ValidationCallBack(object sender, ValidationEventArgs args) @@ -624,12 +671,12 @@ private void LoadMetafiles() var iteminfo0 = new Metafile("ItemInfo0"); // TODO: split items into multiple ItemInfo files (DA does ~700 each) - foreach (var item in Items.Values) + foreach (var item in WorldData.Values()) { iteminfo0.Nodes.Add(new MetafileNode(item.Name, item.Properties.Restrictions.Level.Min, (int)item.Properties.Restrictions.@Class, item.Properties.Physical.Weight, item.Properties.Vendor.ShopTab, item.Properties.Vendor.Description)); } - Metafiles.Add(iteminfo0.Name, iteminfo0.Compile()); + WorldData.Set(iteminfo0.Name, iteminfo0.Compile()); #endregion ItemInfo @@ -639,7 +686,7 @@ private void LoadMetafiles() { var sclass = new Metafile("SClass" + i); sclass.Nodes.Add("Skill"); - foreach (var skill in Skills.Values) + foreach (var skill in WorldData.Values().Where(x => x.Type.Contains("skill"))) // placeholder; change to skills where class == i, are learnable from trainer, and sort by level { sclass.Nodes.Add(new MetafileNode(skill.Name, @@ -653,7 +700,7 @@ private void LoadMetafiles() } sclass.Nodes.Add("Skill_End"); sclass.Nodes.Add("Spell"); - foreach (var spell in Spells.Values) + foreach (var spell in WorldData.Values().Where(x => x.Type.Contains("spell"))) // placeholder; change to skills where class == i, are learnable from trainer, and sort by level { sclass.Nodes.Add(new MetafileNode(spell.Name, @@ -666,7 +713,7 @@ private void LoadMetafiles() )); } sclass.Nodes.Add("Spell_End"); - Metafiles.Add(sclass.Name, sclass.Compile()); + WorldData.Set(sclass.Name, sclass.Compile()); } #endregion SClass @@ -678,19 +725,19 @@ private void LoadMetafiles() { npcillust.Nodes.Add(new MetafileNode(kvp.Key, kvp.Value /* portrait filename */)); } - Metafiles.Add(npcillust.Name, npcillust.Compile()); + WorldData.Set(npcillust.Name, npcillust.Compile()); #endregion NPCIllust #region NationDesc var nationdesc = new Metafile("NationDesc"); - foreach (var nation in Nations.Values) + foreach (var nation in WorldData.Values()) { Logger.DebugFormat("Adding flag {0} for nation {1}", nation.Flag, nation.Name); nationdesc.Nodes.Add(new MetafileNode("nation_" + nation.Flag, nation.Name)); } - Metafiles.Add(nationdesc.Name, nationdesc.Compile()); + WorldData.Set(nationdesc.Name, nationdesc.Compile()); #endregion NationDesc } @@ -847,24 +894,17 @@ public void SetMerchantMenuHandlers() // FIXME: *User here should now use the ConcurrentDictionaries instead public void DeleteUser(string username) { - Users.Remove(username); + WorldData.Remove(username); } public void AddUser(User userobj) { - Users[userobj.Name] = userobj; + WorldData.Set(userobj.Name, userobj); } public User FindUser(string username) { - if (Users.ContainsKey(username)) - { - return Users[username]; - } - else - { - return null; - } + return WorldData.Get(username); } public override void Shutdown() @@ -985,7 +1025,7 @@ private void ControlMessage_LogoffUser(HybrasylControlMessage message) var userName = (string)message.Arguments[0]; Logger.WarnFormat("{0}: forcing logoff", userName); User user; - if (Users.TryGetValue(userName, out user)) + if (WorldData.TryGetValue(userName, out user)) { user.Logoff(); } @@ -997,7 +1037,7 @@ private void ControlMessage_MailNotifyUser(HybrasylControlMessage message) var userName = (string)message.Arguments[0]; Logger.DebugFormat("mail: attempting to notify {0} of new mail", userName); User user; - if (Users.TryGetValue(userName, out user)) + if (WorldData.TryGetValue(userName, out user)) { user.UpdateAttributes(StatUpdateFlags.Secondary); Logger.DebugFormat("mail: notification to {0} sent", userName); @@ -1013,7 +1053,7 @@ private void ControlMessage_StatusTick(HybrasylControlMessage message) var userName = (string)message.Arguments[0]; Logger.DebugFormat("statustick: processing tick for {0}", userName); User user; - if (Users.TryGetValue(userName, out user)) + if (WorldData.TryGetValue(userName, out user)) { user.ProcessStatusTicks(); } @@ -1373,12 +1413,12 @@ private void PacketHandler_0x0E_Talk(Object obj, ClientPacket packet) if (args.Length == 2) { - if (!Users.ContainsKey(args[1])) + if (!WorldData.ContainsKey(args[1])) { user.SendMessage("User not logged in.", MessageTypes.SYSTEM); return; } - var target = Users[args[1]]; + var target = WorldData.Get(args[1]); if (target.IsExempt) user.SendMessage("Access denied.", MessageTypes.SYSTEM); else @@ -1394,9 +1434,9 @@ private void PacketHandler_0x0E_Talk(Object obj, ClientPacket packet) { if (args.Length == 2) { - if (Nations.ContainsKey(args[1])) + if (WorldData.ContainsKey(args[1])) { - user.Nation = Nations[args[1]]; + user.Nation = WorldData.Get(args[1]); user.SendSystemMessage($"Citizenship set to {args[1]}"); } } @@ -1410,12 +1450,12 @@ private void PacketHandler_0x0E_Talk(Object obj, ClientPacket packet) if (args.Length == 2) { - if (!Users.ContainsKey(args[1])) + if (!WorldData.ContainsKey(args[1])) { user.SendMessage("User not logged in.", MessageTypes.SYSTEM); return; } - var target = Users[args[1]]; + var target = WorldData.Get(args[1]); if (target.IsExempt) user.SendMessage("Access denied.", MessageTypes.SYSTEM); else @@ -1435,14 +1475,14 @@ private void PacketHandler_0x0E_Talk(Object obj, ClientPacket packet) { if (!ushort.TryParse(args[1], out number)) { - if (!Users.ContainsKey(args[1])) + if (!WorldData.ContainsKey(args[1])) { user.SendMessage("Invalid map number or user name", 3); return; } else { - var target = Users[args[1]]; + var target = WorldData.Get(args[1]); number = target.Map.Id; x = target.X; y = target.Y; @@ -1456,9 +1496,9 @@ private void PacketHandler_0x0E_Talk(Object obj, ClientPacket packet) byte.TryParse(args[3], out y); } - if (Maps.ContainsKey(number)) + if (WorldData.ContainsKey(number)) { - var map = Maps[number]; + var map = WorldData.Get(number); if (x < map.X && y < map.Y) user.Teleport(number, x, y); else user.SendMessage("Invalid x/y", 3); } @@ -1506,11 +1546,11 @@ private void PacketHandler_0x0E_Talk(Object obj, ClientPacket packet) return; } - var queryMaps = from amap in MapCatalog - where searchTerm.IsMatch(amap.Key) + var queryMaps = from amap in WorldData.Values() + where searchTerm.IsMatch(amap.Name) select amap; var result = queryMaps.Aggregate("", - (current, map) => current + String.Format("{0} - {1}\n", map.Value.Id, map.Value.Name)); + (current, map) => current + String.Format("{0} - {1}\n", map.Id, map.Name)); if (result.Length > 65400) result = String.Format("{0}\n(Results truncated)", result.Substring(0, 65400)); @@ -1770,9 +1810,9 @@ where searchTerm.IsMatch(amap.Key) gcmContents = gcmContents + String.Format("{0}:{1} disposed\n", pair.Key, serverType); } } - foreach (var tehuser in Users) + foreach (var tehuser in WorldData.Values()) { - userContents = userContents + tehuser.Value.Name + "\n"; + userContents = userContents + tehuser.Name + "\n"; } foreach (var tehotheruser in ActiveUsersByName) { @@ -1809,11 +1849,11 @@ where searchTerm.IsMatch(amap.Key) // HURR O(N) IS MY FRIEND // change this to use itemcatalog pls - foreach (var template in Items) + foreach (var template in WorldData.Values()) { - if (template.Value.Name.Equals(itemName, StringComparison.CurrentCultureIgnoreCase)) + if (template.Name.Equals(itemName, StringComparison.CurrentCultureIgnoreCase)) { - var item = CreateItem(template.Key); + var item = CreateItem(template.Id); if (count > item.MaximumStack) item.Count = item.MaximumStack; else @@ -1845,7 +1885,7 @@ where searchTerm.IsMatch(amap.Key) skillName = string.Join(" ", args, 1, args.Length - 1); - Castable skill = Skills.Where(x => x.Value.Name == skillName).FirstOrDefault().Value; + Castable skill = WorldData.Get(skillName); user.AddSkill(skill); } break; @@ -1859,7 +1899,7 @@ where searchTerm.IsMatch(amap.Key) spellName = string.Join(" ", args, 1, args.Length - 1); - Castable spell = Spells.Where(x => x.Value.Name == spellName).FirstOrDefault().Value; + Castable spell = WorldData.Get(spellName); user.AddSpell(spell); } break; @@ -1875,7 +1915,7 @@ where searchTerm.IsMatch(amap.Key) { Sprite = 1, World = Game.World, - Map = Game.World.Maps[500], + Map = Game.World.WorldData.Get(500), Level = 1, DisplayText = "TestMob", BaseHp = 100, @@ -1891,7 +1931,7 @@ where searchTerm.IsMatch(amap.Key) X = 50, Y = 51 }; - Game.World.Maps[500].InsertCreature(creature); + Game.World.WorldData.Get(500).InsertCreature(creature); user.SendVisibleCreature(creature); } break; @@ -2300,7 +2340,7 @@ private void PacketHandler_0x10_ClientJoin(Object obj, ClientPacket packet) var spawnpoint = loginUser.Nation.SpawnPoints.First(); loginUser.Teleport(spawnpoint.MapName, spawnpoint.X, spawnpoint.Y); } - else if (Maps.ContainsKey(loginUser.Location.MapId)) + else if (WorldData.ContainsKey(loginUser.Location.MapId)) { loginUser.Teleport(loginUser.Location.MapId, (byte)loginUser.Location.X, (byte)loginUser.Location.Y); } @@ -2349,7 +2389,7 @@ private void PacketHandler_0x18_ShowPlayerList(Object obj, ClientPacket packet) { var me = (User)obj; - var list = from user in Users.Values + var list = from user in WorldData.Values() orderby user.IsMaster descending, user.Level descending, user.BaseHp + user.BaseMp * 2 descending, user.Name ascending select user; @@ -2928,7 +2968,7 @@ private void PacketHandler_0x3B_AccessMessages(Object obj, ClientPacket packet) // TODO: This has the potential to be a somewhat expensive operation, optimize this. var boardList = - Messageboards.Values.Where(mb => mb.CheckAccessLevel(user.Name, BoardAccessLevel.Read)); + WorldData.Values().Where(mb => mb.CheckAccessLevel(user.Name, BoardAccessLevel.Read)); // Mail is always the first board and has a fixed id of 0 response.WriteUInt16((ushort)(boardList.Count() + 1)); @@ -2958,7 +2998,7 @@ private void PacketHandler_0x3B_AccessMessages(Object obj, ClientPacket packet) else { Board board; - if (MessageboardIndex.TryGetValue(boardId, out board)) + if (WorldData.TryGetValueByIndex(boardId, out board)) { user.Enqueue(board.RenderToPacket()); return; @@ -3048,7 +3088,7 @@ private void PacketHandler_0x3B_AccessMessages(Object obj, ClientPacket packet) { // Get board message Board board; - if (MessageboardIndex.TryGetValue(boardId, out board)) + if (WorldData.TryGetValue(boardId, out board)) { // TODO: handle this better if (!board.CheckAccessLevel(user.Name, BoardAccessLevel.Read)) @@ -3137,7 +3177,7 @@ private void PacketHandler_0x3B_AccessMessages(Object obj, ClientPacket packet) response.WriteBoolean(false); response.WriteString8("Try waiting a moment before sending another message."); } - if (MessageboardIndex.TryGetValue(boardId, out board)) + if (WorldData.TryGetValue(boardId, out board)) { if (board.CheckAccessLevel(user.Name, BoardAccessLevel.Write)) { @@ -3186,7 +3226,7 @@ private void PacketHandler_0x3B_AccessMessages(Object obj, ClientPacket packet) var boardId = packet.ReadUInt16(); var postId = packet.ReadUInt16(); Board board; - if (MessageboardIndex.TryGetValue(boardId, out board)) + if (WorldData.TryGetValue(boardId, out board)) { if (user.IsPrivileged || board.CheckAccessLevel(user.Name, BoardAccessLevel.Moderate)) { @@ -3222,7 +3262,7 @@ private void PacketHandler_0x3B_AccessMessages(Object obj, ClientPacket packet) response.WriteByte(0x06); // Send post response User recipientUser; - if (Users.TryGetValue(recipient, out recipientUser)) + if (WorldData.TryGetValue(recipient, out recipientUser)) { try { @@ -3271,7 +3311,7 @@ private void PacketHandler_0x3B_AccessMessages(Object obj, ClientPacket packet) Logger.WarnFormat("mail: {0} tried to highlight message {1} but isn't GM! Hijinx suspected.", user.Name, postId); } - if (MessageboardIndex.TryGetValue(boardId, out board)) + if (WorldData.TryGetValue(boardId, out board)) { board.Messages[postId - 1].Highlighted = true; response.WriteBoolean(true); @@ -3320,7 +3360,7 @@ private void PacketHandler_0x3F_MapPointClick(Object obj, ClientPacket packet) if (user.IsAtWorldMap) { MapPoint targetmap; - if (MapPoints.TryGetValue(target, out targetmap)) + if (WorldData.TryGetValue(target, out targetmap)) { user.Teleport(targetmap.DestinationMap, targetmap.DestinationX, targetmap.DestinationY); } @@ -3863,8 +3903,8 @@ private void PacketHandler_0x7B_RequestMetafile(Object obj, ClientPacket packet) { var x6F = new ServerPacket(0x6F); x6F.WriteBoolean(all); - x6F.WriteUInt16((ushort)Metafiles.Count); - foreach (var metafile in Metafiles.Values) + x6F.WriteUInt16((ushort)WorldData.Count()); + foreach (var metafile in WorldData.Values()) { x6F.WriteString8(metafile.Name); x6F.WriteUInt32(metafile.Checksum); @@ -3874,9 +3914,9 @@ private void PacketHandler_0x7B_RequestMetafile(Object obj, ClientPacket packet) else { var name = packet.ReadString8(); - if (Metafiles.ContainsKey(name)) + if (WorldData.ContainsKey(name)) { - var file = Metafiles[name]; + var file = WorldData.Get(name); var x6F = new ServerPacket(0x6F); x6F.WriteBoolean(all); @@ -4182,7 +4222,7 @@ public void Update() public ItemObject CreateItem(int id, int quantity = 1) { - if (Items.ContainsKey(id)) + if (WorldData.ContainsKey(id)) { var item = new ItemObject(id, this); if (quantity > item.MaximumStack) @@ -4215,10 +4255,10 @@ public bool TryGetItemTemplate(string name, out Item item) public object ScriptMethod(string name, params object[] args) { object result = null; - - if (Methods.ContainsKey(name)) + + if (WorldData.ContainsKey(name)) { - var method = Methods[name]; + var method = WorldData.Get(name); result = method.Invoke(null, args); } diff --git a/hybrasyl/WorldDataStore.cs b/hybrasyl/WorldDataStore.cs new file mode 100644 index 00000000..747c3fbe --- /dev/null +++ b/hybrasyl/WorldDataStore.cs @@ -0,0 +1,127 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Windows.Markup; +using Microsoft.Scripting.Ast; + +namespace Hybrasyl +{ + public class WorldDataStore + { + private ConcurrentDictionary> _dataStore; + private ConcurrentDictionary> _index; + + public WorldDataStore() + { + _dataStore = new ConcurrentDictionary>(); + _index = new ConcurrentDictionary>(); + } + + /// + /// Get a substore for a given type T. + /// + /// + /// + private ConcurrentDictionary GetSubStore() + { + if (_dataStore.ContainsKey(typeof(T))) + { + return _dataStore[typeof(T)]; + } + _dataStore.TryAdd(typeof(T), new ConcurrentDictionary()); + return _dataStore[typeof(T)]; + } + + private ConcurrentDictionary GetSubIndex() + { + if (_index.ContainsKey(typeof(T))) + { + return _index[typeof(T)]; + } + _index.TryAdd(typeof(T), new ConcurrentDictionary()); + return _index[typeof(T)]; + } + + /// + /// Given a type and a key, return the typed object matching the key, or a default value. + /// + /// The type to be returned + /// The key for the object + /// + public T Get(dynamic key) + { + if (_dataStore.ContainsKey(typeof(T))) + { + return (T) _dataStore[typeof(T)][key]; + } + return default(T); + } + + public T GetByIndex(dynamic key) + { + if (_index.ContainsKey(typeof(T))) + { + return (T) _index[typeof(T)][key]; + } + return default(T); + } + + public bool TryGetValue(dynamic key, out T tresult) + { + tresult = default(T); + var sub = GetSubStore(); + if (!sub.ContainsKey(key.ToString())) return false; + tresult = (T) sub[key.ToString()]; + return true; + } + + public bool TryGetValueByIndex(dynamic key, out T tresult) + { + tresult = default(T); + var sub = GetSubIndex(); + if (!sub.ContainsKey(key)) return false; + tresult = (T)sub[key]; + return true; + } + + /// + /// Given a key and a value, set the + /// + /// The type we want to store. + /// The key to be used for the object. + /// The actual object to be stored. + /// + public bool Set(dynamic key, T value) => GetSubStore().TryAdd(key.ToString(), value); + + public bool SetWithIndex(dynamic key, T value, dynamic index) => GetSubStore().TryAdd(key.ToString(), value) && + GetSubIndex().TryAdd(index, value); + + + + /// + /// Returns all the objects contained in the datastore of the specified type's substore. + /// + /// The type to be returned. + /// + public IEnumerable Values() => GetSubStore().Values.Cast(); + + /// + /// Returns all the keys contained in the datastore for the specified type's substore. + /// + /// + /// + public IEnumerable Keys() => GetSubStore().Keys; + public bool ContainsKey(dynamic key) => GetSubStore().ContainsKey(key.ToString()); + public int Count() => GetSubStore().Count; + + public IDictionary GetDictionary() => (IDictionary) _dataStore[typeof(T)]; + + public bool Remove(dynamic key) + { + dynamic ignored; + return GetSubStore().TryRemove(key, out ignored); + } + + } +} diff --git a/hybrasyl/packages.config b/hybrasyl/packages.config index 29b68fb6..b933c320 100644 --- a/hybrasyl/packages.config +++ b/hybrasyl/packages.config @@ -5,7 +5,7 @@ - + From cad8bfb90353682ee6f4eb2f99567822c1fa502f Mon Sep 17 00:00:00 2001 From: Justin Baugh Date: Sat, 10 Dec 2016 16:20:40 -0500 Subject: [PATCH 12/26] Fix index usage for castables, fix bug in ondeath with map nullref --- hybrasyl/Monolith.cs | 5 +++-- hybrasyl/Objects/Monster.cs | 2 +- hybrasyl/World.cs | 17 +++-------------- hybrasyl/WorldDataStore.cs | 2 +- 4 files changed, 8 insertions(+), 18 deletions(-) diff --git a/hybrasyl/Monolith.cs b/hybrasyl/Monolith.cs index 3571d577..976134bc 100644 --- a/hybrasyl/Monolith.cs +++ b/hybrasyl/Monolith.cs @@ -59,7 +59,7 @@ internal Monolith() public void Start() { -/* try + try { foreach (var map in _spawnGroups.SelectMany(spawnGroup => spawnGroup.Maps)) { @@ -82,8 +82,9 @@ public void Start() { throw; - }*/ + } } + public void Spawn(SpawnGroup spawnGroup) { diff --git a/hybrasyl/Objects/Monster.cs b/hybrasyl/Objects/Monster.cs index 62ee4d91..cb4c32ea 100644 --- a/hybrasyl/Objects/Monster.cs +++ b/hybrasyl/Objects/Monster.cs @@ -43,7 +43,6 @@ public Monster() public override void OnDeath() { Shout("AAAAAAAAAAaaaaa!!!"); - Map.Remove(this); // Now that we're dead, award loot. // FIXME: Implement loot tables / full looting. var hitter = LastHitter as User; @@ -54,6 +53,7 @@ public override void OnDeath() var golds = new Gold(_spawn.Loot.Gold); World.Insert(golds); Map.Insert(golds, X,Y); + Map.Remove(this); World.Remove(this); } diff --git a/hybrasyl/World.cs b/hybrasyl/World.cs index 3375e7e7..0075eecb 100644 --- a/hybrasyl/World.cs +++ b/hybrasyl/World.cs @@ -482,18 +482,7 @@ private bool LoadData() { string name = string.Empty; Castables.Castable newCastable = Serializer.Deserialize(XmlReader.Create(xml), new Castables.Castable()); - if (newCastable.Book == Castables.Book.PrimarySkill || - newCastable.Book == Castables.Book.SecondarySkill || - newCastable.Book == Castables.Book.UtilitySkill) - { - //Skills.Add(newCastable.Id, newCastable); - WorldData.Set(newCastable.Id, newCastable); - } - else - { - //Spells.Add(newCastable.Id, newCastable); - WorldData.Set(newCastable.Id, newCastable); - } + WorldData.SetWithIndex(newCastable.Id, newCastable, newCastable.Name); Logger.DebugFormat("Castables: loaded {0}, id {1}", newCastable.Name, newCastable.Id); } catch (Exception e) @@ -1885,7 +1874,7 @@ where searchTerm.IsMatch(amap.Name) skillName = string.Join(" ", args, 1, args.Length - 1); - Castable skill = WorldData.Get(skillName); + Castable skill = WorldData.GetByIndex(skillName); user.AddSkill(skill); } break; @@ -1899,7 +1888,7 @@ where searchTerm.IsMatch(amap.Name) spellName = string.Join(" ", args, 1, args.Length - 1); - Castable spell = WorldData.Get(spellName); + Castable spell = WorldData.GetByIndex(spellName); user.AddSpell(spell); } break; diff --git a/hybrasyl/WorldDataStore.cs b/hybrasyl/WorldDataStore.cs index 747c3fbe..47bbc7fc 100644 --- a/hybrasyl/WorldDataStore.cs +++ b/hybrasyl/WorldDataStore.cs @@ -53,7 +53,7 @@ public T Get(dynamic key) { if (_dataStore.ContainsKey(typeof(T))) { - return (T) _dataStore[typeof(T)][key]; + return (T) _dataStore[typeof(T)][key.ToString()]; } return default(T); } From 657d8ec8e5473b4c3e22268818fdf652e5d96ee7 Mon Sep 17 00:00:00 2001 From: Justin Baugh Date: Sun, 11 Dec 2016 14:03:43 -0500 Subject: [PATCH 13/26] Add documentation --- hybrasyl/WorldDataStore.cs | 77 ++++++++++++++++++++++++++++++++++---- 1 file changed, 69 insertions(+), 8 deletions(-) diff --git a/hybrasyl/WorldDataStore.cs b/hybrasyl/WorldDataStore.cs index 47bbc7fc..135a11f3 100644 --- a/hybrasyl/WorldDataStore.cs +++ b/hybrasyl/WorldDataStore.cs @@ -12,6 +12,9 @@ public class WorldDataStore private ConcurrentDictionary> _dataStore; private ConcurrentDictionary> _index; + /// + /// Constructor, takes no arguments. + /// public WorldDataStore() { _dataStore = new ConcurrentDictionary>(); @@ -21,7 +24,7 @@ public WorldDataStore() /// /// Get a substore for a given type T. /// - /// + /// The type to fetch /// private ConcurrentDictionary GetSubStore() { @@ -33,6 +36,11 @@ private ConcurrentDictionary GetSubStore() return _dataStore[typeof(T)]; } + /// + /// Get a subindex for the given type T. + /// + /// The type to fetch + /// private ConcurrentDictionary GetSubIndex() { if (_index.ContainsKey(typeof(T))) @@ -58,6 +66,14 @@ public T Get(dynamic key) return default(T); } + /// + /// Given a type and a key, return the typed object matching the key in the subindex, + /// or a default value. + /// + /// The type to be returned + /// The index key for the object + /// Found object + public T GetByIndex(dynamic key) { if (_index.ContainsKey(typeof(T))) @@ -67,6 +83,13 @@ public T GetByIndex(dynamic key) return default(T); } + /// + /// Try to find a typed value in the store given a key. + /// + /// The type to be returned + /// The key + /// The out parameter which will contain the object, if found + /// True or false depending on whether or not item was found public bool TryGetValue(dynamic key, out T tresult) { tresult = default(T); @@ -76,6 +99,13 @@ public bool TryGetValue(dynamic key, out T tresult) return true; } + /// + /// Try to find a typed value in the store given an index key. + /// + /// The type to be returned + /// The index key + /// The out parameter which will contain the object, if found + /// True or false depending on whether or not item was found public bool TryGetValueByIndex(dynamic key, out T tresult) { tresult = default(T); @@ -86,18 +116,25 @@ public bool TryGetValueByIndex(dynamic key, out T tresult) } /// - /// Given a key and a value, set the + /// Store an object in the datastore with the given key. /// - /// The type we want to store. - /// The key to be used for the object. - /// The actual object to be stored. - /// + /// The type to be stored + /// The key to be used for the object + /// The actual object to be stored + /// Boolean indicating success public bool Set(dynamic key, T value) => GetSubStore().TryAdd(key.ToString(), value); + /// + /// Store an object in the datastore with the given key and index key. + /// + /// The type to be stored + /// The key for the object + /// The actual object to be stored + /// The index key for the object + /// Boolean indicating success public bool SetWithIndex(dynamic key, T value, dynamic index) => GetSubStore().TryAdd(key.ToString(), value) && GetSubIndex().TryAdd(index, value); - - + /// /// Returns all the objects contained in the datastore of the specified type's substore. @@ -112,11 +149,35 @@ public bool SetWithIndex(dynamic key, T value, dynamic index) => GetSubStore< /// /// public IEnumerable Keys() => GetSubStore().Keys; + + /// + /// Checks to see whether a key exists in the datastore for a given type. + /// + /// The type to check + /// The key to check + /// Boolean indicating whether or not the key exists public bool ContainsKey(dynamic key) => GetSubStore().ContainsKey(key.ToString()); + + /// + /// Return a count of typed objects in the datastore. + /// + /// The type for which to produce a count + /// Integer number of objects public int Count() => GetSubStore().Count; + /// + /// Get an IDictionary which will only contain values of the specified type. + /// + /// The type to return + /// IDictionary of objects of the specified type. public IDictionary GetDictionary() => (IDictionary) _dataStore[typeof(T)]; + /// + /// Remove an object from the datastore. + /// + /// The type of the object to remove + /// The key corresponding to the object to be removed + /// public bool Remove(dynamic key) { dynamic ignored; From 76a46b8aa39d04fd8c6681c3ceed4258aac250a3 Mon Sep 17 00:00:00 2001 From: Michael Norris Date: Sun, 11 Dec 2016 15:42:37 -0500 Subject: [PATCH 14/26] clear spell thing --- .vs/VSWorkspaceState.json | 8 ++++++++ .vs/slnx.sqlite | Bin 0 -> 304128 bytes hybrasyl/Objects/User.cs | 10 ++++++++-- 3 files changed, 16 insertions(+), 2 deletions(-) create mode 100644 .vs/VSWorkspaceState.json create mode 100644 .vs/slnx.sqlite diff --git a/.vs/VSWorkspaceState.json b/.vs/VSWorkspaceState.json new file mode 100644 index 00000000..5206e135 --- /dev/null +++ b/.vs/VSWorkspaceState.json @@ -0,0 +1,8 @@ +{ + "ExpandedNodes": [ + "", + "\\hybrasyl" + ], + "SelectedNode": "\\hybrasyl\\Hybrasyl.sln", + "PreviewInSolutionExplorer": false +} \ No newline at end of file diff --git a/.vs/slnx.sqlite b/.vs/slnx.sqlite new file mode 100644 index 0000000000000000000000000000000000000000..faac8f4845508344fb8789d9b78ec4d71c3da749 GIT binary patch literal 304128 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 HXte6R@|{D84zg5;C_{x3FxhYmjr&L15&R(Fk4j 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�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 literal 0 HcmV?d00001 diff --git a/hybrasyl/Objects/User.cs b/hybrasyl/Objects/User.cs index 3a1e7f1b..25148dca 100644 --- a/hybrasyl/Objects/User.cs +++ b/hybrasyl/Objects/User.cs @@ -1417,6 +1417,12 @@ public void SendClearSkill(int slot) x2D.WriteByte((byte)slot); Enqueue(x2D); } + public void SendClearSpell(int slot) + { + var x2D = new ServerPacket(0x18); + x2D.WriteByte((byte)slot); + Enqueue(x2D); + } ///

/// Send an ItemObject update packet (essentially placing the ItemObject in a given slot, as far as the client is concerned. @@ -1469,7 +1475,7 @@ public void SendSpellUpdate(Castable item, int slot) { if (item == null) { - SendClearSkill(slot); + SendClearSpell(slot); return; } Logger.DebugFormat("Adding spell {0} to slot {2}", @@ -1480,7 +1486,7 @@ public void SendSpellUpdate(Castable item, int slot) var spellType = item.Intents[0].UseType; //var spellType = isClick ? 2 : 5; x17.WriteByte((byte)spellType); //spell type? how are we determining this? - x17.WriteString8(item.Name + " (" + item.CastableLevel + "/" + item.MaxLevel + ")"); + x17.WriteString8(item.Name + " (" + item.CastableLevel + "/" + item.MaxLevel.Peasant + ")"); x17.WriteString8(item.Name); //prompt? what is this? x17.WriteByte((byte)item.Lines); x17.WriteByte(0); //current level From c402008e9ca59937d43414f41cf502d7c691ff4f Mon Sep 17 00:00:00 2001 From: Justin Baugh Date: Sun, 11 Dec 2016 16:36:20 -0500 Subject: [PATCH 15/26] update gitignore to latest --- .gitignore | 404 ++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 275 insertions(+), 129 deletions(-) diff --git a/.gitignore b/.gitignore index 40c4d82f..79db921b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,129 +1,275 @@ -config.xml -config.php - -# Build Folders (you can keep bin if you'd like, to store dlls and pdbs) -[Oo]bj/ - -# mstest test results -TestResults - -## Ignore Visual Studio temporary files, build results, and -## files generated by popular Visual Studio add-ons. - -# User-specific files -*.suo -*.user -*.sln.docstates - -# Build results -[Dd]ebug/ -x64/ -*.vshost.exe -*.vshost.exe.config -*_i.c -*_p.c -*.ilk -*.meta -*.obj -*.pch -*.pdb -*.pgc -*.pgd -*.rsp -*.sbr -*.tlb -*.tli -*.tlh -*.tmp -*.log -*.vspscc -*.vssscc -.builds - -# Visual C++ cache files -ipch/ -*.aps -*.ncb -*.opensdf -*.sdf - -# Visual Studio profiler -*.psess -*.vsp -*.vspx - -# Guidance Automation Toolkit -*.gpState - -# ReSharper is a .NET coding add-in -_ReSharper* - -# NCrunch -*.ncrunch* -.*crunch*.local.xml - -# 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 -*.Publish.xml - -# NuGet Packages Directory -packages - -# Windows Azure Build Output -csx -*.build.csdef - -# Windows Store app package directory -AppPackages/ - -# Others -[Oo]bj -sql -TestResults -[Tt]est[Rr]esult* -*.Cache -ClientBin -[Ss]tyle[Cc]op.* -~$* -*.dbmdl -Generated_Code #added for RIA/Silverlight projects - -# 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 - -# Eclipse - -.project -.buildpath -.settings -local - -# Annoying mac bullshit - -.DS_Store - -# etc - -hybrasyl/bin/Release -hybrasyl/bin/Debug - -# Ignore installer files -nsis/*.exe +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates +*.vcxproj.filters + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# 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 + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ +**/Properties/launchSettings.json + +*_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 +*.VC.db +*.VC.VC.opendb + +# 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 + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# 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 + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# 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/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt + +# 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 +*.jfm +*.pfx +*.publishsettings +node_modules/ +orleans.codegen.cs + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# 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 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# 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 +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml + +# CodeRush +.cr/ + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/ From 91d7f0c7a836ccc4e25faac37fe115ae227475df Mon Sep 17 00:00:00 2001 From: Michael Norris Date: Sun, 11 Dec 2016 23:31:47 -0500 Subject: [PATCH 16/26] book changes, motion fixes for assailing. --- .vs/slnx.sqlite | Bin 304128 -> 304128 bytes hybrasyl/Book.cs | 38 +++++++------- hybrasyl/Enums.cs | 1 + hybrasyl/Hybrasyl.csproj | 21 ++++---- hybrasyl/Objects/User.cs | 111 ++++++++++++++++++++------------------- hybrasyl/packages.config | 4 +- 6 files changed, 88 insertions(+), 87 deletions(-) diff --git a/.vs/slnx.sqlite b/.vs/slnx.sqlite index faac8f4845508344fb8789d9b78ec4d71c3da749..bc9c2b95e542362f4f40424765d9bf2f05771bc0 100644 GIT binary patch delta 31 ncmZoTA=Gd}Xo57O)I=F)MybYx)&$1Z1g6#m=B)`V3l;+arw0mn delta 31 ncmZoTA=Gd}Xo57O().SingleOrDefault(x => x.Name.ToLower() == item[i]); - } + dynamic item; + if (!TryGetValue(jArray[i], out item)) continue; + book[i] = Game.World.WorldData.Values().SingleOrDefault(x => x.Name.ToLower() == (string)item.Name); + var castable = book[i]; + if (castable != null) castable.CastableLevel = (byte)item.Level; } return book; } @@ -53,12 +52,11 @@ public override object ReadJson(JsonReader reader, Type objectType, object exist for (byte i = 0; i < jArray.Count; i++) { - string[] item; - if (TryGetValue(jArray[i], out item)) - { - book[i] = - Game.World.WorldData.Values().SingleOrDefault(x => x.Name.ToLower() == item[i]); - } + dynamic item; + if (!TryGetValue(jArray[i], out item)) continue; + book[i] = Game.World.WorldData.Values().SingleOrDefault(x => x.Name.ToLower() == (string)item.Name); + var castable = book[i]; + if (castable != null) castable.CastableLevel = (byte)item.Level; } return book; } @@ -71,12 +69,12 @@ public override bool CanConvert(Type objectType) return objectType == typeof(Inventory); } - public bool TryGetValue(JToken token, out string[] item) + public bool TryGetValue(JToken token, out dynamic item) { item = null; if (!token.HasValues) return false; - item = token.ToObject(); + item = token.ToObject(); return true; } } diff --git a/hybrasyl/Enums.cs b/hybrasyl/Enums.cs index 2e07071a..20aabede 100644 --- a/hybrasyl/Enums.cs +++ b/hybrasyl/Enums.cs @@ -349,6 +349,7 @@ public enum PlayerCondition : int InDialog = 0x40, InComa = 0x80, Casting = 0x100, + Pvp = 0x200, AliveExchange = (Alive | InExchange) } diff --git a/hybrasyl/Hybrasyl.csproj b/hybrasyl/Hybrasyl.csproj index 5b88af21..7314b8ba 100644 --- a/hybrasyl/Hybrasyl.csproj +++ b/hybrasyl/Hybrasyl.csproj @@ -107,18 +107,19 @@ packages\IronPython.2.7.5\lib\Net45\Microsoft.Scripting.Metadata.dll True - - packages\Newtonsoft.Json.8.0.2\lib\net45\Newtonsoft.Json.dll + + packages\Newtonsoft.Json.9.0.2-beta1\lib\net45\Newtonsoft.Json.dll True - - packages\StackExchange.Redis.1.0.488\lib\net45\StackExchange.Redis.dll + + packages\StackExchange.Redis.1.1.608\lib\net45\StackExchange.Redis.dll True + @@ -236,11 +237,11 @@ - \ No newline at end of file diff --git a/hybrasyl/Objects/User.cs b/hybrasyl/Objects/User.cs index 4f08f71e..399e88e9 100644 --- a/hybrasyl/Objects/User.cs +++ b/hybrasyl/Objects/User.cs @@ -2217,15 +2217,11 @@ public override void Attack(Castable castObject, Creature target = null) try { - motion = - castObject.Effects.Animations.OnCast.Motions.SingleOrDefault( - x => x.Class.Contains((Class) Class)); + motion = castObject.Effects.Animations.OnCast.Motions.SingleOrDefault(x => x.Class.Contains((Class) Class)); } catch (InvalidOperationException) { - motion = - castObject.Effects.Animations.OnCast.Motions.FirstOrDefault( - x => x.Class.Contains((Class) Class)); + motion = castObject.Effects.Animations.OnCast.Motions.FirstOrDefault(x => x.Class.Contains((Class) Class)); Logger.ErrorFormat("{1}: contains more than one motion for a class definition, using first one found!", castObject.Name); } @@ -2259,8 +2255,6 @@ public override void Attack(Castable castObject) foreach (var intent in intents) { //isclick should always be 0 for a skill. - var targetAreas = new List>(); - var possibleTargets = new List(); possibleTargets.AddRange(Map.EntityTree.GetObjects(new Rectangle(this.X - intent.Radius, this.Y, (this.X + intent.Radius) - (this.X - intent.Radius), (this.Y + intent.Radius) - (this.Y - intent.Radius))).Where(obj => obj is Creature && obj != this && obj.GetType() != typeof(User))); @@ -2278,16 +2272,15 @@ public override void Attack(Castable castObject) foreach (var target in actualTargets) { - if (target != null && target is Monster) + if (target is Monster) { - Random rand = new Random(); + var rand = new Random(); if (damage.Formula == null) //will need to be expanded. also will need to account for damage scripts { var simple = damage.Simple; - var damageType = EnumUtil.ParseEnum(damage.Type.ToString(), - Enums.DamageType.Magical); + var damageType = EnumUtil.ParseEnum(damage.Type.ToString(), Enums.DamageType.Magical); var dmg = rand.Next(Convert.ToInt32(simple.Min), Convert.ToInt32(simple.Max)); //these need to be set to integers as attributes. note to fix. target.Damage(dmg, OffensiveElement, damageType, this); @@ -2295,14 +2288,13 @@ public override void Attack(Castable castObject) else { var formula = damage.Formula; - var damageType = EnumUtil.ParseEnum(damage.Type.ToString(), - Enums.DamageType.Magical); - FormulaParser parser = new FormulaParser(this, castObject, target); + var damageType = EnumUtil.ParseEnum(damage.Type.ToString(), Enums.DamageType.Magical); + var parser = new FormulaParser(this, castObject, target); var dmg = parser.Eval(formula); if (dmg == 0) dmg = 1; target.Damage(dmg, OffensiveElement, damageType, this); - var effectAnimation = new ServerPacketStructures.EffectAnimation() {SourceId = this.Id ,Speed = (short)castObject.Effects.Animations.OnCast.Target.Speed, TargetId = target.Id, TargetAnimation = /*castObject.Effects.Animations.OnCast.Target.Id*/ 237 }; + var effectAnimation = new ServerPacketStructures.EffectAnimation() {SourceId = this.Id ,Speed = (short)castObject.Effects.Animations.OnCast.Target.Speed, TargetId = target.Id, TargetAnimation = castObject.Effects.Animations.OnCast.Target.Id }; Enqueue(effectAnimation.Packet()); SendAnimation(effectAnimation.Packet()); @@ -2321,33 +2313,27 @@ public override void Attack(Castable castObject) try { - motion = - castObject.Effects.Animations.OnCast.Motions.SingleOrDefault( - x => x.Class.Contains((Class)Class)); + motion = castObject.Effects.Animations.OnCast.Motions.SingleOrDefault(x => x.Class.Contains((Class)Class)); } catch (InvalidOperationException) { - motion = - castObject.Effects.Animations.OnCast.Motions.FirstOrDefault( - x => x.Class.Contains((Class)Class)); + motion = castObject.Effects.Animations.OnCast.Motions.FirstOrDefault(x => x.Class.Contains((Class)Class)); Logger.ErrorFormat("{1}: contains more than one motion for a class definition, using first one found!", castObject.Name); } var sound = new ServerPacketStructures.PlaySound { Sound = (byte)castObject.Effects.Sound.Id }; - if (motion != null) { var playerAnimation = new ServerPacketStructures.PlayerAnimation() { Animation = (byte)motion.Id, - Speed = (ushort)(motion.Speed / 5), + Speed = (ushort)(motion.Speed / 5), //handles the speed offset in this specific packet. UserId = Id }; Enqueue(playerAnimation.Packet()); SendAnimation(playerAnimation.Packet()); - } Enqueue(sound.Packet()); PlaySound(sound.Packet()); @@ -2368,39 +2354,56 @@ public void AssailAttack(Direction direction, Creature target = null) case Direction.East: { var obj = Map.EntityTree.FirstOrDefault(x => x.X == X + 1 && x.Y == Y); - if (obj is Monster) target = (Monster) obj; - if (obj is User) + var monster = obj as Monster; + if (monster != null) target = monster; + var user = obj as User; + if (user != null && user.Status.HasFlag(PlayerCondition.Pvp)) { - var user = (User) obj; - //need to do something for if pvpflagged. + target = user; } } break; case Direction.West: { var obj = Map.EntityTree.FirstOrDefault(x => x.X == X - 1 && x.Y == Y); - if (obj is Monster) target = (Monster) obj; + var monster = obj as Monster; + if (monster != null) target = monster; + var user = obj as User; + if (user != null && user.Status.HasFlag(PlayerCondition.Pvp)) + { + target = user; + } } break; case Direction.North: { var obj = Map.EntityTree.FirstOrDefault(x => x.X == X && x.Y == Y - 1); - if (obj is Monster) target = (Monster) obj; + var monster = obj as Monster; + if (monster != null) target = monster; + var user = obj as User; + if (user != null && user.Status.HasFlag(PlayerCondition.Pvp)) + { + target = user; + } } break; case Direction.South: { var obj = Map.EntityTree.FirstOrDefault(x => x.X == X && x.Y == Y + 1); - if (obj is Monster) target = (Monster) obj; + var monster = obj as Monster; + if (monster != null) target = monster; + var user = obj as User; + if (user != null && user.Status.HasFlag(PlayerCondition.Pvp)) + { + target = user; + } } break; - default: - break; } //try to get the creature we're facing and set it as the target. } - foreach (Castable c in SkillBook) + foreach (var c in SkillBook) { if (c.IsAssail) { @@ -2408,9 +2411,9 @@ public void AssailAttack(Direction direction, Creature target = null) } } //animation handled here as to not repeatedly send assails. - //this.LastAttack = DateTime.Now; - var assail = new ServerPacketStructures.PlayerAnimation() {Animation = 1, Speed = 20, UserId = this.Id}; - var sound = new ServerPacketStructures.PlaySound() {Sound = 01}; + var motionId = (byte)SkillBook.FirstOrDefault(x => x.IsAssail).Effects.Animations.OnCast.Motions.FirstOrDefault(y => y.Class.Contains((Class) Class)).Id; + var assail = new ServerPacketStructures.PlayerAnimation() {Animation = motionId , Speed = 20, UserId = this.Id}; + var sound = new ServerPacketStructures.PlaySound() {Sound = (byte)SkillBook.FirstOrDefault(x => x.IsAssail).Effects.Sound.Id}; Enqueue(assail.Packet()); Enqueue(sound.Packet()); SendAnimation(assail.Packet()); @@ -2420,31 +2423,29 @@ public void AssailAttack(Direction direction, Creature target = null) private string GroupProfileSegment() { - StringBuilder sb = new StringBuilder(); + var sb = new StringBuilder(); // Only build this string if the user's in a group. Otherwise an empty // string should be sent. - if (Grouped) - { - sb.Append("Group members"); - sb.Append((char)0x0A); + if (!Grouped) return sb.ToString(); + sb.Append("Group members"); + sb.Append((char)0x0A); - // The user's name should go first, and should not have an asterisk. - // In practice this will mean that the user's name appears first and - // is grayed out, while all other names are white. - sb.Append(" " + Name); - sb.Append((char)0x0A); + // The user's name should go first, and should not have an asterisk. + // In practice this will mean that the user's name appears first and + // is grayed out, while all other names are white. + sb.Append(" " + Name); + sb.Append((char)0x0A); - foreach (var member in Group.Members) + foreach (var member in Group.Members) + { + if (member.Name != Name) { - if (member.Name != Name) - { - sb.Append(" " + member.Name); - sb.Append((char)0x0A); - } + sb.Append(" " + member.Name); + sb.Append((char)0x0A); } - sb.Append(String.Format("Total {0}", Group.Members.Count)); } + sb.Append($"Total {Group.Members.Count}"); return sb.ToString(); } diff --git a/hybrasyl/packages.config b/hybrasyl/packages.config index b933c320..dda0f289 100644 --- a/hybrasyl/packages.config +++ b/hybrasyl/packages.config @@ -8,9 +8,9 @@ - + - + \ No newline at end of file From 35d8380b45be753821bdc9209034fa8b517aae13 Mon Sep 17 00:00:00 2001 From: Michael Norris Date: Mon, 12 Dec 2016 21:41:19 -0500 Subject: [PATCH 17/26] Fix for inventory to properly use JObjects and dynamics for items. --- hybrasyl/Inventory.cs | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/hybrasyl/Inventory.cs b/hybrasyl/Inventory.cs index abe0b68f..269686b7 100644 --- a/hybrasyl/Inventory.cs +++ b/hybrasyl/Inventory.cs @@ -357,36 +357,37 @@ public override void WriteJson(JsonWriter writer, object value, JsonSerializer s var output = new object[inventory.Size]; for (byte i = 0; i < inventory.Size; i++) { - var itemInfo = new Dictionary(); + dynamic itemInfo = new JObject(); if (inventory[i] != null) { - itemInfo["Name"] = inventory[i].Name; - itemInfo["Count"] = inventory[i].Count; + itemInfo.Name = inventory[i].Name; + itemInfo.Count = inventory[i].Count; + itemInfo.Id = inventory[i].TemplateId; output[i] = itemInfo; } } - Newtonsoft.Json.Linq.JArray ja = Newtonsoft.Json.Linq.JArray.FromObject(output); + var ja = JArray.FromObject(output); serializer.Serialize(writer, ja); } public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) { JArray jArray = JArray.Load(reader); - Inventory inv = new Inventory(jArray.Count); + var inv = new Inventory(jArray.Count); for (byte i = 0; i < jArray.Count; i++) { Item itmType = null; - Dictionary item; + dynamic item; if (TryGetValue(jArray[i], out item)) { - itmType = Game.World.WorldData.Get(item.FirstOrDefault().Value); + itmType = Game.World.WorldData.Get(item.Id); //itmType = Game.World.WorldData.Values().Where(x => x.Name == (string)item.FirstOrDefault().Value).FirstOrDefault().Name; if (itmType != null) { inv[i] = new ItemObject(itmType.Id, Game.World) { - Count = item.ContainsKey("Count") ? Convert.ToInt32(item["Count"]) : 1 + Count = item.Count ?? 1 }; //this will need to be expanded later based on ItemObject properties being saved back to the database. } @@ -402,12 +403,12 @@ public override bool CanConvert(Type objectType) return objectType == typeof(Inventory); } - public bool TryGetValue(Newtonsoft.Json.Linq.JToken token, out Dictionary item) + public bool TryGetValue(JToken token, out dynamic item) { item = null; if (!token.HasValues) return false; - item = token.ToObject>(); + item = token.ToObject(); return true; } } From 56eddf825ea3fc69979582a1cb6ad0d15a6a4b4b Mon Sep 17 00:00:00 2001 From: Michael Norris Date: Mon, 12 Dec 2016 22:19:45 -0500 Subject: [PATCH 18/26] package change test --- hybrasyl/Hybrasyl.csproj | 2 +- hybrasyl/packages.config | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/hybrasyl/Hybrasyl.csproj b/hybrasyl/Hybrasyl.csproj index 7314b8ba..d4f80dc7 100644 --- a/hybrasyl/Hybrasyl.csproj +++ b/hybrasyl/Hybrasyl.csproj @@ -108,7 +108,7 @@ True - packages\Newtonsoft.Json.9.0.2-beta1\lib\net45\Newtonsoft.Json.dll + packages\Newtonsoft.Json.9.0.1\lib\net45\Newtonsoft.Json.dll True diff --git a/hybrasyl/packages.config b/hybrasyl/packages.config index dda0f289..9403e1ca 100644 --- a/hybrasyl/packages.config +++ b/hybrasyl/packages.config @@ -8,7 +8,7 @@ - + From f429285038d6c110ec8ed83e3a75f9556b0c5244 Mon Sep 17 00:00:00 2001 From: Michael Norris Date: Tue, 13 Dec 2016 17:43:50 -0500 Subject: [PATCH 19/26] default motion for assail, slight changes to creature packet --- hybrasyl/Objects/Creature.cs | 14 ++++++++++++++ hybrasyl/Objects/Monster.cs | 8 +++----- hybrasyl/Objects/User.cs | 18 +++++++++++++++--- hybrasyl/World.cs | 5 +++-- 4 files changed, 35 insertions(+), 10 deletions(-) diff --git a/hybrasyl/Objects/Creature.cs b/hybrasyl/Objects/Creature.cs index d6786e7b..72bb8a52 100644 --- a/hybrasyl/Objects/Creature.cs +++ b/hybrasyl/Objects/Creature.cs @@ -357,6 +357,20 @@ public void SendAnimation(ServerPacket packet) } } + public void SendCastLine(ServerPacket packet) + { + Logger.InfoFormat("SendCastLine"); + Logger.InfoFormat($"SendCastLine byte format is: {BitConverter.ToString(packet.ToArray())}"); + foreach (var user in Map.EntityTree.GetObjects(GetViewport()).OfType()) + { + var nPacket = (ServerPacket)packet.Clone(); + Logger.InfoFormat($"SendCastLine to {user.Name}"); + user.Enqueue(nPacket); + + } + + } + public virtual void UpdateAttributes(StatUpdateFlags flags) { } diff --git a/hybrasyl/Objects/Monster.cs b/hybrasyl/Objects/Monster.cs index cb4c32ea..9897af39 100644 --- a/hybrasyl/Objects/Monster.cs +++ b/hybrasyl/Objects/Monster.cs @@ -124,11 +124,9 @@ public override void Attack(Castable castObject) public override void ShowTo(VisibleObject obj) { - if (obj is User) - { - var user = obj as User; - user.SendVisibleCreature(this); - } + if (!(obj is User)) return; + var user = obj as User; + user.SendVisibleCreature(this); } public bool IsIdle() diff --git a/hybrasyl/Objects/User.cs b/hybrasyl/Objects/User.cs index 399e88e9..92d5a22d 100644 --- a/hybrasyl/Objects/User.cs +++ b/hybrasyl/Objects/User.cs @@ -1305,9 +1305,18 @@ public void SendVisibleCreature(Creature creature) x07.WriteUInt16(creature.Y); x07.WriteUInt32(creature.Id); x07.WriteUInt16((ushort) (creature.Sprite + 0x4000)); - x07.WriteInt32(0); // Unknown what this is + x07.WriteByte(0); // Unknown what this is + x07.WriteByte(0); + x07.WriteByte(0); + x07.WriteByte(0); + x07.WriteByte((byte)creature.Direction); + x07.WriteByte(0); + x07.WriteByte(1); + x07.WriteString8(creature.Name); x07.DumpPacket(); Enqueue(x07); + + } public void SendUpdateToUser(Client client) @@ -2411,9 +2420,12 @@ public void AssailAttack(Direction direction, Creature target = null) } } //animation handled here as to not repeatedly send assails. - var motionId = (byte)SkillBook.FirstOrDefault(x => x.IsAssail).Effects.Animations.OnCast.Motions.FirstOrDefault(y => y.Class.Contains((Class) Class)).Id; + var firstAssail = SkillBook.FirstOrDefault(x => x.IsAssail); + var motion = firstAssail?.Effects.Animations.OnCast.Motions.FirstOrDefault(y => y.Class.Contains((Class) Class)); + + var motionId = motion != null ? (byte)motion.Id : (byte)1; var assail = new ServerPacketStructures.PlayerAnimation() {Animation = motionId , Speed = 20, UserId = this.Id}; - var sound = new ServerPacketStructures.PlaySound() {Sound = (byte)SkillBook.FirstOrDefault(x => x.IsAssail).Effects.Sound.Id}; + var sound = new ServerPacketStructures.PlaySound() {Sound = firstAssail != null ? (byte)firstAssail.Effects.Sound.Id : (byte)1}; Enqueue(assail.Packet()); Enqueue(sound.Packet()); SendAnimation(assail.Packet()); diff --git a/hybrasyl/World.cs b/hybrasyl/World.cs index 0075eecb..8fa21bc9 100644 --- a/hybrasyl/World.cs +++ b/hybrasyl/World.cs @@ -3837,10 +3837,11 @@ private void PacketHandler_0x4E_CastLine(object obj, ClientPacket packet) var user = (User) obj; var textLength = packet.ReadByte(); var text = packet.Read(textLength); - if (!user.CanCast) return; + if (!user.Status.HasFlag(PlayerCondition.Casting)) return; var x0D = new ServerPacketStructures.CastLine() {ChatType = 2, LineLength = textLength, LineText = Encoding.UTF8.GetString(text), TargetId = user.Id}; var enqueue = x0D.Packet(); - user.Enqueue(enqueue); + + user.SendCastLine(enqueue); } From ee90164291bed57c0fc62ba4994964e51150b117 Mon Sep 17 00:00:00 2001 From: Justin Baugh Date: Mon, 19 Dec 2016 22:20:26 -0500 Subject: [PATCH 20/26] Fix various nullrefs due to sdk; bump sdk requirement to 0.5.5.14 --- hybrasyl/Hybrasyl.csproj | 6 ++-- hybrasyl/Map.cs | 61 ++++++++++++++++++++-------------- hybrasyl/Objects/ItemObject.cs | 9 ++--- hybrasyl/World.cs | 54 +++++++++++++++++------------- hybrasyl/packages.config | 2 +- 5 files changed, 76 insertions(+), 56 deletions(-) diff --git a/hybrasyl/Hybrasyl.csproj b/hybrasyl/Hybrasyl.csproj index d4f80dc7..72328420 100644 --- a/hybrasyl/Hybrasyl.csproj +++ b/hybrasyl/Hybrasyl.csproj @@ -16,6 +16,7 @@ .\ true + false publish\ true Disk @@ -28,7 +29,6 @@ true 0 1.0.0.%2a - false false true @@ -67,8 +67,8 @@ packages\FastMember.1.0.0.11\lib\net40\FastMember.dll - - packages\Hybrasyl.XML.0.5.5.10\lib\net452\Hybrasyl.XML.dll + + packages\Hybrasyl.XML.0.5.5.14\lib\net452\Hybrasyl.XML.dll True diff --git a/hybrasyl/Map.cs b/hybrasyl/Map.cs index e89c8f76..7ed2623f 100755 --- a/hybrasyl/Map.cs +++ b/hybrasyl/Map.cs @@ -30,6 +30,7 @@ using System.Text; using System.Threading; using Microsoft.Scripting.Runtime; +using System.Linq; namespace Hybrasyl { @@ -152,7 +153,7 @@ public byte[] GetBytes() public class Map { public static readonly ILog Logger = LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType); - + public ushort Id { get; set; } public byte X { get; set; } public byte Y { get; set; } @@ -194,7 +195,7 @@ public Map(Maps.Map newMap, World theWorld) X = newMap.X; Y = newMap.Y; Name = newMap.Name; - EntityTree = new QuadTree(0,0,X,Y); + EntityTree = new QuadTree(0, 0, X, Y); Music = newMap.Music; foreach (var warpElement in newMap.Warps) @@ -203,7 +204,7 @@ public Map(Maps.Map newMap, World theWorld) warp.X = warpElement.X; warp.Y = warpElement.Y; - if (warpElement.MapTarget !=null) + if (warpElement.MapTarget != null) { var maptarget = warpElement.MapTarget as Maps.WarpMapTarget; // map warp @@ -219,12 +220,17 @@ public Map(Maps.Map newMap, World theWorld) warp.DestinationMapName = worldmaptarget; warp.WarpType = WarpType.WorldMap; } - - warp.MinimumLevel = warpElement.Restrictions.Level.Min; - warp.MaximumLevel = warpElement.Restrictions.Level.Max; - warp.MinimumAbility = warpElement.Restrictions.Ab.Min; - warp.MaximumAbility = warpElement.Restrictions.Ab.Max; - warp.MobUse = warpElement.Restrictions.NoMobUse; + if (warpElement.Restrictions?.Level != null) + { + warp.MinimumLevel = warpElement.Restrictions.Level.Min; + warp.MaximumLevel = warpElement.Restrictions.Level.Max; + } + if (warpElement.Restrictions?.Ab != null) + { + warp.MinimumAbility = warpElement.Restrictions.Ab.Min; + warp.MaximumAbility = warpElement.Restrictions.Ab.Max; + } + warp.MobUse = warpElement.Restrictions?.NoMobUse ?? true; Warps[new Tuple(warp.X, warp.Y)] = warp; } @@ -242,14 +248,17 @@ public Map(Maps.Map newMap, World theWorld) Y = npcElement.Y, Name = npcElement.Name, Sprite = npcTemplate.Appearance.Sprite, - Direction = (Enums.Direction) npcElement.Direction, + Direction = (Enums.Direction)npcElement.Direction, Portrait = npcTemplate.Appearance.Portrait, }; - if (npcTemplate.Roles.Post != null) { merchant.Jobs ^= MerchantJob.Post; } - if (npcTemplate.Roles.Bank != null) { merchant.Jobs ^= MerchantJob.Bank; } - if (npcTemplate.Roles.Repair != null) { merchant.Jobs ^= MerchantJob.Repair; } - if (npcTemplate.Roles.Train != null) { merchant.Jobs ^= MerchantJob.Train; } - if (npcTemplate.Roles.Vend != null) { merchant.Jobs ^= MerchantJob.Vend; } + if (npcTemplate.Roles != null) + { + if (npcTemplate.Roles.Post != null) { merchant.Jobs ^= MerchantJob.Post; } + if (npcTemplate.Roles.Bank != null) { merchant.Jobs ^= MerchantJob.Bank; } + if (npcTemplate.Roles.Repair != null) { merchant.Jobs ^= MerchantJob.Repair; } + if (npcTemplate.Roles.Train != null) { merchant.Jobs ^= MerchantJob.Train; } + if (npcTemplate.Roles.Vend != null) { merchant.Jobs ^= MerchantJob.Vend; } + } InsertNpc(merchant); } @@ -257,23 +266,25 @@ public Map(Maps.Map newMap, World theWorld) { // TODO: implement reactor loading support } + if (newMap.Signs != null) { + foreach (var postElement in newMap.Signs.Signposts) + { + var signpostElement = postElement as Maps.Signpost; + var signpost = new Objects.Signpost(signpostElement.X, signpostElement.Y, signpostElement.Message); + InsertSignpost(signpost); - foreach (var postElement in newMap.Signs.Signposts) - { - var signpostElement = postElement as Maps.Signpost; - var signpost = new Objects.Signpost(signpostElement.X, signpostElement.Y, signpostElement.Message); - InsertSignpost(signpost); - - } - foreach(var postElement in newMap.Signs.MessageBoards) - { + } + foreach (var postElement in newMap.Signs.MessageBoards) + { var boardElement = postElement as Maps.MessageBoard; var board = new Objects.Signpost(boardElement.X, boardElement.Y, string.Empty, true, boardElement.Name); InsertSignpost(board); - Logger.InfoFormat("{0}: {1} - messageboard loaded", this.Name, board.Name ); + Logger.InfoFormat("{0}: {1} - messageboard loaded", this.Name, board.Name); + } } Load(); } + public Map() { diff --git a/hybrasyl/Objects/ItemObject.cs b/hybrasyl/Objects/ItemObject.cs index b4d7db9c..b194d736 100644 --- a/hybrasyl/Objects/ItemObject.cs +++ b/hybrasyl/Objects/ItemObject.cs @@ -115,7 +115,8 @@ public bool CheckRequirements(User userobj, out String message) public new ushort Sprite => Template.Properties.Appearance.Sprite; - public ItemPropertiesUse Use => Template.Properties.Use; + public bool Usable => Template.Properties.Use != null; + public Use Use => Template.Properties.Use; public ushort EquipSprite { @@ -133,9 +134,9 @@ public ItemObjectType ItemObjectType { if (Template.Properties.Equipment != null) return ItemObjectType.Equipment; - else - return Template.Properties.Use != null ? ItemObjectType.CanUse : ItemObjectType.CannotUse; - + else if (Template.Properties.Use != null) + return ItemObjectType.CanUse; + return ItemObjectType.CannotUse; } } diff --git a/hybrasyl/World.cs b/hybrasyl/World.cs index 8fa21bc9..c80765ed 100644 --- a/hybrasyl/World.cs +++ b/hybrasyl/World.cs @@ -433,7 +433,6 @@ private bool LoadData() { Items.VariantGroup newGroup = Serializer.Deserialize(XmlReader.Create(xml), new Items.VariantGroup()); Logger.DebugFormat("Item variants: loaded {0}", newGroup.Name); - //ItemVariants.Add(newGroup.Name, newGroup); WorldData.Set(newGroup.Name, newGroup); } @@ -448,32 +447,37 @@ private bool LoadData() // Load items foreach (var xml in Directory.GetFiles(ItemDirectory)) { - try - { +// try + // { Item newItem = Serializer.Deserialize(XmlReader.Create(xml), new Item()); Logger.DebugFormat("Items: loaded {0}, id {1}", newItem.Name, newItem.Id); - //Items.Add(newItem.Id, newItem); WorldData.SetWithIndex(newItem.Id, newItem, new Tuple(Sex.Neutral, newItem.Name)); - //ItemCatalog.Add(new Tuple(Sex.Neutral, newItem.Name), newItem); - foreach (var targetGroup in newItem.Properties.Variants.Group) + // Handle some null cases; there's probably a nicer way to do this + if (newItem.Properties.StatEffects.Combat == null) { newItem.Properties.StatEffects.Combat = new StatEffectsCombat(); } + if (newItem.Properties.StatEffects.Element == null) { newItem.Properties.StatEffects.Element = new StatEffectsElement(); } + if (newItem.Properties.StatEffects.Base == null) { newItem.Properties.StatEffects.Base = new StatEffectsBase(); } + if (newItem.Properties.Variants != null) { - foreach (var variant in WorldData.Get(targetGroup).Variant) + foreach (var targetGroup in newItem.Properties.Variants.Group) { - var variantItem = ResolveVariant(newItem, variant, targetGroup); - //variantItem.Name = $"{variant.Name} {newItem.Name}"; - Logger.DebugFormat("ItemObject {0}: variantgroup {1}, subvariant {2}", variantItem.Name, targetGroup, variant.Name); - if (WorldData.ContainsKey(variantItem.Id)) Logger.ErrorFormat("Item already exists with Key {0} : {1}. Cannot add {2}", variantItem.Id, WorldData.Get(variantItem.Id).Name, variantItem.Name); - //Items.Add(variantItem.Id, variantItem); - WorldData.SetWithIndex(variantItem.Id, variantItem, - new Tuple(Sex.Neutral, variantItem.Name)); - //ItemCatalog.Add(new Tuple(Sex.Neutral, variantItem.Name), variantItem); + foreach (var variant in WorldData.Get(targetGroup).Variant) + { + var variantItem = ResolveVariant(newItem, variant, targetGroup); + //variantItem.Name = $"{variant.Name} {newItem.Name}"; + Logger.DebugFormat("ItemObject {0}: variantgroup {1}, subvariant {2}", variantItem.Name, targetGroup, variant.Name); + if (WorldData.ContainsKey(variantItem.Id)) Logger.ErrorFormat("Item already exists with Key {0} : {1}. Cannot add {2}", variantItem.Id, WorldData.Get(variantItem.Id).Name, variantItem.Name); + //Items.Add(variantItem.Id, variantItem); + WorldData.SetWithIndex(variantItem.Id, variantItem, + new Tuple(Sex.Neutral, variantItem.Name)); + //ItemCatalog.Add(new Tuple(Sex.Neutral, variantItem.Name), variantItem); + } } } - } - catch (Exception e) - { - Logger.ErrorFormat("Error parsing {0}: {1}", xml, e); - } + // } + // catch (Exception e) + // { + // Logger.ErrorFormat("Error parsing {0}: {1}", xml, e); + // } } foreach (var xml in Directory.GetFiles(CastableDirectory)) @@ -558,6 +562,10 @@ public Item ResolveVariant(Item item, Items.Variant variant, string variantGroup variantItem.Name = variant.Modifier + " " + item.Name; variantItem.Properties.Flags = variant.Properties.Flags; + if (item.Name == "Beryl Earrings") + { + Logger.Info("hi"); + } variantItem.Properties.Physical.Value = variant.Properties.Physical.Value == 100 ? item.Properties.Physical.Value : Convert.ToUInt32(Math.Round(item.Properties.Physical.Value * (variant.Properties.Physical.Value * .01))); variantItem.Properties.Physical.Durability = variant.Properties.Physical.Durability == 100 ? item.Properties.Physical.Durability : Convert.ToUInt32(Math.Round(item.Properties.Physical.Durability * (variant.Properties.Physical.Durability * .01))); variantItem.Properties.Physical.Weight = variant.Properties.Physical.Weight == 100 ? item.Properties.Physical.Weight : Convert.ToInt32(Math.Round(item.Properties.Physical.Weight * (variant.Properties.Physical.Weight * .01))); @@ -576,6 +584,7 @@ public Item ResolveVariant(Item item, Items.Variant variant, string variantGroup } case "elemental": { + variantItem.Properties.StatEffects.Element = new StatEffectsElement(); variantItem.Properties.StatEffects.Element.Offense = variant.Properties.StatEffects.Element.Offense; variantItem.Properties.StatEffects.Element.Defense = variant.Properties.StatEffects.Element.Defense; break; @@ -662,8 +671,8 @@ private void LoadMetafiles() // TODO: split items into multiple ItemInfo files (DA does ~700 each) foreach (var item in WorldData.Values()) { - iteminfo0.Nodes.Add(new MetafileNode(item.Name, item.Properties.Restrictions.Level.Min, (int)item.Properties.Restrictions.@Class, item.Properties.Physical.Weight, - item.Properties.Vendor.ShopTab, item.Properties.Vendor.Description)); + iteminfo0.Nodes.Add(new MetafileNode(item.Name, item.Properties.Restrictions?.Level?.Min ?? 1, (int)(item.Properties.Restrictions?.@Class ?? Items.Class.Peasant), + item.Properties.Physical.Weight, item.Properties.Vendor?.ShopTab ?? 0, item.Properties.Vendor?.Description ?? String.Empty)); } WorldData.Set(iteminfo0.Name, iteminfo0.Compile()); @@ -880,7 +889,6 @@ public void SetMerchantMenuHandlers() #endregion Set Handlers - // FIXME: *User here should now use the ConcurrentDictionaries instead public void DeleteUser(string username) { WorldData.Remove(username); diff --git a/hybrasyl/packages.config b/hybrasyl/packages.config index 9403e1ca..c98e489b 100644 --- a/hybrasyl/packages.config +++ b/hybrasyl/packages.config @@ -5,7 +5,7 @@ - + From 5b42935f1d24079ecb7ddfb255d2c230d1cf1d38 Mon Sep 17 00:00:00 2001 From: Justin Baugh Date: Mon, 19 Dec 2016 22:29:12 -0500 Subject: [PATCH 21/26] clean up a few things --- hybrasyl/World.cs | 32 ++++++++++++++------------------ 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/hybrasyl/World.cs b/hybrasyl/World.cs index c80765ed..f9bec12d 100644 --- a/hybrasyl/World.cs +++ b/hybrasyl/World.cs @@ -447,8 +447,8 @@ private bool LoadData() // Load items foreach (var xml in Directory.GetFiles(ItemDirectory)) { -// try - // { + try + { Item newItem = Serializer.Deserialize(XmlReader.Create(xml), new Item()); Logger.DebugFormat("Items: loaded {0}, id {1}", newItem.Name, newItem.Id); WorldData.SetWithIndex(newItem.Id, newItem, new Tuple(Sex.Neutral, newItem.Name)); @@ -463,21 +463,21 @@ private bool LoadData() foreach (var variant in WorldData.Get(targetGroup).Variant) { var variantItem = ResolveVariant(newItem, variant, targetGroup); - //variantItem.Name = $"{variant.Name} {newItem.Name}"; Logger.DebugFormat("ItemObject {0}: variantgroup {1}, subvariant {2}", variantItem.Name, targetGroup, variant.Name); - if (WorldData.ContainsKey(variantItem.Id)) Logger.ErrorFormat("Item already exists with Key {0} : {1}. Cannot add {2}", variantItem.Id, WorldData.Get(variantItem.Id).Name, variantItem.Name); - //Items.Add(variantItem.Id, variantItem); + if (WorldData.ContainsKey(variantItem.Id)) + { + Logger.ErrorFormat("Item already exists with Key {0} : {1}. Cannot add {2}", variantItem.Id, WorldData.Get(variantItem.Id).Name, variantItem.Name); + } WorldData.SetWithIndex(variantItem.Id, variantItem, - new Tuple(Sex.Neutral, variantItem.Name)); - //ItemCatalog.Add(new Tuple(Sex.Neutral, variantItem.Name), variantItem); + new Tuple(Sex.Neutral, variantItem.Name)); } } } - // } - // catch (Exception e) - // { - // Logger.ErrorFormat("Error parsing {0}: {1}", xml, e); - // } + } + catch (Exception e) + { + Logger.ErrorFormat("Error parsing {0}: {1}", xml, e); + } } foreach (var xml in Directory.GetFiles(CastableDirectory)) @@ -560,12 +560,9 @@ public Item ResolveVariant(Item item, Items.Variant variant, string variantGroup { var variantItem = item.Clone(); - variantItem.Name = variant.Modifier + " " + item.Name; + variantItem.Name = $"{variant.Modifier} {item.Name}"; variantItem.Properties.Flags = variant.Properties.Flags; - if (item.Name == "Beryl Earrings") - { - Logger.Info("hi"); - } + variantItem.Properties.Physical.Value = variant.Properties.Physical.Value == 100 ? item.Properties.Physical.Value : Convert.ToUInt32(Math.Round(item.Properties.Physical.Value * (variant.Properties.Physical.Value * .01))); variantItem.Properties.Physical.Durability = variant.Properties.Physical.Durability == 100 ? item.Properties.Physical.Durability : Convert.ToUInt32(Math.Round(item.Properties.Physical.Durability * (variant.Properties.Physical.Durability * .01))); variantItem.Properties.Physical.Weight = variant.Properties.Physical.Weight == 100 ? item.Properties.Physical.Weight : Convert.ToInt32(Math.Round(item.Properties.Physical.Weight * (variant.Properties.Physical.Weight * .01))); @@ -584,7 +581,6 @@ public Item ResolveVariant(Item item, Items.Variant variant, string variantGroup } case "elemental": { - variantItem.Properties.StatEffects.Element = new StatEffectsElement(); variantItem.Properties.StatEffects.Element.Offense = variant.Properties.StatEffects.Element.Offense; variantItem.Properties.StatEffects.Element.Defense = variant.Properties.StatEffects.Element.Defense; break; From e9d7fafda8faa816ca5d32bccf42247b7358b9dd Mon Sep 17 00:00:00 2001 From: Justin Baugh Date: Tue, 20 Dec 2016 13:39:54 -0500 Subject: [PATCH 22/26] [SERVER-186] HP/MP should never be allowed to be zero; update references for properties --- hybrasyl/Hybrasyl.csproj | 4 +-- hybrasyl/Objects/Creature.cs | 4 +-- hybrasyl/Objects/ItemObject.cs | 59 +++++++++++++++------------------- hybrasyl/packages.config | 2 +- 4 files changed, 31 insertions(+), 38 deletions(-) diff --git a/hybrasyl/Hybrasyl.csproj b/hybrasyl/Hybrasyl.csproj index 72328420..9f6493a7 100644 --- a/hybrasyl/Hybrasyl.csproj +++ b/hybrasyl/Hybrasyl.csproj @@ -67,8 +67,8 @@ packages\FastMember.1.0.0.11\lib\net40\FastMember.dll - - packages\Hybrasyl.XML.0.5.5.14\lib\net452\Hybrasyl.XML.dll + + packages\Hybrasyl.XML.0.5.5.16\lib\net452\Hybrasyl.XML.dll True diff --git a/hybrasyl/Objects/Creature.cs b/hybrasyl/Objects/Creature.cs index 72bb8a52..899c25fb 100644 --- a/hybrasyl/Objects/Creature.cs +++ b/hybrasyl/Objects/Creature.cs @@ -136,7 +136,7 @@ public uint MaximumHp return uint.MaxValue; if (value < uint.MinValue) - return uint.MinValue; + return 1; return (uint)BindToRange(value, StatLimitConstants.MIN_BASE_HPMP, StatLimitConstants.MAX_BASE_HPMP); } @@ -152,7 +152,7 @@ public uint MaximumMp return uint.MaxValue; if (value < uint.MinValue) - return uint.MinValue; + return 1; return (uint)BindToRange(value, StatLimitConstants.MIN_BASE_HPMP, StatLimitConstants.MAX_BASE_HPMP); } diff --git a/hybrasyl/Objects/ItemObject.cs b/hybrasyl/Objects/ItemObject.cs index b194d736..334424e5 100644 --- a/hybrasyl/Objects/ItemObject.cs +++ b/hybrasyl/Objects/ItemObject.cs @@ -143,50 +143,43 @@ public ItemObjectType ItemObjectType public WeaponType WeaponType => Template.Properties.Equipment.WeaponType; public byte EquipmentSlot => Convert.ToByte(Template.Properties.Equipment.Slot); public int Weight => Template.Properties.Physical.Weight; - public int MaximumStack => Template.Properties.Stackable.Max; - public bool Stackable => Template.Properties.Stackable.Max > 1; + public int MaximumStack => Template.MaximumStack; + public bool Stackable => Template.Stackable; public uint MaximumDurability => Template.Properties.Physical.Durability; - public byte Level => Template.Properties.Restrictions.Level.Min; - public byte Ability => (byte)Template.Properties.Restrictions.Ab.Min; - public Enums.Class Class => (Enums.Class) Template.Properties.Restrictions.@Class; - public Sex Sex => (Sex)Template.Properties.Restrictions.Gender; - - public int BonusHp => Template.Properties.StatEffects.@Base.Hp; - public int BonusMp => Template.Properties.StatEffects.@Base.Mp; - public sbyte BonusStr => Template.Properties.StatEffects.@Base.Str; - public sbyte BonusInt => Template.Properties.StatEffects.@Base.@Int; - public sbyte BonusWis => Template.Properties.StatEffects.@Base.Wis; - public sbyte BonusCon => Template.Properties.StatEffects.@Base.Con; - public sbyte BonusDex => Template.Properties.StatEffects.@Base.Dex; - public sbyte BonusDmg => Template.Properties.StatEffects.Combat.Dmg; - public sbyte BonusHit => Template.Properties.StatEffects.Combat.Hit; - public sbyte BonusAc => Template.Properties.StatEffects.Combat.Ac; - public sbyte BonusMr => Template.Properties.StatEffects.Combat.Mr; - public sbyte BonusRegen => Template.Properties.StatEffects.Combat.Regen; + public byte Level => Template.Level; + public byte Ability => Template.Ability; + public Enums.Class Class => (Enums.Class)(Template.Class); + public Sex Sex => (Sex)Template.Gender; + + public int BonusHp => Template.BonusHp; + public int BonusMp => Template.BonusMp; + public sbyte BonusStr => Template.BonusStr; + public sbyte BonusInt => Template.BonusInt; + public sbyte BonusWis => Template.BonusWis; + public sbyte BonusCon => Template.BonusCon; + public sbyte BonusDex => Template.BonusDex; + public sbyte BonusDmg => Template.BonusDmg; + public sbyte BonusHit => Template.BonusHit; + public sbyte BonusAc => Template.BonusAc; + public sbyte BonusMr => Template.BonusMr; + public sbyte BonusRegen => Template.BonusRegen; public byte Color => Convert.ToByte(Template.Properties.Appearance.Color); public byte BodyStyle => Convert.ToByte(Template.Properties.Appearance.BodyStyle); - public Enums.Element Element - { - get - { - if (WeaponType == WeaponType.None) - return (Enums.Element) Template.Properties.StatEffects.Element.Defense; - return (Enums.Element) Template.Properties.StatEffects.Element.Offense; - } - } - public ushort MinLDamage => Template.Properties.Damage.Large.Min; - public ushort MaxLDamage => Template.Properties.Damage.Large.Max; - public ushort MinSDamage => Template.Properties.Damage.Small.Min; - public ushort MaxSDamage => Template.Properties.Damage.Small.Max; + public Enums.Element Element => (Enums.Element)Template.Element; + + public ushort MinLDamage => Template.MinLDamage; + public ushort MaxLDamage => Template.MaxLDamage; + public ushort MinSDamage => Template.MinSDamage; + public ushort MaxSDamage => Template.MaxSDamage; public ushort DisplaySprite => Template.Properties.Appearance.DisplaySprite; public uint Value => Template.Properties.Physical.Value; - public sbyte Regen => Template.Properties.StatEffects.Combat.Regen; + public sbyte Regen => Template.Regen; public bool Enchantable => Template.Properties.Flags.HasFlag(ItemFlags.Enchantable); diff --git a/hybrasyl/packages.config b/hybrasyl/packages.config index c98e489b..37424a0b 100644 --- a/hybrasyl/packages.config +++ b/hybrasyl/packages.config @@ -5,7 +5,7 @@ - + From 8f4a15acd2037d45a63c0babdc62146f82e0f099 Mon Sep 17 00:00:00 2001 From: Justin Baugh Date: Tue, 20 Dec 2016 14:48:05 -0500 Subject: [PATCH 23/26] Add scripting handles for mp/hp; ensure heal sends stat update --- hybrasyl/Objects/Creature.cs | 2 +- hybrasyl/Objects/ItemObject.cs | 1 - hybrasyl/Scripting/HybrasylUser.cs | 19 +++++++++++++++++++ 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/hybrasyl/Objects/Creature.cs b/hybrasyl/Objects/Creature.cs index 899c25fb..c62a9ea2 100644 --- a/hybrasyl/Objects/Creature.cs +++ b/hybrasyl/Objects/Creature.cs @@ -428,7 +428,7 @@ public virtual void Heal(double heal, Creature healer = null) Hp = heal > uint.MaxValue ? MaximumHp : Math.Min(MaximumHp, (uint)(Hp + heal)); SendDamageUpdate(this); - + if (this is User) { UpdateAttributes(StatUpdateFlags.Current); } } public virtual void RegenerateMp(double mp, Creature regenerator = null) diff --git a/hybrasyl/Objects/ItemObject.cs b/hybrasyl/Objects/ItemObject.cs index 334424e5..b0a916bb 100644 --- a/hybrasyl/Objects/ItemObject.cs +++ b/hybrasyl/Objects/ItemObject.cs @@ -216,7 +216,6 @@ public Item GetVariant(int variantId) public void Invoke(User trigger) { - trigger.SendMessage("Not implemented.", 3); // Run through all the different potential uses. We allow combinations of any // use specified in the item XML. Logger.InfoFormat($"User {trigger.Name}: used item {Name}"); diff --git a/hybrasyl/Scripting/HybrasylUser.cs b/hybrasyl/Scripting/HybrasylUser.cs index 582be14b..d05a9b43 100644 --- a/hybrasyl/Scripting/HybrasylUser.cs +++ b/hybrasyl/Scripting/HybrasylUser.cs @@ -41,6 +41,25 @@ public class HybrasylUser internal HybrasylMap Map { get; set; } public string Name => User.Name; + public uint Hp + { + get { return User.Hp; } + set { + User.Hp = value; + User.UpdateAttributes(StatUpdateFlags.Current); + } + } + + public uint Mp + { + get { return User.Mp; } + set + { + User.Mp = value; + User.UpdateAttributes(StatUpdateFlags.Current); + } + } + public HybrasylUser(User user) { User = user; From 54ce70d62b4b4bb4f7e38502a101c0d439388482 Mon Sep 17 00:00:00 2001 From: Justin Baugh Date: Tue, 20 Dec 2016 17:25:58 -0500 Subject: [PATCH 24/26] [SERVER-148] Use random spawnpoints for login/resurrect spawning --- hybrasyl/Hybrasyl.csproj | 4 ++-- hybrasyl/Objects/User.cs | 4 ++-- hybrasyl/World.cs | 2 +- hybrasyl/packages.config | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/hybrasyl/Hybrasyl.csproj b/hybrasyl/Hybrasyl.csproj index 9f6493a7..87ea65ea 100644 --- a/hybrasyl/Hybrasyl.csproj +++ b/hybrasyl/Hybrasyl.csproj @@ -67,8 +67,8 @@ packages\FastMember.1.0.0.11\lib\net40\FastMember.dll - - packages\Hybrasyl.XML.0.5.5.16\lib\net452\Hybrasyl.XML.dll + + packages\Hybrasyl.XML.0.5.5.17\lib\net452\Hybrasyl.XML.dll True diff --git a/hybrasyl/Objects/User.cs b/hybrasyl/Objects/User.cs index 92d5a22d..82ef166a 100644 --- a/hybrasyl/Objects/User.cs +++ b/hybrasyl/Objects/User.cs @@ -612,8 +612,8 @@ public void Resurrect() // Teleport user to national spawn point Status |= PlayerCondition.Alive; if (Nation.SpawnPoints.Count != 0) - { - var spawnpoint = Nation.SpawnPoints.First(); + { + var spawnpoint = Nation.RandomSpawnPoint; Teleport(spawnpoint.MapName, spawnpoint.X, spawnpoint.Y); } else diff --git a/hybrasyl/World.cs b/hybrasyl/World.cs index f9bec12d..932c79ad 100644 --- a/hybrasyl/World.cs +++ b/hybrasyl/World.cs @@ -2330,7 +2330,7 @@ private void PacketHandler_0x10_ClientJoin(Object obj, ClientPacket packet) else if(loginUser.Nation.SpawnPoints.Count != 0 && loginUser.SinceLastLogin > Hybrasyl.Constants.NATION_SPAWN_TIMEOUT) { - var spawnpoint = loginUser.Nation.SpawnPoints.First(); + var spawnpoint = loginUser.Nation.RandomSpawnPoint; loginUser.Teleport(spawnpoint.MapName, spawnpoint.X, spawnpoint.Y); } else if (WorldData.ContainsKey(loginUser.Location.MapId)) diff --git a/hybrasyl/packages.config b/hybrasyl/packages.config index 37424a0b..b394db8d 100644 --- a/hybrasyl/packages.config +++ b/hybrasyl/packages.config @@ -5,7 +5,7 @@ - + From f53cd885d5846b42b6f29333b09f5b51ffec896f Mon Sep 17 00:00:00 2001 From: Justin Baugh Date: Tue, 20 Dec 2016 17:30:37 -0500 Subject: [PATCH 25/26] 0.5.5 release --- CHANGELOG.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a7ddeac..afcb78aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,30 @@ generally add fairly significant features, whereas ones without are primarily for bugfixing and other updates.* +# Hybrasyl Server 0.5.5 ("Devlin") + +*Released: December 20, 2016* - [View this release on GitHub](https://github.com/hybrasyl/server/releases/tag/0.5.5) + +### Features + +* Spell targeting implemented +* Cast lines for castables supported +* Creatures and players can now die and have OnDeath events +* Basic player status support (such as poison, sleep, etc) implemented +* Monster spawning support implemented (regularly spawning new creatures in an area, using spawngroups) +* Castable support is mostly implemented; 0.5.6 will complete the implementation (NPC learning / forgetting skills, etc) + +### Bug Fixes + +* National support for spawn locations fixed / updated +* Two-handed equipment should now properly prevent a shield from being equipped +* Two-handed flags on items should work as expected +* Items allowing negative stats / HP / MP now fixed +* Client can now login again after logging out +* Assail now properly uses the sound from the first assail in your list + +Required SDK Version: at least 0.5.5.17 + # Hybrasyl Server 0.5.2 ("Dar") *Released: June 6, 2016* - [View this release on GitHub](https://github.com/hybrasyl/server/releases/tag/0.5.2) @@ -97,3 +121,4 @@ * Poor, long-suffering Riona in Mileth, critically wounded in a prior release, will now respond to Aislings again. +x \ No newline at end of file From 29279b6048b2059a29433721298541ef74d97353 Mon Sep 17 00:00:00 2001 From: Justin Baugh Date: Tue, 20 Dec 2016 17:42:56 -0500 Subject: [PATCH 26/26] Final release notes for 0.5.5 --- CHANGELOG.md | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index afcb78aa..e7faed9e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,7 +15,9 @@ * Creatures and players can now die and have OnDeath events * Basic player status support (such as poison, sleep, etc) implemented * Monster spawning support implemented (regularly spawning new creatures in an area, using spawngroups) -* Castable support is mostly implemented; 0.5.6 will complete the implementation (NPC learning / forgetting skills, etc) +* Castable support is mostly implemented; 0.5.6 will complete the implementation (NPC learning / forgetting skills, proper directional usage, etc) +* Skills and spells are now movable on the client pane +* Use skill / use spell handlers implemented ### Bug Fixes @@ -23,9 +25,14 @@ * Two-handed equipment should now properly prevent a shield from being equipped * Two-handed flags on items should work as expected * Items allowing negative stats / HP / MP now fixed -* Client can now login again after logging out * Assail now properly uses the sound from the first assail in your list +### Known Issues + +* Server socket state can sometimes get a little wonky which can require a restart. We're working on it. +* Client sometimes cannot login again after logging off. +* Packet throttling is disabled pending reimplementation. This means you can spam attack things at the moment, and also is related to the socket state issues. + Required SDK Version: at least 0.5.5.17 # Hybrasyl Server 0.5.2 ("Dar")