diff --git a/.gitignore b/.gitignore index 6f60bdb..2a793aa 100644 --- a/.gitignore +++ b/.gitignore @@ -81,4 +81,5 @@ cabal.project.local~ **/.idea .pyc -__pycache__ \ No newline at end of file +__pycache__ +venv \ No newline at end of file diff --git a/.style.yapf b/.style.yapf new file mode 100644 index 0000000..44da46b --- /dev/null +++ b/.style.yapf @@ -0,0 +1,2 @@ +[style] +INDENT_DICTIONARY_VALUE = true diff --git a/README.md b/README.md index f3c616e..12f4609 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,10 @@ A Runescape server engine written in Haskell. Targets revision 317. Takes some inspiration from [luna-rs](https://github.com/luna-rs/luna) and [317refactor](https://github.com/Jameskmonger/317refactor). +## Engine demo (v0.0.1, 2023-06-14) +[![2023-06-14](https://img.youtube.com/vi/q1qQ_Inp_QI/0.jpg)](https://www.youtube.com/watch?v=q1qQ_Inp_QI) + + ## What works - [x] Players can login - [x] Players can walk around the world and load the map around them @@ -14,15 +18,19 @@ Takes some inspiration from - [x] Players can equip armour and weapons - [x] Players can seen and interact with NPCs - [x] Players can seen and interact with game objects -- [-] The world can be configured using Python scripts +- [x] The world can be configured using Python scripts +- [x] Players can drop and pick up items +- [x] Dialogue interfaces can be invoked and trigger callbacks +- [x] Scripts can be invoked dynamically through timeouts or callbacks ## Priorities -- [ ] Augment scripting API actions and events +- [ ] Instanced game objects - [ ] Game data loading - [ ] Item, NPC, Object definitions - [ ] Collision map and pathing - [ ] Static object set - [ ] Persistence mechanism + ## Running requirements - Revision-317 cache files - Revision-317 client with encryption disabled diff --git a/app/PotatoCactus/Boot/GameThreadMain.hs b/app/PotatoCactus/Boot/GameThreadMain.hs index 191e3dc..7a5d693 100644 --- a/app/PotatoCactus/Boot/GameThreadMain.hs +++ b/app/PotatoCactus/Boot/GameThreadMain.hs @@ -1,7 +1,7 @@ module PotatoCactus.Boot.GameThreadMain where import Control.Concurrent (Chan, forkFinally, readChan, threadDelay, writeChan) -import Data.IORef ( readIORef, writeIORef ) +import Data.IORef (readIORef, writeIORef) import Data.Typeable (typeOf) import GHC.Clock (getMonotonicTimeNSec) import PotatoCactus.Boot.GameChannel (gameChannel) @@ -32,7 +32,7 @@ mainLoop = do newWorld <- reduceUntilNextTick_ world gameChannel newWorld2 <- dispatchScriptEvents newWorld - -- logger_ Info $ (show newWorld2) + -- logger_ Info $ (show newWorld2) writeIORef worldInstance newWorld2 -- TODO - Investigate blocking IO for freeze on player disconnect bug - keotl 2023-03-27 diff --git a/app/PotatoCactus/Client/ClientUpdate.hs b/app/PotatoCactus/Client/ClientUpdate.hs index 8257cfb..8732e6c 100644 --- a/app/PotatoCactus/Client/ClientUpdate.hs +++ b/app/PotatoCactus/Client/ClientUpdate.hs @@ -11,7 +11,11 @@ import GHC.IORef (readIORef) import Network.Socket import Network.Socket.ByteString (recv, send, sendAll) import PotatoCactus.Client.GameObjectUpdate.EncodeGameObjectUpdate (encodeGameObjectUpdate) +import PotatoCactus.Client.GroundItemsUpdate.EncodeGroundItemsUpdate (encodeGroundItemsUpdate) +import PotatoCactus.Client.GroundItemsUpdate.GroundItemsUpdateDiff (GroundItemClientView) +import PotatoCactus.Client.Interface.EncodeInterfaceUpdate (encodeInterfaceUpdate) import PotatoCactus.Client.LocalEntityList (LocalEntityList, updateLocalEntities) +import PotatoCactus.Game.Entity.GroundItem.GroundItem (GroundItem) import PotatoCactus.Game.Entity.Npc.Npc (Npc) import PotatoCactus.Game.Entity.Object.DynamicObjectCollection (DynamicObject) import PotatoCactus.Game.Entity.Object.GameObject (GameObject (GameObject)) @@ -19,7 +23,7 @@ import PotatoCactus.Game.Message.ObjectClickPayload (ObjectClickPayload (ObjectC import PotatoCactus.Game.Movement.MovementEntity (MovementEntity (PlayerWalkMovement_), hasChangedRegion) import PotatoCactus.Game.Movement.PlayerWalkMovement (PlayerWalkMovement (lastRegionUpdate_)) import PotatoCactus.Game.Movement.PositionXY (fromXY, toXY) -import PotatoCactus.Game.Player (Player (Player, equipment, inventory, movement, serverIndex, username)) +import PotatoCactus.Game.Player (Player (Player, equipment, interfaces, inventory, movement, serverIndex, username)) import qualified PotatoCactus.Game.Player as P import PotatoCactus.Game.PlayerUpdate.Equipment (Equipment (container)) import PotatoCactus.Game.Position (GetPosition (getPosition), Position (Position, x, y)) @@ -42,6 +46,7 @@ data ClientLocalState_ = ClientLocalState_ { localPlayers :: LocalEntityList Player, localNpcs :: LocalEntityList Npc, gameObjects :: [DynamicObject], + groundItems :: [GroundItemClientView], localPlayerIndex :: Int } @@ -50,6 +55,7 @@ defaultState = { localPlayers = [], localNpcs = [], gameObjects = [], + groundItems = [], localPlayerIndex = -1 } @@ -78,6 +84,8 @@ updateClient sock client localState W.WorldUpdatedMessage = do sendAll sock (updateRunEnergyPacket 100) + sendAll sock (encodeInterfaceUpdate (interfaces p)) + -- case clickedEntity world of -- Nothing -> pure () -- Just (ObjectClickPayload objectId position index) -> do @@ -105,16 +113,19 @@ updateClient sock client localState W.WorldUpdatedMessage = do -- -- ((getPosition p) {x = 1 + x (getPosition p)}) -- ) -- ) - let (newObjects, packets) = encodeGameObjectUpdate (gameObjects localState) world p - in do - sendAll sock packets - return - ClientLocalState_ - { localPlayers = newLocalPlayers, - localNpcs = newLocalNpcs, - localPlayerIndex = serverIndex p, - gameObjects = newObjects - } + let (newObjects, objectPackets) = encodeGameObjectUpdate (gameObjects localState) world p + in let (newGroundItems, groundItemPackets) = encodeGroundItemsUpdate (groundItems localState) world p + in do + sendAll sock objectPackets + sendAll sock groundItemPackets + return + ClientLocalState_ + { localPlayers = newLocalPlayers, + localNpcs = newLocalNpcs, + localPlayerIndex = serverIndex p, + gameObjects = newObjects, + groundItems = newGroundItems + } Nothing -> do logger_ Error $ "Could not find player for client update " ++ W.username client return localState diff --git a/app/PotatoCactus/Client/GameObjectUpdate/EncodeGameObjectUpdate.hs b/app/PotatoCactus/Client/GameObjectUpdate/EncodeGameObjectUpdate.hs index 33ffa92..0465b85 100644 --- a/app/PotatoCactus/Client/GameObjectUpdate/EncodeGameObjectUpdate.hs +++ b/app/PotatoCactus/Client/GameObjectUpdate/EncodeGameObjectUpdate.hs @@ -7,6 +7,7 @@ import qualified PotatoCactus.Game.Entity.Object.DynamicObjectCollection as Obje import PotatoCactus.Game.Movement.MovementEntity (hasChangedRegion) import PotatoCactus.Game.Player (Player (movement)) import PotatoCactus.Game.Position (GetPosition (getPosition), chunkX, chunkY) +import qualified PotatoCactus.Game.Position as Pos import PotatoCactus.Game.World (World (objects)) import PotatoCactus.Network.Packets.Out.AddObjectPacket (addObjectPacket) import PotatoCactus.Network.Packets.Out.ClearChunkObjectsPacket (clearChunksAroundPlayer) @@ -58,7 +59,7 @@ findObjectsAround :: (GetPosition a) => a -> World -> [DynamicObject] findObjectsAround player world = let refPos = getPosition player in Prelude.concat - [ findByChunkXY (x + chunkX refPos) (y + chunkY refPos) (objects world) + [ findByChunkXY (x + chunkX refPos) (y + chunkY refPos) (Pos.z refPos) (objects world) | x <- [-2 .. 1], y <- [-2 .. 1] ] diff --git a/app/PotatoCactus/Client/GroundItemsUpdate/EncodeGroundItemsUpdate.hs b/app/PotatoCactus/Client/GroundItemsUpdate/EncodeGroundItemsUpdate.hs new file mode 100644 index 0000000..e10190f --- /dev/null +++ b/app/PotatoCactus/Client/GroundItemsUpdate/EncodeGroundItemsUpdate.hs @@ -0,0 +1,61 @@ +module PotatoCactus.Client.GroundItemsUpdate.EncodeGroundItemsUpdate (encodeGroundItemsUpdate) where + +import Data.ByteString (ByteString, concat, empty) +import PotatoCactus.Client.GroundItemsUpdate.GroundItemsUpdateDiff (GroundItemClientView, GroundItemDiff (Added, Removed, Retained), computeDiff, fromGroundItem) +import PotatoCactus.Game.Entity.GroundItem.GroundItem (GroundItem) +import PotatoCactus.Game.Entity.GroundItem.GroundItemCollection (findByChunkXYForPlayer) +import PotatoCactus.Game.Movement.MovementEntity (hasChangedRegion) +import PotatoCactus.Game.Player (Player) +import qualified PotatoCactus.Game.Player as P +import PotatoCactus.Game.Position (GetPosition (getPosition), chunkX, chunkY) +import qualified PotatoCactus.Game.Position as Pos +import PotatoCactus.Game.World (World (groundItems)) +import PotatoCactus.Network.Packets.Out.AddGroundItemPacket (addGroundItemPacket) +import PotatoCactus.Network.Packets.Out.RemoveGroundItemPacket (removeGroundItemPacket) +import PotatoCactus.Network.Packets.Out.SetPlacementReferencePacket (setPlacementReferencePacket) + +encodeGroundItemsUpdate :: [GroundItemClientView] -> World -> Player -> ([GroundItemClientView], ByteString) +encodeGroundItemsUpdate oldGroundItems world player = + let newItems = findItemsAround player world + in if hasChangedRegion (P.movement player) + then + ( newItems, + Data.ByteString.concat + ( -- clearChunksAroundPlayer player : -- Assuming + -- already cleared by game object update. We should + -- probably handle both in the same function to get + -- rid of this implicit dependency. + map (encodeSingle player) (computeDiff [] newItems) + ) + ) + else + ( newItems, + Data.ByteString.concat + (map (encodeSingle player) (computeDiff oldGroundItems newItems)) + ) + +encodeSingle :: Player -> GroundItemDiff -> ByteString +encodeSingle p (Added groundItem) = + Data.ByteString.concat + [ setPlacementReferencePacket p (getPosition groundItem), + addGroundItemPacket (getPosition groundItem) groundItem + ] +encodeSingle p (Removed groundItem) = + Data.ByteString.concat + [ setPlacementReferencePacket p (getPosition groundItem), + removeGroundItemPacket (getPosition groundItem) groundItem + ] +encodeSingle p (Retained _) = empty + +findItemsAround :: Player -> World -> [GroundItemClientView] +findItemsAround player world = + let refPos = getPosition player + in Prelude.concat + [ map fromGroundItem $ + findByChunkXYForPlayer + (groundItems world) + (P.username player) + (x + chunkX refPos, y + chunkY refPos, Pos.z refPos) + | x <- [-2 .. 1], + y <- [-2 .. 1] + ] diff --git a/app/PotatoCactus/Client/GroundItemsUpdate/GroundItemsUpdateDiff.hs b/app/PotatoCactus/Client/GroundItemsUpdate/GroundItemsUpdateDiff.hs new file mode 100644 index 0000000..3888352 --- /dev/null +++ b/app/PotatoCactus/Client/GroundItemsUpdate/GroundItemsUpdateDiff.hs @@ -0,0 +1,44 @@ +module PotatoCactus.Client.GroundItemsUpdate.GroundItemsUpdateDiff (fromGroundItem, computeDiff, GroundItemClientView (..), GroundItemDiff (..)) where + +import Data.Maybe (mapMaybe) +import PotatoCactus.Game.Definitions.ItemDefinitions (ItemId) +import PotatoCactus.Game.Entity.GroundItem.GroundItem (GroundItem) +import qualified PotatoCactus.Game.Entity.GroundItem.GroundItem as GroundItem +import PotatoCactus.Game.Position (GetPosition (getPosition), Position) + +data GroundItemClientView = GroundItemClientView + { itemId :: ItemId, + quantity :: Int, + position :: Position + } + deriving (Eq, Show) + +instance GetPosition GroundItemClientView where + getPosition = position + +fromGroundItem :: GroundItem -> GroundItemClientView +fromGroundItem i = + GroundItemClientView + { itemId = GroundItem.itemId i, + quantity = GroundItem.quantity i, + position = GroundItem.position i + } + +data GroundItemDiff = Added GroundItemClientView | Retained GroundItemClientView | Removed GroundItemClientView deriving (Eq, Show) + +computeDiff :: [GroundItemClientView] -> [GroundItemClientView] -> [GroundItemDiff] +computeDiff old new = + map (mapNewObject old) new + ++ mapMaybe (mapOldObject new) old + +mapNewObject :: [GroundItemClientView] -> GroundItemClientView -> GroundItemDiff +mapNewObject oldSet object = + if object `elem` oldSet + then Retained object + else Added object + +mapOldObject :: [GroundItemClientView] -> GroundItemClientView -> Maybe GroundItemDiff +mapOldObject newSet object = + if object `notElem` newSet + then Just $ Removed object + else Nothing diff --git a/app/PotatoCactus/Client/Interface/EncodeInterfaceUpdate.hs b/app/PotatoCactus/Client/Interface/EncodeInterfaceUpdate.hs new file mode 100644 index 0000000..3269829 --- /dev/null +++ b/app/PotatoCactus/Client/Interface/EncodeInterfaceUpdate.hs @@ -0,0 +1,40 @@ +module PotatoCactus.Client.Interface.EncodeInterfaceUpdate (encodeInterfaceUpdate) where + +import Data.ByteString (ByteString, concat, empty) +import Data.Maybe (catMaybes) +import PotatoCactus.Game.Interface.InterfaceController (Interface (Interface, configuredElements), InterfaceController (inputInterface, mainInterface, shouldCloseInterfaces, walkableInterface)) +import PotatoCactus.Game.Scripting.Actions.CreateInterface (InterfaceElement (ChatboxRootWindowElement, ModelAnimationElement, NpcChatheadElement, PlayerChatheadElement, TextElement)) +import PotatoCactus.Network.Packets.Out.ChatboxInterfacePacket (chatboxInterfacePacket) +import PotatoCactus.Network.Packets.Out.CloseInterfacesPacket (closeInterfacesPacket) +import PotatoCactus.Network.Packets.Out.InterfaceAnimationPacket (interfaceAnimationPacket) +import PotatoCactus.Network.Packets.Out.InterfaceChatheadPacket (interfaceNpcChatheadPacket, interfacePlayerChatheadPacket) +import PotatoCactus.Network.Packets.Out.InterfaceTextPacket (interfaceTextPacket) + +encodeInterfaceUpdate :: InterfaceController -> ByteString +encodeInterfaceUpdate c = + if shouldCloseInterfaces c + then closeInterfacesPacket + else + Data.ByteString.concat $ + map encodeInterface_ $ + catMaybes + [ mainInterface c, + walkableInterface c, + inputInterface c + ] + +encodeInterface_ :: Interface -> ByteString +encodeInterface_ Interface {configuredElements = elements} = + Data.ByteString.concat $ map encodeInterfaceElement_ elements + +encodeInterfaceElement_ :: InterfaceElement -> ByteString +encodeInterfaceElement_ (ChatboxRootWindowElement widgetId) = + chatboxInterfacePacket . fromIntegral $ widgetId +encodeInterfaceElement_ (TextElement widgetId text) = + interfaceTextPacket (fromIntegral widgetId) text +encodeInterfaceElement_ (NpcChatheadElement widgetId npcId) = + interfaceNpcChatheadPacket (fromIntegral widgetId) npcId +encodeInterfaceElement_ (PlayerChatheadElement widgetId) = + interfacePlayerChatheadPacket (fromIntegral widgetId) +encodeInterfaceElement_ (ModelAnimationElement widgetId animationId) = + interfaceAnimationPacket (fromIntegral widgetId) (fromIntegral animationId) diff --git a/app/PotatoCactus/Config/Constants.hs b/app/PotatoCactus/Config/Constants.hs index f185326..b5a500a 100644 --- a/app/PotatoCactus/Config/Constants.hs +++ b/app/PotatoCactus/Config/Constants.hs @@ -6,6 +6,9 @@ tickInterval = 600 * 1000 entityViewingDistance :: Int entityViewingDistance = 15 +groundItemGlobalDespawnDelay :: Int +groundItemGlobalDespawnDelay = 100 + maxPlayers :: Int maxPlayers = 2000 diff --git a/app/PotatoCactus/Game/Definitions/ItemDefinitions.hs b/app/PotatoCactus/Game/Definitions/ItemDefinitions.hs index 2f8ca4a..19ed34d 100644 --- a/app/PotatoCactus/Game/Definitions/ItemDefinitions.hs +++ b/app/PotatoCactus/Game/Definitions/ItemDefinitions.hs @@ -40,6 +40,7 @@ initializeDb = do addMockItem_ 1067 "Iron platelegs" False, addMockItem_ 1137 "Iron med helm" False, addMockItem_ 1155 "Bronze full helm" False, + addMockItem_ 1947 "Grain" False, addMockItem_ 617 "Coins" True ] writeIORef itemDb updated diff --git a/app/PotatoCactus/Game/Entity/EntityData.hs b/app/PotatoCactus/Game/Entity/EntityData.hs new file mode 100644 index 0000000..cdf01fe --- /dev/null +++ b/app/PotatoCactus/Game/Entity/EntityData.hs @@ -0,0 +1,13 @@ +module PotatoCactus.Game.Entity.EntityData (EntityData, create, setValue) where + +import Data.Aeson (Value) +import qualified Data.Map.Lazy as Map + +type EntityData = Map.Map String Value + +create :: EntityData +create = Map.empty + +setValue :: EntityData -> String -> Value -> EntityData +setValue store key val = + Map.insert key val store diff --git a/app/PotatoCactus/Game/Entity/GroundItem/GroundItem.hs b/app/PotatoCactus/Game/Entity/GroundItem/GroundItem.hs new file mode 100644 index 0000000..30758c2 --- /dev/null +++ b/app/PotatoCactus/Game/Entity/GroundItem/GroundItem.hs @@ -0,0 +1,36 @@ +module PotatoCactus.Game.Entity.GroundItem.GroundItem where + +import PotatoCactus.Game.Definitions.ItemDefinitions (ItemId) +import qualified PotatoCactus.Game.ItemContainer as ItemStack +import PotatoCactus.Game.Position (GetPosition (getPosition), Position) + +data GroundItem = GroundItem + { itemId :: ItemId, + quantity :: Int, + position :: Position, + player :: Maybe String, + despawnTime :: Int + } + deriving (Show) + +instance GetPosition GroundItem where + getPosition = position + +instance Eq GroundItem where + x == y = (itemId x, quantity x, position x) == (itemId y, quantity y, position y) + +type GroundItemKey = (ItemId, Int, Position) + +matches :: GroundItemKey -> GroundItem -> Bool +matches (keyItemId, keyQuantity, keyPosition) item = + itemId item == keyItemId + && quantity item == keyQuantity + && position item == keyPosition + +isExpired :: Int -> GroundItem -> Bool +isExpired time i = + time >= despawnTime i + +toItemStack :: GroundItem -> ItemStack.ItemStack +toItemStack item = + ItemStack.ItemStack (itemId item) (quantity item) diff --git a/app/PotatoCactus/Game/Entity/GroundItem/GroundItemCollection.hs b/app/PotatoCactus/Game/Entity/GroundItem/GroundItemCollection.hs new file mode 100644 index 0000000..2087150 --- /dev/null +++ b/app/PotatoCactus/Game/Entity/GroundItem/GroundItemCollection.hs @@ -0,0 +1,158 @@ +module PotatoCactus.Game.Entity.GroundItem.GroundItemCollection (GroundItemCollection, create, insert, findByChunkXYForPlayer, findMatchingItem, advanceTime, remove) where + +import qualified Data.IntMap as IntMap +import Data.List (delete, find, partition) +import qualified Data.Map as Map +import Data.Maybe (fromJust, fromMaybe, mapMaybe, maybeToList) +import PotatoCactus.Config.Constants (groundItemGlobalDespawnDelay) +import PotatoCactus.Game.Definitions.ItemDefinitions (ItemId) +import PotatoCactus.Game.Entity.GroundItem.GroundItem (GroundItem) +import qualified PotatoCactus.Game.Entity.GroundItem.GroundItem as GroundItem +import qualified PotatoCactus.Game.Entity.Object.DynamicObjectCollection as IntMap +import PotatoCactus.Game.ItemContainer (ItemStack (Empty)) +import PotatoCactus.Game.Position (GetPosition (getPosition), Position (x, y, z), chunkX, chunkY) + +type PlayerVisibilityKey = String + +everyone :: PlayerVisibilityKey +everyone = "" + +data GroundItemCollection = GroundItemCollection + { content_ :: Map.Map PlayerVisibilityKey (IntMap.IntMap [GroundItem]) + } + deriving (Show) + +create :: GroundItemCollection +create = GroundItemCollection Map.empty + +insert :: GroundItemCollection -> GroundItem -> GroundItemCollection +insert collection item = + collection + { content_ = + Map.alter + (Just . insert_ item . fromMaybe IntMap.empty) + (visibilityKey_ item) + (content_ collection) + } + +insert_ :: GroundItem -> IntMap.IntMap [GroundItem] -> IntMap.IntMap [GroundItem] +insert_ item = + IntMap.alter + (Just . maybe [item] (item :)) + (key_ . getPosition $ item) + +remove :: GroundItemCollection -> (ItemId, Int, Position, Maybe PlayerVisibilityKey) -> (ItemStack, GroundItemCollection) +remove collection (itemId, quantity, position, Just username) = + let content = content_ collection + in let (localRemoved, withLocalRemoved) = + removeItem_ + (itemId, quantity, position) + (fromMaybe IntMap.empty (content Map.!? username)) + in case localRemoved of + -- Try to remove from player scope first + Just removedItem -> + ( removedItem, + GroundItemCollection (Map.insert username withLocalRemoved content) + ) + Nothing -> remove collection (itemId, quantity, position, Nothing) +remove GroundItemCollection {content_ = content} (itemId, quantity, position, Nothing) = + let (removed, withRemoved) = + removeItem_ + (itemId, quantity, position) + (fromMaybe IntMap.empty (content Map.!? everyone)) + in case removed of + Just removedStack -> (removedStack, GroundItemCollection (Map.insert everyone withRemoved content)) + Nothing -> (Empty, GroundItemCollection content) + +removeItem_ :: (ItemId, Int, Position) -> IntMap.IntMap [GroundItem] -> (Maybe ItemStack, IntMap.IntMap [GroundItem]) +removeItem_ (itemId, quantity, position) m = + let chunkItems = fromMaybe [] (m IntMap.!? key_ position) + in case find (GroundItem.matches (itemId, quantity, position)) chunkItems of + Nothing -> (Nothing, m) + Just removedItem -> + ( Just . GroundItem.toItemStack $ removedItem, + IntMap.adjust (delete removedItem) (key_ position) m + ) + +findByChunkXYForPlayer :: GroundItemCollection -> String -> (Int, Int, Int) -> [GroundItem] +findByChunkXYForPlayer GroundItemCollection {content_ = content} username chunkPos = + concatMap + (itemsInChunkXY_ chunkPos . (content Map.!?)) + [username, everyone] + +findMatchingItem :: (ItemId, Position, PlayerVisibilityKey) -> GroundItemCollection -> Maybe GroundItem +findMatchingItem (itemId, position, username) GroundItemCollection {content_ = content} = + case findMatchingItem_ (itemId, position) (fromMaybe IntMap.empty (content Map.!? username)) of + Just i -> Just i + Nothing -> findMatchingItem_ (itemId, position) (fromMaybe IntMap.empty (content Map.!? everyone)) + +findMatchingItem_ :: (ItemId, Position) -> IntMap.IntMap [GroundItem] -> Maybe GroundItem +findMatchingItem_ (itemId, position) m = + let chunkItems = fromMaybe [] (m IntMap.!? key_ position) + in find + ( \i -> + GroundItem.itemId i == itemId + && GroundItem.position i == position + ) + chunkItems + +itemsInChunkXY_ :: (Int, Int, Int) -> Maybe (IntMap.IntMap [GroundItem]) -> [GroundItem] +itemsInChunkXY_ _ Nothing = + [] +itemsInChunkXY_ chunkPos (Just m) = + fromMaybe [] $ m IntMap.!? chunkKey_ chunkPos + +advanceTime :: GroundItemCollection -> Int -> GroundItemCollection +advanceTime GroundItemCollection {content_ = content} time = + GroundItemCollection (transitionExpired_ time content) + +transitionExpired_ :: Int -> Map.Map PlayerVisibilityKey (IntMap.IntMap [GroundItem]) -> Map.Map PlayerVisibilityKey (IntMap.IntMap [GroundItem]) +transitionExpired_ time m = + let (expired, updated) = + Map.mapAccumWithKey + ( \expired k chunkMap -> + let (newExpired, updated) = removeExpired_ time chunkMap + in if k == everyone + then (expired, updated) + else (newExpired ++ expired, updated) + ) + [] + m + in foldl + ( \m item -> + Map.alter (Just . insert_ item . fromMaybe IntMap.empty) everyone m + ) + updated + (mapMaybe transitionItem_ expired) + +transitionItem_ :: GroundItem -> Maybe GroundItem +transitionItem_ item = + Just + item + { GroundItem.despawnTime = GroundItem.despawnTime item + groundItemGlobalDespawnDelay, + GroundItem.player = Nothing + } + +removeExpired_ :: Int -> IntMap.IntMap [GroundItem] -> ([GroundItem], IntMap.IntMap [GroundItem]) +removeExpired_ time = + IntMap.mapAccum + ( \expired l -> + let (newExpired, active) = partition (GroundItem.isExpired time) l + in (newExpired ++ expired, active) + ) + [] + +key_ :: Position -> Int +key_ pos = chunkKey_ (chunkX pos, chunkY pos, z pos) + +chunkKey_ :: (Int, Int, Int) -> Int +chunkKey_ (x, y, z) = + x + + y * 10 ^ 5 + + z * 10 ^ 10 + +visibilityKey_ :: GroundItem -> PlayerVisibilityKey +visibilityKey_ GroundItem.GroundItem {GroundItem.player = Nothing} = + everyone +visibilityKey_ GroundItem.GroundItem {GroundItem.player = Just username} = + username diff --git a/app/PotatoCactus/Game/Entity/Interaction/Interaction.hs b/app/PotatoCactus/Game/Entity/Interaction/Interaction.hs index 008f67e..ddf98a0 100644 --- a/app/PotatoCactus/Game/Entity/Interaction/Interaction.hs +++ b/app/PotatoCactus/Game/Entity/Interaction/Interaction.hs @@ -1,7 +1,7 @@ module PotatoCactus.Game.Entity.Interaction.Interaction where import PotatoCactus.Game.Entity.Interaction.State (InteractionState (InProgress, Pending, PendingPathing)) -import PotatoCactus.Game.Entity.Interaction.Target (InteractionTarget (None, NpcTarget, ObjectTarget), canStartInteractionFromPos) +import PotatoCactus.Game.Entity.Interaction.Target (GroundItemInteractionType (ItemPickup), InteractionTarget (GroundItemTarget, None, NpcTarget, ObjectTarget), canStartInteractionFromPos) import PotatoCactus.Game.Entity.Npc.Npc (Npc, NpcIndex) import PotatoCactus.Game.Position (GetPosition (getPosition), Position, isNextTo) @@ -37,4 +37,9 @@ advanceInteraction _ (Interaction (ObjectTarget objectKey actionIndex) Pending) (True, True) -> Interaction (ObjectTarget objectKey actionIndex) InProgress (False, _) -> Interaction (ObjectTarget objectKey actionIndex) Pending _ -> Interaction None Pending +advanceInteraction _ (Interaction (GroundItemTarget itemId quantity position ItemPickup) Pending) (pos, isStopped) = + case (isStopped, position == pos) of + (True, True) -> Interaction (GroundItemTarget itemId quantity position ItemPickup) InProgress + (False, _) -> Interaction (GroundItemTarget itemId quantity position ItemPickup) Pending + _ -> Interaction None Pending advanceInteraction _ interaction _ = interaction diff --git a/app/PotatoCactus/Game/Entity/Interaction/Target.hs b/app/PotatoCactus/Game/Entity/Interaction/Target.hs index edd1308..c1f3d50 100644 --- a/app/PotatoCactus/Game/Entity/Interaction/Target.hs +++ b/app/PotatoCactus/Game/Entity/Interaction/Target.hs @@ -1,17 +1,22 @@ module PotatoCactus.Game.Entity.Interaction.Target where +import PotatoCactus.Game.Definitions.ItemDefinitions (ItemId) import PotatoCactus.Game.Entity.Npc.Npc (Npc, NpcIndex) import PotatoCactus.Game.Entity.Object.GameObjectKey (GameObjectKey) +import PotatoCactus.Game.Message.ItemOnObjectPayload (ItemOnObjectPayload) import PotatoCactus.Game.Position (GetPosition (getPosition), Position, isNextTo, isWithin) data InteractionTarget - = ObjectTarget GameObjectKey Int + = ObjectTarget GameObjectKey (Either Int ItemOnObjectPayload) | NpcTarget NpcIndex NpcInteractionType + | GroundItemTarget ItemId Int Position GroundItemInteractionType | None deriving (Show) data NpcInteractionType = NpcAttack | NpcAction Int deriving (Show) +data GroundItemInteractionType = ItemPickup deriving (Show) + -- TODO - Some entities are larger than 1 tile. e.g. trees. - keotl 2023-03-15 -- TODO - For those entities, we have to somehow check the clickbox - keotl 2023-03-15 -- TODO - Some entities, e.g. stairs, can only start the interaction on from a single tile. - keotl 2023-03-15 diff --git a/app/PotatoCactus/Game/Entity/Object/DynamicObjectCollection.hs b/app/PotatoCactus/Game/Entity/Object/DynamicObjectCollection.hs index df97ede..bec254f 100644 --- a/app/PotatoCactus/Game/Entity/Object/DynamicObjectCollection.hs +++ b/app/PotatoCactus/Game/Entity/Object/DynamicObjectCollection.hs @@ -4,6 +4,7 @@ import Data.IntMap (IntMap, delete, empty, toList) import Data.IntMap.Lazy (insert) import PotatoCactus.Game.Entity.Object.GameObject (GameObject (objectType, position), GameObjectType) import PotatoCactus.Game.Position (GetPosition (getPosition), Position (Position, x, y, z), chunkX, chunkY) +import qualified PotatoCactus.Game.Position as Pos import Prelude hiding (id) data DynamicObject = Added GameObject | Removed GameObject deriving (Eq, Show) @@ -42,14 +43,15 @@ rawKey_ (position, objectType) = + objectType * 10 ^ 11 -- TODO - use a more suitable data structure - keotl 2023-03-12 -findByChunkXY :: Int -> Int -> DynamicObjectCollection -> [DynamicObject] -findByChunkXY x y collection = +findByChunkXY :: Int -> Int -> Int -> DynamicObjectCollection -> [DynamicObject] +findByChunkXY x y z collection = filter ( \object -> ( chunkX . getPosition $ object, - chunkY . getPosition $ object + chunkY . getPosition $ object, + Pos.z . getPosition $ object ) - == (x, y) + == (x, y, z) ) (map snd $ toList . elements_ $ collection) diff --git a/app/PotatoCactus/Game/Interface/InterfaceButtonDispatch.hs b/app/PotatoCactus/Game/Interface/InterfaceButtonDispatch.hs index aa45547..c52d56b 100644 --- a/app/PotatoCactus/Game/Interface/InterfaceButtonDispatch.hs +++ b/app/PotatoCactus/Game/Interface/InterfaceButtonDispatch.hs @@ -1,18 +1,28 @@ module PotatoCactus.Game.Interface.InterfaceButtonDispatch where +import PotatoCactus.Game.Interface.InterfaceController (dispatchButtonClick) import qualified PotatoCactus.Game.Movement.MovementEntity as PM +import PotatoCactus.Game.Player (PlayerIndex) import qualified PotatoCactus.Game.Player as P -import PotatoCactus.Game.World (World, updatePlayer) +import PotatoCactus.Game.World (World, updatePlayerByIndex) -dispatchInterfaceButtonClick :: World -> String -> Int -> World -dispatchInterfaceButtonClick world playerName 152 = - updatePlayer +dispatchInterfaceButtonClick :: World -> PlayerIndex -> Int -> World +dispatchInterfaceButtonClick world playerIndex 152 = + updatePlayerByIndex world - playerName + playerIndex (\p -> (p {P.movement = PM.setRunning (P.movement p) False})) -dispatchInterfaceButtonClick world playerName 153 = - updatePlayer +dispatchInterfaceButtonClick world playerIndex 153 = + updatePlayerByIndex world - playerName + playerIndex (\p -> (p {P.movement = PM.setRunning (P.movement p) True})) -dispatchInterfaceButtonClick world _ _ = world +dispatchInterfaceButtonClick world playerIndex buttonId = + updatePlayerByIndex + world + playerIndex + ( \p -> + p + { P.interfaces = dispatchButtonClick (P.interfaces p) buttonId + } + ) diff --git a/app/PotatoCactus/Game/Interface/InterfaceController.hs b/app/PotatoCactus/Game/Interface/InterfaceController.hs new file mode 100644 index 0000000..30a1bab --- /dev/null +++ b/app/PotatoCactus/Game/Interface/InterfaceController.hs @@ -0,0 +1,125 @@ +module PotatoCactus.Game.Interface.InterfaceController (InterfaceController (..), create, clearStandardInterfaces, Interface (..), configureInterface, closeInterface, dispatchButtonClick) where + +import Data.IntMap (IntMap, (!?)) +import Data.Maybe (catMaybes, mapMaybe) +import PotatoCactus.Game.Scripting.Actions.CreateInterface (CreateInterfaceRequest (CreateInterfaceRequest), InterfaceElement, InterfaceType (Input, Standard, Walkable)) +import PotatoCactus.Game.Scripting.Actions.ScriptInvocation (ScriptInvocation) +import PotatoCactus.Game.Typing (Advance, advance) +import PotatoCactus.Utils.Flow ((|>)) + +data InterfaceController = InterfaceController + { mainInterface :: Maybe Interface, -- main open window. e.g. bank interface + inputInterface :: Maybe Interface, + walkableInterface :: Maybe Interface, -- interfaces kept open on movement, e.g. Tutorial island progress + triggeredCallbacks :: [ScriptInvocation], + pendingCallbacks_ :: [ScriptInvocation], + shouldCloseInterfaces :: Bool, + pendingClosingInterfaces_ :: Bool + -- TODO - shouldClose for walkable interface - keotl 2023-05-03 + } + deriving (Show) + +instance Advance InterfaceController where + advance ic = + ic + { mainInterface = advanceMaybe_ (mainInterface ic), + inputInterface = advanceMaybe_ (inputInterface ic), + walkableInterface = advanceMaybe_ (walkableInterface ic), + triggeredCallbacks = pendingCallbacks_ ic, + pendingCallbacks_ = [], + shouldCloseInterfaces = pendingClosingInterfaces_ ic, + pendingClosingInterfaces_ = False + } + +advanceMaybe_ :: Maybe Interface -> Maybe Interface +advanceMaybe_ Nothing = Nothing +advanceMaybe_ (Just i) = Just $ advance i + +data Interface = Interface + { configuredElements :: [InterfaceElement], -- Elements to configure on this tick + onClose :: Maybe ScriptInvocation, -- Script to invoke when dismissed gracefully. + callbacks :: IntMap ScriptInvocation + } + deriving (Show) + +instance Advance Interface where + advance i = + i + { configuredElements = [] + } + +create :: InterfaceController +create = + InterfaceController + { mainInterface = Nothing, + inputInterface = Nothing, + walkableInterface = Nothing, + triggeredCallbacks = [], + pendingCallbacks_ = [], + shouldCloseInterfaces = False, + pendingClosingInterfaces_ = True + } + +clearStandardInterfaces :: InterfaceController -> InterfaceController +clearStandardInterfaces c = + c + { mainInterface = Nothing, + inputInterface = Nothing, + pendingClosingInterfaces_ = True + } + +configureInterface :: InterfaceController -> CreateInterfaceRequest -> InterfaceController +configureInterface c (CreateInterfaceRequest Standard elements onClose callbacks) = + c + { mainInterface = Just $ Interface elements onClose callbacks, + shouldCloseInterfaces = False, + pendingClosingInterfaces_ = False + } +configureInterface c (CreateInterfaceRequest Input elements onClose callbacks) = + c + { inputInterface = Just $ Interface elements onClose callbacks, + shouldCloseInterfaces = False, + pendingClosingInterfaces_ = False + } +configureInterface c (CreateInterfaceRequest Walkable elements onClose callbacks) = + c + { walkableInterface = Just $ Interface elements onClose callbacks, + shouldCloseInterfaces = False, + pendingClosingInterfaces_ = False + } + +closeInterface :: InterfaceController -> InterfaceType -> InterfaceController +closeInterface c Standard = + c + |> (\x -> enqueueOnCloseCallback_ x (mainInterface x)) + |> (\x -> x {pendingClosingInterfaces_ = True, mainInterface = Nothing}) +closeInterface c Walkable = + c + |> (\x -> enqueueOnCloseCallback_ x (walkableInterface x)) + |> (\x -> x {walkableInterface = Nothing}) +closeInterface c Input = + c + |> (\x -> enqueueOnCloseCallback_ x (inputInterface x)) + |> (\x -> x {pendingClosingInterfaces_ = True, inputInterface = Nothing}) + +enqueueOnCloseCallback_ :: InterfaceController -> Maybe Interface -> InterfaceController +enqueueOnCloseCallback_ c Nothing = c +enqueueOnCloseCallback_ c (Just Interface {onClose = Nothing}) = c +enqueueOnCloseCallback_ c (Just Interface {onClose = Just script}) = + c + { pendingCallbacks_ = script : pendingCallbacks_ c + } + +dispatchButtonClick :: InterfaceController -> Int -> InterfaceController +dispatchButtonClick c buttonId = + let invoked = + [ invokedCallback_ (mainInterface c) buttonId, + invokedCallback_ (inputInterface c) buttonId, + invokedCallback_ (walkableInterface c) buttonId + ] + in c {pendingCallbacks_ = pendingCallbacks_ c ++ catMaybes invoked} + +invokedCallback_ :: Maybe Interface -> Int -> Maybe ScriptInvocation +invokedCallback_ Nothing _ = Nothing +invokedCallback_ (Just Interface {callbacks = c}) buttonId = + c !? buttonId diff --git a/app/PotatoCactus/Game/ItemContainer.hs b/app/PotatoCactus/Game/ItemContainer.hs index c893440..a5ab706 100644 --- a/app/PotatoCactus/Game/ItemContainer.hs +++ b/app/PotatoCactus/Game/ItemContainer.hs @@ -1,6 +1,7 @@ module PotatoCactus.Game.ItemContainer where import Data.List (findIndex) +import Data.Maybe (isJust, isNothing) import PotatoCactus.Game.Definitions.ItemDefinitions (ItemDefinition (stackable), ItemId, itemDefinition) import PotatoCactus.Game.Typing (Advance (advance)) import PotatoCactus.Utils.Iterable (replaceAtIndex) @@ -14,7 +15,7 @@ data ItemStack quantity :: Int } | Empty - deriving (Show) + deriving (Show, Eq) data ItemContainer = ItemContainer { capacity :: Int, @@ -73,7 +74,11 @@ canAddItems container = all (canAddItem container) combine_ :: ItemStack -> ItemStack -> ItemStack combine_ Empty b = b combine_ a Empty = a -combine_ a b = a {quantity = quantity a + quantity b} +combine_ a b = + let q = quantity a + quantity b + in if q > 0 + then a {quantity = q} + else Empty addItem :: ItemContainer -> ItemStack -> ItemContainer addItem container item = @@ -109,3 +114,35 @@ replaceStack index container item = atIndex :: Int -> ItemContainer -> ItemStack atIndex index container = content container !! index + +subtractItem :: ItemContainer -> (ItemId, Int) -> ItemContainer +subtractItem container (itemId, quantity) = + let stack = ItemStack itemId (- quantity) + in case findIndex + (isItem itemId) + (content container) of + Just i -> + container + { content = + replaceAtIndex + i + (combine_ (content container !! i) stack) + (content container), + willUpdate_ = True + } + Nothing -> container + +removeStack :: ItemContainer -> (ItemId, Int) -> ItemContainer +removeStack container (itemId, index) = + if index > capacity container + 1 + || not (isItem itemId (atIndex index container)) + then container + else fst (replaceStack index container Empty) + +isEmptyStack :: ItemStack -> Bool +isEmptyStack Empty = True +isEmptyStack _ = False + +isItem :: ItemId -> ItemStack -> Bool +isItem _ Empty = False +isItem desiredItemId stack = itemId stack == desiredItemId diff --git a/app/PotatoCactus/Game/Message/GameChannelMessage.hs b/app/PotatoCactus/Game/Message/GameChannelMessage.hs index 683cb1d..96c8f77 100644 --- a/app/PotatoCactus/Game/Message/GameChannelMessage.hs +++ b/app/PotatoCactus/Game/Message/GameChannelMessage.hs @@ -1,25 +1,32 @@ module PotatoCactus.Game.Message.GameChannelMessage where import PotatoCactus.Game.Definitions.EquipmentDefinitions (EquipmentSlot) +import PotatoCactus.Game.Definitions.ItemDefinitions (ItemId) +import PotatoCactus.Game.Entity.Npc.Npc (NpcIndex) import PotatoCactus.Game.Message.EquipItemMessagePayload (EquipItemMessagePayload) +import PotatoCactus.Game.Message.ItemOnObjectPayload (ItemOnObjectPayload) +import PotatoCactus.Game.Message.ObjectClickPayload (ObjectClickPayload) import PotatoCactus.Game.Message.RegisterClientPayload (RegisterClientPayload) import PotatoCactus.Game.Movement.PositionXY (PositionXY) import PotatoCactus.Game.Movement.WalkingStep (WalkingStep) -import PotatoCactus.Game.PlayerUpdate.ChatMessage (ChatMessage) -import PotatoCactus.Game.Message.ObjectClickPayload (ObjectClickPayload) import PotatoCactus.Game.Player (PlayerIndex) -import PotatoCactus.Game.Entity.Npc.Npc (NpcIndex) +import PotatoCactus.Game.PlayerUpdate.ChatMessage (ChatMessage) +import PotatoCactus.Game.Scripting.Actions.CreateInterface (WidgetId) data GameChannelMessage = RegisterClientMessage RegisterClientPayload | UnregisterClientMessage String | PlayerWalkMessage String PositionXY Bool [WalkingStep] | PlayerCommandMessage PlayerIndex String [String] - | InterfaceButtonClickMessage String Int + | InterfaceButtonClickMessage PlayerIndex Int | PlayerChatMessage String ChatMessage + | PlayerContinueDialogueMessage PlayerIndex Int | EquipItemMessage String EquipItemMessagePayload | UnequipItemMessage String EquipmentSlot | ObjectClickMessage PlayerIndex ObjectClickPayload + | ItemOnObjectMessage PlayerIndex ItemOnObjectPayload | NpcAttackMessage PlayerIndex NpcIndex | NpcClickMessage PlayerIndex NpcIndex Int + | DropItemMessage PlayerIndex WidgetId ItemId Int + | PickupGroundItemMessage PlayerIndex ItemId PositionXY | UpdateWorldMessage diff --git a/app/PotatoCactus/Game/Message/ItemOnObjectPayload.hs b/app/PotatoCactus/Game/Message/ItemOnObjectPayload.hs new file mode 100644 index 0000000..1891c30 --- /dev/null +++ b/app/PotatoCactus/Game/Message/ItemOnObjectPayload.hs @@ -0,0 +1,14 @@ +module PotatoCactus.Game.Message.ItemOnObjectPayload where + +import PotatoCactus.Game.Definitions.GameObjectDefinitions (GameObjectId) +import PotatoCactus.Game.Movement.PositionXY (PositionXY) +import PotatoCactus.Game.Scripting.Actions.CreateInterface (WidgetId) + +data ItemOnObjectPayload = ItemOnObjectPayload + { interfaceId :: WidgetId, + objectId :: GameObjectId, + position :: PositionXY, + itemIndex :: Int, + itemId :: Int + } + deriving (Show) diff --git a/app/PotatoCactus/Game/Player.hs b/app/PotatoCactus/Game/Player.hs index 943aafe..9ab621e 100644 --- a/app/PotatoCactus/Game/Player.hs +++ b/app/PotatoCactus/Game/Player.hs @@ -5,11 +5,16 @@ import Data.Foldable (fold) import PotatoCactus.Game.Combat.CombatEntity (CombatEntity, CombatTarget (NpcTarget), clearTargetIfEngagedWith) import qualified PotatoCactus.Game.Combat.CombatEntity as CombatEntity import PotatoCactus.Game.Combat.Hit (Hit) +import PotatoCactus.Game.Definitions.ItemDefinitions (ItemId) import qualified PotatoCactus.Game.Entity.Animation.Animation as Anim +import qualified PotatoCactus.Game.Entity.EntityData as EntityData import PotatoCactus.Game.Entity.Interaction.Interaction (Interaction) import qualified PotatoCactus.Game.Entity.Interaction.Interaction as Interaction import PotatoCactus.Game.Entity.Npc.Npc (NpcIndex) +import PotatoCactus.Game.Interface.InterfaceController (InterfaceController, clearStandardInterfaces, configureInterface) +import qualified PotatoCactus.Game.Interface.InterfaceController as IC import PotatoCactus.Game.ItemContainer (ItemContainer, playerEquipmentContainer, playerInventory) +import qualified PotatoCactus.Game.ItemContainer as Container import PotatoCactus.Game.Movement.MovementEntity (immediatelySetPosition, playerWalkMovement) import qualified PotatoCactus.Game.Movement.MovementEntity as M (MovementEntity, issueWalkCommand) import PotatoCactus.Game.Movement.PositionXY (PositionXY) @@ -21,6 +26,7 @@ import PotatoCactus.Game.PlayerUpdate.PlayerUpdate (PlayerUpdate) import PotatoCactus.Game.PlayerUpdate.UpdateMask (PlayerUpdateMask, animationFlag, appearanceFlag, primaryHealthUpdateFlag, secondaryHealthUpdateFlag) import qualified PotatoCactus.Game.PlayerUpdate.UpdateMask as Mask import PotatoCactus.Game.Position (GetPosition (getPosition), Position (Position)) +import PotatoCactus.Game.Scripting.Actions.CreateInterface (CreateInterfaceRequest, InterfaceType) import PotatoCactus.Game.Typing (Keyable (key)) type PlayerIndex = Int @@ -39,6 +45,8 @@ data Player = Player combat :: CombatEntity, animation :: Maybe Anim.Animation, chatboxMessages :: [String], + interfaces :: InterfaceController, + entityData :: EntityData.EntityData, skipUpdate_ :: Bool } deriving (Show) @@ -53,7 +61,9 @@ issueWalkCommand :: (PositionXY, Bool, [WalkingStep]) -> Player -> Player issueWalkCommand (startPos, isRunning, steps) p = p { movement = M.issueWalkCommand (movement p) startPos steps, - combat = CombatEntity.clearTarget . combat $ p + combat = CombatEntity.clearTarget . combat $ p, + interaction = Interaction.create, + interfaces = clearStandardInterfaces . interfaces $ p } create :: String -> Position -> Player @@ -72,6 +82,8 @@ create username position = combat = CombatEntity.create 10, animation = Nothing, chatboxMessages = [], + interfaces = IC.create, + entityData = EntityData.create, skipUpdate_ = True } @@ -122,3 +134,38 @@ setPosition p pos = p { movement = immediatelySetPosition (movement p) pos } + +createInterface :: Player -> CreateInterfaceRequest -> Player +createInterface p req = + p + { interfaces = configureInterface (interfaces p) req + } + +clearStandardInterface :: Player -> Player +clearStandardInterface p = + p + { interfaces = clearStandardInterfaces $ interfaces p + } + +updateEntityData :: Player -> (EntityData.EntityData -> EntityData.EntityData) -> Player +updateEntityData p transform = + p + { entityData = transform (entityData p) + } + +giveItem :: Player -> Container.ItemStack -> Player +giveItem p stack = + p + { inventory = Container.addItem (inventory p) stack + } + +subtractItem :: Player -> (ItemId, Int) -> Player +subtractItem p stack = + p + { inventory = Container.subtractItem (inventory p) stack + } +removeItemStack :: Player -> (ItemId, Int) -> Player +removeItemStack p stack = + p + { inventory = Container.removeStack (inventory p) stack + } diff --git a/app/PotatoCactus/Game/PlayerUpdate/AdvancePlayer.hs b/app/PotatoCactus/Game/PlayerUpdate/AdvancePlayer.hs index 2e18bd7..dfdad7f 100644 --- a/app/PotatoCactus/Game/PlayerUpdate/AdvancePlayer.hs +++ b/app/PotatoCactus/Game/PlayerUpdate/AdvancePlayer.hs @@ -12,6 +12,7 @@ import PotatoCactus.Game.Player (Player (..)) import PotatoCactus.Game.PlayerUpdate.ProcessPlayerUpdate (processPlayerUpdate) import PotatoCactus.Game.Position (getPosition) import PotatoCactus.Game.Typing (Advance (advance)) +import PotatoCactus.Utils.Flow ((|>)) advancePlayer :: (NpcIndex -> Maybe Npc) -> Player -> Player advancePlayer findNpc p = @@ -19,10 +20,8 @@ advancePlayer findNpc p = then p {skipUpdate_ = False} else let updated = - foldl - processPlayerUpdate - (clearTransientProperties_ p) - (pendingUpdates p) + p |> processPendingUpdates_ + |> commonUpdates_ in let updatedInteraction = advanceInteraction findNpc @@ -34,9 +33,6 @@ advancePlayer findNpc p = Nothing -> updated { movement = advance (movement updated), - inventory = advance (inventory updated), - equipment = advance (equipment updated), - combat = advance . combat $ p, interaction = create, pendingUpdates = [] } @@ -44,18 +40,12 @@ advancePlayer findNpc p = let desiredPath = findPathNaive 666 (getPosition p) (getPosition npc) in updated { movement = Movement.immediatelyQueueMovement (movement updated) desiredPath, - inventory = advance (inventory updated), - equipment = advance (equipment updated), - combat = advance . combat $ p, interaction = updatedInteraction, pendingUpdates = [] } _ -> updated { movement = advance (movement updated), - inventory = advance (inventory updated), - equipment = advance (equipment updated), - combat = advance . combat $ p, interaction = updatedInteraction, pendingUpdates = [] } @@ -68,3 +58,19 @@ clearTransientProperties_ p = animation = Nothing, chatboxMessages = [] } + +processPendingUpdates_ :: Player -> Player +processPendingUpdates_ p = + foldl + processPlayerUpdate + (clearTransientProperties_ p) + (pendingUpdates p) + +commonUpdates_ :: Player -> Player +commonUpdates_ p = + p + { inventory = advance . inventory $ p, + equipment = advance . equipment $ p, + combat = advance . combat $ p, + interfaces = advance . interfaces $ p + } diff --git a/app/PotatoCactus/Game/PlayerUpdate/PlayerUpdate.hs b/app/PotatoCactus/Game/PlayerUpdate/PlayerUpdate.hs index 00e2af5..c57ebda 100644 --- a/app/PotatoCactus/Game/PlayerUpdate/PlayerUpdate.hs +++ b/app/PotatoCactus/Game/PlayerUpdate/PlayerUpdate.hs @@ -1,11 +1,14 @@ module PotatoCactus.Game.PlayerUpdate.PlayerUpdate where import PotatoCactus.Game.Definitions.EquipmentDefinitions (EquipmentSlot) -import PotatoCactus.Game.Message.EquipItemMessagePayload -import PotatoCactus.Game.PlayerUpdate.ChatMessage (ChatMessage) -import PotatoCactus.Game.Message.ObjectClickPayload (ObjectClickPayload) -import PotatoCactus.Game.Entity.Npc.Npc (NpcIndex) +import PotatoCactus.Game.Definitions.ItemDefinitions (ItemId) import PotatoCactus.Game.Entity.Interaction.Target (NpcInteractionType) +import PotatoCactus.Game.Entity.Npc.Npc (NpcIndex) +import PotatoCactus.Game.Message.EquipItemMessagePayload (EquipItemMessagePayload) +import PotatoCactus.Game.Message.ItemOnObjectPayload (ItemOnObjectPayload) +import PotatoCactus.Game.Message.ObjectClickPayload (ObjectClickPayload) +import PotatoCactus.Game.PlayerUpdate.ChatMessage (ChatMessage) +import PotatoCactus.Game.Position data PlayerUpdate = EquipItem EquipItemMessagePayload @@ -14,5 +17,8 @@ data PlayerUpdate | SayChatMessage ChatMessage | SayForcedChatMessage String | InteractWithObject ObjectClickPayload + | InteractWithObjectWithItem ItemOnObjectPayload | InteractWithNpc NpcIndex NpcInteractionType + | InteractWithGroundItem ItemId Int Position + | ContinueDialogue deriving (Show) diff --git a/app/PotatoCactus/Game/PlayerUpdate/ProcessPlayerUpdate.hs b/app/PotatoCactus/Game/PlayerUpdate/ProcessPlayerUpdate.hs index 810130b..8636246 100644 --- a/app/PotatoCactus/Game/PlayerUpdate/ProcessPlayerUpdate.hs +++ b/app/PotatoCactus/Game/PlayerUpdate/ProcessPlayerUpdate.hs @@ -3,17 +3,21 @@ module PotatoCactus.Game.PlayerUpdate.ProcessPlayerUpdate where import Data.Bits ((.|.)) import PotatoCactus.Game.Definitions.EquipmentDefinitions (EquipmentDefinition (slot), equipmentDefinition) import PotatoCactus.Game.Entity.Interaction.Interaction (createForTarget) -import PotatoCactus.Game.Entity.Interaction.Target (InteractionTarget (NpcTarget, ObjectTarget)) +import PotatoCactus.Game.Entity.Interaction.Target (GroundItemInteractionType (ItemPickup), InteractionTarget (GroundItemTarget, NpcTarget, ObjectTarget)) import PotatoCactus.Game.Entity.Object.GameObjectKey (GameObjectKey (GameObjectKey)) -import PotatoCactus.Game.ItemContainer (ItemStack (Empty, ItemStack, itemId), addItems, atIndex, canAddItems, replaceStack) +import PotatoCactus.Game.Interface.InterfaceController (clearStandardInterfaces) +import qualified PotatoCactus.Game.Interface.InterfaceController as IC +import PotatoCactus.Game.ItemContainer (ItemStack (Empty, ItemStack, itemId), StackPolicy (Standard), addItems, atIndex, canAddItems, replaceStack) import PotatoCactus.Game.Message.EquipItemMessagePayload (EquipItemMessagePayload (EquipItemMessagePayload, itemIndex)) +import qualified PotatoCactus.Game.Message.ItemOnObjectPayload as IonO import PotatoCactus.Game.Message.ObjectClickPayload (ObjectClickPayload (index, objectId, position)) import PotatoCactus.Game.Movement.PositionXY (fromXY) -import PotatoCactus.Game.Player (Player (chatMessage, equipment, interaction, inventory, updateMask)) +import PotatoCactus.Game.Player (Player (chatMessage, equipment, interaction, interfaces, inventory, updateMask)) import PotatoCactus.Game.PlayerUpdate.Equipment (Equipment (container), equipItem, unequipItem) -import PotatoCactus.Game.PlayerUpdate.PlayerUpdate (PlayerUpdate (EquipItem, InteractWithNpc, InteractWithObject, SayChatMessage, UnequipItem)) +import PotatoCactus.Game.PlayerUpdate.PlayerUpdate (PlayerUpdate (ContinueDialogue, EquipItem, InteractWithGroundItem, InteractWithNpc, InteractWithObject, InteractWithObjectWithItem, SayChatMessage, UnequipItem)) import PotatoCactus.Game.PlayerUpdate.UpdateMask (appearanceFlag, chatFlag) import PotatoCactus.Game.Position (GetPosition (getPosition), Position (z)) +import qualified PotatoCactus.Game.Scripting.Actions.CreateInterface as I processPlayerUpdate :: Player -> PlayerUpdate -> Player processPlayerUpdate p (SayChatMessage message) = @@ -32,7 +36,8 @@ processPlayerUpdate p (EquipItem (EquipItemMessagePayload _ itemIndex 3214)) = ( p { inventory = addItems inventoryWithRemoved replaced, equipment = updatedEquipment, - updateMask = updateMask p .|. appearanceFlag + updateMask = updateMask p .|. appearanceFlag, + interfaces = clearStandardInterfaces . interfaces $ p } ) else p @@ -43,7 +48,8 @@ processPlayerUpdate p (UnequipItem slot) = p { inventory = addItems (inventory p) removedItems, equipment = updatedEquipment, - updateMask = updateMask p .|. appearanceFlag + updateMask = updateMask p .|. appearanceFlag, + interfaces = clearStandardInterfaces . interfaces $ p } else p processPlayerUpdate p (InteractWithObject payload) = @@ -52,9 +58,30 @@ processPlayerUpdate p (InteractWithObject payload) = createForTarget ( ObjectTarget (GameObjectKey (objectId payload) (fromXY (position payload) (z . getPosition $ p))) - (index payload) - ) + (Left $ index payload) + ), + interfaces = clearStandardInterfaces . interfaces $ p + } +processPlayerUpdate p (InteractWithObjectWithItem payload) = + p + { interaction = + createForTarget + ( ObjectTarget + (GameObjectKey (IonO.objectId payload) (fromXY (IonO.position payload) (z . getPosition $ p))) + (Right payload) + ), + interfaces = clearStandardInterfaces . interfaces $ p } processPlayerUpdate p (InteractWithNpc npcId interactionType) = - p {interaction = createForTarget (NpcTarget npcId interactionType)} + p + { interaction = createForTarget (NpcTarget npcId interactionType), + interfaces = clearStandardInterfaces . interfaces $ p + } +processPlayerUpdate p (InteractWithGroundItem itemId quantity pos) = + p + { interaction = createForTarget (GroundItemTarget itemId quantity pos ItemPickup), + interfaces = clearStandardInterfaces . interfaces $ p + } +processPlayerUpdate p ContinueDialogue = + p {interfaces = IC.closeInterface (interfaces p) I.Standard} processPlayerUpdate p _ = p diff --git a/app/PotatoCactus/Game/Reducer.hs b/app/PotatoCactus/Game/Reducer.hs index 7390511..dee946b 100644 --- a/app/PotatoCactus/Game/Reducer.hs +++ b/app/PotatoCactus/Game/Reducer.hs @@ -1,17 +1,24 @@ module PotatoCactus.Game.Reducer where import qualified PotatoCactus.Boot.GameChannel as C +import qualified PotatoCactus.Game.Entity.GroundItem.GroundItem as GroundItem +import qualified PotatoCactus.Game.Entity.GroundItem.GroundItemCollection as GroundItemCollection import PotatoCactus.Game.Entity.Interaction.Target (NpcInteractionType (NpcAction, NpcAttack)) import PotatoCactus.Game.Entity.Object.GameObject (GameObject (GameObject)) import PotatoCactus.Game.Interface.InterfaceButtonDispatch (dispatchInterfaceButtonClick) import PotatoCactus.Game.Message.GameChannelMessage (GameChannelMessage (..)) import PotatoCactus.Game.Message.ObjectClickPayload (ObjectClickPayload (objectId)) import qualified PotatoCactus.Game.Message.RegisterClientPayload as C +import qualified PotatoCactus.Game.Movement.PositionXY as Position import qualified PotatoCactus.Game.Player as P -import PotatoCactus.Game.PlayerUpdate.PlayerUpdate (PlayerUpdate (EquipItem, InteractWithNpc, InteractWithObject, SayChatMessage, UnequipItem)) -import PotatoCactus.Game.Scripting.ScriptUpdates (GameEvent (PlayerCommandEvent)) +import PotatoCactus.Game.PlayerUpdate.PlayerUpdate (PlayerUpdate (ContinueDialogue, EquipItem, InteractWithGroundItem, InteractWithNpc, InteractWithObject, InteractWithObjectWithItem, SayChatMessage, UnequipItem)) +import PotatoCactus.Game.Position (GetPosition (getPosition)) +import qualified PotatoCactus.Game.Position as Position +import PotatoCactus.Game.Scripting.Actions.CreateInterface (InterfaceType (Standard)) +import PotatoCactus.Game.Scripting.ScriptUpdates (GameEvent (DropItemEvent, PlayerCommandEvent)) import PotatoCactus.Game.Typing (advance) -import PotatoCactus.Game.World (ClientHandle (username), World (World, clients, players, tick), addPlayer, queueEvent, removePlayerByUsername, updatePlayer, updatePlayerByIndex) +import PotatoCactus.Game.World (ClientHandle (username), World (World, clients, groundItems, players, tick), addPlayer, queueEvent, removePlayerByUsername, updatePlayer, updatePlayerByIndex) +import PotatoCactus.Game.World.MobList (findByIndex) reduceWorld :: World -> GameChannelMessage -> World reduceWorld world (RegisterClientMessage message) = @@ -34,8 +41,27 @@ reduceWorld world (NpcAttackMessage playerId npcIndex) = updatePlayerByIndex world playerId (\p -> P.queueUpdate p (InteractWithNpc npcIndex NpcAttack)) reduceWorld world (NpcClickMessage playerId npcIndex actionIndex) = updatePlayerByIndex world playerId (\p -> P.queueUpdate p (InteractWithNpc npcIndex (NpcAction actionIndex))) +reduceWorld world (PickupGroundItemMessage playerId itemId pos) = + let player = findByIndex (players world) playerId + in case player of + Nothing -> world + Just p -> + let position = Position.fromXY pos (Position.z . getPosition $ p) + in case GroundItemCollection.findMatchingItem (itemId, position, P.username p) (groundItems world) of + Nothing -> world + Just groundItem -> + updatePlayerByIndex + world + playerId + (`P.queueUpdate` InteractWithGroundItem itemId (GroundItem.quantity groundItem) position) reduceWorld world (PlayerCommandMessage playerId cmd args) = queueEvent world $ PlayerCommandEvent playerId cmd args +reduceWorld world (PlayerContinueDialogueMessage playerId _) = + updatePlayerByIndex world playerId (`P.queueUpdate` ContinueDialogue) +reduceWorld world (ItemOnObjectMessage playerId payload) = + updatePlayerByIndex world playerId (\p -> P.queueUpdate p (InteractWithObjectWithItem payload)) +reduceWorld world (DropItemMessage playerId widgetId itemId index) = + queueEvent world $ DropItemEvent playerId widgetId itemId index reduceWorld world UpdateWorldMessage = advance world diff --git a/app/PotatoCactus/Game/Scripting/Actions/CreateInterface.hs b/app/PotatoCactus/Game/Scripting/Actions/CreateInterface.hs new file mode 100644 index 0000000..402dba3 --- /dev/null +++ b/app/PotatoCactus/Game/Scripting/Actions/CreateInterface.hs @@ -0,0 +1,27 @@ +module PotatoCactus.Game.Scripting.Actions.CreateInterface where + +import PotatoCactus.Game.Definitions.NpcDefinitions (NpcDefinitionId) +import PotatoCactus.Game.Entity.Animation.Animation (AnimationId) +import PotatoCactus.Game.Scripting.Actions.ScriptInvocation (ScriptInvocation) +import Data.IntMap (IntMap) + +type WidgetId = Int + +data InterfaceType = Standard | Input | Walkable deriving (Show) + +data CreateInterfaceRequest = CreateInterfaceRequest + { + interfaceType :: InterfaceType, + elements :: [InterfaceElement], + onClose :: Maybe ScriptInvocation, + callbacks :: IntMap ScriptInvocation + } + deriving (Show) + +data InterfaceElement + = ChatboxRootWindowElement WidgetId + | TextElement WidgetId String + | NpcChatheadElement WidgetId NpcDefinitionId + | PlayerChatheadElement WidgetId + | ModelAnimationElement WidgetId AnimationId + deriving (Show) diff --git a/app/PotatoCactus/Game/Scripting/Actions/ScriptInvocation.hs b/app/PotatoCactus/Game/Scripting/Actions/ScriptInvocation.hs new file mode 100644 index 0000000..bffce40 --- /dev/null +++ b/app/PotatoCactus/Game/Scripting/Actions/ScriptInvocation.hs @@ -0,0 +1,14 @@ +{-# LANGUAGE DeriveGeneric #-} + +module PotatoCactus.Game.Scripting.Actions.ScriptInvocation where + +import Data.Aeson (FromJSON, Value) +import GHC.Generics (Generic) + +data ScriptInvocation = ScriptInvocation + { f :: String, + args :: [Value] + } + deriving (Show, Generic) + +instance FromJSON ScriptInvocation diff --git a/app/PotatoCactus/Game/Scripting/Bridge/Serialization/ActionResultMapper.hs b/app/PotatoCactus/Game/Scripting/Bridge/Serialization/ActionResultMapper.hs index 7068631..0fe9ff8 100644 --- a/app/PotatoCactus/Game/Scripting/Bridge/Serialization/ActionResultMapper.hs +++ b/app/PotatoCactus/Game/Scripting/Bridge/Serialization/ActionResultMapper.hs @@ -4,18 +4,26 @@ module PotatoCactus.Game.Scripting.Bridge.Serialization.ActionResultMapper (mapResult) where -import Data.Aeson (FromJSON, Result (Error, Success), Value (Object, String), decode, decodeStrict, (.:)) +import Data.Aeson (FromJSON, Result (Error, Success), Value (Object, String), decode, decodeStrict, (.:), (.:?)) import Data.Aeson.Types (Object, parse) import Data.ByteString (ByteString) import Data.ByteString.Lazy (fromStrict) +import Data.Maybe (catMaybes, mapMaybe) import Data.Time.Format.ISO8601 (iso8601ParseM) +import Debug.Trace (trace) import GHC.Generics (Generic) +import PotatoCactus.Config.Constants (groundItemGlobalDespawnDelay) import PotatoCactus.Game.Entity.Animation.Animation (Animation (Animation), AnimationPriority (High, Low, Normal)) +import PotatoCactus.Game.Entity.GroundItem.GroundItem (GroundItem (GroundItem, despawnTime)) import PotatoCactus.Game.Entity.Object.DynamicObjectCollection (DynamicObject (Added, Removed)) +import PotatoCactus.Game.Entity.Object.GameObject (GameObject (GameObject)) import qualified PotatoCactus.Game.Entity.Object.GameObject as O import PotatoCactus.Game.Position (Position (Position)) +import PotatoCactus.Game.Scripting.Actions.CreateInterface (CreateInterfaceRequest (CreateInterfaceRequest)) +import qualified PotatoCactus.Game.Scripting.Actions.CreateInterface as I +import PotatoCactus.Game.Scripting.Actions.ScriptInvocation (ScriptInvocation (ScriptInvocation)) import PotatoCactus.Game.Scripting.Actions.SpawnNpcRequest (SpawnNpcRequest (SpawnNpcRequest)) -import PotatoCactus.Game.Scripting.ScriptUpdates (ScriptActionResult (AddGameObject, ClearPlayerInteraction, InternalNoop, InternalProcessingComplete, NpcQueueWalk, NpcSetAnimation, NpcSetForcedChat, SendMessage, ServerPrintMessage, SetPlayerPosition, SpawnNpc)) +import PotatoCactus.Game.Scripting.ScriptUpdates (ScriptActionResult (AddGameObject, ClearPlayerInteraction, ClearStandardInterface, CreateInterface, GiveItem, InternalNoop, InternalProcessingComplete, InvokeScript, NpcQueueWalk, NpcSetAnimation, NpcSetForcedChat, RemoveGroundItem, RemoveItemStack, SendMessage, ServerPrintMessage, SetPlayerAnimation, SetPlayerEntityData, SetPlayerPosition, SpawnGroundItem, SpawnNpc, SubtractItem)) mapResult :: ByteString -> ScriptActionResult mapResult bytes = @@ -154,6 +162,177 @@ decodeBody "setPlayerPosition" body = body of Error msg -> InternalNoop Success decoded -> decoded +decodeBody "setPlayerAnimation" body = + case parse + ( \obj -> do + playerIndex <- obj .: "playerIndex" + animationId <- obj .: "animationId" + delay <- obj .: "delay" + priority <- obj .: "priority" + return + ( SetPlayerAnimation + playerIndex + ( Animation + animationId + delay + (decodeAnimationPriority_ priority) + ) + ) + ) + body of + Error msg -> InternalNoop + Success decoded -> decoded +decodeBody "invokeScript" body = + case parse + ( \obj -> do + f <- obj .: "f" + args <- obj .: "args" + delay <- obj .: "delay" + return $ InvokeScript (ScriptInvocation f args) delay + ) + body of + Error msg -> trace msg InternalNoop + Success decoded -> decoded +decodeBody "createInterface" body = + case parse + ( \obj -> do + interfaceType <- obj .: "type" + playerIndex <- obj .: "playerIndex" + elements <- obj .: "elements" + onClose <- obj .:? "onClose" + callbacks <- obj .: "callbacks" + return + ( CreateInterface + playerIndex + $ I.CreateInterfaceRequest + (decodeInterfaceType_ interfaceType) + ( mapMaybe + decodeInterfaceElement_ + elements + ) + (decodeScriptInvocation_ onClose) + callbacks + ) + ) + body of + Error msg -> trace msg InternalNoop + Success decoded -> decoded +decodeBody "clearStandardInterface" body = + case parse + ( \obj -> do + playerIndex <- obj .: "playerIndex" + return (ClearStandardInterface playerIndex) + ) + body of + Error msg -> trace msg InternalNoop + Success decoded -> decoded +decodeBody "setPlayerEntityData" body = + case parse + ( \obj -> do + playerIndex <- obj .: "playerIndex" + key <- obj .: "key" + val <- obj .: "val" + return + (SetPlayerEntityData playerIndex key val) + ) + body of + Error msg -> trace msg InternalNoop + Success decoded -> decoded +decodeBody "spawnObject" body = + case parse + ( \obj -> do + objectId <- obj .: "objectId" + positionObj <- obj .: "position" + objectType <- obj .: "objectType" + facingDirection <- obj .: "facingDirection" + return + ( AddGameObject $ + Added $ + GameObject objectId (decodePos_ positionObj) objectType facingDirection + ) + ) + body of + Error msg -> trace msg InternalNoop + Success decoded -> decoded +decodeBody "removeObject" body = + case parse + ( \obj -> do + objectId <- obj .: "objectId" + positionObj <- obj .: "position" + objectType <- obj .: "objectType" + facingDirection <- obj .: "facingDirection" + return + ( AddGameObject $ + Removed $ + GameObject objectId (decodePos_ positionObj) objectType facingDirection + ) + ) + body of + Error msg -> trace msg InternalNoop + Success decoded -> decoded +decodeBody "giveItem" body = + case parse + ( \obj -> do + playerIndex <- obj .: "playerIndex" + itemId <- obj .: "itemId" + quantity <- obj .: "quantity" + + return (GiveItem playerIndex itemId quantity) + ) + body of + Error msg -> trace msg InternalNoop + Success decoded -> decoded +decodeBody "subtractItem" body = + case parse + ( \obj -> do + playerIndex <- obj .: "playerIndex" + itemId <- obj .: "itemId" + quantity <- obj .: "quantity" + + return (SubtractItem playerIndex itemId quantity) + ) + body of + Error msg -> trace msg InternalNoop + Success decoded -> decoded +decodeBody "removeItemStack" body = + case parse + ( \obj -> do + playerIndex <- obj .: "playerIndex" + itemId <- obj .: "itemId" + index <- obj .: "index" + + return (RemoveItemStack playerIndex itemId index) + ) + body of + Error msg -> trace msg InternalNoop + Success decoded -> decoded +decodeBody "spawnGroundItem" body = + case parse + ( \obj -> do + itemId <- obj .: "itemId" + quantity <- obj .: "quantity" + posObj <- obj .: "position" + player <- obj .:? "player" + despawnTime <- obj .: "despawnTime" + + return (SpawnGroundItem $ GroundItem itemId quantity (decodePos_ posObj) player despawnTime) + ) + body of + Error msg -> trace msg InternalNoop + Success decoded -> decoded +decodeBody "removeGroundItem" body = + case parse + ( \obj -> do + itemId <- obj .: "itemId" + quantity <- obj .: "quantity" + posObj <- obj .: "position" + player <- obj .:? "removedByPlayer" + + return (RemoveGroundItem itemId quantity (decodePos_ posObj) player) + ) + body of + Error msg -> trace msg InternalNoop + Success decoded -> decoded decodeBody _ _ = InternalNoop data DecodedMessage = DecodedMessage @@ -179,3 +358,84 @@ decodeAnimationPriority_ :: String -> AnimationPriority decodeAnimationPriority_ "high" = High decodeAnimationPriority_ "low" = Low decodeAnimationPriority_ _ = Normal + +decodeInterfaceElement_ :: Object -> Maybe I.InterfaceElement +decodeInterfaceElement_ body = + case parse + ( \obj -> do + elementType <- obj .: "type" + return $ mapInterfaceElement_ elementType body + ) + body of + Error msg -> Nothing + Success decoded -> decoded + +mapInterfaceElement_ :: String -> Object -> Maybe I.InterfaceElement +mapInterfaceElement_ "text" body = + case parse + ( \obj -> do + widgetId <- obj .: "widgetId" + msg <- obj .: "msg" + return (I.TextElement widgetId msg) + ) + body of + Error msg -> Nothing + Success decoded -> Just decoded +mapInterfaceElement_ "chatboxRoot" body = + case parse + ( \obj -> do + widgetId <- obj .: "widgetId" + return (I.ChatboxRootWindowElement widgetId) + ) + body of + Error msg -> Nothing + Success decoded -> Just decoded +mapInterfaceElement_ "npcChathead" body = + case parse + ( \obj -> do + widgetId <- obj .: "widgetId" + npcId <- obj .: "npcId" + return (I.NpcChatheadElement widgetId npcId) + ) + body of + Error msg -> Nothing + Success decoded -> Just decoded +mapInterfaceElement_ "playerChathead" body = + case parse + ( \obj -> do + widgetId <- obj .: "widgetId" + return (I.PlayerChatheadElement widgetId) + ) + body of + Error msg -> Nothing + Success decoded -> Just decoded +mapInterfaceElement_ "modelAnimation" body = + case parse + ( \obj -> do + widgetId <- obj .: "widgetId" + animationId <- obj .: "animationId" + return (I.ModelAnimationElement widgetId animationId) + ) + body of + Error msg -> Nothing + Success decoded -> Just decoded +mapInterfaceElement_ _ _ = Nothing + +decodeScriptInvocation_ :: Maybe Object -> Maybe ScriptInvocation +decodeScriptInvocation_ Nothing = Nothing +decodeScriptInvocation_ (Just body) = + case parse + ( \obj -> do + f <- obj .: "f" + args <- obj .: "args" + return (ScriptInvocation f args) + ) + body of + Error msg -> Nothing + Success decoded -> Just decoded + +decodeInterfaceType_ :: String -> I.InterfaceType +decodeInterfaceType_ "standard" = I.Standard +decodeInterfaceType_ "walkable" = I.Walkable +decodeInterfaceType_ "input" = I.Input +decodeInterfaceType_ _ = I.Standard diff --git a/app/PotatoCactus/Game/Scripting/Bridge/Serialization/GameEventMapper.hs b/app/PotatoCactus/Game/Scripting/Bridge/Serialization/GameEventMapper.hs index 27f018b..75dd7e9 100644 --- a/app/PotatoCactus/Game/Scripting/Bridge/Serialization/GameEventMapper.hs +++ b/app/PotatoCactus/Game/Scripting/Bridge/Serialization/GameEventMapper.hs @@ -4,15 +4,19 @@ module PotatoCactus.Game.Scripting.Bridge.Serialization.GameEventMapper (mapEven import Data.Aeson (ToJSON, Value (Null)) import Data.Aeson.Text (encodeToTextBuilder) +import Data.Aeson.Types (listValue) +import GHC.ExecutionStack (Location (functionName)) import GHC.Generics (Generic) +import PotatoCactus.Game.Scripting.Actions.ScriptInvocation (ScriptInvocation (ScriptInvocation)) import PotatoCactus.Game.Scripting.Bridge.BridgeMessage (BridgeMessage, EmptyPayload, bridgeMessage) import PotatoCactus.Game.Scripting.Bridge.ControlMessages (doneSendingEventsMessage) import PotatoCactus.Game.Scripting.Bridge.Serialization.Models.CommandDto (commandDto) +import PotatoCactus.Game.Scripting.Bridge.Serialization.Models.DropItemDto (dropItemDto) import PotatoCactus.Game.Scripting.Bridge.Serialization.Models.InteractionDto (playerInteractionToDto) import PotatoCactus.Game.Scripting.Bridge.Serialization.Models.NpcAttackDto (npcAttackDto) import PotatoCactus.Game.Scripting.Bridge.Serialization.Models.NpcReferenceDto (npcReferenceDto) import PotatoCactus.Game.Scripting.Bridge.Serialization.Models.PlayerAttackDto (playerAttackToDto) -import PotatoCactus.Game.Scripting.ScriptUpdates (GameEvent (NpcAttackEvent, NpcCannotReachTargetEvent, NpcDeadEvent, NpcEntityTickEvent, PlayerAttackEvent, PlayerCommandEvent, PlayerInteractionEvent, ServerInitEvent)) +import PotatoCactus.Game.Scripting.ScriptUpdates (GameEvent (DropItemEvent, NpcAttackEvent, NpcCannotReachTargetEvent, NpcDeadEvent, NpcEntityTickEvent, PlayerAttackEvent, PlayerCommandEvent, PlayerInteractionEvent, ScriptInvokedEvent, ServerInitEvent)) mapEvent :: GameEvent -> BridgeMessage (GameEventDto Value) mapEvent ServerInitEvent = @@ -38,6 +42,10 @@ mapEvent (NpcEntityTickEvent npc) = GameEventDto "NpcEntityTickEvent" (npcReferenceDto npc) mapEvent (PlayerCommandEvent playerIndex cmd args) = bridgeMessage "gameEvent" $ GameEventDto "PlayerCommandEvent" (commandDto playerIndex cmd args) +mapEvent (DropItemEvent playerId widgetId itemId index) = + bridgeMessage "gameEvent" $ GameEventDto "DropItemEvent" (dropItemDto playerId widgetId itemId index) +mapEvent (ScriptInvokedEvent (ScriptInvocation functionName args)) = + bridgeMessage "invokeScript" $ GameEventDto functionName (listValue id args) data GameEventDto b = GameEventDto { event :: String, diff --git a/app/PotatoCactus/Game/Scripting/Bridge/Serialization/Models/DropItemDto.hs b/app/PotatoCactus/Game/Scripting/Bridge/Serialization/Models/DropItemDto.hs new file mode 100644 index 0000000..8193020 --- /dev/null +++ b/app/PotatoCactus/Game/Scripting/Bridge/Serialization/Models/DropItemDto.hs @@ -0,0 +1,17 @@ +{-# LANGUAGE OverloadedStrings #-} + +module PotatoCactus.Game.Scripting.Bridge.Serialization.Models.DropItemDto where + +import Data.Aeson (Value, object, (.=)) +import PotatoCactus.Game.Definitions.ItemDefinitions (ItemId) +import PotatoCactus.Game.Player (PlayerIndex) +import PotatoCactus.Game.Scripting.Actions.CreateInterface (WidgetId) + +dropItemDto :: PlayerIndex -> WidgetId -> ItemId -> Int -> Value +dropItemDto playerId widgetId itemId index = + object + [ "playerIndex" .= playerId, + "widgetId" .= widgetId, + "itemId" .= itemId, + "index" .= index + ] diff --git a/app/PotatoCactus/Game/Scripting/Bridge/Serialization/Models/EntityDataDto.hs b/app/PotatoCactus/Game/Scripting/Bridge/Serialization/Models/EntityDataDto.hs new file mode 100644 index 0000000..717d83a --- /dev/null +++ b/app/PotatoCactus/Game/Scripting/Bridge/Serialization/Models/EntityDataDto.hs @@ -0,0 +1,11 @@ +module PotatoCactus.Game.Scripting.Bridge.Serialization.Models.EntityDataDto where + +import Data.Aeson (Object, Value (Object), (.=)) +import Data.Aeson.Key (fromString) +import Data.Aeson.Types (object) +import qualified Data.Map as Map +import PotatoCactus.Game.Entity.EntityData (EntityData) + +entityDataDto :: EntityData -> Value +entityDataDto d = + object $ map (\(k, v) -> fromString k .= v) (Map.assocs d) diff --git a/app/PotatoCactus/Game/Scripting/Bridge/Serialization/Models/InteractionDto.hs b/app/PotatoCactus/Game/Scripting/Bridge/Serialization/Models/InteractionDto.hs index f8ab945..2f8439b 100644 --- a/app/PotatoCactus/Game/Scripting/Bridge/Serialization/Models/InteractionDto.hs +++ b/app/PotatoCactus/Game/Scripting/Bridge/Serialization/Models/InteractionDto.hs @@ -8,9 +8,10 @@ import Data.Aeson.Types (Value (String)) import GHC.Generics (Generic) import qualified PotatoCactus.Game.Entity.Interaction.Interaction as I import PotatoCactus.Game.Entity.Interaction.State (InteractionState (InProgress, Pending, PendingPathing)) -import PotatoCactus.Game.Entity.Interaction.Target (InteractionTarget (None, NpcTarget, ObjectTarget), NpcInteractionType (NpcAction, NpcAttack)) +import PotatoCactus.Game.Entity.Interaction.Target (GroundItemInteractionType (ItemPickup), InteractionTarget (GroundItemTarget, None, NpcTarget, ObjectTarget), NpcInteractionType (NpcAction, NpcAttack)) import PotatoCactus.Game.Entity.Npc.Npc (Npc (definitionId)) import PotatoCactus.Game.Entity.Object.GameObjectKey (GameObjectKey (GameObjectKey, position)) +import qualified PotatoCactus.Game.Message.ItemOnObjectPayload as IonO import PotatoCactus.Game.Player (Player) import qualified PotatoCactus.Game.Player as P import qualified PotatoCactus.Game.Scripting.Bridge.Serialization.Models.PositionDto as Pos @@ -26,9 +27,11 @@ playerInteractionToDto p i = eventName :: I.Interaction -> String eventName I.Interaction {I.target = None} = "Noop" -eventName I.Interaction {I.target = (ObjectTarget _ _)} = "ObjectInteractionEvent" +eventName I.Interaction {I.target = (ObjectTarget _ (Left _))} = "ObjectInteractionEvent" +eventName I.Interaction {I.target = (ObjectTarget _ (Right _))} = "ItemOnObjectInteractionEvent" eventName I.Interaction {I.target = (NpcTarget _ NpcAttack)} = "NpcAttackInteractionEvent" -- TODO - Can this be consolidated with NpcAttackEvent? - keotl 2023-04-27 eventName I.Interaction {I.target = (NpcTarget _ _)} = "NpcInteractionEvent" +eventName I.Interaction {I.target = (GroundItemTarget _ _ _ ItemPickup)} = "PickupItemInteractionEvent" interactionToDto :: I.Interaction -> Value interactionToDto I.Interaction {I.target = None} = @@ -36,7 +39,7 @@ interactionToDto I.Interaction {I.target = None} = [ "target" .= Null, "state" .= String "pending" ] -interactionToDto I.Interaction {I.target = (ObjectTarget (GameObjectKey id position) actionIndex), I.state = s} = +interactionToDto I.Interaction {I.target = (ObjectTarget (GameObjectKey id position) (Left actionIndex)), I.state = s} = object [ "target" .= object @@ -47,6 +50,19 @@ interactionToDto I.Interaction {I.target = (ObjectTarget (GameObjectKey id posit ], "state" .= mapState s ] +interactionToDto I.Interaction {I.target = (ObjectTarget (GameObjectKey id position) (Right payload)), I.state = s} = + object + [ "target" + .= object + [ "type" .= String "itemOnObject", + "objectId" .= id, + "position" .= Pos.toDto position, + "itemId" .= IonO.itemId payload, + "itemIndex" .= IonO.itemIndex payload, + "interfaceId" .= IonO.interfaceId payload + ], + "state" .= mapState s + ] interactionToDto I.Interaction {I.target = (NpcTarget npcIndex NpcAttack), I.state = s} = object [ "target" @@ -66,6 +82,17 @@ interactionToDto I.Interaction {I.target = (NpcTarget npcId (NpcAction actionInd ], "state" .= mapState s ] +interactionToDto I.Interaction {I.target = (GroundItemTarget itemId quantity pos ItemPickup), I.state = s} = + object + [ "target" + .= object + [ "type" .= String "groundItem", + "itemId" .= itemId, + "quantity" .= quantity, + "position" .= Pos.toDto pos + ], + "state" .= mapState s + ] mapState :: InteractionState -> String mapState Pending = "pending" diff --git a/app/PotatoCactus/Game/Scripting/Bridge/Serialization/Models/PlayerDto.hs b/app/PotatoCactus/Game/Scripting/Bridge/Serialization/Models/PlayerDto.hs index b996823..31a7dc9 100644 --- a/app/PotatoCactus/Game/Scripting/Bridge/Serialization/Models/PlayerDto.hs +++ b/app/PotatoCactus/Game/Scripting/Bridge/Serialization/Models/PlayerDto.hs @@ -2,11 +2,12 @@ module PotatoCactus.Game.Scripting.Bridge.Serialization.Models.PlayerDto (PlayerDto, playerDto) where -import Data.Aeson (ToJSON, Value) +import Data.Aeson (Object, ToJSON, Value) import GHC.Generics (Generic) import qualified PotatoCactus.Game.Player as P import qualified PotatoCactus.Game.PlayerUpdate.Equipment as EQ import PotatoCactus.Game.Scripting.Bridge.Serialization.Models.CombatDto (CombatDto, combatDto) +import PotatoCactus.Game.Scripting.Bridge.Serialization.Models.EntityDataDto (entityDataDto) import PotatoCactus.Game.Scripting.Bridge.Serialization.Models.InteractionDto (interactionToDto) import PotatoCactus.Game.Scripting.Bridge.Serialization.Models.ItemContainerDto (itemContainerDto) import PotatoCactus.Game.Scripting.Bridge.Serialization.Models.MovementDto (MovementDto, playerMovementDto) @@ -18,8 +19,8 @@ data PlayerDto = PlayerDto combat :: CombatDto, interaction :: Value, inventory :: [Value], - equipment :: [Value] - -- TODO - appearance - keotl 2023-04-20 + equipment :: [Value], + entityData :: Value } deriving (Show, Generic) @@ -34,5 +35,6 @@ playerDto p = combat = combatDto . P.combat $ p, interaction = interactionToDto . P.interaction $ p, inventory = itemContainerDto . P.inventory $ p, - equipment = itemContainerDto . EQ.container . P.equipment $ p + equipment = itemContainerDto . EQ.container . P.equipment $ p, + entityData = entityDataDto . P.entityData $ p } diff --git a/app/PotatoCactus/Game/Scripting/MockScriptInteractions.hs b/app/PotatoCactus/Game/Scripting/BuiltinGameEventProcessor.hs similarity index 87% rename from app/PotatoCactus/Game/Scripting/MockScriptInteractions.hs rename to app/PotatoCactus/Game/Scripting/BuiltinGameEventProcessor.hs index 4701dfd..678f4b0 100644 --- a/app/PotatoCactus/Game/Scripting/MockScriptInteractions.hs +++ b/app/PotatoCactus/Game/Scripting/BuiltinGameEventProcessor.hs @@ -1,4 +1,4 @@ -module PotatoCactus.Game.Scripting.MockScriptInteractions where +module PotatoCactus.Game.Scripting.BuiltinGameEventProcessor where import Debug.Trace (trace) import qualified PotatoCactus.Game.Combat.CombatEntity as Combat @@ -18,7 +18,7 @@ import PotatoCactus.Game.Message.RegisterClientPayload (RegisterClientPayload (p import PotatoCactus.Game.Movement.PositionXY (fromXY) import PotatoCactus.Game.Player (Player (serverIndex)) import PotatoCactus.Game.Position (GetPosition (getPosition), Position (x, z)) -import PotatoCactus.Game.Scripting.ScriptUpdates (GameEvent (NpcAttackEvent, NpcCannotReachTargetEvent, NpcDeadEvent, NpcEntityTickEvent, PlayerAttackEvent, PlayerInteractionEvent), ScriptActionResult (AddGameObject, ClearPlayerInteraction, DispatchAttackNpcToPlayer, DispatchAttackPlayerToNpc, InternalRemoveNpcTargetReferences, NpcMoveTowardsTarget, NpcSetAnimation, UpdateNpc)) +import PotatoCactus.Game.Scripting.ScriptUpdates (GameEvent (DropItemEvent, NpcAttackEvent, NpcCannotReachTargetEvent, NpcDeadEvent, NpcEntityTickEvent, PlayerAttackEvent, PlayerInteractionEvent), ScriptActionResult (AddGameObject, ClearPlayerInteraction, DispatchAttackNpcToPlayer, DispatchAttackPlayerToNpc, InternalRemoveNpcTargetReferences, NpcMoveTowardsTarget, NpcSetAnimation, RemoveItemStack)) import PotatoCactus.Game.Typing (key) import PotatoCactus.Game.World (World (tick)) @@ -27,9 +27,9 @@ dispatchScriptEvent world (PlayerInteractionEvent player interaction) = trace ("Dispatched interaction event " ++ show interaction) ( case (target interaction, state interaction) of - (ObjectTarget (GameObjectKey 1530 pos) 1, InProgress) -> + (ObjectTarget (GameObjectKey 1530 pos) (Left 1), InProgress) -> return (ClearPlayerInteraction (serverIndex player) : openDoor_ pos) - (ObjectTarget (GameObjectKey 1531 pos) 1, InProgress) -> + (ObjectTarget (GameObjectKey 1531 pos) (Left 1), InProgress) -> return (ClearPlayerInteraction (serverIndex player) : closeDoor_ pos) (NpcTarget npcId NpcAttack, InProgress) -> return @@ -40,7 +40,6 @@ dispatchScriptEvent world (PlayerInteractionEvent player interaction) = ) dispatchScriptEvent world (NpcCannotReachTargetEvent npc target) = return [NpcMoveTowardsTarget npc] - dispatchScriptEvent world (PlayerAttackEvent player target) = trace "dispatched attack event" diff --git a/app/PotatoCactus/Game/Scripting/Events/ApplyScriptActionResult.hs b/app/PotatoCactus/Game/Scripting/Events/ApplyScriptActionResult.hs index e6788eb..cddcb63 100644 --- a/app/PotatoCactus/Game/Scripting/Events/ApplyScriptActionResult.hs +++ b/app/PotatoCactus/Game/Scripting/Events/ApplyScriptActionResult.hs @@ -1,8 +1,11 @@ module PotatoCactus.Game.Scripting.Events.ApplyScriptActionResult (applyScriptResult) where +import Data.Maybe (isJust) import Debug.Trace (trace) import PotatoCactus.Game.Combat.CombatEntity (CombatEntity (target), CombatTarget (NpcTarget, PlayerTarget), clearTarget) import qualified PotatoCactus.Game.Entity.Animation.Animation as Anim +import qualified PotatoCactus.Game.Entity.EntityData as EntityData +import qualified PotatoCactus.Game.Entity.GroundItem.GroundItemCollection as GroundItemCollection import PotatoCactus.Game.Entity.Interaction.Interaction (create) import PotatoCactus.Game.Entity.Npc.Npc (Npc (respawn)) import qualified PotatoCactus.Game.Entity.Npc.Npc as NPC @@ -10,14 +13,15 @@ import PotatoCactus.Game.Entity.Npc.NpcMovement (immediatelyQueueMovement) import qualified PotatoCactus.Game.Entity.Npc.NpcMovement as NM import PotatoCactus.Game.Entity.Npc.RespawnStrategy (RespawnStrategy (Never), respawning) import PotatoCactus.Game.Entity.Object.DynamicObjectCollection (addDynamicObject) +import qualified PotatoCactus.Game.ItemContainer as ItemContainer import PotatoCactus.Game.Movement.PathPlanner (findPath, findPathNaive) import PotatoCactus.Game.Player (Player (interaction), clearTargetIfEngagedWithNpc) import qualified PotatoCactus.Game.Player as P import qualified PotatoCactus.Game.PlayerUpdate.PlayerAnimationDefinitions as PAnim import PotatoCactus.Game.Position (GetPosition (getPosition)) import qualified PotatoCactus.Game.Scripting.Actions.SpawnNpcRequest as SpawnReq -import PotatoCactus.Game.Scripting.ScriptUpdates (ScriptActionResult (AddGameObject, ClearPlayerInteraction, DispatchAttackNpcToPlayer, DispatchAttackPlayerToNpc, InternalNoop, InternalProcessingComplete, InternalRemoveNpcTargetReferences, NpcMoveTowardsTarget, NpcQueueWalk, NpcSetAnimation, NpcSetForcedChat, SendMessage, ServerPrintMessage, SetPlayerPosition, SpawnNpc, UpdateNpc)) -import PotatoCactus.Game.World (World (npcs, objects, players)) +import PotatoCactus.Game.Scripting.ScriptUpdates (GameEvent (ScriptInvokedEvent), ScriptActionResult (AddGameObject, ClearPlayerInteraction, ClearStandardInterface, CreateInterface, DispatchAttackNpcToPlayer, DispatchAttackPlayerToNpc, GiveItem, InternalNoop, InternalProcessingComplete, InternalRemoveNpcTargetReferences, InvokeScript, NpcMoveTowardsTarget, NpcQueueWalk, NpcSetAnimation, NpcSetForcedChat, RemoveGroundItem, RemoveItemStack, SendMessage, ServerPrintMessage, SetPlayerAnimation, SetPlayerEntityData, SetPlayerPosition, SpawnGroundItem, SpawnNpc, SubtractItem)) +import PotatoCactus.Game.World (World (npcs, objects, players), groundItems) import qualified PotatoCactus.Game.World as W import PotatoCactus.Game.World.MobList (findByIndex, remove, updateAll, updateAtIndex) import PotatoCactus.Game.World.Selectors (isNpcAt) @@ -28,14 +32,6 @@ applyScriptResult world (AddGameObject obj) = world { objects = addDynamicObject obj (objects world) } --- applyScriptResult world (UpdatePlayer playerId p) = --- world --- { players = updateAtIndex (players world) playerId (const p) --- } -applyScriptResult world (UpdateNpc npcId npc) = - world - { npcs = updateAtIndex (npcs world) npcId (const npc) - } applyScriptResult world (ClearPlayerInteraction playerId) = world { players = updateAtIndex (players world) playerId (\p -> p {interaction = create, P.combat = clearTarget . P.combat $ p}) @@ -147,3 +143,79 @@ applyScriptResult world (SetPlayerPosition playerIndex pos) = world { players = updateAtIndex (players world) playerIndex (`P.setPosition` pos) } +applyScriptResult world (InvokeScript invocation delay) = + W.scheduleCallback world invocation (W.tick world + max delay 1) +applyScriptResult world (CreateInterface playerIndex request) = + world + { players = updateAtIndex (players world) playerIndex (`P.createInterface` request) + } +applyScriptResult world (ClearStandardInterface playerIndex) = + world + { players = updateAtIndex (players world) playerIndex P.clearStandardInterface + } +applyScriptResult world (SetPlayerEntityData playerIndex key val) = + world + { players = + updateAtIndex + (players world) + playerIndex + (`P.updateEntityData` (\d -> EntityData.setValue d key val)) + } +applyScriptResult world (SetPlayerAnimation playerIndex anim) = + world + { players = updateAtIndex (players world) playerIndex (P.setAnimation anim) + } +applyScriptResult world (GiveItem playerIndex itemId quantity) = + world + { players = updateAtIndex (players world) playerIndex (`P.giveItem` ItemContainer.ItemStack itemId quantity) + } +applyScriptResult world (SubtractItem playerIndex itemId quantity) = + world + { players = updateAtIndex (players world) playerIndex (`P.subtractItem` (itemId, quantity)) + } +applyScriptResult world (RemoveItemStack playerIndex itemId index) = + world + { players = updateAtIndex (players world) playerIndex (`P.removeItemStack` (itemId, index)) + } +applyScriptResult world (SpawnGroundItem item) = + world + { groundItems = GroundItemCollection.insert (groundItems world) item + } +applyScriptResult world (RemoveGroundItem itemId quantity position removedByPlayer) = + let playerKey = case removedByPlayer of + Nothing -> Nothing + Just playerId -> fmap P.username (findByIndex (players world) playerId) + in let (removed, updated) = + GroundItemCollection.remove + (groundItems world) + (itemId, quantity, position, playerKey) + in case (removedByPlayer, removed) of + (_, ItemContainer.Empty) -> world + (Nothing, _) -> + world + { groundItems = updated + } + (Just playerIndex, item) -> + case findByIndex (players world) playerIndex of + Nothing -> world + Just player -> + if ItemContainer.canAddItem (P.inventory player) item + then + world + { groundItems = updated, + players = + updateAtIndex + (players world) + playerIndex + (`P.giveItem` item) + } + else + world + { players = + updateAtIndex + (players world) + playerIndex + ( `P.sendChatboxMessage` + "You need more inventory space to carry that item." + ) + } diff --git a/app/PotatoCactus/Game/Scripting/Events/PlayerEvents.hs b/app/PotatoCactus/Game/Scripting/Events/PlayerEvents.hs index dc36885..620d733 100644 --- a/app/PotatoCactus/Game/Scripting/Events/PlayerEvents.hs +++ b/app/PotatoCactus/Game/Scripting/Events/PlayerEvents.hs @@ -5,8 +5,9 @@ import PotatoCactus.Game.Combat.CombatEntity (CombatEntity (cooldown), CombatTar import qualified PotatoCactus.Game.Combat.CombatEntity as Combat import PotatoCactus.Game.Entity.Interaction.Interaction (Interaction (state)) import PotatoCactus.Game.Entity.Interaction.State (InteractionState (InProgress)) -import PotatoCactus.Game.Player (Player (combat, interaction)) -import PotatoCactus.Game.Scripting.ScriptUpdates (GameEvent (PlayerAttackEvent, PlayerInteractionEvent)) +import qualified PotatoCactus.Game.Interface.InterfaceController as IC +import PotatoCactus.Game.Player (Player (Player, combat, interaction, interfaces)) +import PotatoCactus.Game.Scripting.ScriptUpdates (GameEvent (PlayerAttackEvent, PlayerInteractionEvent, ScriptInvokedEvent)) createPlayerEvents :: Player -> [GameEvent] createPlayerEvents player = @@ -14,6 +15,7 @@ createPlayerEvents player = [ interactionEvent_ player, attackEvent_ player ] + ++ interfaceEvents_ player interactionEvent_ :: Player -> Maybe GameEvent interactionEvent_ p = @@ -29,3 +31,7 @@ attackEvent_ p = if 0 == (cooldown . combat $ p) then Just $ PlayerAttackEvent p target else Nothing + +interfaceEvents_ :: Player -> [GameEvent] +interfaceEvents_ Player {interfaces = ic} = + map ScriptInvokedEvent (IC.triggeredCallbacks ic) diff --git a/app/PotatoCactus/Game/Scripting/ProcessTickUpdates.hs b/app/PotatoCactus/Game/Scripting/ProcessTickUpdates.hs index b1ba626..96c5b0a 100644 --- a/app/PotatoCactus/Game/Scripting/ProcessTickUpdates.hs +++ b/app/PotatoCactus/Game/Scripting/ProcessTickUpdates.hs @@ -1,9 +1,9 @@ module PotatoCactus.Game.Scripting.ProcessTickUpdates (dispatchScriptEvents, readBridgeEventsUntilDone) where import PotatoCactus.Game.Scripting.Bridge.Communication (readScriptResult, sendEventsAsync) +import PotatoCactus.Game.Scripting.BuiltinGameEventProcessor (dispatchScriptEvent) import PotatoCactus.Game.Scripting.Events.ApplyScriptActionResult (applyScriptResult) import PotatoCactus.Game.Scripting.Events.CreateGameEvents (createGameEvents) -import PotatoCactus.Game.Scripting.MockScriptInteractions (dispatchScriptEvent) import PotatoCactus.Game.Scripting.ScriptUpdates (GameEvent, ScriptActionResult (InternalProcessingComplete)) import PotatoCactus.Game.World (World) import PotatoCactus.Interop.ScriptEngineProcess (ScriptEngineHandle, getInstance) diff --git a/app/PotatoCactus/Game/Scripting/ScriptUpdates.hs b/app/PotatoCactus/Game/Scripting/ScriptUpdates.hs index 4f345ea..f1b71ba 100644 --- a/app/PotatoCactus/Game/Scripting/ScriptUpdates.hs +++ b/app/PotatoCactus/Game/Scripting/ScriptUpdates.hs @@ -1,26 +1,33 @@ module PotatoCactus.Game.Scripting.ScriptUpdates where +import Data.Aeson (Value) import PotatoCactus.Game.Combat.CombatEntity (CombatTarget) import PotatoCactus.Game.Combat.Hit (Hit) +import PotatoCactus.Game.Definitions.ItemDefinitions (ItemId) import PotatoCactus.Game.Entity.Animation.Animation (Animation) +import PotatoCactus.Game.Entity.GroundItem.GroundItem (GroundItem (GroundItem)) import PotatoCactus.Game.Entity.Interaction.Interaction (Interaction) import PotatoCactus.Game.Entity.Npc.Npc (Npc, NpcIndex) import PotatoCactus.Game.Entity.Object.DynamicObjectCollection (DynamicObject) import PotatoCactus.Game.Entity.Object.GameObject (GameObject) import PotatoCactus.Game.Player (Player, PlayerIndex) import PotatoCactus.Game.Position (Position (Position)) +import PotatoCactus.Game.Scripting.Actions.CreateInterface (CreateInterfaceRequest, WidgetId) +import PotatoCactus.Game.Scripting.Actions.ScriptInvocation (ScriptInvocation) import PotatoCactus.Game.Scripting.Actions.SpawnNpcRequest (SpawnNpcRequest) data GameEvent = ServerInitEvent - | PlayerInteractionEvent Player Interaction -- maps to NpcInteractionEvent, ObjectInteractionEvent and NpcAttackInteractionEvent + | PlayerInteractionEvent Player Interaction -- maps to NpcInteractionEvent, ObjectInteractionEvent, ItemOnObjectInteractionEvent, NpcAttackInteractionEvent and PickupItemInteractionEvent | PlayerAttackEvent Player CombatTarget | NpcAttackEvent Npc CombatTarget | NpcCannotReachTargetEvent Npc CombatTarget | NpcDeadEvent Npc | NpcEntityTickEvent Npc | PlayerCommandEvent PlayerIndex String [String] - deriving(Show) + | ScriptInvokedEvent ScriptInvocation + | DropItemEvent PlayerIndex WidgetId ItemId Int + deriving (Show) data ScriptActionResult = AddGameObject DynamicObject @@ -34,9 +41,18 @@ data ScriptActionResult | SpawnNpc SpawnNpcRequest | SendMessage PlayerIndex String | SetPlayerPosition PlayerIndex Position + | SetPlayerAnimation PlayerIndex Animation | InternalRemoveNpcTargetReferences NpcIndex | InternalProcessingComplete -- Sentinel token to indicate script execution complete + | InvokeScript ScriptInvocation Int + | CreateInterface PlayerIndex CreateInterfaceRequest + | ClearStandardInterface PlayerIndex + | GiveItem PlayerIndex ItemId Int + | SubtractItem PlayerIndex ItemId Int -- Remove quantity of item, from anywhere in the inventory + | RemoveItemStack PlayerIndex ItemId Int -- Remove stack of item at index + | SpawnGroundItem GroundItem + | RemoveGroundItem ItemId Int Position (Maybe PlayerIndex) + | SetPlayerEntityData PlayerIndex String Value | InternalNoop | ServerPrintMessage String -- for testing - | UpdateNpc NpcIndex Npc -- deprecated deriving (Show) diff --git a/app/PotatoCactus/Game/World.hs b/app/PotatoCactus/Game/World.hs index 299f322..7d5113e 100644 --- a/app/PotatoCactus/Game/World.hs +++ b/app/PotatoCactus/Game/World.hs @@ -5,6 +5,8 @@ import Data.IORef (newIORef) import Data.List (find) import GHC.IO (unsafePerformIO) import PotatoCactus.Config.Constants (maxNpcs, maxPlayers) +import PotatoCactus.Game.Entity.GroundItem.GroundItemCollection (GroundItemCollection) +import qualified PotatoCactus.Game.Entity.GroundItem.GroundItemCollection as GroundItemCollection import PotatoCactus.Game.Entity.Npc.AdvanceNpc (advanceNpc) import qualified PotatoCactus.Game.Entity.Npc.Npc as NPC import PotatoCactus.Game.Entity.Object.DynamicObjectCollection (DynamicObjectCollection (DynamicObjectCollection), create) @@ -14,8 +16,11 @@ import PotatoCactus.Game.Player (PlayerIndex) import qualified PotatoCactus.Game.Player as P (Player (serverIndex), create, username) import PotatoCactus.Game.PlayerUpdate.AdvancePlayer (advancePlayer) import PotatoCactus.Game.Position (Position (Position)) -import PotatoCactus.Game.Scripting.ScriptUpdates (GameEvent) +import PotatoCactus.Game.Scripting.Actions.ScriptInvocation (ScriptInvocation) +import PotatoCactus.Game.Scripting.ScriptUpdates (GameEvent (ScriptInvokedEvent)) import PotatoCactus.Game.Typing (Advance (advance), ShouldDiscard (shouldDiscard)) +import PotatoCactus.Game.World.CallbackScheduler (CallbackScheduler) +import qualified PotatoCactus.Game.World.CallbackScheduler as Scheduler import PotatoCactus.Game.World.EntityPositionFinder (combatTargetPosOrDefault) import PotatoCactus.Game.World.MobList (MobList, add, create, findByIndex, findByPredicate, remove, removeByPredicate, updateAll, updateAtIndex, updateByPredicate) import PotatoCactus.Utils.Iterable (replace) @@ -36,8 +41,10 @@ data World = World npcs :: MobList NPC.Npc, clients :: [ClientHandle], objects :: DynamicObjectCollection, + groundItems :: GroundItemCollection, triggeredEvents :: [GameEvent], -- Additional events to dispatch on this tick. For events not tied to a specific entity. - pendingEvents_ :: [GameEvent] -- Additional events to dispatch on the next tick. + pendingEvents_ :: [GameEvent], -- Additional events to dispatch on the next tick. + scheduler :: CallbackScheduler } deriving (Show) @@ -52,10 +59,16 @@ instance Advance World where { tick = tick w + 1, players = updateAll (players w) (advancePlayer (findByIndex newNpcs)), npcs = removeByPredicate newNpcs shouldDiscard, - triggeredEvents = pendingEvents_ w, - pendingEvents_ = [] + groundItems = GroundItemCollection.advanceTime (groundItems w) (tick w + 1), + triggeredEvents = pendingEvents_ w ++ invokedScripts_ w, + pendingEvents_ = [], + scheduler = Scheduler.clearCallbacksForTick (scheduler w) (tick w + 1) } +invokedScripts_ :: World -> [GameEvent] +invokedScripts_ w = + map ScriptInvokedEvent $ Scheduler.callbacksForTick (scheduler w) (tick w + 1) + defaultWorldValue :: World defaultWorldValue = World @@ -64,8 +77,10 @@ defaultWorldValue = npcs = PotatoCactus.Game.World.MobList.create maxNpcs, clients = [], objects = PotatoCactus.Game.Entity.Object.DynamicObjectCollection.create, + groundItems = GroundItemCollection.create, triggeredEvents = [], - pendingEvents_ = [] + pendingEvents_ = [], + scheduler = Scheduler.create } worldInstance = unsafePerformIO $ newIORef defaultWorldValue @@ -114,3 +129,9 @@ addNpc world npc = queueEvent :: World -> GameEvent -> World queueEvent w e = w {pendingEvents_ = e : pendingEvents_ w} + +scheduleCallback :: World -> ScriptInvocation -> Int -> World +scheduleCallback w script tick = + w + { scheduler = Scheduler.queueCallback (scheduler w) script tick + } diff --git a/app/PotatoCactus/Game/World/CallbackScheduler.hs b/app/PotatoCactus/Game/World/CallbackScheduler.hs new file mode 100644 index 0000000..a773722 --- /dev/null +++ b/app/PotatoCactus/Game/World/CallbackScheduler.hs @@ -0,0 +1,38 @@ +{-# LANGUAGE LambdaCase #-} + +module PotatoCactus.Game.World.CallbackScheduler (CallbackScheduler, queueCallback, callbacksForTick, clearCallbacksForTick, create) where + +import Data.IntMap (IntMap, alter, delete, empty, (!?)) +import Data.Maybe (fromMaybe) +import PotatoCactus.Game.Scripting.Actions.ScriptInvocation (ScriptInvocation) + +data CallbackScheduler = CallbackScheduler + { scheduled_ :: IntMap [ScriptInvocation] + } + deriving (Show) + +create :: CallbackScheduler +create = CallbackScheduler empty + +queueCallback :: CallbackScheduler -> ScriptInvocation -> Int -> CallbackScheduler +queueCallback scheduler script tick = + scheduler + { scheduled_ = + alter + ( \case + Nothing -> Just [script] + Just existing -> Just (script : existing) + ) + tick + (scheduled_ scheduler) + } + +callbacksForTick :: CallbackScheduler -> Int -> [ScriptInvocation] +callbacksForTick scheduler tick = + fromMaybe [] (scheduled_ scheduler !? tick) + +clearCallbacksForTick :: CallbackScheduler -> Int -> CallbackScheduler +clearCallbacksForTick scheduler tick = + scheduler + { scheduled_ = delete tick (scheduled_ scheduler) + } diff --git a/app/PotatoCactus/Network/Binary.hs b/app/PotatoCactus/Network/Binary.hs index c7b0d9d..21a66c9 100644 --- a/app/PotatoCactus/Network/Binary.hs +++ b/app/PotatoCactus/Network/Binary.hs @@ -2,8 +2,9 @@ module PotatoCactus.Network.Binary where -import Data.Binary (Word16, Word32, Word64, Word8) +import Data.Binary (Put, Word16, Word32, Word64, Word8, putWord8) import Data.Binary.BitPut (BitPut, putByteString, putNBits, runBitPut) +import Data.Binary.Put (runPut) import Data.Binary.Strict.Get (getWord16be, getWord32be, getWord32le, getWord8, runGet) import Data.Bits (Bits (rotateL, shiftR, (.&.))) import Data.ByteString (length, pack, tail) @@ -82,14 +83,14 @@ toIntME_ x = encodeStr :: String -> ByteString encodeStr input = toStrict $ - runBitPut + runPut ( do mapM_ mapChar_ input - putNBits 8 $ toWord_ 10 + putWord8 10 ) -mapChar_ :: Char -> BitPut -mapChar_ c = putNBits 8 $ toWord_ (ord c) +mapChar_ :: Char -> Put +mapChar_ c = putWord8 . fromIntegral $ ord c nibbles :: [Word8] -> [Word8] nibbles = Prelude.foldr (\x -> (++) [x `shiftR` 4, x .&. 0xf]) [] diff --git a/app/PotatoCactus/Network/InboundPacketMapper.hs b/app/PotatoCactus/Network/InboundPacketMapper.hs index 2f1b043..85138d4 100644 --- a/app/PotatoCactus/Network/InboundPacketMapper.hs +++ b/app/PotatoCactus/Network/InboundPacketMapper.hs @@ -4,11 +4,15 @@ import PotatoCactus.Game.Message.GameChannelMessage (GameChannelMessage (Unregis import PotatoCactus.Game.Player (PlayerIndex) import PotatoCactus.Network.Packets.In.ButtonClickPacket (buttonClickMessage) import PotatoCactus.Network.Packets.In.ChatMessagePacket (playerChatMessage) +import PotatoCactus.Network.Packets.In.ContinueDialoguePacket (continueDialoguePacket) +import PotatoCactus.Network.Packets.In.DropItemPacket (dropItemPacket) import PotatoCactus.Network.Packets.In.EquipItemPacket (equipItemPacket) import PotatoCactus.Network.Packets.In.ItemContainerClickPacket (itemContainerClickPacket) +import PotatoCactus.Network.Packets.In.ItemOnObjectPacket (itemOnObjectPacket) import PotatoCactus.Network.Packets.In.NpcActionPacket (npcActionPacket) import PotatoCactus.Network.Packets.In.NpcAttackPacket (npcAttackPacket) import PotatoCactus.Network.Packets.In.ObjectActionPacket (objectActionPacket) +import PotatoCactus.Network.Packets.In.PickupGroundItemPacket (pickupGroundItemPacket) import PotatoCactus.Network.Packets.In.PlayerCommandPacket (commandPacket) import PotatoCactus.Network.Packets.In.PlayerWalkPacket (playerMapWalk, playerWalkMessage) import PotatoCactus.Network.Packets.Opcodes (socketClosedOpcode) @@ -21,16 +25,21 @@ mapPacket playerId clientIdentifier packet = 17 -> npcActionPacket playerId packet 18 -> npcActionPacket playerId packet 21 -> npcActionPacket playerId packet + 40 -> Just $ continueDialoguePacket playerId packet 41 -> Just $ equipItemPacket clientIdentifier packet + 70 -> Just $ objectActionPacket playerId packet 72 -> Just $ npcAttackPacket playerId packet 73 -> Just $ objectActionPacket playerId packet + 87 -> Just $ dropItemPacket playerId packet 98 -> Just $ playerWalkMessage clientIdentifier packet -- Red X walk 103 -> Just $ commandPacket playerId packet 132 -> Just $ objectActionPacket playerId packet 145 -> itemContainerClickPacket clientIdentifier packet 155 -> npcActionPacket playerId packet 164 -> Just $ playerWalkMessage clientIdentifier packet -- Yellow X walk - 185 -> Just $ buttonClickMessage clientIdentifier packet -- Interface button + 185 -> Just $ buttonClickMessage playerId packet -- Interface button + 192 -> Just $ itemOnObjectPacket playerId packet + 236 -> Just $ pickupGroundItemPacket playerId packet 248 -> Just $ playerMapWalk clientIdentifier packet -- Minimap walk 252 -> Just $ objectActionPacket playerId packet 254 -> Just $ UnregisterClientMessage clientIdentifier diff --git a/app/PotatoCactus/Network/Packets/In/ButtonClickPacket.hs b/app/PotatoCactus/Network/Packets/In/ButtonClickPacket.hs index 71e380c..28e2eae 100644 --- a/app/PotatoCactus/Network/Packets/In/ButtonClickPacket.hs +++ b/app/PotatoCactus/Network/Packets/In/ButtonClickPacket.hs @@ -4,11 +4,12 @@ import Data.Binary.Get (getWord16be, runGet) import Data.ByteString.Lazy (fromStrict) import PotatoCactus.Game.Message.GameChannelMessage (GameChannelMessage (InterfaceButtonClickMessage)) import PotatoCactus.Network.Packets.Reader (InboundPacket (payload)) +import PotatoCactus.Game.Player (PlayerIndex) -buttonClickMessage :: String -> InboundPacket -> GameChannelMessage -buttonClickMessage username packet = +buttonClickMessage :: PlayerIndex -> InboundPacket -> GameChannelMessage +buttonClickMessage playerId packet = InterfaceButtonClickMessage - username + playerId $ fromIntegral ( runGet getWord16be diff --git a/app/PotatoCactus/Network/Packets/In/ContinueDialoguePacket.hs b/app/PotatoCactus/Network/Packets/In/ContinueDialoguePacket.hs new file mode 100644 index 0000000..74ebc30 --- /dev/null +++ b/app/PotatoCactus/Network/Packets/In/ContinueDialoguePacket.hs @@ -0,0 +1,17 @@ +module PotatoCactus.Network.Packets.In.ContinueDialoguePacket where + +import Data.Binary.Get (getWord16be, runGet) +import Data.ByteString.Lazy (fromStrict) +import PotatoCactus.Game.Message.GameChannelMessage (GameChannelMessage (PlayerContinueDialogueMessage)) +import PotatoCactus.Game.Player (PlayerIndex) +import PotatoCactus.Network.Packets.Reader (InboundPacket (InboundPacket, payload)) + +continueDialoguePacket :: PlayerIndex -> InboundPacket -> GameChannelMessage +continueDialoguePacket playerIndex packet = + PlayerContinueDialogueMessage + playerIndex + $ fromIntegral + ( runGet + getWord16be + (fromStrict $ payload packet) + ) diff --git a/app/PotatoCactus/Network/Packets/In/DropItemPacket.hs b/app/PotatoCactus/Network/Packets/In/DropItemPacket.hs new file mode 100644 index 0000000..9b1ee01 --- /dev/null +++ b/app/PotatoCactus/Network/Packets/In/DropItemPacket.hs @@ -0,0 +1,26 @@ +module PotatoCactus.Network.Packets.In.DropItemPacket (dropItemPacket) where + +import Data.Binary (Get) +import Data.Binary.Get (getWord16be, getWord16le, runGet) +import Data.Bits (Bits (xor)) +import Data.ByteString.Lazy (fromStrict) +import Data.Word (Word16) +import PotatoCactus.Game.Message.GameChannelMessage (GameChannelMessage (DropItemMessage)) +import PotatoCactus.Game.Player (PlayerIndex) +import PotatoCactus.Network.Packets.Reader (InboundPacket (payload)) + +dropItemPacket :: PlayerIndex -> InboundPacket -> GameChannelMessage +dropItemPacket playerIndex packet = + let (itemId, widgetId, index) = runGet readPayload_ (fromStrict . payload $ packet) + in DropItemMessage + playerIndex + (fromIntegral widgetId) + (fromIntegral itemId) + (fromIntegral index) + +readPayload_ :: Get (Word16, Word16, Word16) +readPayload_ = do + itemId <- getWord16be + widgetId <- getWord16be + index <- getWord16be + return (itemId `xor` 128, widgetId, index `xor` 128) diff --git a/app/PotatoCactus/Network/Packets/In/ItemContainerClickPacket.hs b/app/PotatoCactus/Network/Packets/In/ItemContainerClickPacket.hs index 3c9406f..ff31670 100644 --- a/app/PotatoCactus/Network/Packets/In/ItemContainerClickPacket.hs +++ b/app/PotatoCactus/Network/Packets/In/ItemContainerClickPacket.hs @@ -5,9 +5,8 @@ import Data.Binary.Get (getInt16le, getWord16be, getWord16le, runGet) import Data.Bits (Bits (xor)) import Data.ByteString.Lazy (fromStrict) import Data.Word (Word16) -import Debug.Trace (trace) -import PotatoCactus.Network.Packets.Reader (InboundPacket (InboundPacket, opcode, payload)) import PotatoCactus.Game.Message.GameChannelMessage (GameChannelMessage (UnequipItemMessage)) +import PotatoCactus.Network.Packets.Reader (InboundPacket (InboundPacket, opcode, payload)) itemContainerClickPacket :: String -> InboundPacket -> Maybe GameChannelMessage itemContainerClickPacket username packet = diff --git a/app/PotatoCactus/Network/Packets/In/ItemOnObjectPacket.hs b/app/PotatoCactus/Network/Packets/In/ItemOnObjectPacket.hs new file mode 100644 index 0000000..7cbda8b --- /dev/null +++ b/app/PotatoCactus/Network/Packets/In/ItemOnObjectPacket.hs @@ -0,0 +1,39 @@ +module PotatoCactus.Network.Packets.In.ItemOnObjectPacket (itemOnObjectPacket) where + +import Data.Binary.Get (Get, getInt16le, getWord16be, getWord16le, runGet) +import Data.Bits (xor) +import Data.ByteString.Lazy (fromStrict) +import Data.Int (Int16) +import Data.Word (Word16) +import PotatoCactus.Game.Message.GameChannelMessage (GameChannelMessage (ItemOnObjectMessage)) +import PotatoCactus.Game.Message.ItemOnObjectPayload (ItemOnObjectPayload (ItemOnObjectPayload)) +import PotatoCactus.Game.Movement.PositionXY (PositionXY (PositionXY)) +import PotatoCactus.Game.Player (PlayerIndex) +import PotatoCactus.Network.Packets.Reader (InboundPacket (payload)) + +itemOnObjectPacket :: PlayerIndex -> InboundPacket -> GameChannelMessage +itemOnObjectPacket playerId packet = + let ( itemInterfaceId, + objectId, + objectY, + itemIndexId, + objectX, + itemId + ) = runGet readPayload_ (fromStrict . payload $ packet) + in ItemOnObjectMessage playerId $ + ItemOnObjectPayload + (fromIntegral itemInterfaceId) + (fromIntegral objectId) + (PositionXY (fromIntegral objectX) (fromIntegral objectY)) + (fromIntegral itemIndexId) + (fromIntegral itemId) + +readPayload_ :: Get (Word16, Word16, Int16, Word16, Int16, Word16) +readPayload_ = do + itemInterfaceId <- getWord16be + objectId <- getWord16le + objectY <- getInt16le + itemIndexId <- getWord16le + objectX <- getInt16le + itemId <- getWord16be + return (itemInterfaceId, objectId, objectY `xor` 128, itemIndexId, objectX `xor` 128, itemId) diff --git a/app/PotatoCactus/Network/Packets/In/PickupGroundItemPacket.hs b/app/PotatoCactus/Network/Packets/In/PickupGroundItemPacket.hs new file mode 100644 index 0000000..b4a4b04 --- /dev/null +++ b/app/PotatoCactus/Network/Packets/In/PickupGroundItemPacket.hs @@ -0,0 +1,24 @@ +module PotatoCactus.Network.Packets.In.PickupGroundItemPacket (pickupGroundItemPacket) where + +import Data.Binary (Word16) +import Data.Binary.Get (Get, getWord16be, getWord16le, runGet) +import Data.ByteString.Lazy (fromStrict) +import PotatoCactus.Game.Message.GameChannelMessage (GameChannelMessage (PickupGroundItemMessage)) +import PotatoCactus.Game.Movement.PositionXY (PositionXY (PositionXY)) +import PotatoCactus.Game.Player (PlayerIndex) +import PotatoCactus.Network.Packets.Reader (InboundPacket (payload)) + +pickupGroundItemPacket :: PlayerIndex -> InboundPacket -> GameChannelMessage +pickupGroundItemPacket playerIndex packet = + let (y, itemId, x) = runGet readPayload_ (fromStrict . payload $ packet) + in PickupGroundItemMessage + playerIndex + (fromIntegral itemId) + (PositionXY (fromIntegral x) (fromIntegral y)) + +readPayload_ :: Get (Word16, Word16, Word16) +readPayload_ = do + y <- getWord16le + itemId <- getWord16be + x <- getWord16le + return (y, itemId, x) diff --git a/app/PotatoCactus/Network/Packets/Out/AddGroundItemPacket.hs b/app/PotatoCactus/Network/Packets/Out/AddGroundItemPacket.hs new file mode 100644 index 0000000..413f2fd --- /dev/null +++ b/app/PotatoCactus/Network/Packets/Out/AddGroundItemPacket.hs @@ -0,0 +1,23 @@ +module PotatoCactus.Network.Packets.Out.AddGroundItemPacket where + +import Data.Binary.Put (putWord16be, putWord16le, putWord8) +import Data.Bits (Bits (xor, (.&.)), shiftL) +import Data.ByteString (ByteString) +import PotatoCactus.Client.GroundItemsUpdate.GroundItemsUpdateDiff (GroundItemClientView (quantity), itemId) +import PotatoCactus.Game.Position (GetPosition (getPosition), Position (x, y)) +import PotatoCactus.Network.Packets.Packet (fixedPacket2) + +addGroundItemPacket :: Position -> GroundItemClientView -> ByteString +addGroundItemPacket refPos item = + fixedPacket2 + 44 + ( do + let offset = + ( ((x refPos - (x . getPosition $ item)) `shiftL` 4) + + ((y refPos - (y . getPosition $ item)) .&. 7) + ) + in do + putWord16le . fromIntegral $ (itemId item `xor` 128) + putWord16be . fromIntegral . quantity $ item + putWord8 . fromIntegral $ offset + ) diff --git a/app/PotatoCactus/Network/Packets/Out/ChatboxInterfacePacket.hs b/app/PotatoCactus/Network/Packets/Out/ChatboxInterfacePacket.hs new file mode 100644 index 0000000..ab54ad9 --- /dev/null +++ b/app/PotatoCactus/Network/Packets/Out/ChatboxInterfacePacket.hs @@ -0,0 +1,14 @@ +module PotatoCactus.Network.Packets.Out.ChatboxInterfacePacket where + +import Data.Binary (Word16) +import Data.Binary.Put (putWord16le) +import Data.ByteString (ByteString) +import PotatoCactus.Network.Packets.Packet (fixedPacket2) + +chatboxInterfacePacket :: Word16 -> ByteString +chatboxInterfacePacket interfaceId = + fixedPacket2 + 164 + ( do + putWord16le interfaceId + ) diff --git a/app/PotatoCactus/Network/Packets/Out/CloseInterfacesPacket.hs b/app/PotatoCactus/Network/Packets/Out/CloseInterfacesPacket.hs new file mode 100644 index 0000000..cd83038 --- /dev/null +++ b/app/PotatoCactus/Network/Packets/Out/CloseInterfacesPacket.hs @@ -0,0 +1,9 @@ +module PotatoCactus.Network.Packets.Out.CloseInterfacesPacket where + +import Data.Binary.Put (putByteString) +import Data.ByteString (ByteString, empty) +import PotatoCactus.Network.Packets.Packet (fixedPacket2) + +closeInterfacesPacket :: ByteString +closeInterfacesPacket = + fixedPacket2 219 (putByteString empty) diff --git a/app/PotatoCactus/Network/Packets/Out/InterfaceAnimationPacket.hs b/app/PotatoCactus/Network/Packets/Out/InterfaceAnimationPacket.hs new file mode 100644 index 0000000..9567c3d --- /dev/null +++ b/app/PotatoCactus/Network/Packets/Out/InterfaceAnimationPacket.hs @@ -0,0 +1,15 @@ +module PotatoCactus.Network.Packets.Out.InterfaceAnimationPacket where + +import Data.Binary (Word16) +import Data.Binary.Put (putWord16be) +import Data.ByteString (ByteString) +import PotatoCactus.Network.Packets.Packet (fixedPacket2) + +interfaceAnimationPacket :: Word16 -> Word16 -> ByteString +interfaceAnimationPacket interfaceId animationId = + fixedPacket2 + 200 + ( do + putWord16be interfaceId + putWord16be animationId + ) diff --git a/app/PotatoCactus/Network/Packets/Out/InterfaceChatheadPacket.hs b/app/PotatoCactus/Network/Packets/Out/InterfaceChatheadPacket.hs new file mode 100644 index 0000000..5c303ed --- /dev/null +++ b/app/PotatoCactus/Network/Packets/Out/InterfaceChatheadPacket.hs @@ -0,0 +1,25 @@ +module PotatoCactus.Network.Packets.Out.InterfaceChatheadPacket where + +import Data.Binary.Put (putWord16le) +import Data.Bits (xor) +import Data.ByteString (ByteString) +import Data.Word (Word16) +import PotatoCactus.Game.Definitions.NpcDefinitions (NpcDefinitionId) +import PotatoCactus.Network.Packets.Packet (fixedPacket2) + +interfaceNpcChatheadPacket :: Word16 -> NpcDefinitionId -> ByteString +interfaceNpcChatheadPacket interfaceId npcId = + fixedPacket2 + 75 + ( do + putWord16le . fromIntegral $ (npcId `xor` 128) + putWord16le (interfaceId `xor` 128) + ) + +interfacePlayerChatheadPacket :: Word16 -> ByteString +interfacePlayerChatheadPacket interfaceId = + fixedPacket2 + 185 + ( do + putWord16le (interfaceId `xor` 128) + ) diff --git a/app/PotatoCactus/Network/Packets/Out/InterfacePacket.hs b/app/PotatoCactus/Network/Packets/Out/InterfacePacket.hs new file mode 100644 index 0000000..90f5b24 --- /dev/null +++ b/app/PotatoCactus/Network/Packets/Out/InterfacePacket.hs @@ -0,0 +1,14 @@ +module PotatoCactus.Network.Packets.Out.InterfacePacket where + +import Data.Binary (Word16) +import Data.Binary.Put (putWord16be) +import Data.ByteString (ByteString) +import PotatoCactus.Network.Packets.Packet (fixedPacket2) + +interfacePacket :: Word16 -> ByteString +interfacePacket interfaceId = + fixedPacket2 + 97 + ( do + putWord16be interfaceId + ) diff --git a/app/PotatoCactus/Network/Packets/Out/InterfaceTextPacket.hs b/app/PotatoCactus/Network/Packets/Out/InterfaceTextPacket.hs new file mode 100644 index 0000000..93cf504 --- /dev/null +++ b/app/PotatoCactus/Network/Packets/Out/InterfaceTextPacket.hs @@ -0,0 +1,17 @@ +module PotatoCactus.Network.Packets.Out.InterfaceTextPacket where + +import Data.Binary (Word16) +import Data.Binary.Put (putByteString, putWord16be) +import Data.Bits (xor) +import Data.ByteString (ByteString) +import PotatoCactus.Network.Binary (encodeStr) +import PotatoCactus.Network.Packets.Packet (varShortPacket2) + +interfaceTextPacket :: Word16 -> String -> ByteString +interfaceTextPacket interfaceId text = + varShortPacket2 + 126 + ( do + putByteString . encodeStr $ text + putWord16be (interfaceId `xor` 128) + ) diff --git a/app/PotatoCactus/Network/Packets/Out/RemoveGroundItemPacket.hs b/app/PotatoCactus/Network/Packets/Out/RemoveGroundItemPacket.hs new file mode 100644 index 0000000..7761cc7 --- /dev/null +++ b/app/PotatoCactus/Network/Packets/Out/RemoveGroundItemPacket.hs @@ -0,0 +1,21 @@ +module PotatoCactus.Network.Packets.Out.RemoveGroundItemPacket where + +import Data.Binary.Put (putWord16be, putWord8) +import Data.Bits (shiftL, (.&.)) +import Data.ByteString (ByteString) +import PotatoCactus.Client.GroundItemsUpdate.GroundItemsUpdateDiff (GroundItemClientView (itemId)) +import PotatoCactus.Game.Position (GetPosition (getPosition), Position (x, y), localToRefX, localToRefY) +import PotatoCactus.Network.Packets.Packet (fixedPacket2) + +removeGroundItemPacket :: Position -> GroundItemClientView -> ByteString +removeGroundItemPacket refPos item = + fixedPacket2 + 156 + ( let offset = + ( ((x refPos - (x . getPosition $ item)) `shiftL` 4) + + ((y refPos - (y . getPosition $ item)) .&. 7) + ) + in do + putWord8 $ fromIntegral (128 - offset) + putWord16be . fromIntegral . itemId $ item + ) diff --git a/app/PotatoCactus/Network/Packets/Out/WalkableInterfacePacket.hs b/app/PotatoCactus/Network/Packets/Out/WalkableInterfacePacket.hs new file mode 100644 index 0000000..bc6e8f0 --- /dev/null +++ b/app/PotatoCactus/Network/Packets/Out/WalkableInterfacePacket.hs @@ -0,0 +1,14 @@ +module PotatoCactus.Network.Packets.Out.WalkableInterfacePacket where + +import Data.Binary (Word16) +import Data.Binary.Put (putWord16le) +import Data.ByteString (ByteString) +import PotatoCactus.Network.Packets.Packet (fixedPacket2) + +walkableInterfacePacket :: Word16 -> ByteString +walkableInterfacePacket interfaceId = + fixedPacket2 + 208 + ( do + putWord16le interfaceId + ) diff --git a/app/PotatoCactus/Persistence/PlayerRepository.hs b/app/PotatoCactus/Persistence/PlayerRepository.hs index b9750b1..6e1a751 100644 --- a/app/PotatoCactus/Persistence/PlayerRepository.hs +++ b/app/PotatoCactus/Persistence/PlayerRepository.hs @@ -15,7 +15,9 @@ mockPlayer_ :: String -> Player mockPlayer_ username = (Player.create username mockPosition_) {inventory = mockInventory_} mockPosition_ :: Position -mockPosition_ = Position 3222 3218 0 +-- mockPosition_ = Position 3167 3304 0 -- windmill testing +-- mockPosition_ = Position 3165 3307 2 -- windmill top +mockPosition_ = Position 3222 3218 0 -- lumbridge castle courtyard mockInventory_ :: ItemContainer mockInventory_ = @@ -24,6 +26,7 @@ mockInventory_ = [ ItemStack 1115 1, ItemStack 1067 1, ItemStack 1137 1, - ItemStack 1155 1, - ItemStack 617 100000 + ItemStack 1155 1 + -- ItemStack 617 100000 + -- ItemStack 1947 1 ] diff --git a/docs/Scripting API.md b/docs/Scripting API.md index f7798d9..ad68e82 100644 --- a/docs/Scripting API.md +++ b/docs/Scripting API.md @@ -8,23 +8,31 @@ Python scripts is considered *read-only*. Event names and payload types can be imported from `potato_cactus.api.events`. -| Event | Key | Description | -|---------------------------|------------|---------------------------------------------------------------------| -| ServerInitEvent | N/A | Sent on server initialization. Use to spawn entities, objects, etc. | -| NpcEntityTickEvent | `npcId` | Sent every tick for NPCs. Use for AI, movement, etc. | -| ObjectInteractionEvent | `objectId` | Sent on the tick when the player triggers the interaction. | -| NpcInteractionEvent | `npcId` | Sent on the tick when the player triggers the interaction. | -| NpcAttackInteractionEvent | `npcId` | TODO is this still necessary? | -| PlayerAttackEvent | | | -| PlayerCommandEvent | `command` | Sent when a client command is issued. (e.g. `::position` | -| NpcAttackEvent | | | -| NpcDeadEvent | | | +| Event | Key | Description | +|------------------------------|------------|---------------------------------------------------------------------------------------------| +| ServerInitEvent | N/A | Sent on server initialization. Use to spawn entities, objects, etc. | +| NpcEntityTickEvent | `npcId` | Sent every tick for NPCs. Use for AI, movement, etc. | +| ObjectInteractionEvent | `objectId` | Sent on the tick when the player triggers the interaction. | +| ItemOnObjectInteractionEvent | `objectId` | When a player uses an item on a game object. | +| NpcInteractionEvent | `npcId` | Sent on the tick when the player triggers the interaction. | +| NpcAttackInteractionEvent | `npcId` | TODO is this still necessary? | +| PickupItemInteractionEvent | `itemId` | Sent when a user picks up a ground item. Override to prevent default behaviour. | +| PlayerAttackEvent | | | +| PlayerCommandEvent | `command` | Sent when a client command is issued. (e.g. `::position` | +| NpcAttackEvent | | | +| NpcDeadEvent | | | +| DropItemEvent | `itemId` | Sent when a user drops an item from their inventory. Override to prevent default behaviour. | ### Event Handlers To register an event handler, annotate a function with `@EventHandler` supplying the event name and the required key attribute. +For events with a key argument, a default handler can be configured by +passing `"default"` as the required key parameter. The default handler +is **only invoked when no specific handler is found**. + + ```python from potato_cactus import EventHandler, GameEvent from potato_cactus.api.events import NpcInteractionEventPayload @@ -36,15 +44,22 @@ def onNpcInteraction(e: NpcInteractionEventPayload): ## Actions Action constructors are imported from `potato_cactus.api.actions`. -| Action | Description | -|------------------------|-----------------------------------------------------------------------------------------------------------| -| ClearPlayerInteraction | Marks the interaction as complete to prevent further `inProgress` events from being raised. | -| NpcQueueWalk | Queues an NPC movement to a target position. The pathfinding calculation is done by the engine. | -| NpcSetAnimation | Sets the NPC animation. | -| NpcSetForcedChat | Sets a forced chat message for an NPC. | -| SpawnGameObject | Adds a dynamic game object to the world, overriding an existing object of the same type at that position. | -| RemoveGameObject | Adds a "removed" dynamic game object, which can be used to subtract an object from the static object set. | -| ServerPrintMessage | Prints to the server console. For testing. | -| SpawnNpc | Spawns an NPC at a point. | -| SendMessage | Sends a server message to the player's chatbox | +| Action | Description | +|------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| ClearPlayerInteraction | Marks the interaction as complete to prevent further `inProgress` events from being raised. | +| NpcQueueWalk | Queues an NPC movement to a target position. The pathfinding calculation is done by the engine. | +| NpcSetAnimation | Sets the NPC animation. | +| NpcSetForcedChat | Sets a forced chat message for an NPC. | +| SpawnGameObject | Adds a dynamic game object to the world, overriding an existing object of the same type at that position. | +| RemoveGameObject | Adds a "removed" dynamic game object, which can be used to subtract an object from the static object set. | +| ServerPrintMessage | Prints to the server console. For testing. | +| SpawnNpc | Spawns an NPC at a point. | +| SendMessage | Sends a server message to the player's chatbox | +| CreateInterface | Configure a player interface using low-level primitives. See `potato_cactus.api.dto.interface` for supported primitives. | +| ClearStandardInterface | Clear the primary interfaces, as though the player issued an action. | +| SetPlayerEntityData | Write a key/value pair to the player data store. | +| SubtractItem | Deduct an item amount from the player inventory. | +| RemoveItemStack | Remove an item stack from the player inventory **at a specific index**. | +| SpawnGroundItem | Spawn an item on the ground. e.g. items dropped by players. | +| RemoveGroundItem | Removes a ground item at pos. e.g. Player picking up an item. If `removedByPlayer`is specified, will look for items visible to that player and add it to their inventory. | diff --git a/notes/317-definitions.md b/notes/317-definitions.md index 325d7a0..a969cd3 100644 --- a/notes/317-definitions.md +++ b/notes/317-definitions.md @@ -2113,3 +2113,2659 @@ killerwatt 18679 Pure black interface 18681 Black interface that has a ghost go towards the screen ``` + +# NPCs +https://www.rune-server.ee/runescape-development/rs2-server/snippets/245097-npc-id-list.html +``` +//----NpcID-----NpcName-------------------------combat--health +npc = 0 Hans 0 0 +npc = 1 Man 2 0 +npc = 2 Man 2 0 +npc = 3 Man 2 0 +npc = 4 Woman 2 0 +npc = 5 Woman 2 0 +npc = 6 Woman 2 0 +npc = 7 Farmer 7 12 +npc = 8 Thief 16 17 +npc = 9 Guard 21 22 +npc = 10 Guard 22 0 +npc = 11 Tramp 2 0 +npc = 12 Barbarian 7 0 +npc = 13 Wizard 9 0 +npc = 14 Druid 33 0 +npc = 15 Warrior_woman 24 0 +npc = 16 Man 2 0 +npc = 17 Barbarian_woman 8 80 +npc = 18 Al-Kharid_warrior 10 38 +npc = 19 White_Knight 36 0 +npc = 20 Paladin 62 0 +npc = 21 Hero 0 0 +npc = 22 Forester 15 0 +npc = 23 Knight_of_Ardougne 46 0 +npc = 24 Man 2 0 +npc = 25 Woman 2 0 +npc = 26 Knight_of_Ardougne 46 0 +npc = 27 Archer 37 0 +npc = 28 Zoo_keeper 0 0 +npc = 29 Chuck 1 0 +npc = 30 Barman 0 0 +npc = 31 Priest 0 0 +npc = 32 Guard 20 0 +npc = 33 Door_man 0 0 +npc = 34 Watchman 33 0 +npc = 35 Soldier 28 400 +npc = 36 Wyson_the_gardener 0 0 +npc = 37 Sigbert_the_Adventurer 0 0 +npc = 38 Shipyard_worker 11 0 +npc = 39 Shipyard_worker 11 0 +npc = 40 Shark 1 0 +npc = 41 Chicken 1 3 +npc = 42 Sheep 0 0 +npc = 43 Sheep 0 0 +npc = 44 Duck 1 0 +npc = 45 Duck 1 0 +npc = 46 Duckling 1 0 +npc = 47 Rat 1 0 +npc = 48 Oomlie_Bird 46 0 +npc = 49 Hellhound 122 135 +npc = 50 King_black_dragon 276 2600 +npc = 51 Baby_dragon 48 0 +npc = 52 Baby_blue_dragon 48 0 +npc = 53 Red_dragon 152 150 +npc = 54 Black_dragon 227 175 +npc = 55 Blue_dragon 111 125 +npc = 56 Dryad 1 0 +npc = 57 Fairy 0 0 +npc = 58 Shadow_spider 52 0 +npc = 59 Giant_spider 2 0 +npc = 60 Giant_spider 27 0 +npc = 61 Spider 1 0 +npc = 62 Jungle_spider 44 0 +npc = 63 Deadly_red_spider 34 0 +npc = 64 Ice_spider 61 100 +npc = 65 Leprechaun 12 0 +npc = 66 Gnome 1 0 +npc = 67 Gnome 1 0 +npc = 68 Gnome 1 0 +npc = 69 Lizard_man 22 0 +npc = 70 Turael 0 0 +npc = 71 Orc 20 0 +npc = 72 Troll 69 0 +npc = 73 Zombie 13 0 +npc = 74 Zombie 18 0 +npc = 75 Zombie 24 40 +npc = 76 Zombie 25 0 +npc = 77 Summoned_Zombie 13 50 +npc = 78 Giant_bat 27 0 +npc = 79 Death_wing 83 0 +npc = 80 Camel 0 0 +npc = 81 Cow 2 8 +npc = 82 Lesser_demon 82 80 +npc = 83 Greater_demon 92 100 +npc = 84 Black_Demon 172 2000 +npc = 85 Golem 55 0 +npc = 86 Giant_rat 3 5 +npc = 87 Giant_rat 6 10 +npc = 88 Dungeon_rat 12 0 +npc = 89 Unicorn 15 100 +npc = 90 Skeleton 25 100 +npc = 91 Skeleton 21 0 +npc = 92 Skeleton 22 100 +npc = 93 Skeleton 45 0 +npc = 94 Skeleton_Mage 16 0 +npc = 95 Wolf 64 0 +npc = 96 White_wolf 25 0 +npc = 97 White_wolf 38 0 +npc = 98 Dog 0 0 +npc = 99 Guard_dog 44 0 +npc = 100 Goblin 2 0 +npc = 101 Goblin 5 30 +npc = 102 Goblin 13 0 +npc = 103 Ghost 19 0 +npc = 104 Ghost 19 0 +npc = 105 Bear 21 0 +npc = 106 Bear 19 0 +npc = 107 Scorpion 14 0 +npc = 108 Poison_Scorpion 20 0 +npc = 109 Pit_Scorpion 28 0 +npc = 110 Fire_giant 86 0 +npc = 111 Ice_giant 53 2400 +npc = 112 Moss_giant 42 60 +npc = 113 Jogre 145 700 +npc = 114 Ogre 53 60 +npc = 115 Ogre 53 0 +npc = 116 Cyclops 56 0 +npc = 117 Hill_giant 28 0 +npc = 118 Dwarf 10 0 +npc = 119 Chaos_dwarf 48 0 +npc = 120 Dwarf 20 0 +npc = 121 Dwarf 9 0 +npc = 122 Hobgoblin 28 40 +npc = 123 Hobgoblin 42 78 +npc = 124 Earth_warrior 150 91 +npc = 125 Ice_warrior 57 60 +npc = 126 Otherworldly_being 64 0 +npc = 127 Magic_axe 42 0 +npc = 128 Snake 5 0 +npc = 129 Skavid 2 0 +npc = 130 Yeti 58 0 +npc = 131 Penguin 2 0 +npc = 132 Monkey 3 40 +npc = 133 Black_unicorn 27 0 +npc = 134 Poison_spider 64 0 +npc = 135 Mammoth 41 0 +npc = 136 Terrorbird 28 0 +npc = 137 Mounted_terrorbird_gnome 31 0 +npc = 138 Mounted_terrorbird_gnome 49 0 +npc = 139 Entrana_Fire_Bird 2 0 +npc = 140 Souless 18 0 +npc = 141 Big_Wolf 73 0 +npc = 142 Wolf 25 75 +npc = 143 Jungle_Wolf 64 0 +npc = 144 King_Scorpion 32 0 +npc = 145 Ice_warrior 57 0 +npc = 146 Gull 0 0 +npc = 147 Cormorant 0 0 +npc = 148 Albatross 0 0 +npc = 149 Gull 0 0 +npc = 150 Gull 0 0 +npc = 151 Fly_trap 0 0 +npc = 152 Tree 0 0 +npc = 153 Butterfly 0 0 +npc = 154 Butterfly 0 0 +npc = 155 Butterfly 0 0 +npc = 156 Butterfly 0 0 +npc = 157 Butterfly 0 0 +npc = 158 Shadow_warrior 48 0 +npc = 159 Gnome_child 1 0 +npc = 160 Gnome_child 1 0 +npc = 161 Gnome_child 1 0 +npc = 162 Gnome_trainer 0 0 +npc = 163 Gnome_guard 23 0 +npc = 164 Gnome_guard 23 0 +npc = 165 Gnome_shop_keeper 0 0 +npc = 166 Gnome_banker 0 0 +npc = 167 Gnome_baller 0 0 +npc = 168 Gnome_woman 1 0 +npc = 169 Gnome_woman 1 0 +npc = 170 Gnome_pilot 0 0 +npc = 171 Brimstail 0 0 +npc = 172 Dark_wizard 0 0 +npc = 173 Invrigar_the_Necromancer 20 0 +npc = 174 Dark_wizard 7 0 +npc = 175 Mugger 6 8 +npc = 176 Witch 25 0 +npc = 177 Witch 25 0 +npc = 178 Black_Knight 33 0 +npc = 179 Black_Knight 33 0 +npc = 180 Highwayman 5 0 +npc = 181 Chaos_druid 13 0 +npc = 182 Pirate 23 0 +npc = 183 Pirate 23 0 +npc = 184 Pirate 26 0 +npc = 185 Pirate 23 0 +npc = 186 Thug 10 0 +npc = 187 Rogue 15 0 +npc = 188 Monk_of_Zamorak 22 20 +npc = 189 Monk_of_Zamorak 17 20 +npc = 190 Monk_of_Zamorak 45 20 +npc = 191 Tribesman 32 0 +npc = 192 Dark_warrior 8 0 +npc = 193 Chaos_druid_warrior 37 0 +npc = 194 Necromancer 26 0 +npc = 195 Bandit 22 0 +npc = 196 Guard_Bandit 22 0 +npc = 197 Barbarian_guard 8 0 +npc = 198 Guild_master 0 0 +npc = 199 Gunthor_the_brave 29 0 +npc = 200 Lord_Daquarius 68 0 +npc = 201 Jailer 47 0 +npc = 202 Black_Heather 34 0 +npc = 203 Donny_the_lad 34 0 +npc = 204 Speedy_Keith 34 0 +npc = 205 Salarin_the_twisted 70 0 +npc = 206 Dwarf 10 0 +npc = 207 Dwarf_youngster 0 0 +npc = 208 Dwarf_Commander 0 0 +npc = 209 Nulodion 0 2000 +npc = 210 Grail_Maiden 0 0 +npc = 211 Sir_Percival 0 0 +npc = 212 King_Percival 0 0 +npc = 213 Merlin 0 0 +npc = 214 Peasant 0 0 +npc = 215 Peasant 0 0 +npc = 216 High_Priest 0 0 +npc = 217 Crone 0 0 +npc = 218 Galahad 0 0 +npc = 219 Fisherman 0 0 +npc = 220 The_Fisher_King 0 0 +npc = 221 Black_Knight_Titan 120 0 +npc = 222 Monk 5 0 +npc = 223 Brother_Kojo 0 0 +npc = 224 Dungeon_rat 12 0 +npc = 225 Bonzo 0 0 +npc = 226 Sinister_Stranger 0 0 +npc = 227 Morris 0 0 +npc = 228 Big_Dave 0 0 +npc = 229 Joshua 0 0 +npc = 230 Grandpa_Jack 0 0 +npc = 231 Forester 0 0 +npc = 232 Mountain_Dwarf 0 0 +npc = 233 Fishing_spot 0 0 +npc = 234 Fishing_spot 0 0 +npc = 235 Fishing_spot 0 0 +npc = 236 Fishing_spot 0 0 +npc = 237 Renegade_Knight 37 0 +npc = 238 Thrantax_the_Mighty 92 0 +npc = 239 Sir_Lancelot 0 0 +npc = 240 Sir_Gawain 0 0 +npc = 241 Sir_Kay 0 0 +npc = 242 Sir_Bedivere 0 0 +npc = 243 Sir_Tristram 0 0 +npc = 244 Sir_Pelleas 0 0 +npc = 245 Sir_Lucan 0 0 +npc = 246 Sir_Palomedes 0 0 +npc = 247 Sir_Mordred 39 0 +npc = 248 Morgan_Le_Faye 0 0 +npc = 249 Merlin 0 0 +npc = 250 The_Lady_of_the_Lake 0 0 +npc = 251 King_Arthur 0 0 +npc = 252 Beggar 0 0 +npc = 253 Khazard_Guard 23 0 +npc = 254 Khazard_Guard 23 0 +npc = 255 Khazard_Guard 23 0 +npc = 256 Khazard_Guard 23 0 +npc = 257 Khazard_Guard 25 0 +npc = 258 General_Khazard 112 0 +npc = 259 Khazard_barman 0 0 +npc = 260 Kelvin 0 0 +npc = 261 Joe 0 0 +npc = 262 Fightslave 0 0 +npc = 263 Hengrad 0 0 +npc = 264 Lady_Servil 0 0 +npc = 265 Jeremy_Servil 0 0 +npc = 266 Jeremy_Servil 0 0 +npc = 267 Justin_Servil 0 0 +npc = 268 Local 0 0 +npc = 269 Bouncer 137 0 +npc = 270 Khazard_Ogre 63 0 +npc = 271 Khazard_Scorpion 44 0 +npc = 272 Lucien 14 0 +npc = 273 Lucien 0 0 +npc = 274 Guardian_of_Armadyl 45 400 +npc = 275 Guardian_of_Armadyl 43 1000 +npc = 276 Winelda 0 0 +npc = 277 Fire_Warrior_of_Lesarkus 84 0 +npc = 278 Cook 0 0 +npc = 279 Brother_Omad 0 0 +npc = 280 Brother_Cedric 0 0 +npc = 281 Monk 3 0 +npc = 282 Thief 14 0 +npc = 283 Head_Thief 26 0 +npc = 284 Doric 0 0 +npc = 285 Veronica 0 0 +npc = 286 Professor_Oddenstein 0 0 +npc = 287 Ernest 0 0 +npc = 288 Ernest 0 0 +npc = 289 Councillor_Halgrive 0 0 +npc = 290 Doctor_Orbon 0 0 +npc = 291 Farmer_Brumty 0 0 +npc = 292 plaguesheep_1 1 0 +npc = 293 plaguesheep_2 1 0 +npc = 294 plaguesheep_3 1 0 +npc = 295 plaguesheep_4 1 0 +npc = 296 General_Bentnoze 0 0 +npc = 297 General_Wartface 0 0 +npc = 298 Goblin 5 0 +npc = 299 Goblin 5 0 +npc = 300 Sedridor 0 0 +npc = 301 Twig 0 0 +npc = 302 Hadley 0 0 +npc = 303 Gerald 0 0 +npc = 304 Almera 0 0 +npc = 305 Hudon 0 0 +npc = 306 Golrie 0 0 +npc = 307 Hetty 0 0 +npc = 308 Master_fisher 0 0 +npc = 309 Fishing_spot 0 0 +npc = 310 Fishing_spot 0 0 +npc = 311 Fishing_spot 0 0 +npc = 312 Fishing_spot 0 0 +npc = 313 Fishing_spot 0 0 +npc = 314 Fishing_spot 0 0 +npc = 315 Fishing_spot 0 0 +npc = 316 Fishing_spot 0 0 +npc = 317 Fishing_spot 0 0 +npc = 318 Fishing_spot 0 0 +npc = 319 Fishing_spot 0 0 +npc = 320 Fishing_spot 0 0 +npc = 321 Fishing_spot 0 0 +npc = 322 Fishing_spot 0 0 +npc = 323 Fishing_spot 0 0 +npc = 324 Fishing_spot 0 0 +npc = 325 Fishing_spot 0 0 +npc = 326 Fishing_spot 0 0 +npc = 327 Fishing_spot 0 0 +npc = 328 Fishing_spot 0 0 +npc = 329 Fishing_spot 0 0 +npc = 330 Fishing_spot 0 0 +npc = 331 Fishing_spot 0 0 +npc = 332 Fishing_spot 0 0 +npc = 333 Fishing_spot 0 0 +npc = 334 Fishing_spot 0 0 +npc = 335 Elena 0 0 +npc = 336 DeVinci 0 0 +npc = 337 DeVinci 0 0 +npc = 338 Chancy 0 0 +npc = 339 Chancy 0 0 +npc = 340 Hops 0 0 +npc = 341 Hops 0 0 +npc = 342 Guidor's_wife 0 0 +npc = 343 Guidor 0 0 +npc = 344 Guard 0 0 +npc = 345 Guard 0 0 +npc = 346 Guard 0 0 +npc = 347 Mourner 11 0 +npc = 348 Mourner 24 0 +npc = 349 Kilron 0 0 +npc = 350 Omart 0 0 +npc = 351 Man 4 0 +npc = 352 Woman 3 0 +npc = 353 Woman 4 0 +npc = 354 Woman 3 0 +npc = 355 Child 0 0 +npc = 356 Child 0 0 +npc = 357 Mourner 18 0 +npc = 358 Priest 0 0 +npc = 359 Man 0 0 +npc = 360 Woman 4 0 +npc = 361 Woman 12 0 +npc = 362 Woman 3 0 +npc = 363 Woman 14 0 +npc = 364 King_Lathas 0 0 +npc = 365 Paladin 59 0 +npc = 366 Jerico 0 0 +npc = 367 Chemist 0 0 +npc = 368 Guard 0 0 +npc = 369 Mourner 24 0 +npc = 370 Mourner 13 0 +npc = 371 Mourner 12 0 +npc = 372 Mourner 0 0 +npc = 373 Nurse_Sarah 0 0 +npc = 374 Ogre 63 0 +npc = 375 Redbeard_Frank 0 0 +npc = 376 Captain_Tobias 0 0 +npc = 377 Seaman_Lorris 0 0 +npc = 378 Seaman_Thresnor 0 0 +npc = 379 Luthas 0 0 +npc = 380 Customs_officer 0 0 +npc = 381 Captain_Barnaby 0 0 +npc = 382 Dwarf 0 0 +npc = 383 Stankers 0 0 +npc = 384 Barbarian_guard 0 0 +npc = 385 Kharid_Scorpion 0 0 +npc = 386 Kharid_Scorpion 0 0 +npc = 387 Kharid_Scorpion 0 0 +npc = 388 Seer 0 0 +npc = 389 Thormac 0 0 +npc = 390 Big_fish 0 0 +npc = 391 River_troll 14 100 +npc = 392 River_troll 29 0 +npc = 393 River_troll 49 0 +npc = 394 River_troll 79 0 +npc = 395 River_troll 120 0 +npc = 396 River_troll 159 0 +npc = 397 Cow 2 8 +npc = 398 Legends_Guard 0 0 +npc = 399 Legends_Guard 0 0 +npc = 400 Radimus_Erkle 0 0 +npc = 401 Jungle_Forester 0 0 +npc = 402 Jungle_Forester 0 0 +npc = 403 Fishing_spot 0 0 +npc = 404 Fishing_spot 0 0 +npc = 405 Fishing_spot 0 0 +npc = 406 Fishing_spot 0 0 +npc = 407 Strange_plant 0 0 +npc = 408 Strange_plant 0 0 +npc = 409 Genie 0 0 +npc = 410 Mysterious_Old_Man 0 0 +npc = 411 Swarm 0 0 +npc = 412 Bat 6 0 +npc = 413 Rock_Golem 14 0 +npc = 414 Rock_Golem 29 0 +npc = 415 Rock_Golem 49 0 +npc = 416 Rock_Golem 79 0 +npc = 417 Rock_Golem 120 0 +npc = 418 Rock_Golem 159 0 +npc = 419 Zombie 14 0 +npc = 420 Zombie 29 0 +npc = 421 Zombie 49 0 +npc = 422 Zombie 79 0 +npc = 423 Zombie 120 0 +npc = 424 Zombie 159 0 +npc = 425 Shade 14 0 +npc = 426 Shade 29 0 +npc = 427 Shade 49 0 +npc = 428 Shade 79 0 +npc = 429 Shade 120 0 +npc = 430 Shade 159 0 +npc = 431 Watchman 14 0 +npc = 432 Watchman 29 0 +npc = 433 Watchman 49 0 +npc = 434 Watchman 79 0 +npc = 435 Watchman 120 0 +npc = 436 Watchman 159 0 +npc = 437 Cap'n_Izzy_No-Beard 0 0 +npc = 438 Tree_spirit 14 0 +npc = 439 Tree_spirit 29 0 +npc = 440 Tree_spirit 49 0 +npc = 441 Tree_spirit 79 0 +npc = 442 Tree_spirit 120 0 +npc = 443 Tree_spirit 159 0 +npc = 444 Goblin 5 0 +npc = 445 Goblin 5 0 +npc = 446 Giant_rat 3 0 +npc = 447 Jail_guard 26 0 +npc = 448 Jail_guard 26 0 +npc = 449 Jail_guard 26 0 +npc = 450 Gull 0 0 +npc = 451 Gull 0 0 +npc = 452 Seth_Groats 0 0 +npc = 453 Suit_of_armour 19 0 +npc = 454 Sanfew 0 0 +npc = 455 Kaqemeex 0 0 +npc = 456 Father_Aereck 0 0 +npc = 457 Restless_ghost 0 0 +npc = 458 Father_Urhney 0 0 +npc = 459 Skeleton 13 0 +npc = 460 Wizard_Frumscone 0 0 +npc = 461 Magic_Store_owner 0 0 +npc = 462 Wizard_Distentor 0 0 +npc = 463 Murphy 0 0 +npc = 464 Murphy 0 0 +npc = 465 Murphy 0 0 +npc = 466 Murphy 0 0 +npc = 467 Shark 0 0 +npc = 468 Shark 0 0 +npc = 469 King_Bolren 0 0 +npc = 470 Commander_Montai 0 0 +npc = 471 Bolkoy 0 0 +npc = 472 Remsai 0 0 +npc = 473 Elkoy 0 0 +npc = 474 Elkoy 0 0 +npc = 475 Khazard_trooper 19 0 +npc = 476 Khazard_trooper 19 0 +npc = 477 Khazard_warlord 112 1400 +npc = 478 Khazard_commander 48 0 +npc = 479 Gnome_troop 1 0 +npc = 480 Gnome_troop 1 0 +npc = 481 Tracker_gnome_ 1 0 +npc = 482 Tracker_gnome_ 2 0 +npc = 483 Tracker_gnome_ 3 0 +npc = 484 Local_Gnome 0 0 +npc = 485 Local_Gnome 0 0 +npc = 486 Kalron 0 0 +npc = 487 Observatory_assistant 0 0 +npc = 488 Observatory_professor 0 0 +npc = 489 Goblin_guard 42 0 +npc = 490 Observatory_professor 0 0 +npc = 491 Ghost 24 0 +npc = 492 Spirit_of_Scorpius 0 0 +npc = 493 Grave_Scorpion 12 0 +npc = 494 Banker 0 0 +npc = 495 Banker 0 0 +npc = 496 Banker 0 0 +npc = 497 Banker 0 0 +npc = 498 Banker 0 0 +npc = 499 Banker 0 0 +npc = 500 Mosol_Rei 0 0 +npc = 501 Spirit_of_Zadimus 0 0 +npc = 502 Undead_One 68 0 +npc = 503 Undead_One 61 0 +npc = 504 Undead_One 68 0 +npc = 505 Undead_One 73 0 +npc = 506 Rashiliyia 0 0 +npc = 507 Nazastarool 91 0 +npc = 508 Nazastarool 68 0 +npc = 509 Nazastarool 93 800 +npc = 510 Hajedy 0 0 +npc = 511 Vigroy 0 0 +npc = 512 Kaleb_Paramaya 0 0 +npc = 513 Yohnus 0 0 +npc = 514 Seravel 0 0 +npc = 515 Yanni_Salika 0 0 +npc = 516 Obli 0 0 +npc = 517 Fernahei 0 0 +npc = 518 Captain_Shanks 0 0 +npc = 519 Bob 0 0 +npc = 520 Shop_keeper 0 0 +npc = 521 Shop_assistant 0 0 +npc = 522 Shop_keeper 0 0 +npc = 523 Shop_assistant 0 0 +npc = 524 Shop_keeper 0 0 +npc = 525 Shop_assistant 0 0 +npc = 526 Shop_keeper 0 0 +npc = 527 Shop_assistant 0 0 +npc = 528 Shop_keeper 0 0 +npc = 529 Shop_assistant 0 0 +npc = 530 Shop_keeper 0 0 +npc = 531 Shop_assistant 0 0 +npc = 532 Shop_keeper 0 0 +npc = 533 Shop_assistant 0 0 +npc = 534 Fairy_shop_keeper 0 0 +npc = 535 Fairy_shop_assistant 0 2000 +npc = 536 Valaine 0 0 +npc = 537 Scavvo 0 0 +npc = 538 Peksa 0 0 +npc = 539 Silk_trader 0 0 +npc = 540 Gem_trader 0 0 +npc = 541 Zeke 0 0 +npc = 542 Louie_legs 0 0 +npc = 543 Kebab_seller 0 0 +npc = 544 Ranael 0 0 +npc = 545 Dommik 0 0 +npc = 546 Zaff 0 0 +npc = 547 Baraek 0 0 +npc = 548 Thessalia 0 0 +npc = 549 Horvik 0 0 +npc = 550 Lowe 0 0 +npc = 551 Shop_keeper 0 2000 +npc = 552 Shop_assistant 0 0 +npc = 553 Aubury 0 2000 +npc = 554 Fancy_dress_shop_owner 0 0 +npc = 555 Shop_keeper 0 2000 +npc = 556 Grum 0 0 +npc = 557 Wydin 0 0 +npc = 558 Gerrant 0 0 +npc = 559 Brian 0 0 +npc = 560 Jiminua 0 0 +npc = 561 Shop_keeper 0 0 +npc = 562 Candle_maker 0 0 +npc = 563 Arhein 0 0 +npc = 564 Jukat 0 0 +npc = 565 Lunderwin 0 0 +npc = 566 Irksol 0 0 +npc = 567 Fairy 0 0 +npc = 568 Zambo 0 0 +npc = 569 Silver_merchant 0 0 +npc = 570 Gem_merchant 0 0 +npc = 571 Baker 0 0 +npc = 572 Spice_seller 0 0 +npc = 573 Fur_trader 0 0 +npc = 574 Silk_merchant 0 0 +npc = 575 Hickton 0 0 +npc = 576 Harry 0 0 +npc = 577 Cassie 0 0 +npc = 578 Frincos 0 0 +npc = 579 Drogo_dwarf 0 0 +npc = 580 Flynn 0 0 +npc = 581 Wayne 0 0 +npc = 582 Dwarf 0 0 +npc = 583 Betty 0 0 +npc = 584 Herquin 0 0 +npc = 585 Rommik 0 0 +npc = 586 Gaius 0 0 +npc = 587 Jatix 0 0 +npc = 588 Davon 0 0 +npc = 589 Zenesha 0 0 +npc = 590 Aemad 0 0 +npc = 591 Kortan 0 0 +npc = 592 Roachey 0 0 +npc = 593 Frenita 0 0 +npc = 594 Nurmof 0 0 +npc = 595 Tea_seller 0 0 +npc = 596 Fat_Tony 0 0 +npc = 597 Noterazzo 0 0 +npc = 598 Hairdresser 0 0 +npc = 599 Make-over_mage 0 0 +npc = 600 Hudo 0 0 +npc = 601 Rometti 0 0 +npc = 602 Gulluck 0 0 +npc = 603 Heckel_Funch 0 0 +npc = 604 Thurgo 0 0 +npc = 605 Sir_Vyvin 0 0 +npc = 606 Squire 0 0 +npc = 607 Gunnjorn 0 0 +npc = 608 Sir_Amik_Varze 0 0 +npc = 609 Fortress_Guard 20 0 +npc = 610 Black_Knight 0 0 +npc = 611 Witch 0 0 +npc = 612 Greldo 0 0 +npc = 613 Digsite_workman 0 0 +npc = 614 Digsite_workman 0 0 +npc = 615 Student 0 0 +npc = 616 Student 0 0 +npc = 617 Student 0 0 +npc = 618 Examiner 0 0 +npc = 619 Archaeological_expert 0 0 +npc = 620 Panning_guide 0 0 +npc = 621 Gnome_baller 0 0 +npc = 622 Gnome_baller 0 0 +npc = 623 Gnome_baller 0 0 +npc = 624 Gnome_baller 0 0 +npc = 625 Gnome_baller 0 0 +npc = 626 Gnome_baller 0 0 +npc = 627 Gnome_baller 0 0 +npc = 628 Gnome_baller 0 0 +npc = 629 Gnome_baller 0 0 +npc = 630 Gnome_baller 0 0 +npc = 631 Gnome_baller 0 0 +npc = 632 Gnome_baller 0 0 +npc = 633 Gnome_winger 0 0 +npc = 634 Gnome_winger 0 0 +npc = 635 Gnome_ball_referee 0 0 +npc = 636 Cheerleader 0 0 +npc = 637 Juliet 0 0 +npc = 638 Apothecary 0 0 +npc = 639 Romeo 0 0 +npc = 640 Father_Lawrence 0 0 +npc = 641 Tramp 0 0 +npc = 642 Katrine 0 0 +npc = 643 Weaponsmaster 23 0 +npc = 644 Straven 23 0 +npc = 645 Jonny_the_beard 2 0 +npc = 646 Curator 0 0 +npc = 647 Reldo 0 0 +npc = 648 King_Roald 0 0 +npc = 649 Archer 0 0 +npc = 650 Warrior 0 0 +npc = 651 Monk 0 0 +npc = 652 Wizard 0 0 +npc = 653 Fairy_Queen 0 0 +npc = 654 Shamus 0 0 +npc = 655 Tree_spirit 101 0 +npc = 656 Cave_monk 0 0 +npc = 657 Monk_of_Entrana 0 0 +npc = 658 Monk_of_Entrana 0 0 +npc = 659 Party_Pete 0 0 +npc = 660 Knight 0 0 +npc = 661 Megan 0 0 +npc = 662 Lucy 0 0 +npc = 663 Man 0 0 +npc = 664 Dimintheis 0 0 +npc = 665 Boot 0 0 +npc = 666 Caleb 0 0 +npc = 667 Chronozon 170 0 +npc = 668 Johnathon 0 0 +npc = 669 Hazelmere 0 0 +npc = 670 King_Narnode_Shareen 0 0 +npc = 671 Glough 0 0 +npc = 672 Anita 0 0 +npc = 673 Charlie 0 0 +npc = 674 Foreman 23 0 +npc = 675 Shipyard_worker 0 0 +npc = 676 Femi 0 0 +npc = 677 Black_Demon 172 0 +npc = 678 Guard 37 0 +npc = 679 Ranging_Guild_Doorman 0 0 +npc = 680 Leatherworker 0 0 +npc = 681 Weapon_poison_salesman 0 0 +npc = 682 Armour_salesman 0 0 +npc = 683 Bow_and_Arrow_salesman 0 0 +npc = 684 Tower_Advisor 0 0 +npc = 685 Tower_Advisor 0 0 +npc = 686 Tower_Advisor 0 0 +npc = 687 Tower_Advisor 0 0 +npc = 688 Tower_Archer 19 0 +npc = 689 Tower_Archer 34 0 +npc = 690 Tower_Archer 49 0 +npc = 691 Tower_Archer 64 0 +npc = 692 Tribal_Weapon_Salesman 0 0 +npc = 693 Competition_Judge 0 0 +npc = 694 Ticket_Merchant 0 0 +npc = 695 Bailey 0 0 +npc = 696 Caroline 0 0 +npc = 697 Kennith 0 0 +npc = 698 Holgart 0 0 +npc = 699 Holgart 0 0 +npc = 700 Holgart 0 0 +npc = 701 Kent 0 0 +npc = 702 Fisherman 0 0 +npc = 703 Fisherman 0 0 +npc = 704 Fisherman 0 0 +npc = 705 Fisherman 0 0 +npc = 706 Wizard_Mizgog 0 0 +npc = 707 Wizard_Grayzag 41 0 +npc = 708 Imp 2 0 +npc = 709 Imp 3 0 +npc = 710 Alrena 0 0 +npc = 711 Bravek 0 0 +npc = 712 Carla 0 0 +npc = 713 Clerk 0 0 +npc = 714 Edmond 0 0 +npc = 715 Elena 0 0 +npc = 716 Head_mourner 0 0 +npc = 717 Mourner 0 0 +npc = 718 Mourner 0 0 +npc = 719 Mourner 0 0 +npc = 720 Recruiter 0 0 +npc = 721 Ted_Rehnison 0 0 +npc = 722 Martha_Rehnison 0 0 +npc = 723 Billy_Rehnison 0 0 +npc = 724 Milli_Rehnison 0 0 +npc = 725 Jethick 0 0 +npc = 726 Man 0 0 +npc = 727 Man 0 0 +npc = 728 Man 0 0 +npc = 729 Man 0 0 +npc = 730 Man 0 0 +npc = 731 Bartender 0 0 +npc = 732 Bartender 0 0 +npc = 733 Bartender 0 0 +npc = 734 Bartender 0 0 +npc = 735 Bartender 0 0 +npc = 736 Barmaid 0 0 +npc = 737 Bartender 0 0 +npc = 738 Bartender 0 0 +npc = 739 Bartender 0 0 +npc = 740 Trufitus 0 0 +npc = 741 Duke_Horacio 0 0 +npc = 742 Elvarg 83 0 +npc = 743 Ned 0 0 +npc = 744 Klarense 0 0 +npc = 745 Wormbrain 2 0 +npc = 746 Oracle 0 0 +npc = 747 Oziach 0 0 +npc = 748 Giant_rat 6 0 +npc = 749 Ghost 19 90 +npc = 750 Skeleton 22 0 +npc = 751 Zombie 25 0 +npc = 752 Lesser_demon 82 520 +npc = 753 Melzar_the_mad 43 0 +npc = 754 Cabin_Boy_Jenkins 0 0 +npc = 755 Morgan 0 0 +npc = 756 Dr_Harlow 0 0 +npc = 757 Count_Draynor 34 0 +npc = 758 Fred_the_Farmer 0 0 +npc = 759 Gertrude's_cat 0 0 +npc = 760 Kitten 0 0 +npc = 761 Kitten 0 0 +npc = 762 Kitten 0 0 +npc = 763 Kitten 0 0 +npc = 764 Kitten 0 0 +npc = 765 Kitten 0 0 +npc = 766 Kitten 0 0 +npc = 767 Crate 0 0 +npc = 768 Cat 0 0 +npc = 769 Cat 0 0 +npc = 770 Cat 0 0 +npc = 771 Cat 0 0 +npc = 772 Cat 0 0 +npc = 773 Cat 0 0 +npc = 774 Overgrown_cat 0 0 +npc = 775 Overgrown_cat 0 0 +npc = 776 Overgrown_cat 0 0 +npc = 777 Overgrown_cat 0 0 +npc = 778 Overgrown_cat 0 0 +npc = 779 Overgrown_cat 0 0 +npc = 780 Gertrude 0 0 +npc = 781 Shilop 0 0 +npc = 782 Philop 0 0 +npc = 783 Wilough 0 0 +npc = 784 Kanel 0 0 +npc = 785 Civilian 0 0 +npc = 786 Civilian 0 0 +npc = 787 Civilian 0 0 +npc = 788 Garv 0 0 +npc = 789 Grubor 0 0 +npc = 790 Trobert 0 0 +npc = 791 Seth 0 0 +npc = 792 Grip 22 0 +npc = 793 Alfonse_the_waiter 0 0 +npc = 794 Charlie_the_cook 0 0 +npc = 795 Ice_Queen 111 300 +npc = 796 Achietties 0 0 +npc = 797 Helemos 0 0 +npc = 798 Velrak_the_explorer 0 0 +npc = 799 Pirate_Guard 19 0 +npc = 800 Fishing_spot 0 0 +npc = 801 Abbot_Langley 0 0 +npc = 802 Brother_Jered 0 0 +npc = 803 Monk 5 0 +npc = 804 Tanner 0 0 +npc = 805 _Master_crafter 0 0 +npc = 806 Donovan_the_Family_Handyman 0 0 +npc = 807 Pierre 0 0 +npc = 808 Hobbes 0 0 +npc = 809 Louisa 0 0 +npc = 810 Mary 0 0 +npc = 811 Stanford 0 0 +npc = 812 Guard 0 0 +npc = 813 Gossip 0 0 +npc = 814 Anna 0 0 +npc = 815 Bob 0 0 +npc = 816 Carol 0 0 +npc = 817 David 0 0 +npc = 818 Elizabeth 0 0 +npc = 819 Frank 0 0 +npc = 820 Poison_Salesman 0 0 +npc = 821 Sinclair_Guard_dog 1 0 +npc = 822 Ana 0 0 +npc = 823 anabarrel 0 0 +npc = 824 Female_slave 0 0 +npc = 825 Male_slave 0 0 +npc = 826 Escaping_slave 0 0 +npc = 827 Rowdy_slave 10 0 +npc = 828 Mercenary 45 0 +npc = 829 Mercenary 45 0 +npc = 830 Mercenary_Captain 47 0 +npc = 831 Captain_Siad 0 0 +npc = 832 Al_Shabim 0 0 +npc = 833 Bedabin_Nomad 0 0 +npc = 834 Bedabin_Nomad_Guard 0 0 +npc = 835 Irena 0 0 +npc = 836 Shantay 0 0 +npc = 837 Shantay_Guard 22 0 +npc = 838 Shantay_Guard 22 0 +npc = 839 Desert_Wolf 27 0 +npc = 840 Ugthanki 42 0 +npc = 841 Mine_cart_driver 0 0 +npc = 842 Rowdy_Guard 43 0 +npc = 843 RPDT_employee 0 0 +npc = 844 Wizard_Cromperty 0 0 +npc = 845 Horacio 0 0 +npc = 846 Kangai_Mau 0 0 +npc = 847 Head_chef 0 0 +npc = 848 Blurberry 0 0 +npc = 849 Barman 0 0 +npc = 850 Aluft_Gianne 0 0 +npc = 851 Gnome_Waiter 0 0 +npc = 852 Ogre_chieftain 81 0 +npc = 853 Og 0 0 +npc = 854 Grew 0 0 +npc = 855 Toban 0 0 +npc = 856 Gorad 68 0 +npc = 857 Ogre_guard 83 0 +npc = 858 Ogre_guard 83 0 +npc = 859 Ogre_guard 83 0 +npc = 860 Ogre_guard 83 0 +npc = 861 Ogre_guard 83 0 +npc = 862 City_guard 83 0 +npc = 863 Scared_skavid 0 0 +npc = 864 Mad_skavid 0 0 +npc = 865 Skavid 0 0 +npc = 866 Skavid 0 0 +npc = 867 Skavid 0 0 +npc = 868 Skavid 0 0 +npc = 869 Skavid 0 0 +npc = 870 Enclave_guard 83 0 +npc = 871 Ogre_shaman 113 0 +npc = 872 Watchtower_wizard 0 0 +npc = 873 Ogre_trader 70 0 +npc = 874 Ogre_merchant 70 0 +npc = 875 Ogre_trader 70 0 +npc = 876 Ogre_trader 70 0 +npc = 877 Tower_guard 28 0 +npc = 878 Colonel_Radick 38 0 +npc = 879 Delrith 27 0 +npc = 880 Weakened_Delrith 1 0 +npc = 881 Traiborn 0 0 +npc = 882 Gypsy 0 0 +npc = 883 Sir_Prysin 0 0 +npc = 884 Captain_Rovin 0 0 +npc = 885 Ceril_Carnillean 0 0 +npc = 886 Claus_the_chef 0 0 +npc = 887 Guard 0 0 +npc = 888 Philipe_Carnillean 0 0 +npc = 889 Henryeta_Carnillean 0 0 +npc = 890 Butler_Jones 0 0 +npc = 891 Alomone 13 0 +npc = 892 Hazeel 296 0 +npc = 893 Clivet 13 0 +npc = 894 Hazeel_Cultist 13 0 +npc = 895 Boy 0 0 +npc = 896 Nora_T._Hagg 0 0 +npc = 897 Witches_experiment 19 0 +npc = 898 Witches_experiment_second_form 30 0 +npc = 899 Witches_experiment_third_form 42 0 +npc = 900 Witches_experiment_fourth_form 53 0 +npc = 901 Mouse 0 0 +npc = 902 Gundai 0 0 +npc = 903 Lundail 0 0 +npc = 904 Chamber_guardian 0 0 +npc = 905 Kolodion 0 0 +npc = 906 Kolodion 0 0 +npc = 907 Kolodion 0 0 +npc = 908 Kolodion 0 0 +npc = 909 Kolodion 0 0 +npc = 910 Kolodion 0 0 +npc = 911 Kolodion 112 0 +npc = 912 Battle_mage 54 200 +npc = 913 Battle_mage 54 200 +npc = 914 Battle_mage 54 200 +npc = 915 Leela 0 0 +npc = 916 Joe 0 0 +npc = 917 Jail_guard 26 0 +npc = 918 Ned 0 0 +npc = 919 Lady_Keli 0 0 +npc = 920 Prince_Ali 0 0 +npc = 921 Prince_Ali 0 0 +npc = 922 Aggie 0 0 +npc = 923 Hassan 0 0 +npc = 924 Osman 0 0 +npc = 925 Border_Guard 0 0 +npc = 926 Border_Guard 0 0 +npc = 927 Fishing_spot 0 0 +npc = 928 Gujuo 0 0 +npc = 929 Ungadulu 70 0 +npc = 930 Ungadulu 169 0 +npc = 931 Jungle_Savage 90 0 +npc = 932 Fionella 0 0 +npc = 933 Siegfried_Erkle 0 0 +npc = 934 Nezikchened 187 0 +npc = 935 Viyeldi 79 0 +npc = 936 San_Tojalon 106 0 +npc = 937 Irvig_Senay 100 0 +npc = 938 Ranalph_Devere 92 0 +npc = 939 Boulder 0 0 +npc = 940 Echned_Zekin 187 0 +npc = 941 Green_dragon 79 100 +npc = 942 Master_Chef 0 0 +npc = 943 Survival_Expert 0 0 +npc = 944 Combat_Instructor 146 0 +npc = 945 RuneScape_Guide 0 0 +npc = 946 Magic_Instructor 0 0 +npc = 947 Financial_Advisor 0 0 +npc = 948 Mining_Instructor 0 0 +npc = 949 Quest_Guide 0 0 +npc = 950 Giant_rat 3 0 +npc = 951 Chicken 3 0 +npc = 952 Fishing_spot 0 0 +npc = 953 Banker 0 0 +npc = 954 Brother_Brace 0 0 +npc = 955 Cow 2 0 +npc = 956 Drunken_Dwarf 0 0 +npc = 957 Mubariz 0 0 +npc = 958 Fadli 0 0 +npc = 959 A'abla 0 0 +npc = 960 Sabreen 0 0 +npc = 961 Tafani 0 0 +npc = 962 Jaraah 0 0 +npc = 963 Zahwa 0 0 +npc = 964 Ima 0 0 +npc = 965 Sabeil 0 0 +npc = 966 Jadid 0 0 +npc = 967 Dalal 0 0 +npc = 968 Afrah 0 0 +npc = 969 Jeed 0 0 +npc = 970 Diango 0 0 +npc = 971 Chadwell 0 0 +npc = 972 Koftik 0 0 +npc = 973 Koftik 0 0 +npc = 974 Koftik 0 0 +npc = 975 Koftik 0 0 +npc = 976 Koftik 0 0 +npc = 977 Blessed_spider 39 0 +npc = 978 Blessed_Giant_rat 9 0 +npc = 979 Slave 0 0 +npc = 980 Slave 0 0 +npc = 981 Slave 0 0 +npc = 982 Slave 0 0 +npc = 983 Slave 0 0 +npc = 984 Slave 0 0 +npc = 985 Slave 0 0 +npc = 986 Boulder 0 0 +npc = 987 Unicorn 15 0 +npc = 988 Sir_Jerro 62 0 +npc = 989 Sir_Carl 62 0 +npc = 990 Sir_Harry 62 0 +npc = 991 Half-Souless 1 0 +npc = 992 Kardia 0 0 +npc = 993 Witch's_cat 0 0 +npc = 994 Niloof 0 0 +npc = 995 Klank 0 0 +npc = 996 Kamen 0 0 +npc = 997 Kalrag 89 0 +npc = 998 Othainian 91 0 +npc = 999 Doomion 91 0 +npc = 1000 Holthion 91 0 +npc = 1001 Dark_mage 0 0 +npc = 1002 Iban_disciple 13 0 +npc = 1003 Lord_Iban 0 0 +npc = 1004 Spider 1 0 +npc = 1005 Giant_bat 0 0 +npc = 1006 Sea_slug 0 0 +npc = 1007 Zamorak_Wizard 65 400 +npc = 1008 Hamid 0 0 +npc = 1009 Poison_spider 31 0 +npc = 1010 Rantz 0 0 +npc = 1011 Fycie 0 0 +npc = 1012 Bugs 0 0 +npc = 1013 Swamp_toad 0 0 +npc = 1014 Bloated_Toad 0 0 +npc = 1015 Chompy_bird 6 0 +npc = 1016 Chompy_bird 0 0 +npc = 1017 Chicken 1 0 +npc = 1018 Rooster 3 0 +npc = 1019 Fire_elemental 150 400 +npc = 1020 Earth_elemental 150 400 +npc = 1021 Air_elemental 150 400 +npc = 1022 Water_elemental 150 400 +npc = 1023 Earth_elemental 150 400 +npc = 1024 Man 24 0 +npc = 1025 Man 24 0 +npc = 1026 Man 24 0 +npc = 1027 Woman 24 0 +npc = 1028 Woman 24 0 +npc = 1029 Woman 24 0 +npc = 1030 Wolfman 88 0 +npc = 1031 Wolfman 88 0 +npc = 1032 Wolfman 88 0 +npc = 1033 Wolfwoman 88 0 +npc = 1034 Wolfwoman 88 0 +npc = 1035 Wolfwoman 88 0 +npc = 1036 Banker 0 0 +npc = 1037 Man 0 0 +npc = 1038 Rufus 0 0 +npc = 1039 Barker 0 0 +npc = 1040 Fidelio 0 0 +npc = 1041 Sbott 0 0 +npc = 1042 Roavar 0 0 +npc = 1043 Will_o'_the_wisp 0 0 +npc = 1044 Monk_of_Zamorak 22 0 +npc = 1045 Monk_of_Zamorak 17 0 +npc = 1046 Monk_of_Zamorak 30 0 +npc = 1047 Temple_guardian 30 0 +npc = 1048 Drezel 0 0 +npc = 1049 Drezel 0 0 +npc = 1050 Filliman_Tarlock 0 0 +npc = 1051 Nature_Spirit 0 0 +npc = 1052 Ghast 0 0 +npc = 1053 Ghast 30 0 +npc = 1054 Ulizius 0 0 +npc = 1055 Pirate_Jackie_the_Fruit 0 0 +npc = 1056 Mime 0 0 +npc = 1057 Strange_watcher 0 0 +npc = 1058 Strange_watcher 0 0 +npc = 1059 Strange_watcher 0 0 +npc = 1060 Denulth 0 0 +npc = 1061 Sergeant 0 0 +npc = 1062 Sergeant 0 0 +npc = 1063 Soldier 0 0 +npc = 1064 Soldier 0 0 +npc = 1065 Soldier 48 0 +npc = 1066 Soldier 0 0 +npc = 1067 Soldier 0 0 +npc = 1068 Soldier 0 0 +npc = 1069 Soldier 0 0 +npc = 1070 Saba 0 0 +npc = 1071 Tenzing 0 0 +npc = 1072 Eadburg 4 0 +npc = 1073 Archer 42 0 +npc = 1074 Archer 42 0 +npc = 1075 Archer 42 0 +npc = 1076 Guard 37 0 +npc = 1077 Guard 37 0 +npc = 1078 Harold 0 0 +npc = 1079 Tostig 0 0 +npc = 1080 Eohric 0 0 +npc = 1081 Servant 5 0 +npc = 1082 Dunstan 0 0 +npc = 1083 Wistan 0 0 +npc = 1084 Breoca 5 0 +npc = 1085 Ocga 5 0 +npc = 1086 Unferth 6 0 +npc = 1087 Penda 5 0 +npc = 1088 Hygd 4 0 +npc = 1089 Ceolburg 4 0 +npc = 1090 Hild 4 0 +npc = 1091 Bob 0 0 +npc = 1092 White_Knight 36 0 +npc = 1093 Billy 0 0 +npc = 1094 Mountain_Goat 0 0 +npc = 1095 Rock 111 0 +npc = 1096 Stick 104 0 +npc = 1097 Pee_Hat 91 0 +npc = 1098 Kraka 91 0 +npc = 1099 Dung 0 0 +npc = 1100 Ash 0 0 +npc = 1101 Thrower_Troll 67 0 +npc = 1102 Thrower_Troll 67 0 +npc = 1103 Thrower_Troll 67 0 +npc = 1104 Thrower_Troll 67 0 +npc = 1105 Thrower_Troll 67 0 +npc = 1106 Mountain_Troll 69 0 +npc = 1107 Mountain_Troll 69 0 +npc = 1108 Mountain_Troll 69 0 +npc = 1109 Mountain_Troll 69 0 +npc = 1110 Mountain_Troll 69 0 +npc = 1111 Mountain_Troll 69 0 +npc = 1112 Mountain_Troll 69 0 +npc = 1113 Eadgar 0 0 +npc = 1114 Godric 0 0 +npc = 1115 Troll_General 113 0 +npc = 1116 Troll_General 113 0 +npc = 1117 Troll_General 113 0 +npc = 1118 Troll_Spectator 71 0 +npc = 1119 Troll_Spectator 71 0 +npc = 1120 Troll_Spectator 71 0 +npc = 1121 Troll_Spectator 71 0 +npc = 1122 Troll_Spectator 71 0 +npc = 1123 Troll_Spectator 71 0 +npc = 1124 Troll_Spectator 71 0 +npc = 1125 Dad 101 0 +npc = 1126 Twig 71 0 +npc = 1127 Berry 71 0 +npc = 1128 Twig 71 0 +npc = 1129 Berry 71 0 +npc = 1130 Thrower_Troll 68 0 +npc = 1131 Thrower_Troll 68 0 +npc = 1132 Thrower_Troll 68 0 +npc = 1133 Thrower_Troll 68 0 +npc = 1134 Thrower_Troll 68 0 +npc = 1135 Cook 0 0 +npc = 1136 Cook 0 0 +npc = 1137 Cook 0 0 +npc = 1138 Mountain_Troll 71 0 +npc = 1139 Mushroom 0 0 +npc = 1140 Mountain_Goat 0 0 +npc = 1141 Mountain_Goat 0 0 +npc = 1142 Guard 0 0 +npc = 1143 Guard 0 0 +npc = 1144 Guard 0 0 +npc = 1145 Guard 0 0 +npc = 1146 Guard 0 0 +npc = 1147 Guard 0 0 +npc = 1148 Guard 0 0 +npc = 1149 Guard 0 0 +npc = 1150 Guard 0 0 +npc = 1151 Burntmeat 0 0 +npc = 1152 Weird_Old_Man 0 0 +npc = 1153 Kalphite_Worker 28 0 +npc = 1154 Kalphite_Soldier 85 0 +npc = 1155 Kalphite_Guardian 141 0 +npc = 1156 Kalphite_Worker 28 0 +npc = 1157 Kalphite_Guardian 141 0 +npc = 1158 Kalphite_Queen 333 800 +npc = 1159 Kalphite_Queen 333 1100 +npc = 1160 Kalphite_Queen 333 1100 +npc = 1161 Kalphite_Larva 0 0 +npc = 1162 Timfraku 0 0 +npc = 1163 Tiadeche 0 0 +npc = 1164 Tiadeche 0 0 +npc = 1165 Tinsay 0 0 +npc = 1166 Tinsay 0 0 +npc = 1167 Tamayu 0 0 +npc = 1168 Tamayu 0 0 +npc = 1169 Tamayu 0 0 +npc = 1170 Tamayu 0 0 +npc = 1171 Lubufu 0 0 +npc = 1172 The_Shaikahan 83 260 +npc = 1173 The_Shaikahan 83 0 +npc = 1174 Fishing_spot 0 0 +npc = 1175 Fishing_spot 0 0 +npc = 1176 Fishing_spot 0 0 +npc = 1177 Fishing_spot 0 0 +npc = 1178 Fishing_spot 0 0 +npc = 1179 Gull 0 0 +npc = 1180 Cormorant 0 0 +npc = 1181 Albatross 0 0 +npc = 1182 Lord_Iorwerth 0 0 +npc = 1183 Elf_warrior 90 0 +npc = 1184 Elf_warrior 108 0 +npc = 1185 Elven_city_guard 0 0 +npc = 1186 Idris 0 0 +npc = 1187 Essyllt 0 0 +npc = 1188 Morvran 0 0 +npc = 1189 Fishing_spot 0 0 +npc = 1190 Fishing_spot 0 0 +npc = 1191 Fishing_spot 0 0 +npc = 1192 Rabbit 2 0 +npc = 1193 Rabbit 2 0 +npc = 1194 Rabbit 2 0 +npc = 1195 Grizzly_bear 42 0 +npc = 1196 Grizzly_bear_cub 33 0 +npc = 1197 Grizzly_bear_cub 36 0 +npc = 1198 Dire_Wolf 88 0 +npc = 1199 Elf_Tracker 0 0 +npc = 1200 Tyras_guard 110 0 +npc = 1201 Elf_warrior 0 0 +npc = 1202 Arianwyn 0 0 +npc = 1203 Tyras_guard 110 0 +npc = 1204 Tyras_guard 110 0 +npc = 1205 Tyras_guard 0 0 +npc = 1206 Tyras_guard 0 0 +npc = 1207 General_Hining 0 0 +npc = 1208 Quartermaster 0 0 +npc = 1209 Koftik 0 0 +npc = 1210 Kings_messenger 0 0 +npc = 1211 Will_o'_the_wisp 0 0 +npc = 1212 Will_o'_the_wisp 0 0 +npc = 1213 Tegid 0 0 +npc = 1214 Thistle 0 0 +npc = 1215 Parrots 0 0 +npc = 1216 Parroty_Pete 0 0 +npc = 1217 Gardener 3 0 +npc = 1218 Ghoul 42 0 +npc = 1219 Leech 52 0 +npc = 1220 Vampire 72 0 +npc = 1221 Spider 0 0 +npc = 1222 vampire_misty 1 0 +npc = 1223 Vampire 61 0 +npc = 1224 vampire_juve_hound 1 0 +npc = 1225 vampire_count 25 0 +npc = 1226 Tree 0 0 +npc = 1227 Myre_Blamish_Snail 9 0 +npc = 1228 Blood_Blamish_Snail 20 0 +npc = 1229 Ochre_Blamish_Snail 10 0 +npc = 1230 Bruise_Blamish_Snail 20 0 +npc = 1231 Bark_Blamish_Snail 15 0 +npc = 1232 Myre_Blamish_Snail 10 0 +npc = 1233 Blood_Blamish_Snail 20 0 +npc = 1234 Ochre_Blamish_Snail 15 0 +npc = 1235 Bruise_Blamish_Snail 20 0 +npc = 1236 Fishing_spot 0 0 +npc = 1237 Fishing_spot 0 0 +npc = 1238 Fishing_spot 0 0 +npc = 1239 Bedabin_Nomad_Fighter 56 0 +npc = 1240 Loar_Shadow 40 0 +npc = 1241 Loar_Shade 40 150 +npc = 1242 Shade_Spirit 0 0 +npc = 1243 Phrin_Shadow 60 0 +npc = 1244 Phrin_Shade 60 0 +npc = 1245 Riyl_Shadow 80 0 +npc = 1246 Riyl_Shade 80 240 +npc = 1247 Asyn_Shadow 100 0 +npc = 1248 Asyn_Shade 100 0 +npc = 1249 Fiyr_Shadow 120 0 +npc = 1250 Fiyr_Shade 120 0 +npc = 1251 Afflicted(Ulsquire) 0 0 +npc = 1252 Ulsquire_Shauncy 0 0 +npc = 1253 Afflicted(Razmire) 0 0 +npc = 1254 Razmire_Keelgan 0 0 +npc = 1255 Mort'ton_Local 0 0 +npc = 1256 Mort'ton_Local 0 0 +npc = 1257 Afflicted 37 0 +npc = 1258 Afflicted 34 0 +npc = 1259 Mort'ton_local 0 0 +npc = 1260 Mort'ton_local 0 0 +npc = 1261 Afflicted 32 0 +npc = 1262 Afflicted 30 0 +npc = 1263 Wizard 0 0 +npc = 1264 Saradomin_Wizard 108 400 +npc = 1265 Rock_Crab 200 5000 +npc = 1266 Rocks 0 50 +npc = 1267 Rock_Crab 13 50 +npc = 1268 Rocks 0 50 +npc = 1269 Olaf_the_Bard 0 0 +npc = 1270 Lalli 0 0 +npc = 1271 Golden_sheep 0 0 +npc = 1272 Golden_sheep 0 0 +npc = 1273 Fossegrimen 0 0 +npc = 1274 Ospak 0 0 +npc = 1275 Styrmir 0 0 +npc = 1276 Torbrund 0 0 +npc = 1277 Fridgeir 0 0 +npc = 1278 Longhall_Bouncer 0 0 +npc = 1279 The_Draugen 69 0 +npc = 1280 Butterfly 0 0 +npc = 1281 Sigli_the_Huntsman 0 0 +npc = 1282 Sigmund_The_Merchant 0 0 +npc = 1283 Swensen_the_Navigator 0 0 +npc = 1284 Bjorn 0 0 +npc = 1285 Eldgrim 0 0 +npc = 1286 Manni_the_Reveller 0 0 +npc = 1287 Council_workman 0 0 +npc = 1288 Peer_the_Seer 0 0 +npc = 1289 Thorvald_the_Warrior 0 0 +npc = 1290 Koschei_the_deathless 0 0 +npc = 1291 Koschei_the_deathless 0 0 +npc = 1292 Koschei_the_deathless 0 0 +npc = 1293 Koschei_the_deathless 0 0 +npc = 1294 Brundt_the_Chieftain 0 0 +npc = 1295 Askeladden 0 0 +npc = 1296 Guard 0 0 +npc = 1297 Guard 0 0 +npc = 1298 Town_Guard 0 0 +npc = 1299 Town_Guard 0 0 +npc = 1300 Thora_the_Barkeep 0 0 +npc = 1301 Yrsa 0 0 +npc = 1302 Fisherman 0 0 +npc = 1303 Skulgrimen 0 0 +npc = 1304 Sailor 0 0 +npc = 1305 Agnar 48 0 +npc = 1306 Freidir 48 0 +npc = 1307 Borrokar 48 0 +npc = 1308 Lanzig 48 0 +npc = 1309 Pontak 48 0 +npc = 1310 Freygerd 48 0 +npc = 1311 Lensa 48 0 +npc = 1312 Jennella 48 0 +npc = 1313 Sassilik 48 0 +npc = 1314 Inga 48 0 +npc = 1315 Fish_monger 0 0 +npc = 1316 Fur_trader 0 0 +npc = 1317 Market_Guard 48 0 +npc = 1318 Warrior 48 0 +npc = 1319 Fox 0 0 +npc = 1320 Bunny 2 0 +npc = 1321 Bunny 2 0 +npc = 1322 Gull 0 0 +npc = 1323 Gull 0 0 +npc = 1324 Gull 0 0 +npc = 1325 Gull 0 0 +npc = 1326 Bear_Cub 15 0 +npc = 1327 Bear_Cub 15 0 +npc = 1328 Unicorn_Foal 12 0 +npc = 1329 Black_unicorn_Foal 22 0 +npc = 1330 Wolf 64 0 +npc = 1331 Fishing_spot 0 0 +npc = 1332 Fishing_spot 0 0 +npc = 1333 Fishing_spot 0 0 +npc = 1334 Jossik 0 0 +npc = 1335 Jossik 0 0 +npc = 1336 Larrissa 0 0 +npc = 1337 Larrissa 0 0 +npc = 1338 Dagannoth 74 0 +npc = 1339 Dagannoth 74 0 +npc = 1340 Dagannoth 74 0 +npc = 1341 Dagannoth 92 0 +npc = 1342 Dagannoth 92 0 +npc = 1343 Dagannoth 92 0 +npc = 1344 Dagannoth 100 0 +npc = 1345 Dagannoth 100 0 +npc = 1346 Dagannoth 100 0 +npc = 1347 Dagannoth 100 0 +npc = 1348 Dagannoth_mother 100 0 +npc = 1349 Dagannoth_mother 100 0 +npc = 1350 Dagannoth_mother 100 0 +npc = 1351 Dagannoth_mother 100 0 +npc = 1352 Dagannoth_mother 100 0 +npc = 1353 Dagannoth_mother 100 0 +npc = 1354 Dagannoth_mother 100 0 +npc = 1355 Dagannoth_mother 100 0 +npc = 1356 Dagannoth_mother 100 0 +npc = 1357 Sam 0 0 +npc = 1358 Rachael 0 0 +npc = 1359 Queen_Sigrid 0 0 +npc = 1360 Banker 0 0 +npc = 1361 Arnor 1 0 +npc = 1362 Haming 1 0 +npc = 1363 Moldof 1 0 +npc = 1364 Helga 1 0 +npc = 1365 Matilda 1 0 +npc = 1366 Ashild 1 0 +npc = 1367 Skraeling 2 0 +npc = 1368 Skraeling 2 0 +npc = 1369 Fishmonger 0 0 +npc = 1370 Greengrocer 0 0 +npc = 1371 Prince_Brand 0 0 +npc = 1372 Princess_Astrid 0 0 +npc = 1373 King_Vargas 0 0 +npc = 1374 Guard 0 0 +npc = 1375 Advisor_Ghrim 0 0 +npc = 1376 Derrik 0 0 +npc = 1377 Farmer 0 0 +npc = 1378 Flower_Girl 0 0 +npc = 1379 Ragnar 1 0 +npc = 1380 Einar 1 0 +npc = 1381 Alrik 1 0 +npc = 1382 Thorhild 1 0 +npc = 1383 Halla 1 0 +npc = 1384 Yrsa 1 0 +npc = 1385 Sailor 0 0 +npc = 1386 Rannveig 2 0 +npc = 1387 Thora 2 0 +npc = 1388 Valgerd 2 0 +npc = 1389 Skraeling 2 0 +npc = 1390 Broddi 2 0 +npc = 1391 Skraeling 2 0 +npc = 1392 Ragnvald 2 0 +npc = 1393 Fishmonger 0 0 +npc = 1394 Greengrocer 0 0 +npc = 1395 Lumberjack_Leif 0 0 +npc = 1396 Miner_Magnus 0 0 +npc = 1397 Fisherman_Frodi 0 0 +npc = 1398 Gardener_Gunnhild 0 0 +npc = 1399 Fishing_spot 0 0 +npc = 1400 Gull 0 0 +npc = 1401 Chicken 1 0 +npc = 1402 Chicken 1 0 +npc = 1403 Rooster 2 0 +npc = 1404 Rabbit 2 0 +npc = 1405 Fishing_spot 0 0 +npc = 1406 Fishing_spot 0 0 +npc = 1407 Daero 0 0 +npc = 1408 Waydar 0 0 +npc = 1409 Waydar 0 0 +npc = 1410 Waydar 0 0 +npc = 1411 Garkor 0 0 +npc = 1412 Garkor 0 0 +npc = 1413 Lumo 0 0 +npc = 1414 Lumo 0 0 +npc = 1415 Bunkdo 0 0 +npc = 1416 Bunkdo 0 0 +npc = 1417 Carado 0 0 +npc = 1418 Carado 0 0 +npc = 1419 Lumdo 0 0 +npc = 1420 Karam 0 0 +npc = 1421 Karam 0 0 +npc = 1422 Karam 0 0 +npc = 1423 Bunkwicket 0 0 +npc = 1424 Waymottin 0 0 +npc = 1425 Zooknock 0 0 +npc = 1426 Zooknock 0 0 +npc = 1427 G.L.O._Caranock 0 0 +npc = 1428 G.L.O._Caranock 0 0 +npc = 1429 Dugopul 0 0 +npc = 1430 Salenab 0 0 +npc = 1431 Trefaji 0 0 +npc = 1432 Aberab 0 0 +npc = 1433 Solihib 0 0 +npc = 1434 Daga 0 0 +npc = 1435 Tutab 0 0 +npc = 1436 Ifaba 0 0 +npc = 1437 Hamab 0 0 +npc = 1438 Hafuba 0 0 +npc = 1439 Denadu 0 0 +npc = 1440 Lofu 0 0 +npc = 1441 Kruk 149 0 +npc = 1442 Duke 149 0 +npc = 1443 Oipuis 149 0 +npc = 1444 Uyoro 149 0 +npc = 1445 Ouhai 149 0 +npc = 1446 Uodai 149 0 +npc = 1447 Padulah 149 0 +npc = 1448 Awowogei 0 0 +npc = 1449 Uwogo 0 0 +npc = 1450 Muruwoi 0 0 +npc = 1451 Sleeping_Monkey 0 0 +npc = 1452 Monkey_Child 0 0 +npc = 1453 The_Monkey's_Uncle 0 0 +npc = 1454 The_Monkey's_Aunt 0 0 +npc = 1455 Monkey_Guard 149 117 +npc = 1456 Monkey_Archer 86 0 +npc = 1457 Monkey_Archer 86 90 +npc = 1458 Monkey_Archer 86 90 +npc = 1459 Monkey_Guard 167 150 +npc = 1460 Monkey_Guard 167 150 +npc = 1461 Elder_Guard 0 0 +npc = 1462 Elder_Guard 0 0 +npc = 1463 Monkey 3 30 +npc = 1464 Monkey 3 0 +npc = 1465 Monkey_Zombie 98 90 +npc = 1466 Monkey_Zombie 129 90 +npc = 1467 Monkey_Zombie 82 90 +npc = 1468 Bonzara 0 0 +npc = 1469 Monkey_Minder 0 0 +npc = 1470 Foreman 0 0 +npc = 1471 Skeleton 142 0 +npc = 1472 Jungle_Demon 195 0 +npc = 1473 Spider 1 0 +npc = 1474 Spider 1 0 +npc = 1475 Bird 11 0 +npc = 1476 Bird 5 0 +npc = 1477 Scorpion 38 0 +npc = 1478 Jungle_spider 37 0 +npc = 1479 Snake 24 0 +npc = 1480 mm_transmogrification_small_ninja_monkey 1 0 +npc = 1481 mm_transmogrification_medium_ninja_monkey 1 0 +npc = 1482 mm_transmogrification_normal_gorilla 1 0 +npc = 1483 mm_transmogrification_bearded_gorilla 1 0 +npc = 1484 mm_transmogrification_ancient_monkey 1 0 +npc = 1485 mm_transmogrification_small_zombie_monkey 1 0 +npc = 1486 mm_transmogrification_large_zombie_monkey 1 0 +npc = 1487 mm_transmogrification_normal_monkey 1 0 +npc = 1488 toms_food_monkey 1 0 +npc = 1489 toms__general_store_monkey 1 0 +npc = 1490 toms_sword_monkey 1 0 +npc = 1491 toms_magic_monkey 1 0 +npc = 1492 toms_crafting_monkey 1 0 +npc = 1493 toms_basic_monkey 1 0 +npc = 1494 toms_zombie_monkey 1 0 +npc = 1495 toms_zombie_monkey_small 1 0 +npc = 1496 toms_female_monkey 1 0 +npc = 1497 toms_uncle_monkey 1 0 +npc = 1498 toms_caretaker 1 0 +npc = 1499 toms_jail_guard 1 0 +npc = 1500 toms_adviser 1 0 +npc = 1501 toms_child 1 0 +npc = 1502 toms_ninja_guard 35 0 +npc = 1503 toms_monkey_archer 61 0 +npc = 1504 toms_ninja_guard_captin 35 0 +npc = 1505 toms_gorilla 35 0 +npc = 1506 toms_gorilla_pound 35 0 +npc = 1507 toms_gorilla_beard 35 0 +npc = 1508 toms_gorilla_priest 35 0 +npc = 1509 toms_gorilla_guard 35 0 +npc = 1510 toms_acolyte 1 0 +npc = 1511 toms_gnome_assassin 9 0 +npc = 1512 toms_gnome_sapper 1 0 +npc = 1513 toms_gnome_mage 1 0 +npc = 1514 toms_ape_ruler 1 0 +npc = 1515 toms_skeleton 18 0 +npc = 1516 demo 1 0 +npc = 1517 toms_gnome_troop1 1 0 +npc = 1518 toms_gnome_troop2 1 0 +npc = 1519 toms_gnome_troop3 1 0 +npc = 1520 toms_gnome_troop4 1 0 +npc = 1521 Bird 1 0 +npc = 1522 Bird 1 0 +npc = 1523 King_Scorpion 1 0 +npc = 1524 Jungle_spider 1 0 +npc = 1525 toms_jungle_snake 1 0 +npc = 1526 Lanthus 0 0 +npc = 1527 Mine_cart 0 0 +npc = 1528 Zealot 0 0 +npc = 1529 Sheep 1 0 +npc = 1530 Rabbit 1 0 +npc = 1531 Imp 1 0 +npc = 1532 Barricade 0 0 +npc = 1533 Barricade 0 0 +npc = 1534 Barricade 0 0 +npc = 1535 Barricade 0 0 +npc = 1536 Possessed_Pickaxe 50 0 +npc = 1537 Bronze_pickaxe 0 0 +npc = 1538 Corpse 0 0 +npc = 1539 Skeletal_miner 42 0 +npc = 1540 Treus_Dayth 95 0 +npc = 1541 Ghost 99 0 +npc = 1542 Loading_crane 0 0 +npc = 1543 Innocent_looking_key 0 0 +npc = 1544 Mine_cart 0 0 +npc = 1545 Mine_cart 0 0 +npc = 1546 Mine_cart 0 0 +npc = 1547 Mine_cart 0 0 +npc = 1548 Mine_cart 0 0 +npc = 1549 Ghost 29 0 +npc = 1550 Haze 0 0 +npc = 1551 Mischievous_Ghost 0 0 +npc = 1552 Santa 0 2000 +npc = 1553 Ug 0 0 +npc = 1554 Aga 0 0 +npc = 1555 Arrg 113 0 +npc = 1556 Arrg 113 0 +npc = 1557 Ug 1 0 +npc = 1558 Ice_wolf 96 0 +npc = 1559 Ice_wolf 132 0 +npc = 1560 Ice_Troll 124 0 +npc = 1561 Ice_Troll 123 0 +npc = 1562 Ice_Troll 120 0 +npc = 1563 Ice_Troll 121 0 +npc = 1564 Ice_Troll 120 0 +npc = 1565 Ice_Troll 120 0 +npc = 1566 Ice_Troll 121 0 +npc = 1567 Cyreg_Paddlehorn 0 0 +npc = 1568 Curpile_Fyod 0 0 +npc = 1569 Veliaf_Hurtz 0 0 +npc = 1570 Sani_Piliu 0 0 +npc = 1571 Harold_Evans 0 0 +npc = 1572 Radigad_Ponfit 0 0 +npc = 1573 Polmafi_Ferdygris 0 0 +npc = 1574 Ivan_Strom 0 0 +npc = 1575 Skeleton_Hellhound 97 2000 +npc = 1576 Stranger 0 0 +npc = 1577 Vanstrom_Klause 0 0 +npc = 1578 Mist 0 0 +npc = 1579 Vanstrom_Klause 0 0 +npc = 1580 Vanstrom_Klause 0 0 +npc = 1581 Vanstrom_Klause 0 0 +npc = 1582 Fire_giant 86 0 +npc = 1583 Fire_giant 86 0 +npc = 1584 Fire_giant 86 0 +npc = 1585 Fire_giant 86 0 +npc = 1586 Fire_giant 86 0 +npc = 1587 Moss_giant 42 0 +npc = 1588 Moss_giant 42 0 +npc = 1589 Baby_red_dragon 65 200 +npc = 1590 Bronze_dragon 131 380 +npc = 1591 Iron_dragon 189 390 +npc = 1592 Steel_dragon 246 400 +npc = 1593 Wild_Dog 63 0 +npc = 1594 Wild_dog 63 0 +npc = 1595 Saniboch 0 0 +npc = 1596 Mazchna 0 0 +npc = 1597 Vannaka 0 0 +npc = 1598 Chaeldar 0 0 +npc = 1599 Duradel 0 0 +npc = 1600 Cave_crawler 23 0 +npc = 1601 Cave_crawler 23 0 +npc = 1602 Cave_crawler 23 0 +npc = 1603 Cave_crawler 23 0 +npc = 1604 Aberrant_specter 96 220 +npc = 1605 Aberrant_specter 96 220 +npc = 1606 Aberrant_specter 96 0 +npc = 1607 Aberrant_specter 96 0 +npc = 1608 Kurask 106 0 +npc = 1609 Kurask 106 0 +npc = 1610 Gargoyle 111 0 +npc = 1611 Gargoyle 111 0 +npc = 1612 Banshee 23 0 +npc = 1613 Nechryael 115 0 +npc = 1614 Death_spawn 46 0 +npc = 1615 Abyssal_demon 124 250 +npc = 1616 Basilisk 61 0 +npc = 1617 Basilisk 61 0 +npc = 1618 Bloodveld 76 0 +npc = 1619 Bloodveld 76 0 +npc = 1620 Cockatrice 37 0 +npc = 1621 Cockatrice 37 0 +npc = 1622 Rockslug 29 0 +npc = 1623 Rockslug 29 0 +npc = 1624 Dustdevil 93 0 +npc = 1625 Smokedevil 93 170 +npc = 1626 Turoth 86 0 +npc = 1627 Turoth 89 0 +npc = 1628 Turoth 87 0 +npc = 1629 Turoth 85 0 +npc = 1630 Turoth 83 0 +npc = 1631 Turoth 88 0 +npc = 1632 Turoth 88 0 +npc = 1633 Pyrefiend 43 0 +npc = 1634 Pyrefiend 43 0 +npc = 1635 Pyrefiend 43 0 +npc = 1636 Pyrefiend 43 0 +npc = 1637 Jelly 78 300 +npc = 1638 Jelly 78 0 +npc = 1639 Jelly 78 0 +npc = 1640 Jelly 78 0 +npc = 1641 Jelly 78 0 +npc = 1642 Jelly 78 0 +npc = 1643 Infernal_Mage 66 0 +npc = 1644 Infernal_Mage 66 0 +npc = 1645 Infernal_Mage 66 450 +npc = 1646 Infernal_Mage 66 250 +npc = 1647 Infernal_Mage 66 0 +npc = 1648 Crawling_Hand 8 150 +npc = 1649 Crawling_Hand 8 0 +npc = 1650 Crawling_Hand 7 0 +npc = 1651 Crawling_Hand 8 0 +npc = 1652 Crawling_Hand 8 0 +npc = 1653 Crawling_Hand 12 0 +npc = 1654 Crawling_Hand 12 0 +npc = 1655 Crawling_Hand 11 60 +npc = 1656 Crawling_Hand 12 0 +npc = 1657 Crawling_Hand 12 0 +npc = 1658 Robe_Store_owner 0 0 +npc = 1659 Skullball 0 0 +npc = 1660 Skullball_Boss 0 0 +npc = 1661 Agility_Boss 0 0 +npc = 1662 Skullball_Trainer 0 0 +npc = 1663 Agility_Trainer 0 0 +npc = 1664 Agility_Trainer 0 0 +npc = 1665 Werewolf 0 0 +npc = 1666 fenk_gardener_multi 1 0 +npc = 1667 fenk_gardener_multi_2 1 0 +npc = 1668 fenk_fenkenstrain 1 0 +npc = 1669 fenk_fenkenstrain_in_tower 1 0 +npc = 1670 Dr_Fenkenstrain 0 0 +npc = 1671 fenk_creature 1 0 +npc = 1672 fenk_creature_released 1 0 +npc = 1673 Fenkenstrain's_Monster 0 0 +npc = 1674 Lord_Rologarth 0 0 +npc = 1675 Gardener_Ghost 0 0 +npc = 1676 Experiment 51 0 +npc = 1677 Experiment 25 100 +npc = 1678 Experiment 25 100 +npc = 1679 Eluned 0 0 +npc = 1680 Islwyn 0 0 +npc = 1681 Moss_giant 84 0 +npc = 1682 Golrie 0 0 +npc = 1683 Velorina 0 0 +npc = 1684 Necrovarus 0 0 +npc = 1685 Gravingas 0 0 +npc = 1686 Ghost_Disciple 0 0 +npc = 1687 ahoy_akharanu_multi 1 0 +npc = 1688 Ak-Haranu 0 0 +npc = 1689 Slime 0 0 +npc = 1690 Slime 0 0 +npc = 1691 Undead_Cow 2 0 +npc = 1692 Undead_Chicken 1 0 +npc = 1693 Giant_Lobster 32 140 +npc = 1694 Robin 0 0 +npc = 1695 Old_crone 0 0 +npc = 1696 Old_man 0 0 +npc = 1697 Ghost_Villager 0 0 +npc = 1698 Tortured_Soul 59 0 +npc = 1699 Ghost_Shopkeeper 0 0 +npc = 1700 Ghost_Innkeeper 0 0 +npc = 1701 Ghost_Farmer 0 0 +npc = 1702 Ghost_Banker 0 0 +npc = 1703 Ghost_Sailor 0 0 +npc = 1704 Ghost_Captain 0 0 +npc = 1705 Ghost_Captain 0 0 +npc = 1706 Ghost_Guard 0 0 +npc = 1707 Ghost_(?) 1 0 +npc = 1708 Ghost_(?) 1 0 +npc = 1709 Johanhus_Ulsbrecht 0 0 +npc = 1710 H.A.M._Guard 12 0 +npc = 1711 H.A.M._Guard 18 0 +npc = 1712 H.A.M._Guard 22 0 +npc = 1713 H.A.M._Deacon 0 0 +npc = 1714 H.A.M._Member 0 0 +npc = 1715 H.A.M._Member 0 0 +npc = 1716 H.A.M._Member 0 0 +npc = 1717 H.A.M._Member 0 0 +npc = 1718 Jimmy_the_Chisel 0 0 +npc = 1719 Tree 0 0 +npc = 1720 Tree 0 0 +npc = 1721 Tree 0 0 +npc = 1722 Dead_tree 0 0 +npc = 1723 Dead_tree 0 0 +npc = 1724 Dead_tree 0 0 +npc = 1725 Dead_tree 0 0 +npc = 1726 Dead_tree 0 0 +npc = 1727 Dead_tree 0 0 +npc = 1728 Dead_tree 0 0 +npc = 1729 Dead_tree 0 0 +npc = 1730 Dead_tree 0 0 +npc = 1731 Dead_tree 0 0 +npc = 1732 Dead_tree 0 0 +npc = 1733 Dramen_tree 0 0 +npc = 1734 Magic_tree 0 0 +npc = 1735 Maple_tree 0 0 +npc = 1736 Willow 0 0 +npc = 1737 Willow 0 0 +npc = 1738 Willow 0 0 +npc = 1739 Oak 0 0 +npc = 1740 Yew 0 0 +npc = 1741 Evergreen 0 0 +npc = 1742 Evergreen 0 0 +npc = 1743 Evergreen 0 0 +npc = 1744 Tree 0 0 +npc = 1745 Dead_tree 0 0 +npc = 1746 Achey_Tree 0 0 +npc = 1747 tree1 0 0 +npc = 1748 tree1 0 0 +npc = 1749 tree1 0 0 +npc = 1750 tree1 0 0 +npc = 1751 Crow 0 0 +npc = 1752 Crow 0 0 +npc = 1753 Crow 0 0 +npc = 1754 Crow 0 0 +npc = 1755 Crow 0 0 +npc = 1756 Crow 0 0 +npc = 1757 Farmer 7 0 +npc = 1758 Farmer 7 0 +npc = 1759 Farmer 0 0 +npc = 1760 Farmer 0 0 +npc = 1761 Farmer 0 0 +npc = 1762 Sheep 0 0 +npc = 1763 Sheep 0 0 +npc = 1764 Sheep 0 0 +npc = 1765 Sheep 0 0 +npc = 1766 Cow_calf 2 6 +npc = 1767 Cow 2 8 +npc = 1768 Cow_calf 2 6 +npc = 1769 Goblin 2 0 +npc = 1770 Goblin 2 0 +npc = 1771 Goblin 2 0 +npc = 1772 Goblin 2 0 +npc = 1773 Goblin 2 0 +npc = 1774 Goblin 2 0 +npc = 1775 Goblin 2 0 +npc = 1776 Goblin 2 0 +npc = 1777 Ilfeen 0 0 +npc = 1778 William 0 0 +npc = 1779 Ian 0 0 +npc = 1780 Larry 0 0 +npc = 1781 Darren 0 0 +npc = 1782 Edward 0 0 +npc = 1783 Richard 0 0 +npc = 1784 Neil 0 0 +npc = 1785 Edmond 0 0 +npc = 1786 Simon 0 0 +npc = 1787 Sam 0 0 +npc = 1788 Lumdo 0 0 +npc = 1789 Bunkwicket 0 0 +npc = 1790 Waymottin 0 0 +npc = 1791 Jungle_Tree 0 0 +npc = 1792 Jungle_Tree 0 0 +npc = 1793 Tassie_Slipcast 0 0 +npc = 1794 Hammerspike_Stoutbeard 0 0 +npc = 1795 Dwarf_gang_member 44 0 +npc = 1796 Dwarf_gang_member 48 0 +npc = 1797 Dwarf_gang_member 49 0 +npc = 1798 Phantuwti_Fanstuwi_Farsight 0 0 +npc = 1799 Tindel_Marchant 0 0 +npc = 1800 Gnormadium_Avlafrim 0 0 +npc = 1801 Petra_Fiyed 1 0 +npc = 1802 Slagilith 92 0 +npc = 1803 Rock_pile 0 0 +npc = 1804 Slagilith 0 0 +npc = 1805 Guard 0 0 +npc = 1806 Guard 0 0 +npc = 1807 Hamal_the_Chieftain 0 0 +npc = 1808 Ragnar 0 0 +npc = 1809 Svidi 0 0 +npc = 1810 Jokul 0 0 +npc = 1811 mdaughter_multi_bear 1 0 +npc = 1812 The_Kendal 70 0 +npc = 1813 The_Kendal 70 0 +npc = 1814 Camp_dweller 31 0 +npc = 1815 Camp_dweller 31 0 +npc = 1816 Camp_dweller 31 0 +npc = 1817 Camp_dweller 31 0 +npc = 1818 Camp_dweller 25 0 +npc = 1819 Mountain_Goat 0 0 +npc = 1820 Mountain_Goat 0 0 +npc = 1821 Bald_Headed_Eagle 0 0 +npc = 1822 Cave_goblin 3 0 +npc = 1823 Cave_goblin 3 0 +npc = 1824 Cave_goblin 3 0 +npc = 1825 Cave_goblin 3 0 +npc = 1826 Hole_in_the_wall 0 0 +npc = 1827 Wall_Beast 49 0 +npc = 1828 Giant_frog 99 0 +npc = 1829 Big_frog 24 0 +npc = 1830 Frog 0 0 +npc = 1831 Cave_Slime 23 0 +npc = 1832 Cave_Bug 6 200 +npc = 1833 Cave_bug_larva 0 0 +npc = 1834 Candle_seller 0 0 +npc = 1835 Easter_Bunny 0 0 +npc = 1836 dwarfrock_multi_dondakan 1 0 +npc = 1837 Dondakan_the_Dwarf 0 0 +npc = 1838 Dondakan_the_Dwarf 0 0 +npc = 1839 Dondakan_the_Dwarf 0 0 +npc = 1840 Dwarven_Engineer 0 0 +npc = 1841 Rolad 0 0 +npc = 1842 Khorvak,_a_dwarven_engineer 0 0 +npc = 1843 Dwarven_Ferryman 0 0 +npc = 1844 Dwarven_Ferryman 0 0 +npc = 1845 dwarfrock_multi_gold_boatman 1 0 +npc = 1846 Dwarven_Boatman 0 0 +npc = 1847 Miodvetnir 0 0 +npc = 1848 Dernu 0 0 +npc = 1849 Derni 0 0 +npc = 1850 Arzinian_Avatar_of_Strength 130 0 +npc = 1851 Arzinian_Avatar_of_Strength 125 0 +npc = 1852 Arzinian_Avatar_of_Strength 75 60 +npc = 1853 Arzinian_Avatar_of_Ranging 130 0 +npc = 1854 Arzinian_Avatar_of_Ranging 125 0 +npc = 1855 Arzinian_Avatar_of_Ranging 75 60 +npc = 1856 Arzinian_Avatar_of_Magic 130 0 +npc = 1857 Arzinian_Avatar_of_Magic 125 0 +npc = 1858 Arzinian_Avatar_of_Magic 75 60 +npc = 1859 Arzinian_Being_of_Bordanzan 781 2500 +npc = 1860 Brian 0 0 +npc = 1861 Monkey_boy 0 0 +npc = 1862 Ali_Morrisane 0 0 +npc = 1863 Drunken_Ali 0 0 +npc = 1864 Ali_The_barman 0 0 +npc = 1865 Ali_the_Kebab_seller 0 0 +npc = 1866 Market_seller 0 0 +npc = 1867 Ali_the_Camel_Man 0 0 +npc = 1868 Street_urchin 0 0 +npc = 1869 feud_mayor 0 0 +npc = 1870 Ali_the_Mayor 0 0 +npc = 1871 Ali_the_Hag 0 0 +npc = 1872 Ali_the_Snake_Charmer 0 0 +npc = 1873 Ali_the_Camel 0 0 +npc = 1874 Desert_snake 5 0 +npc = 1875 Snake 0 0 +npc = 1876 Blackjack_seller 0 0 +npc = 1877 feud_bandit_boss 0 0 +npc = 1878 Bandit_Leader 0 0 +npc = 1879 feud_arabian_guard_multi 56 0 +npc = 1880 Bandit 56 0 +npc = 1881 Bandit 56 0 +npc = 1882 feud_arabian_guard2_multi 41 0 +npc = 1883 Bandit 41 0 +npc = 1884 Bandit 41 0 +npc = 1885 Bandit_champion 70 0 +npc = 1886 Cowardly_Bandit 56 0 +npc = 1887 feud_villager_multi_1 3 0 +npc = 1888 Villager 3 0 +npc = 1889 Villager 3 0 +npc = 1890 Villager 3 0 +npc = 1891 feud_villager_multi_2 3 0 +npc = 1892 Villager 3 0 +npc = 1893 Villager 3 0 +npc = 1894 Villager 3 0 +npc = 1895 feud_villager_multi_3 3 0 +npc = 1896 Villager 3 0 +npc = 1897 Villager 3 0 +npc = 1898 Villager 3 0 +npc = 1899 feud_menap_boss 0 0 +npc = 1900 feud_menap_boss2 0 0 +npc = 1901 Menaphite_Leader 0 0 +npc = 1902 Ali_the_Operator 0 0 +npc = 1903 feud_egyptian_doorman_multi 55 0 +npc = 1904 Menaphite_Thug 55 0 +npc = 1905 Menaphite_Thug 55 0 +npc = 1906 Tough_Guy 75 0 +npc = 1907 Golem 1 0 +npc = 1908 Broken_clay_golem 0 0 +npc = 1909 Damaged_clay_golem 0 0 +npc = 1910 Clay_golem 0 0 +npc = 1911 Desert_Phoenix 0 0 +npc = 1912 Elissa 0 0 +npc = 1913 Kamil 154 0 +npc = 1914 Dessous 139 0 +npc = 1915 Dessous 139 0 +npc = 1916 Ruantun 0 0 +npc = 1917 Bandit_shopkeeper 0 0 +npc = 1918 Archaeologist 0 0 +npc = 1919 Stranger 95 1700 +npc = 1920 Malak 0 0 +npc = 1921 Bartender 0 0 +npc = 1922 fourdiamonds_elder 1 0 +npc = 1923 Eblis 0 0 +npc = 1924 fourdiamonds_elder2 1 0 +npc = 1925 Eblis 0 0 +npc = 1926 Bandit 74 0 +npc = 1927 Bandit 0 0 +npc = 1928 Bandit 0 0 +npc = 1929 Bandit 0 0 +npc = 1930 Bandit 0 0 +npc = 1931 Bandit 57 0 +npc = 1932 fourdiamonds_troll_child 0 0 +npc = 1933 Troll_child 0 0 +npc = 1934 Troll_child 0 0 +npc = 1935 Ice_troll 0 0 +npc = 1936 Ice_Troll 124 0 +npc = 1937 Ice_Troll 123 0 +npc = 1938 Ice_Troll 120 0 +npc = 1939 Ice_Troll 121 0 +npc = 1940 Ice_Troll 120 0 +npc = 1941 Ice_Troll 120 0 +npc = 1942 Ice_Troll 121 0 +npc = 1943 troll_block_1 34 0 +npc = 1944 Ice_block 0 0 +npc = 1945 troll_block_2 34 0 +npc = 1946 Ice_block 0 0 +npc = 1947 Troll_father 1 0 +npc = 1948 Troll_father 0 0 +npc = 1949 Troll_Mother 1 0 +npc = 1950 Troll_mother 0 0 +npc = 1951 Ice_wolf 132 0 +npc = 1952 Ice_wolf 132 0 +npc = 1953 Ice_wolf 132 0 +npc = 1954 Ice_wolf 132 0 +npc = 1955 Ice_wolf 132 0 +npc = 1956 Ice_wolf 132 0 +npc = 1957 desert_treasure_invisible_npc 1 0 +npc = 1958 Mummy 103 450 +npc = 1959 Mummy 0 0 +npc = 1960 Mummy 96 0 +npc = 1961 Mummy 103 0 +npc = 1962 Mummy 103 0 +npc = 1963 Mummy 103 0 +npc = 1964 Mummy 103 0 +npc = 1965 Mummy 103 0 +npc = 1966 Mummy 103 0 +npc = 1967 Mummy 103 0 +npc = 1968 Mummy 103 0 +npc = 1969 Scarabs 92 0 +npc = 1970 azzanadra 0 0 +npc = 1971 Azzanadra 0 0 +npc = 1972 Rasool 0 0 +npc = 1973 Giant_skeleton 80 0 +npc = 1974 Damis 103 400 +npc = 1975 Damis 174 500 +npc = 1976 Shadow_Hound 63 0 +npc = 1977 Fareed 167 0 +npc = 1978 Slave 0 0 +npc = 1979 Slave 0 0 +npc = 1980 Embalmer 0 0 +npc = 1981 Carpenter 0 0 +npc = 1982 Linen_Worker 0 0 +npc = 1983 Siamun 0 0 +npc = 1984 ics_little_hipriest 1 0 +npc = 1985 ics_little_hipriest_town 1 0 +npc = 1986 High_Priest 0 0 +npc = 1987 ics_little_temple_priest 1 0 +npc = 1988 Priest 0 0 +npc = 1989 Priest 0 0 +npc = 1990 Sphinx 0 0 +npc = 1991 Possessed_Priest 91 0 +npc = 1992 Neite 0 0 +npc = 1993 Crocodile 63 0 +npc = 1994 Jackal 21 0 +npc = 1995 Locust 18 0 +npc = 1996 Vulture 31 0 +npc = 1997 Plague_Frog 11 0 +npc = 1998 Plagued_Cow 0 0 +npc = 1999 Plagued_Cow 0 0 +npc = 2000 Plagued_Cow 0 0 +npc = 2001 Scarabs 98 0 +npc = 2002 ics_little_multi_wanderer 1 0 +npc = 2003 ics_little_multi_wanderer1 1 0 +npc = 2004 ics_little_multi_wanderer2 1 0 +npc = 2005 Wanderer 0 0 +npc = 2006 Wanderer 0 0 +npc = 2007 Het 81 0 +npc = 2008 Apmeken 75 0 +npc = 2009 Scabaras 75 0 +npc = 2010 Crondis 75 0 +npc = 2011 ics_multi_ic 1 0 +npc = 2012 Icthlarin 0 0 +npc = 2013 ics_little_spectre 1 0 +npc = 2014 Klenter 0 0 +npc = 2015 Mummy 84 0 +npc = 2016 Mummy 84 0 +npc = 2017 Mummy 84 0 +npc = 2018 Mummy 84 0 +npc = 2019 Mummy 84 0 +npc = 2020 multi_vanstrom_stranger_entity 1 0 +npc = 2021 Light_creature 0 0 +npc = 2022 Light_creature 0 0 +npc = 2023 Juna 0 0 +npc = 2024 Strange_Old_Man 0 0 +npc = 2025 Ahrim_the_Blighted 98 110 +npc = 2026 Dharok_the_Wretched 115 150 +npc = 2027 Guthan_the_Infested 115 150 +npc = 2028 Karil_the_Tainted 98 150 +npc = 2029 Torag_the_Corrupted 115 150 +npc = 2030 Verac_the_Defiled 115 150 +npc = 2031 Bloodworm 52 50 +npc = 2032 Crypt_rat 43 0 +npc = 2033 Giant_crypt_rat 76 0 +npc = 2034 Crypt_spider 56 0 +npc = 2035 Giant_crypt_spider 79 140 +npc = 2036 Skeleton 77 0 +npc = 2037 Skeleton 77 0 +npc = 2038 Grish 0 0 +npc = 2039 Uglug_Nar 0 0 +npc = 2040 Pilg 0 0 +npc = 2041 Grug 0 0 +npc = 2042 Ogre_guard 0 0 +npc = 2043 Ogre_guard 76 0 +npc = 2044 Zogre 44 0 +npc = 2045 Zogre 44 0 +npc = 2046 Zogre 44 0 +npc = 2047 Zogre 44 0 +npc = 2048 Zogre 44 0 +npc = 2049 Zogre 44 0 +npc = 2050 Skogre 44 0 +npc = 2051 Zogre 44 0 +npc = 2052 Zogre 44 0 +npc = 2053 Zogre 44 0 +npc = 2054 Zogre 44 0 +npc = 2055 Zogre 44 0 +npc = 2056 Zogre 44 0 +npc = 2057 Skogre 44 0 +npc = 2058 Zombie 39 0 +npc = 2059 Zavistic_Rarve 0 0 +npc = 2060 Slash_Bash 111 0 +npc = 2061 Sithik_Ints 1 0 +npc = 2062 Sithik_Ints 1 0 +npc = 2063 Gargh 0 0 +npc = 2064 Scarg 0 0 +npc = 2065 Gruh 0 0 +npc = 2066 Irwin_Feaselbaum 0 0 +npc = 2067 Fishing_spot 0 0 +npc = 2068 Fishing_spot 0 0 +npc = 2069 Cave_goblin_miner 11 0 +npc = 2070 Cave_goblin_miner 11 0 +npc = 2071 Cave_goblin_miner 11 0 +npc = 2072 Cave_goblin_miner 11 0 +npc = 2073 Cave_goblin_guard 26 0 +npc = 2074 Cave_goblin_guard 24 0 +npc = 2075 Cave_goblin_miner 11 0 +npc = 2076 Cave_goblin_miner 11 0 +npc = 2077 Cave_goblin_miner 11 0 +npc = 2078 Cave_goblin_miner 11 0 +npc = 2079 Sigmund 1 0 +npc = 2080 lost_tribe_sigmund_leaving 1 0 +npc = 2081 lost_tribe_sigmund_ham 1 0 +npc = 2082 Sigmund 0 0 +npc = 2083 Sigmund 0 0 +npc = 2084 Mistag 0 0 +npc = 2085 lost_tribe_guide 1 0 +npc = 2086 Kazgar 0 0 +npc = 2087 Ur-tag 0 0 +npc = 2088 Duke_Horacio 0 0 +npc = 2089 Mistag 0 0 +npc = 2090 Sigmund 0 0 +npc = 2091 Secretary 0 0 +npc = 2092 Purple_Pewter_Secretary 0 0 +npc = 2093 Yellow_Fortune_Secretary 0 0 +npc = 2094 Blue_Opal_Secretary 0 0 +npc = 2095 Green_Gemstone_Secretary 0 0 +npc = 2096 White_Chisel_Secretary 0 0 +npc = 2097 Silver_Cog_Secretary 0 0 +npc = 2098 Brown_Engine_Secretary 0 0 +npc = 2099 Red_Axe_Secretary 0 0 +npc = 2100 Purple_Pewter_Director 0 0 +npc = 2101 Blue_Opal_Director 0 0 +npc = 2102 Yellow_Fortune_Director 0 0 +npc = 2103 Green_Gemstone_Director 0 0 +npc = 2104 White_Chisel_Director 0 0 +npc = 2105 Silver_Cog_Director 0 0 +npc = 2106 Brown_Engine_Director 0 0 +npc = 2107 Red_Axe_Director 0 0 +npc = 2108 Red_Axe_Cat 0 0 +npc = 2109 Trader 0 0 +npc = 2110 Trader 0 0 +npc = 2111 Trader 0 0 +npc = 2112 Trader 0 0 +npc = 2113 Trader 0 0 +npc = 2114 Trader 0 0 +npc = 2115 Trader 0 0 +npc = 2116 Trader 0 0 +npc = 2117 Trader 0 0 +npc = 2118 Trader 0 0 +npc = 2119 Trader 0 0 +npc = 2120 Trader 0 0 +npc = 2121 Trader 0 0 +npc = 2122 Trader 0 0 +npc = 2123 Trader 0 0 +npc = 2124 Trader 0 0 +npc = 2125 Trader 0 0 +npc = 2126 Trader 0 0 +npc = 2127 Trade_Referee 0 0 +npc = 2128 Supreme_Commander 0 0 +npc = 2129 Commander_Veldaban 0 0 +npc = 2130 Black_Guard 48 0 +npc = 2131 Black_Guard 48 0 +npc = 2132 Black_Guard 48 0 +npc = 2133 Black_Guard 48 0 +npc = 2134 Black_Guard_Berserker 66 0 +npc = 2135 Black_Guard_Berserker 66 0 +npc = 2136 Black_Guard_Berserker 66 0 +npc = 2137 Gnome_emissary 0 0 +npc = 2138 Gnome_traveller 0 0 +npc = 2139 Gnome_traveller 0 0 +npc = 2140 Dromund's_cat 0 0 +npc = 2141 Blasidar_the_sculptor 0 0 +npc = 2142 Riki_the_sculptor's_model 0 0 +npc = 2143 Riki_the_sculptor's_model 0 0 +npc = 2144 Riki_the_sculptor's_model 0 0 +npc = 2145 Riki_the_sculptor's_model 0 0 +npc = 2146 Riki_the_sculptor's_model 0 0 +npc = 2147 Riki_the_sculptor's_model 0 0 +npc = 2148 Riki_the_sculptor's_model 0 0 +npc = 2149 Riki_the_sculptor's_model 0 0 +npc = 2150 Riki_the_sculptor's_model 0 0 +npc = 2151 Vigr 0 0 +npc = 2152 Santiri 0 0 +npc = 2153 Saro 0 0 +npc = 2154 Gunslik 0 0 +npc = 2155 Wemund 0 0 +npc = 2156 Randivor 0 0 +npc = 2157 Hervi 0 0 +npc = 2158 Nolar 0 0 +npc = 2159 Gulldamar 0 0 +npc = 2160 Tati 0 0 +npc = 2161 Agmundi 0 0 +npc = 2162 Vermundi 0 0 +npc = 2163 Banker 0 0 +npc = 2164 Banker 0 0 +npc = 2165 Librarian 0 0 +npc = 2166 Assistant 0 0 +npc = 2167 Customer 0 2000 +npc = 2168 Customer 0 2000 +npc = 2169 Dromund 0 0 +npc = 2170 Rind_the_gardener 0 0 +npc = 2171 Factory_Manager 0 0 +npc = 2172 Factory_Worker 0 0 +npc = 2173 Factory_Worker 0 0 +npc = 2174 Factory_Worker 0 0 +npc = 2175 Factory_Worker 0 0 +npc = 2176 Inn_Keeper 0 0 +npc = 2177 Inn_Keeper 0 0 +npc = 2178 Barmaid 0 0 +npc = 2179 Barman 0 0 +npc = 2180 Cart_conductor 0 0 +npc = 2181 Cart_conductor 0 0 +npc = 2182 Cart_conductor 0 0 +npc = 2183 Cart_conductor 0 0 +npc = 2184 Cart_conductor 0 0 +npc = 2185 Cart_conductor 0 0 +npc = 2186 Cart_conductor 0 0 +npc = 2187 Rowdy_dwarf 0 0 +npc = 2188 Hegir 0 0 +npc = 2189 Haera 0 0 +npc = 2190 Runvastr 0 0 +npc = 2191 Sune 0 0 +npc = 2192 Bentamir 0 0 +npc = 2193 Ulifed 0 0 +npc = 2194 Reinald 0 0 +npc = 2195 Karl 0 0 +npc = 2196 Gauss 0 0 +npc = 2197 Myndill 0 0 +npc = 2198 Kjut 0 0 +npc = 2199 Tombar 0 0 +npc = 2200 Odmar 0 0 +npc = 2201 Audmann 0 0 +npc = 2202 Drunken_Dwarf 0 0 +npc = 2203 Drunken_Dwarf 0 0 +npc = 2204 Drunken_Dwarf 0 0 +npc = 2205 Dwarven_Boatman 0 0 +npc = 2206 Dwarven_Boatman 0 0 +npc = 2207 Dwarven_Miner 0 0 +npc = 2208 Dwarven_Miner 0 0 +npc = 2209 Dwarven_Miner 0 0 +npc = 2210 Dwarven_Miner 0 0 +npc = 2211 Dwarven_Miner 0 0 +npc = 2212 Dwarven_Miner 0 0 +npc = 2213 Dwarven_Miner 0 0 +npc = 2214 Dwarven_Miner 0 0 +npc = 2215 Dwarven_Miner 0 0 +npc = 2216 Dwarven_Miner 0 0 +npc = 2217 Dwarven_Miner 0 0 +npc = 2218 Dwarven_Miner 0 0 +npc = 2219 Purple_Pewter_Director 0 0 +npc = 2220 Purple_Pewter_Director 0 0 +npc = 2221 Blue_Opal_Director 0 0 +npc = 2222 Yellow_Fortune_Director 0 0 +npc = 2223 Green_Gemstone_Director 0 0 +npc = 2224 White_Chisel_Director 0 0 +npc = 2225 Silver_Cog_Director 0 0 +npc = 2226 Brown_Engine_Director 0 0 +npc = 2227 Red_Axe_Director 0 0 +npc = 2228 Commander_Veldaban 0 0 +npc = 2229 Red_Axe_Cat 0 0 +npc = 2230 Red_Axe_Cat 0 0 +npc = 2231 Black_Guard_Berserker 0 0 +npc = 2232 Black_Guard_Berserker 0 0 +npc = 2233 Olivia 0 0 +npc = 2234 Master_Farmer 0 0 +npc = 2235 Master_Farmer 0 0 +npc = 2236 Market_Guard 20 0 +npc = 2237 Gee 0 0 +npc = 2238 Donie 0 0 +npc = 2239 Pig 0 0 +npc = 2240 Pig 0 0 +npc = 2241 Piglet 0 0 +npc = 2242 Piglet 0 0 +npc = 2243 Piglet 0 0 +npc = 2244 Lumbridge_Guide 0 0 +npc = 2245 Khazard_trooper 19 0 +npc = 2246 Khazard_trooper 19 0 +npc = 2247 Gnome_troop 3 0 +npc = 2248 Gnome_troop 3 0 +npc = 2249 Gnome 3 0 +npc = 2250 Gnome 1 0 +npc = 2251 Gnome 3 0 +npc = 2252 Mounted_terrorbird_gnome 49 0 +npc = 2253 Wise_Old_Man 0 0 +npc = 2254 Bed 0 0 +npc = 2255 Thing_under_the_bed 0 0 +npc = 2256 Paladin 62 65 +npc = 2257 rcu_zammy_mage1 1 0 +npc = 2258 Mage_of_Zamorak 0 0 +npc = 2259 Mage_of_Zamorak 0 0 +npc = 2260 rcu_zammy_mage1_edge 1 0 +npc = 2261 Mage_of_Zamorak 0 0 +npc = 2262 Dark_mage 0 2000 +npc = 2263 Abyssal_leech 41 0 +npc = 2264 Abyssal_guardian 59 0 +npc = 2265 Abyssal_walker 81 0 +npc = 2266 Brian_O'Richard 0 0 +npc = 2267 Rogue_Guard 0 0 +npc = 2268 Rogue_Guard 0 0 +npc = 2269 Rogue_Guard 0 0 +npc = 2270 Martin_Thwait 0 0 +npc = 2271 Emerald_Benedict 0 0 +npc = 2272 Spin_Blades 0 0 +npc = 2273 Spin_Blades 0 0 +npc = 2274 Goblin 2 0 +npc = 2275 Goblin 2 0 +npc = 2276 Goblin 2 0 +npc = 2277 Goblin 2 0 +npc = 2278 Goblin 2 0 +npc = 2279 Goblin 2 0 +npc = 2280 Goblin 2 0 +npc = 2281 Goblin 2 0 +npc = 2282 Sir_Spishyus 0 0 +npc = 2283 Lady_Table 0 0 +npc = 2284 Sir_Kuam_Ferentse 0 0 +npc = 2285 Sir_Leye 20 0 +npc = 2286 Sir_Tinley 0 0 +npc = 2287 Sir_Ren_Itchood 0 0 +npc = 2288 Miss_Cheevers 0 0 +npc = 2289 Ms._Hynn_Terprett 0 0 +npc = 2290 Sir_Tiffy_Cashien 0 0 +npc = 2291 Rug_Merchant 0 0 +npc = 2292 Rug_Merchant 0 0 +npc = 2293 Rug_Merchant 0 0 +npc = 2294 Rug_Merchant 0 0 +npc = 2295 magic_carpet_seller5 1 0 +npc = 2296 Rug_Merchant 0 0 +npc = 2297 magic_carpet_seller6 0 0 +npc = 2298 Rug_Merchant 0 0 +npc = 2299 magic_carpet_seller7 0 0 +npc = 2300 Rug_Station_Attendant 0 0 +npc = 2301 Monkey 0 2000 +npc = 2302 magic_carpet_multi_monkey1 0 0 +npc = 2303 magic_carpet_multi_monkey2 0 0 +npc = 2304 Sarah 0 0 +npc = 2305 Vanessa 0 0 +npc = 2306 Richard 0 0 +npc = 2307 Alice 0 0 +npc = 2308 Capt'_Arnav 0 0 +npc = 2309 --NO_IDEA-- 0 0 +npc = 2310 Cow_calf 2 0 +npc = 2311 Sheepdog 0 0 +npc = 2312 Rooster 0 0 +npc = 2313 Chicken 1 0 +npc = 2314 Chicken 1 0 +npc = 2315 Chicken 1 0 +npc = 2316 Pig 0 0 +npc = 2317 Pig 0 0 +npc = 2318 Piglet 0 0 +npc = 2319 Piglet 0 0 +npc = 2320 Piglet 0 0 +npc = 2321 Blandebir 0 0 +npc = 2322 Metarialus 0 0 +npc = 2323 Elstan 0 0 +npc = 2324 Dantaera 0 0 +npc = 2325 Kragen 0 0 +npc = 2326 Lyra 0 0 +npc = 2327 Francis 0 0 +npc = 2328 Gardener 0 0 +npc = 2329 Iago 0 0 +npc = 2330 Garth 0 0 +npc = 2331 Ellena 0 0 +npc = 2332 Selena 0 0 +npc = 2333 Vasquen 0 0 +npc = 2334 Rhonen 0 0 +npc = 2335 Dreven 0 0 +npc = 2336 Taria 0 0 +npc = 2337 Rhazien 0 0 +npc = 2338 Torrell 0 0 +npc = 2339 Alain 0 0 +npc = 2340 Heskel 0 0 +npc = 2341 Treznor 0 0 +npc = 2342 Fayeth 0 0 +npc = 2343 Bolongo 0 0 +npc = 2344 Gileth 0 0 +npc = 2345 Sick-looking_sheep_(1) 0 0 +npc = 2346 Sick-looking_sheep_(2) 0 0 +npc = 2347 Sick-looking_sheep_(3) 0 0 +npc = 2348 Sick-looking_sheep_(4) 0 0 +npc = 2349 Mourner 11 0 +npc = 2350 Mourner 11 0 +npc = 2351 Mourner 11 0 +npc = 2352 Eudav 0 0 +npc = 2353 Oronwen 0 0 +npc = 2354 Banker 0 0 +npc = 2355 Banker 0 0 +npc = 2356 Dalldav 0 0 +npc = 2357 Gethin 0 0 +npc = 2358 Arianwyn 0 0 +npc = 2359 Elf_warrior 108 0 +npc = 2360 Elf_warrior 108 0 +npc = 2361 Elf_warrior 90 0 +npc = 2362 Elf_warrior 90 0 +npc = 2363 Goreu 0 0 +npc = 2364 Ysgawyn 0 0 +npc = 2365 Arvel 0 0 +npc = 2366 Mawrth 0 0 +npc = 2367 Kelyn 0 0 +npc = 2368 Eoin 0 0 +npc = 2369 Iona 0 0 +npc = 2370 Gnomic_inventor 1 0 +npc = 2371 Gnome 1 0 +npc = 2372 Head_mourner 0 0 +npc = 2373 Mourner 108 0 +npc = 2374 Mourner 0 0 +npc = 2375 roving_female_woodelf_temp 1 0 +npc = 2376 Eluned 0 0 +npc = 2377 Sick-looking_sheep_(1) 0 0 +npc = 2378 Sick-looking_sheep_(2) 0 0 +npc = 2379 Sick-looking_sheep_(3) 0 0 +npc = 2380 Sick-looking_sheep_(4) 0 0 +npc = 2381 secret_ghost1 1 0 +npc = 2382 secret_ghost2 1 0 +npc = 2383 secret_ghost3 1 0 +npc = 2384 secret_ghost4 1 0 +npc = 2385 secret_ghost5 1 0 +npc = 2386 secret_ghost6 1 0 +npc = 2387 secret_ghost7 1 0 +npc = 2388 secret_ghost8 1 0 +npc = 2389 secret_ghost9 1 0 +npc = 2390 secret_ghost10 1 0 +npc = 2391 secret_ghost11 1 0 +npc = 2392 secret_ghost12 1 0 +npc = 2393 secret_ghost13 1 0 +npc = 2394 secret_ghost14 1 0 +npc = 2395 secret_ghost15 1 0 +npc = 2396 secret_ghost16 1 0 +npc = 2397 Mysterious_ghost 0 0 +npc = 2398 Mysterious_ghost 0 0 +npc = 2399 Mysterious_ghost 0 0 +npc = 2400 Mysterious_ghost 0 0 +npc = 2401 Mysterious_ghost 0 0 +npc = 2402 Mysterious_ghost 0 0 +npc = 2403 Red_Axe_Secretary 0 0 +npc = 2404 Red_Axe_Director 0 0 +npc = 2405 Red_Axe_Cat 0 0 +npc = 2406 Gnome_emissary 0 0 +npc = 2407 Gnome_traveller 0 0 +npc = 2408 Gnome_traveller 0 0 +npc = 2409 Cart_conductor 0 0 +npc = 2410 Red_Axe_Director 0 0 +npc = 2411 Red_Axe_Director 0 0 +npc = 2412 Red_Axe_Henchman 0 0 +npc = 2413 Red_Axe_Henchman 0 0 +npc = 2414 Red_Axe_Henchman 0 0 +npc = 2415 Colonel_Grimsson 0 0 +npc = 2416 Colonel_Grimsson 0 0 +npc = 2417 Ogre_shaman 0 0 +npc = 2418 Ogre_shaman 0 0 +npc = 2419 Grunsh 0 0 +npc = 2420 Gnome_emissary 0 0 +npc = 2421 Gnome_companion 0 0 +npc = 2422 Gnome_companion 0 0 +npc = 2423 Chaos_dwarf 48 0 +npc = 2424 Gunslik 0 0 +npc = 2425 Nolar 0 0 +npc = 2426 Factory_Worker 0 0 +npc = 2427 Cart_conductor 0 0 +npc = 2428 Gauss 0 0 +npc = 2429 Drunken_Dwarf 0 0 +npc = 2430 Rowdy_dwarf 0 0 +npc = 2431 Ulifed 0 0 +npc = 2432 Red_Axe_Henchman 1 0 +npc = 2433 Red_Axe_Henchman 1 0 +npc = 2434 Ogre_shaman 1 0 +npc = 2435 viking_dagganoth_cave_ferryman_rellekka 1 0 +npc = 2436 Jarvald 0 0 +npc = 2437 Jarvald 0 0 +npc = 2438 Jarvald 0 0 +npc = 2439 Askeladden 0 0 +npc = 2440 Door-support 0 0 +npc = 2441 Door 0 0 +npc = 2442 Door 0 0 +npc = 2443 Door-support 0 0 +npc = 2444 Door 0 0 +npc = 2445 Door 0 0 +npc = 2446 Door-support 0 0 +npc = 2447 Door 0 0 +npc = 2448 Door 0 0 +npc = 2449 Egg 0 0 +npc = 2450 Egg 0 0 +npc = 2451 Egg 0 0 +npc = 2452 Giant_Rock_Crab 137 0 +npc = 2453 Boulder 0 0 +npc = 2454 Dagannoth_spawn 42 0 +npc = 2455 Dagannoth 90 0 +npc = 2456 Dagannoth 88 0 +npc = 2457 Wallasalki 98 0 +npc = 2458 Freaky_Forester 0 0 +npc = 2459 Pheasant 3 0 +npc = 2460 Pheasant 3 0 +npc = 2461 Pheasant 3 0 +npc = 2462 Pheasant 3 0 +npc = 2463 Evil_Chicken 19 0 +npc = 2464 Evil_Chicken 38 0 +npc = 2465 Evil_Chicken 69 0 +npc = 2466 Evil_Chicken 81 0 +npc = 2467 Evil_Chicken 121 0 +npc = 2468 Evil_Chicken 159 0 +npc = 2469 Frog 0 0 +npc = 2470 Frog 0 0 +npc = 2471 Frog 0 0 +npc = 2472 Frog 0 0 +npc = 2473 Frog 0 0 +npc = 2474 Frog_prince 0 0 +npc = 2475 Frog_princess 0 0 +npc = 2476 Rick_Turpentine 0 0 +npc = 2477 Quiz_Master 0 0 +npc = 2478 Evil_Bob 0 0 +npc = 2479 Evil_Bob 0 0 +npc = 2480 Servant 0 0 +npc = 2481 Servant 0 0 +npc = 2482 Giant_bat 0 0 +npc = 2483 tbwt_tiadeche_multinpc_shore 1 0 +npc = 2484 tbwt_tiadeche_multinpc_house 1 0 +npc = 2485 tbwt_tinsay_multinpc_island 1 0 +npc = 2486 tbwt_tinsay_multinpc_house 1 0 +npc = 2487 tbwt_tamayu_multinpc_jungle 1 0 +npc = 2488 tbwt_tamayu_multinpc_house 1 0 +npc = 2489 Bush_Snake 35 0 +npc = 2490 Bush_Snake 35 0 +npc = 2491 Jungle_spider 44 0 +npc = 2492 Jungle_Spider 44 0 +npc = 2493 Large_mosquito 13 0 +npc = 2494 Mosquito_swarm 17 0 +npc = 2495 Mosquito_swarm 20 0 +npc = 2496 Tribesman 32 0 +npc = 2497 Tribesman 32 0 +npc = 2498 Broodoo_victim 1 0 +npc = 2499 Broodoo_victim 60 0 +npc = 2500 Broodoo_victim 1 0 +npc = 2501 Broodoo_victim 60 0 +npc = 2502 Broodoo_victim 1 0 +npc = 2503 Broodoo_victim 60 0 +npc = 2504 Sharimika 1 0 +npc = 2505 Sharimika 0 0 +npc = 2506 Sharimika 0 0 +npc = 2507 Mamma_Bufetta 1 0 +npc = 2508 Mamma_Bufetta 0 0 +npc = 2509 Mamma_Bufetta 0 0 +npc = 2510 tbwcu_layleen_multinpc 1 0 +npc = 2511 Layleen 0 0 +npc = 2512 Layleen 0 0 +npc = 2513 tbwcu_karaday_multinpc 1 0 +npc = 2514 Karaday 0 0 +npc = 2515 Karaday 0 0 +npc = 2516 tbwcu_safta_doc_multinpc 1 0 +npc = 2517 Safta_Doc 0 0 +npc = 2518 Safta_Doc 0 0 +npc = 2519 tbwcu_gabooty_multinpc 1 0 +npc = 2520 Gabooty 0 0 +npc = 2521 Gabooty 0 0 +npc = 2522 tbwcu_fanellaman_multinpc 1 0 +npc = 2523 Fanellaman 0 0 +npc = 2524 Fanellaman 0 0 +npc = 2525 tbwcu_jagbakoba_multinpc 1 0 +npc = 2526 Jagbakoba 0 0 +npc = 2527 Jagbakoba 0 0 +npc = 2528 tbwcu_murcaily_multinpc 1 0 +npc = 2529 Murcaily 0 0 +npc = 2530 Murcaily 0 0 +npc = 2531 tbwcu_rionasta_multinpc 1 0 +npc = 2532 Rionasta 0 0 +npc = 2533 Rionasta 0 0 +npc = 2534 Mahogany 0 0 +npc = 2535 Teak 0 0 +npc = 2536 Niles 0 0 +npc = 2537 Miles 0 0 +npc = 2538 Giles 0 0 +npc = 2539 Cap'n_Hand 0 0 +npc = 2540 Dr_Jekyll 0 0 +npc = 2541 Mr_Hyde 14 0 +npc = 2542 Mr_Hyde 29 0 +npc = 2543 Mr_Hyde 49 0 +npc = 2544 Mr_Hyde 79 0 +npc = 2545 Mr_Hyde 120 0 +npc = 2546 Mr_Hyde 159 0 +npc = 2547 Dr_Ford 0 0 +npc = 2548 Blackjack_seller 0 0 +npc = 2549 Ali_the_dyer 0 0 +npc = 2550 Dwarven_Miner 0 0 +npc = 2551 Dwarven_Miner 0 0 +npc = 2552 Dwarven_Miner 0 0 +npc = 2553 Blast_Furnace_Foreman 0 0 +npc = 2554 Tin_ore 0 0 +npc = 2555 Copper_ore 0 0 +npc = 2556 Iron_ore 0 0 +npc = 2557 Mithril_ore 0 0 +npc = 2558 Adamantite_ore 0 0 +npc = 2559 Runite_ore 0 0 +npc = 2560 Silver_ore 0 0 +npc = 2561 Gold_ore 0 0 +npc = 2562 Coal 0 0 +npc = 2563 Perfect_gold_ore 0 0 +npc = 2564 Ordan 0 0 +npc = 2565 Jorzik 0 0 +npc = 2566 Wise_Old_Man 0 0 +npc = 2567 Wise_Old_Man 0 0 +npc = 2568 Banker 0 0 +npc = 2569 Banker 0 0 +npc = 2570 Banker 0 0 +npc = 2571 Market_Guard 20 0 +npc = 2572 Olivia 0 0 +npc = 2573 Watchman 253 0 +npc = 2574 Bank_guard 0 0 +npc = 2575 Purepker895 52 0 +npc = 2576 Qutiedoll 16 0 +npc = 2577 1337sp34kr 63 0 +npc = 2578 Elfinlocks 87 0 +npc = 2579 Cool_Mom227 27 0 +npc = 2580 Bernald 0 0 +npc = 2581 Ellamaria 0 0 +npc = 2582 Trolley 0 0 +npc = 2583 Trolley 0 0 +npc = 2584 Trolley 0 0 +npc = 2585 garden_trolley 1 0 +npc = 2586 Billy,_a_guard_of_Falador 0 0 +npc = 2587 Bob,_another_guard_of_Falador 0 0 +npc = 2588 Brother_Althric 0 0 +npc = 2589 PKMaster0036 87 0 +npc = 2590 King_Roald 0 0 +npc = 2591 TzHaar-Mej 103 100 +npc = 2592 TzHaar-Mej 103 100 +npc = 2593 TzHaar-Mej 103 100 +npc = 2594 TzHaar-Mej 103 100 +npc = 2595 TzHaar-Mej 103 100 +npc = 2596 TzHaar-Mej 103 100 +npc = 2597 TzHaar-Mej 103 100 +npc = 2598 TzHaar-Hur 74 90 +npc = 2599 TzHaar-Hur 74 90 +npc = 2600 TzHaar-Hur 74 90 +npc = 2601 TzHaar-Hur 74 90 +npc = 2602 TzHaar-Hur 74 90 +npc = 2603 TzHaar-Hur 74 90 +npc = 2604 TzHaar-Xil 133 110 +npc = 2605 TzHaar-Xil 133 110 +npc = 2606 TzHaar-Xil 133 110 +npc = 2607 TzHaar-Xil 133 110 +npc = 2608 TzHaar-Xil 133 110 +npc = 2609 TzHaar-Xil 133 110 +npc = 2610 TzHaar-Ket 149 110 +npc = 2611 TzHaar-Ket 149 110 +npc = 2612 TzHaar-Ket 149 110 +npc = 2613 TzHaar-Ket 149 110 +npc = 2614 TzHaar-Ket 149 110 +npc = 2615 TzHaar-Ket 149 110 +npc = 2616 TzHaar-Ket 149 110 +npc = 2617 TzHaar-Mej-Jal 0 110 +npc = 2618 TzHaar-Mej-Kah 0 110 +npc = 2619 TzHaar-Ket-Zuh 0 110 +npc = 2620 TzHaar-Hur-Tel 0 110 +npc = 2621 TzHaar-Hur-Koz 0 110 +npc = 2622 TzHaar-Hur-Lek 0 110 +npc = 2623 TzHaar-Mej-Roh 0 100 +npc = 2624 TzHaar-Ket 0 100 +npc = 2625 TzHaar-Ket 0 100 +npc = 2626 Rocks 1 0 +npc = 2627 Tz-Kih 22 20 +npc = 2628 Tz-Kih 22 20 +npc = 2629 Tz-Kek 45 60 +npc = 2630 Tz-Kek 45 60 +npc = 2631 Tok-Xil 90 100 +npc = 2632 Tok-Xil 90 100 +npc = 2740 Tok-Xil 90 100 +npc = 2745 Tztok-Jad 702 700 +npc = 2803 Lizard 42 180 +npc = 2880 Dagannoth_Fledgeling 70 185 +npc = 2881 Dagannoth_Supreme 303 2000 +npc = 2882 Dagannoth_Prime 303 2000 +npc = 2883 Dagannoth_Rex 303 2000 +npc = 2885 Giant_Rock_Crab 137 220 +npc = 2892 Spinolyp 76 200 +npc = 2919 Agrith Naar 99 3000 +npc = 3070 Skeletal_Wyvern 140 380 +npc = 3200 Chaos_Elemental 305 800 +npc = 3340 Giant_Mole 230 550 +npc = 3500 Gelatinnoth_Mother 130 260 +npc = 3494 Flambeed 200 1300 +npc = 3499 Gelatinnoth_Mother 600 1000 +npc = 2783 Dark_Beast 182 350 +``` diff --git a/potato-cactus.cabal b/potato-cactus.cabal index 808f42e..160b4cf 100644 --- a/potato-cactus.cabal +++ b/potato-cactus.cabal @@ -12,7 +12,7 @@ synopsis: A 2005-era Runescape server engine. -- bug-reports: -- The license under which the package is released. --- license: GPLv3 +license: GPL-3.0-only author: Kento Lauzon maintainer: kento.lauzon@ligature.ca @@ -32,6 +32,9 @@ executable potato-cactus PotatoCactus.Client.ClientUpdate PotatoCactus.Client.GameObjectUpdate.EncodeGameObjectUpdate PotatoCactus.Client.GameObjectUpdate.GameObjectUpdateDiff + PotatoCactus.Client.GroundItemsUpdate.EncodeGroundItemsUpdate + PotatoCactus.Client.GroundItemsUpdate.GroundItemsUpdateDiff + PotatoCactus.Client.Interface.EncodeInterfaceUpdate PotatoCactus.Client.LocalEntityList PotatoCactus.Client.PlayerInit PotatoCactus.Interop.ScriptEngineProcess @@ -40,11 +43,15 @@ executable potato-cactus PotatoCactus.Network.InboundPacketMapper PotatoCactus.Network.Packets.In.ButtonClickPacket PotatoCactus.Network.Packets.In.ChatMessagePacket + PotatoCactus.Network.Packets.In.ContinueDialoguePacket + PotatoCactus.Network.Packets.In.DropItemPacket PotatoCactus.Network.Packets.In.EquipItemPacket PotatoCactus.Network.Packets.In.ItemContainerClickPacket + PotatoCactus.Network.Packets.In.ItemOnObjectPacket PotatoCactus.Network.Packets.In.NpcActionPacket PotatoCactus.Network.Packets.In.NpcAttackPacket PotatoCactus.Network.Packets.In.ObjectActionPacket + PotatoCactus.Network.Packets.In.PickupGroundItemPacket PotatoCactus.Network.Packets.In.PlayerCommandPacket PotatoCactus.Network.Packets.In.PlayerWalkPacket PotatoCactus.Network.Packets.Packet @@ -52,9 +59,16 @@ executable potato-cactus PotatoCactus.Network.Packets.Opcodes PotatoCactus.Network.Packets.PacketLengths PotatoCactus.Network.Packets.Out.AddObjectPacket + PotatoCactus.Network.Packets.Out.AddGroundItemPacket + PotatoCactus.Network.Packets.Out.ChatboxInterfacePacket PotatoCactus.Network.Packets.Out.ChatboxMessagePacket PotatoCactus.Network.Packets.Out.ClearChunkObjectsPacket + PotatoCactus.Network.Packets.Out.CloseInterfacesPacket PotatoCactus.Network.Packets.Out.InitializePlayerPacket + PotatoCactus.Network.Packets.Out.InterfaceAnimationPacket + PotatoCactus.Network.Packets.Out.InterfaceChatheadPacket + PotatoCactus.Network.Packets.Out.InterfacePacket + PotatoCactus.Network.Packets.Out.InterfaceTextPacket PotatoCactus.Network.Packets.Out.LoadMapRegionPacket PotatoCactus.Network.Packets.Out.UpdateFriendsListStatusPacket PotatoCactus.Network.Packets.Out.UpdateItemContainerPacket @@ -76,9 +90,11 @@ executable potato-cactus PotatoCactus.Network.Packets.Out.PlayerUpdate.PlayerUpdatePacket PotatoCactus.Network.Packets.Out.PlayerUpdate.EncodeSecondaryHitUpdateBlock PotatoCactus.Network.Packets.Out.PlayerSettingsPackets + PotatoCactus.Network.Packets.Out.RemoveGroundItemPacket PotatoCactus.Network.Packets.Out.RemoveObjectPacket PotatoCactus.Network.Packets.Out.SetPlacementReferencePacket PotatoCactus.Network.Packets.Out.TabInterfacePacket + PotatoCactus.Network.Packets.Out.WalkableInterfacePacket PotatoCactus.Network.ClientHandler PotatoCactus.Network.Binary PotatoCactus.Login.LoginHandler @@ -86,6 +102,9 @@ executable potato-cactus PotatoCactus.Game.Combat.CombatEntity PotatoCactus.Game.Combat.Hit PotatoCactus.Game.Entity.Animation.Animation + PotatoCactus.Game.Entity.EntityData + PotatoCactus.Game.Entity.GroundItem.GroundItem + PotatoCactus.Game.Entity.GroundItem.GroundItemCollection PotatoCactus.Game.Entity.Interaction.Interaction PotatoCactus.Game.Entity.Interaction.State PotatoCactus.Game.Entity.Interaction.Target @@ -105,11 +124,13 @@ executable potato-cactus PotatoCactus.Game.Interface.FriendsList PotatoCactus.Game.Interface.GameTabs PotatoCactus.Game.Interface.InterfaceButtonDispatch + PotatoCactus.Game.Interface.InterfaceController PotatoCactus.Game.Interface.PlayerInteraction PotatoCactus.Game.Interface.PlayerSettings PotatoCactus.Game.ItemContainer PotatoCactus.Game.Message.EquipItemMessagePayload PotatoCactus.Game.Message.GameChannelMessage + PotatoCactus.Game.Message.ItemOnObjectPayload PotatoCactus.Game.Message.ObjectClickPayload PotatoCactus.Game.Message.RegisterClientPayload PotatoCactus.Game.Movement.Direction @@ -130,6 +151,8 @@ executable potato-cactus PotatoCactus.Game.PlayerUpdate.PlayerAnimationDefinitions PotatoCactus.Game.PlayerUpdate.ProcessPlayerUpdate PotatoCactus.Game.Position + PotatoCactus.Game.Scripting.Actions.CreateInterface + PotatoCactus.Game.Scripting.Actions.ScriptInvocation PotatoCactus.Game.Scripting.Actions.SpawnNpcRequest PotatoCactus.Game.Scripting.Bridge.BridgeMessage PotatoCactus.Game.Scripting.Bridge.Communication @@ -140,6 +163,8 @@ executable potato-cactus PotatoCactus.Game.Scripting.Bridge.Serialization.Models.CombatDto PotatoCactus.Game.Scripting.Bridge.Serialization.Models.CombatTargetDto PotatoCactus.Game.Scripting.Bridge.Serialization.Models.CommandDto + PotatoCactus.Game.Scripting.Bridge.Serialization.Models.DropItemDto + PotatoCactus.Game.Scripting.Bridge.Serialization.Models.EntityDataDto PotatoCactus.Game.Scripting.Bridge.Serialization.Models.GameObjectDto PotatoCactus.Game.Scripting.Bridge.Serialization.Models.InteractionDto PotatoCactus.Game.Scripting.Bridge.Serialization.Models.ItemContainerDto @@ -151,16 +176,17 @@ executable potato-cactus PotatoCactus.Game.Scripting.Bridge.Serialization.Models.PlayerDto PotatoCactus.Game.Scripting.Bridge.Serialization.Models.PositionDto PotatoCactus.Game.Scripting.Bridge.Serialization.Models.WorldDto + PotatoCactus.Game.Scripting.BuiltinGameEventProcessor PotatoCactus.Game.Scripting.Events.ApplyScriptActionResult PotatoCactus.Game.Scripting.Events.CreateGameEvents PotatoCactus.Game.Scripting.Events.NpcEvents PotatoCactus.Game.Scripting.Events.PlayerEvents - PotatoCactus.Game.Scripting.MockScriptInteractions PotatoCactus.Game.Scripting.ProcessTickUpdates PotatoCactus.Game.Scripting.ScriptUpdates PotatoCactus.Game.Skills PotatoCactus.Game.Typing PotatoCactus.Game.World + PotatoCactus.Game.World.CallbackScheduler PotatoCactus.Game.World.EntityPositionFinder PotatoCactus.Game.World.MobList PotatoCactus.Game.World.Selectors @@ -185,6 +211,6 @@ executable potato-cactus test-suite tests type: exitcode-stdio-1.0 main-is: Main.hs - build-depends: HUnit ^>=1.6, base ^>=4.14.3.0, network, bytestring, utf8-string, binary, binary-strict, time, containers + build-depends: HUnit ^>=1.6, base ^>=4.14.3.0, network, bytestring, utf8-string, binary, binary-strict, time, containers, aeson hs-source-dirs: tests, app default-language: Haskell2010 \ No newline at end of file diff --git a/pyrightconfig.json b/pyrightconfig.json new file mode 100644 index 0000000..c898cc4 --- /dev/null +++ b/pyrightconfig.json @@ -0,0 +1,36 @@ +{ + "include": [ + "script" + ], + + "exclude": [ + "**/__pycache__" + ], + + "defineConstant": { + "DEBUG": true + }, + + // "stubPath": "src/stubs", + "venv": "venv", + + "reportMissingImports": true, + "reportMissingTypeStubs": false, + + "pythonVersion": "3.10", + "pythonPlatform": "Linux", + + "executionEnvironments": [ + { + "root": "scripts", + "pythonVersion": "3.10", + "pythonPlatform": "Linux", + "extraPaths": [ + "script-engine/" + ] + }, + { + "root": "." + } + ] +} diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..7d2c609 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +isort==5.12.0 +tomli==2.0.1 +yapf==0.33.0 diff --git a/script-engine/main.py b/script-engine/main.py index c1e2d17..34b638e 100644 --- a/script-engine/main.py +++ b/script-engine/main.py @@ -1,7 +1,7 @@ import sys -from potato_cactus.internal.messages.dispatcher import Dispatcher -from potato_cactus.internal.messages.decode import decode_inbound +from potato_cactus.internal.messages.decode import decode_inbound +from potato_cactus.internal.messages.dispatcher import Dispatcher dispatcher = Dispatcher() # time.sleep(10) diff --git a/script-engine/potato_cactus/api/actions.py b/script-engine/potato_cactus/api/actions.py index fbc5f41..de5a107 100644 --- a/script-engine/potato_cactus/api/actions.py +++ b/script-engine/potato_cactus/api/actions.py @@ -1,6 +1,9 @@ -from typing import Literal, Tuple, Union +from typing import Callable, Dict, List, Literal, Optional, Tuple, Union + +from potato_cactus import get_context +from potato_cactus.api.dto.interface import InterfaceElement from potato_cactus.api.dto.position import Position -from potato_cactus.api.dto.object import GameObject +from potato_cactus.api.dto.script_invocation import ScriptInvocation class ScriptAction(object): @@ -20,52 +23,85 @@ def ServerPrintMessage(msg: str) -> ScriptAction: return ScriptAction("serverPrintMessage", {"msg": msg}) -def NpcQueueWalk(npcIndex: int, position: Union[Position, Tuple[int, int, int]]) -> ScriptAction: - return ScriptAction("npcQueueWalk", {"npcIndex": npcIndex, - "position": _map_position(position)}) - - -def SpawnGameObject(obj: GameObject) -> ScriptAction: # AddGameObject [Added] - return ScriptAction("addGameObject", { - "op": "add", - "id": obj.id, - "position": _map_position(obj.position), - "objectType": obj.objectType, - "facingDirection": obj.facingDirection +def NpcQueueWalk( + npcIndex: int, position: Union[Position, Tuple[int, int, + int]]) -> ScriptAction: + return ScriptAction("npcQueueWalk", { + "npcIndex": npcIndex, + "position": _map_position(position) }) -def RemoveGameObject(obj: GameObject) -> ScriptAction: # AddGameObject [Removed] - return ScriptAction("addGameObject", { - "op": "remove", - "id": obj.id, - "position": _map_position(obj.position), - "objectType": obj.objectType, - "facingDirection": obj.facingDirection - }) +def NpcSetAnimation( + npcIndex: int, + animationId: int, + delay: int = 0, + priority: Literal["high", "normal", "low"] = "normal") -> ScriptAction: + return ScriptAction( + "npcSetAnimation", { + "npcIndex": npcIndex, + "animationId": animationId, + "delay": delay, + "priority": priority + }) + + +def SetPlayerAnimation( + playerIndex: int, + animationId: int, + delay: int = 0, + priority: Literal["high", "normal", "low"] = "normal") -> ScriptAction: + return ScriptAction( + "setPlayerAnimation", { + "playerIndex": playerIndex, + "animationId": animationId, + "delay": delay, + "priority": priority + }) -def NpcSetAnimation(npcIndex: int, animationId: int, delay: int = 0, - priority: Literal["high", "normal", "low"] = "normal") -> ScriptAction: - return ScriptAction("npcSetAnimation", { - "npcIndex": npcIndex, - "animationId": animationId, - "delay": delay, - "priority": priority - }) - def NpcSetForcedChat(npcIndex: int, message: str) -> ScriptAction: return ScriptAction("npcSetForcedChat", { "npcIndex": npcIndex, "message": message }) -def SpawnNpc(npcId: int, position: Union[Position, Tuple[int,int,int]], respawnDelay=None) -> ScriptAction: - return ScriptAction("spawnNpc", { - "npcId": npcId, - "position": _map_position(position), - "respawnDelay": respawnDelay or -1 - }) + +def SpawnNpc(npcId: int, + position: Union[Position, Tuple[int, int, int]], + respawnDelay=None) -> ScriptAction: + return ScriptAction( + "spawnNpc", { + "npcId": npcId, + "position": _map_position(position), + "respawnDelay": respawnDelay or -1 + }) + + +def SpawnGameObject(objectId: int, position: Union[Position, Tuple[int, int, + int]], + objectType: int, facingDirection: int) -> ScriptAction: + return ScriptAction( + "spawnObject", { + "objectId": objectId, + "position": _map_position(position), + "objectType": objectType, + "facingDirection": facingDirection + }) + + +def RemoveGameObject(objectId: int, + position: Union[Position, Tuple[int, int, int]], + objectType: int, + facingDirection: int = 0) -> ScriptAction: + return ScriptAction( + "removeObject", { + "objectId": objectId, + "position": _map_position(position), + "objectType": objectType, + "facingDirection": facingDirection + }) + def SendMessage(playerIndex: int, text: str) -> ScriptAction: return ScriptAction("sendMessage", { @@ -73,13 +109,126 @@ def SendMessage(playerIndex: int, text: str) -> ScriptAction: "text": text }) -def SetPlayerPosition(playerIndex: int, position: Union[Position, Tuple[int, int, int]]) -> ScriptAction: + +def SetPlayerPosition( + playerIndex: int, + position: Union[Position, Tuple[int, int, int]]) -> ScriptAction: return ScriptAction("setPlayerPosition", { "playerIndex": playerIndex, "position": _map_position(position) }) + +def InvokeScript(callback: Union[Callable[[], List[ScriptAction]], ScriptInvocation], + delay: int = 1) -> ScriptAction: + if isinstance(callback, Callable): + callback = ScriptInvocation(callback) + + return ScriptAction("invokeScript", { + "f": callback.f, + "args": callback.args, + "delay": delay + }) + + +def CreateInterface( + playerIndex: int, + type: Literal["standard", "input", "walkable"], + elements: List[InterfaceElement], + onClose: Optional[ScriptInvocation] = None, + callbacks: Optional[Dict[int, + ScriptInvocation]] = None) -> ScriptAction: + return ScriptAction( + "createInterface", { + "type": + type, + "playerIndex": + playerIndex, + "elements": + list(map(lambda e: e.serialize(), elements)), + "onClose": + onClose.__dict__ if onClose else None, + "callbacks": [(k, v.__dict__) + for k, v in callbacks.items()] if callbacks else [] + }) + + +def ClearStandardInterface(playerIndex: int) -> ScriptAction: + return ScriptAction("clearStandardInterface", {"playerIndex": playerIndex}) + + +def SetPlayerEntityData(playerIndex: int, key: str, + val: Union[int, str, bool]) -> ScriptAction: + return ScriptAction("setPlayerEntityData", { + "playerIndex": playerIndex, + "key": key, + "val": val + }) + + +def GiveItem(playerIndex: int, itemId: int, quantity: int = 1) -> ScriptAction: + return ScriptAction("giveItem", { + "playerIndex": playerIndex, + "itemId": itemId, + "quantity": quantity + }) + + +def SubtractItem(playerIndex: int, + itemId: int, + quantity: int = 1) -> ScriptAction: + return ScriptAction("subtractItem", { + "playerIndex": playerIndex, + "itemId": itemId, + "quantity": quantity + }) + + +def RemoveItemStack(playerIndex: int, itemId: int, index: int) -> ScriptAction: + return ScriptAction("removeItemStack", { + "playerIndex": playerIndex, + "itemId": itemId, + "index": index + }) + + +def SpawnGroundItem(itemId: int, + quantity: int, + position: Union[Position, Tuple[int, int, int]], + player: Optional[str] = None, + despawn_delay: int = 100) -> ScriptAction: + return ScriptAction( + "spawnGroundItem", { + "itemId": itemId, + "quantity": quantity, + "position": _map_position(position), + "player": player, + "despawnTime": get_context().world.tick + despawn_delay + }) + + +def RemoveGroundItem(itemId: int, + quantity: int, + position: Union[Position, Tuple[int, int, int]], + removedByPlayer: Optional[int] = None) -> ScriptAction: + return ScriptAction( + "removeGroundItem", + { + "itemId": itemId, + "quantity": quantity, + "position": _map_position(position), + "removedByPlayer": + removedByPlayer # For removing scoped items and giving to player + }) + + def _map_position(position: Union[Position, Tuple[int, int, int]]) -> dict: - if isinstance(position, Position): - return {"x": position.x, "y": position.y, "z": position.z} + if hasattr(position, "x") or isinstance(position, Position): + return { + "x": position.x, # type: ignore + "y": position.y, # type: ignore + "z": + position.z # type: ignore + } + return {"x": position[0], "y": position[1], "z": position[2]} diff --git a/script-engine/potato_cactus/api/dto/interaction.py b/script-engine/potato_cactus/api/dto/interaction.py index 76a7d96..7d9b3ae 100644 --- a/script-engine/potato_cactus/api/dto/interaction.py +++ b/script-engine/potato_cactus/api/dto/interaction.py @@ -1,4 +1,4 @@ -from typing import Generic, TypeVar, Literal, Optional +from typing import Generic, Literal, Optional, TypeVar from potato_cactus.api.dto.position import Position @@ -17,6 +17,15 @@ class ObjectInteractionTarget(object): actionIndex: int +class ItemOnObjectInteractionTarget(object): + type: Literal["itemOnObject"] + objectId: int + position: Position + itemId: int + itemIndex: int + interfaceId: int + + class NpcAttackInteractionTarget(object): type: Literal["npcAttack"] npcIndex: int @@ -26,3 +35,10 @@ class NpcInteractionTarget(object): type: Literal["npc"] npcIndex: int actionIndex: int + + +class GroundItemInteractionTarget(object): + type: Literal["groundItem"] + itemId: int + position: Position + quantity: int diff --git a/script-engine/potato_cactus/api/dto/interface.py b/script-engine/potato_cactus/api/dto/interface.py new file mode 100644 index 0000000..02be566 --- /dev/null +++ b/script-engine/potato_cactus/api/dto/interface.py @@ -0,0 +1,64 @@ +from abc import ABC, abstractmethod + + +class InterfaceElement(ABC): + + @abstractmethod + def serialize(self) -> dict: + raise NotImplementedError + + +class ChatboxRootWindowElement(InterfaceElement): + + def __init__(self, widgetId: int): + self.widgetId = widgetId + + def serialize(self) -> dict: + return {"type": "chatboxRoot", "widgetId": self.widgetId} + + +class TextElement(InterfaceElement): + + def __init__(self, widgetId: int, msg: str): + self._widgetId = widgetId + self._msg = msg + + def serialize(self) -> dict: + return {"type": "text", "widgetId": self._widgetId, "msg": self._msg} + + +class NpcChatheadElement(InterfaceElement): + + def __init__(self, widgetId: int, npcId: int) -> None: + self.widgetId = widgetId + self.npcId = npcId + + def serialize(self) -> dict: + return { + "type": "npcChathead", + "widgetId": self.widgetId, + "npcId": self.npcId + } + + +class PlayerChatheadElement(InterfaceElement): + + def __init__(self, widgetId: int) -> None: + self.widgetId = widgetId + + def serialize(self) -> dict: + return {"type": "playerChathead", "widgetId": self.widgetId} + + +class ModelAnimationElement(InterfaceElement): + + def __init__(self, widgetId: int, animationId: int) -> None: + self.widgetId = widgetId + self.animationId = animationId + + def serialize(self) -> dict: + return { + "type": "modelAnimation", + "widgetId": self.widgetId, + "animationId": self.animationId + } diff --git a/script-engine/potato_cactus/api/dto/player.py b/script-engine/potato_cactus/api/dto/player.py index ba9af44..8a49e37 100644 --- a/script-engine/potato_cactus/api/dto/player.py +++ b/script-engine/potato_cactus/api/dto/player.py @@ -14,3 +14,4 @@ class Player(object): interaction: PlayerInteraction inventory: List[Optional[ItemStack]] equipment: List[Optional[ItemStack]] + entityData: dict diff --git a/script-engine/potato_cactus/api/dto/script_invocation.py b/script-engine/potato_cactus/api/dto/script_invocation.py new file mode 100644 index 0000000..95a65b4 --- /dev/null +++ b/script-engine/potato_cactus/api/dto/script_invocation.py @@ -0,0 +1,15 @@ +from typing import Callable, Iterable, Optional, Tuple, Union + + +class ScriptInvocation(object): + + def __init__(self, + f: Union[Callable, Tuple[str, str], str], + args: Optional[Iterable[Union[str, int]]] = None) -> None: + if isinstance(f, Callable): + self.f = f"{f.__module__}.{f.__name__}" + elif isinstance(f, tuple): + self.f = ".".join(f) + else: + self.f = f + self.args = args or tuple() diff --git a/script-engine/potato_cactus/api/events.py b/script-engine/potato_cactus/api/events.py index 7663025..ea5bb5c 100644 --- a/script-engine/potato_cactus/api/events.py +++ b/script-engine/potato_cactus/api/events.py @@ -2,8 +2,12 @@ from typing import List, Optional from potato_cactus.api.dto.combat import CombatTarget -from potato_cactus.api.dto.interaction import NpcAttackInteractionTarget, NpcInteractionTarget, ObjectInteractionTarget, \ - PlayerInteraction +from potato_cactus.api.dto.interaction import (GroundItemInteractionTarget, + ItemOnObjectInteractionTarget, + NpcAttackInteractionTarget, + NpcInteractionTarget, + ObjectInteractionTarget, + PlayerInteraction) class GameEvent(str, Enum): @@ -11,11 +15,14 @@ class GameEvent(str, Enum): NpcEntityTickEvent = "NpcEntityTickEvent" NpcInteractionEvent = "NpcInteractionEvent" ObjectInteractionEvent = "ObjectInteractionEvent" + ItemOnObjectInteractionEvent = "ItemOnObjectInteractionEvent" NpcAttackInteractionEvent = "NpcAttackInteractionEvent" # TODO - Can this be consolidated with NpcAttackEvent? - keotl 2023-04-27 + PickupItemInteractionEvent = "PickupItemInteractionEvent" PlayerAttackEvent = "PlayerAttackEvent" PlayerCommandEvent = "PlayerCommandEvent" NpcAttackEvent = "NpcAttackEvent" NpcDeadEvent = "NpcDeadEvent" + DropItemEvent = "DropItemEvent" class ObjectInteractionEventPayload(object): @@ -23,6 +30,11 @@ class ObjectInteractionEventPayload(object): interaction: PlayerInteraction[ObjectInteractionTarget] +class ItemOnObjectInteractionEventPayload(object): + playerIndex: int + interaction: PlayerInteraction[ItemOnObjectInteractionTarget] + + class NpcInteractionEventPayload(object): playerIndex: int interaction: PlayerInteraction[NpcInteractionTarget] @@ -33,20 +45,34 @@ class NpcAttackInteractionEventPayload(object): interaction: PlayerInteraction[NpcAttackInteractionTarget] +class PickupItemInteractionEventPayload(object): + playerIndex: int + interaction: PlayerInteraction[GroundItemInteractionTarget] + + class PlayerAttackEventPayload(object): playerIndex: int target: Optional[CombatTarget] + class PlayerCommandEventPayload(object): playerIndex: int command: str args: List[str] + class NpcAttackEventPayload(object): npcIndex: int target: Optional[CombatTarget] +class DropItemEventPayload(object): + playerIndex: int + widgetId: int + itemId: int + index: int + + class NpcReference(object): npcIndex: int diff --git a/script-engine/potato_cactus/api/exception.py b/script-engine/potato_cactus/api/exception.py new file mode 100644 index 0000000..f6a22f5 --- /dev/null +++ b/script-engine/potato_cactus/api/exception.py @@ -0,0 +1,2 @@ +class ScriptException(Exception): + pass diff --git a/script-engine/potato_cactus/helper/__init__.py b/script-engine/potato_cactus/helper/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/script-engine/potato_cactus/helper/dialogue.py b/script-engine/potato_cactus/helper/dialogue.py new file mode 100644 index 0000000..883cb45 --- /dev/null +++ b/script-engine/potato_cactus/helper/dialogue.py @@ -0,0 +1,288 @@ +import random +from abc import ABC, abstractmethod +from typing import Callable, Dict, List, Literal, Optional, Tuple, Union + +from potato_cactus import get_context +from potato_cactus.api.actions import (ClearPlayerInteraction, + ClearStandardInterface, CreateInterface, + ScriptAction, ScriptInvocation) +from potato_cactus.api.dto.interface import (ChatboxRootWindowElement, + ModelAnimationElement, + NpcChatheadElement, + PlayerChatheadElement, + TextElement) +from potato_cactus.api.exception import ScriptException +from potato_cactus.internal.util.script_invoker import invoke_script + + +class DialogueCallbackRef(object): + """Describes a callable with the playerIndex as the sole argument. + Use to chain dialogue nodes or circumvent circular reference between dialogue nodes.""" + + def __init__( + self, f: Union[Callable[[int], List[ScriptAction]], + Tuple[str, str]]) -> None: + self.f = f + + +class DialogueScreen(ABC): + + @abstractmethod + def configure(self, playerIndex: int, + onNext: Optional[ScriptInvocation]) -> List[ScriptAction]: + raise NotImplementedError + + def onContinue( + self) -> Optional[Union[ScriptInvocation, DialogueCallbackRef]]: + return None + + +class DialogueNode(object): + + def __init__(self, + module_name: str, + variable_name: str, + screens: Optional[List[DialogueScreen]] = None): + self._screens: List[DialogueScreen] = screens or [] + self._script_ref = (module_name, variable_name) + + def add(self, screen: DialogueScreen) -> "DialogueNode": + self._screens.append(screen) + return self + + @property + def ref(self) -> DialogueCallbackRef: + return DialogueCallbackRef(self._script_ref) + + def __call__(self, playerIndex: int, stage: int = 0) -> List[ScriptAction]: + actions: List[ScriptAction] = [] + if 0 < stage < len(self._screens) + 1: + prev_screen = self._screens[stage - 1] + callback = prev_screen.onContinue() + if callback: + if isinstance(callback, ScriptInvocation): + actions.extend(invoke_script(callback)) + elif isinstance(callback, DialogueCallbackRef): + actions.extend( + invoke_script( + ScriptInvocation(callback.f, (playerIndex, )))) + if -1 < stage < len(self._screens): + actions.extend(self._screens[stage].configure( + playerIndex, + ScriptInvocation(self._script_ref, (playerIndex, stage + 1)))) + return actions + + +Expression = Literal["default", "skeptical", "angry", "worried", "sleepy", + "laughing", "sad", "angry_silent", "angry_laughing"] + + +class NpcDialogueScreen(DialogueScreen): + + def __init__(self, + npcId: int, + npcName: str, + text: List[str], + expression: Expression = "default", + onContinue: Optional[Union[ScriptInvocation, + DialogueCallbackRef]] = None): + self._npcId = npcId + self._npcName = npcName + self._text = text + self._expression: Expression = expression + self._onContinue = onContinue + + def configure(self, playerIndex: int, + onNext: Optional[ScriptInvocation]) -> List[ScriptAction]: + return [ + CreateInterface( + playerIndex, + "standard", + [ + NpcChatheadElement(self._root_window_id + 1, self._npcId), + ModelAnimationElement( + self._root_window_id + 1, + _expression_animation_id(self._expression)), + TextElement(self._root_window_id + 2, self._npcName), + ] + [ + TextElement(self._root_window_id + 3 + i, l) + for i, l in enumerate(self._text) + ] + [ChatboxRootWindowElement(self._root_window_id)], + onClose=onNext), + ] + + def onContinue( + self) -> Optional[Union[ScriptInvocation, DialogueCallbackRef]]: + return self._onContinue + + @property + def _root_window_id(self) -> int: + try: + return _NPC_ROOT_WINDOW_ID[len(self._text)] + except KeyError: + raise ScriptException( + f"Unsupported number of lines '{len(self._text)}' when configuring dialogue." + ) + + +_NPC_ROOT_WINDOW_ID = {0: 4882, 1: 4882, 2: 4887, 3: 4893, 4: 4900} + + +class PlayerDialogueScreen(DialogueScreen): + + def __init__(self, + text: List[str], + expression: Expression = "default", + onContinue: Optional[Union[ScriptInvocation, + DialogueCallbackRef]] = None, + playerNameOverride: Optional[str] = None): + self._text = text + self._expression: Expression = expression + self._onContinue = onContinue + self._playerNameOverride = playerNameOverride + + def configure(self, playerIndex: int, + onNext: Optional[ScriptInvocation]) -> List[ScriptAction]: + return [ + CreateInterface( + playerIndex, + "standard", + [ + PlayerChatheadElement(self._root_window_id + 1), + ModelAnimationElement( + self._root_window_id + 1, + _expression_animation_id(self._expression)), + TextElement(self._root_window_id + 2, + self._player_name(playerIndex)), + ] + [ + TextElement(self._root_window_id + 3 + i, l) + for i, l in enumerate(self._text) + ] + [ChatboxRootWindowElement(self._root_window_id)], + onClose=onNext) + ] + + def onContinue( + self) -> Optional[Union[ScriptInvocation, DialogueCallbackRef]]: + return self._onContinue + + def _player_name(self, playerIndex: int) -> str: + if self._playerNameOverride is not None: + return self._playerNameOverride + player = get_context().find_player_by_index(playerIndex) + if player is not None: + return player.username + + return "Unknown" + + @property + def _root_window_id(self) -> int: + try: + return _PLAYER_ROOT_WINDOW_ID[len(self._text)] + except KeyError: + raise ScriptException( + f"Unsupported number of lines '{len(self._text)}' when configuring dialogue." + ) + + +_PLAYER_ROOT_WINDOW_ID = {0: 968, 1: 968, 2: 973, 3: 979, 4: 986} + + +def _expression_animation_id(expression: Expression) -> int: + try: + return random.choice(_EXPRESSION_ANIMATION_IDS[expression]) + except KeyError: + raise ScriptException( + f"Unknown expression type '{expression}' when configuring dialogue." + ) + + +_EXPRESSION_ANIMATION_IDS: Dict[Expression, List[int]] = { + "skeptical": [588, 589, 590, 591], + "angry": [592, 593, 594, 595], + "worried": [596, 597, 598, 599], + "sleepy": [600, 601, 602, 603], + "laughing": [605, 606, 607, 608], + "sad": [610, 611, 612, 613], + "default": [591], + "angry_silent": [604], + "angry_laughing": [609] +} + + +class OptionsDialogueScreen(DialogueScreen): + + def __init__(self, choices: List[Tuple[str, Union[Callable[[], None], + ScriptInvocation, + DialogueCallbackRef]]]): + self._choices = choices + + def configure(self, playerIndex: int, + onNext: Optional[ScriptInvocation]) -> List[ScriptAction]: + return [ + CreateInterface( + playerIndex, + "standard", + [ + TextElement(self._root_window_id + 2 + i, l) + for i, (l, _) in enumerate(self._choices) + ] + [ChatboxRootWindowElement(self._root_window_id) + ], # type: ignore + onClose=onNext, + callbacks=self._callbacks(playerIndex)) + ] + + @property + def _root_window_id(self): + try: + return _OPTIONS_ROOT_WINDOW_ID[len(self._choices)] + except KeyError: + raise ScriptException( + f"Unknown number of dialogue options '{len(self._choices)}'.") + + def _callbacks(self, playerIndex: int): + try: + button_ids = _OPTIONS_BUTTON_IDS[len(self._choices)] + return { + k: + ScriptInvocation(_close_and_invoke_script, (self._callback( + playerIndex, script_ref).f, playerIndex)) + for k, (_, script_ref) in zip(button_ids, self._choices) + } + except KeyError: + raise ScriptException( + f"Unknown number of dialogue options '{len(self._choices)}.'") + + def _callback( + self, playerIndex: int, callbackRef: Union[Callable[[], None], + ScriptInvocation, + DialogueCallbackRef] + ) -> ScriptInvocation: + if isinstance(callbackRef, ScriptInvocation): + return callbackRef + if isinstance(callbackRef, DialogueCallbackRef): + return ScriptInvocation(callbackRef.f, (playerIndex, )) + return ScriptInvocation(callbackRef) + + +def _close_and_invoke_script(script_descriptor: str, playerIndex: int): + return [ + ClearStandardInterface(playerIndex), + *invoke_script(ScriptInvocation(script_descriptor, (playerIndex, ))) + ] + + +_OPTIONS_ROOT_WINDOW_ID = {2: 14443, 3: 2469, 4: 8207, 5: 8219} +_OPTIONS_BUTTON_IDS = { + 2: [14445, 14446], + 3: [2471, 2472, 2473], + 4: [8209, 8210, 8211, 8212], + 5: [8221, 8222, 8223, 8224, 8225] +} + + +def start_dialogue(node_ref: DialogueCallbackRef, + playerIndex: int) -> List[ScriptAction]: + return [ + *invoke_script(ScriptInvocation(node_ref.f, (playerIndex, ))), + ClearPlayerInteraction(playerIndex) + ] diff --git a/script-engine/potato_cactus/internal/registry/__init__.py b/script-engine/potato_cactus/internal/registry/__init__.py index f28e58f..f21a210 100644 --- a/script-engine/potato_cactus/internal/registry/__init__.py +++ b/script-engine/potato_cactus/internal/registry/__init__.py @@ -1,11 +1,14 @@ import functools from collections import defaultdict -from typing import Tuple, Optional, Union +from typing import Optional, Tuple, Union from potato_cactus.api.events import GameEvent -def EventHandler(event: GameEvent, **options): # TODO - Define options format - keotl 2023-04-18 +def EventHandler( + event: GameEvent, + **options): # TODO - Define options format - keotl 2023-04-18 + def decorator(f): Registry.INSTANCE.register(event, options, f) @@ -27,21 +30,26 @@ def __init__(self): def register(self, event: GameEvent, options: dict, handler): self._content[self.key(event, options)].append(handler) - def get_handlers(self, key_elems: Tuple[Optional[Union[str, int]], ...]): + def get_handlers(self, key_elems: Tuple[Optional[Union[str, int]], ...]): return self._content.get(key_elems) or [] @staticmethod - def key(event: GameEvent, options: dict) -> Tuple[Optional[Union[str, int]], ...]: + def key(event: GameEvent, + options: dict) -> Tuple[Optional[Union[str, int]], ...]: if event == GameEvent.ServerInitEvent: return event, elif event == GameEvent.NpcEntityTickEvent: return event, options.get("npcId") elif event == GameEvent.ObjectInteractionEvent: return event, options.get("objectId") + elif event == GameEvent.ItemOnObjectInteractionEvent: + return event, options.get("objectId") elif event == GameEvent.NpcInteractionEvent: return event, options.get("npcId") elif event == GameEvent.NpcAttackInteractionEvent: return event, options.get("npcId") + elif event == GameEvent.PickupItemInteractionEvent: + return event, options.get("itemId") elif event == GameEvent.PlayerAttackEvent: return event, elif event == GameEvent.NpcAttackEvent: @@ -50,8 +58,9 @@ def key(event: GameEvent, options: dict) -> Tuple[Optional[Union[str, int]], ... return event, options.get("npcId") elif event == GameEvent.PlayerCommandEvent: return event, options.get("command") + elif event == GameEvent.DropItemEvent: + return event, options.get("itemId") return "unassigned", -Registry.INSTANCE = Registry() - +Registry.INSTANCE = Registry() diff --git a/script-engine/potato_cactus/internal/util/script_invoker.py b/script-engine/potato_cactus/internal/util/script_invoker.py new file mode 100644 index 0000000..d84a77a --- /dev/null +++ b/script-engine/potato_cactus/internal/util/script_invoker.py @@ -0,0 +1,23 @@ +import importlib +from typing import List + +from potato_cactus.api.actions import ScriptAction +from potato_cactus.api.dto.script_invocation import ScriptInvocation +from potato_cactus.internal.util.stderr_logger import Logger + + +def invoke_script(script: ScriptInvocation) -> List[ScriptAction]: + try: + modulename, function = script.f.rsplit(".", 1) + module = importlib.import_module(modulename) + res = getattr(module, function)(*script.args) + return res + except KeyboardInterrupt as e: + raise e + except Exception as e: + _logger.warning( + f"Unhandled exception while invoking script '{script.f}'. {e}") + return [] + + +_logger = Logger("script_invoker") diff --git a/script-engine/potato_cactus/internal/util/wrapped_dict.py b/script-engine/potato_cactus/internal/util/wrapped_dict.py index 9845dd1..83121ce 100644 --- a/script-engine/potato_cactus/internal/util/wrapped_dict.py +++ b/script-engine/potato_cactus/internal/util/wrapped_dict.py @@ -11,6 +11,12 @@ def __getattr__(self, item): return WrappedList(raw) return raw + def __getitem__(self, key: str): + return self._content[key] + + def get(self, *args, **kwargs): + return self._content.get(*args, **kwargs) + def __setitem__(self, __name: str, __value) -> None: self._content[__name] = __value @@ -22,6 +28,7 @@ def __str__(self): class WrappedList(object): + def __init__(self, content: list): self._content = content diff --git a/script-engine/potato_cactus/internal/worker/simple_worker.py b/script-engine/potato_cactus/internal/worker/simple_worker.py index 967ec89..d0854a7 100644 --- a/script-engine/potato_cactus/internal/worker/simple_worker.py +++ b/script-engine/potato_cactus/internal/worker/simple_worker.py @@ -1,15 +1,18 @@ +import importlib import pkgutil from inspect import signature -from typing import List, cast, Tuple, Optional, Union +from typing import List, Optional, Tuple, Union, cast from potato_cactus.api.dto.world import World from potato_cactus.api.events import GameEvent from potato_cactus.internal.impl.context_impl import ContextImpl from potato_cactus.internal.messages.inbound import InboundMessage -from potato_cactus.internal.messages.outbound import internal_processingComplete +from potato_cactus.internal.messages.outbound import \ + internal_processingComplete from potato_cactus.internal.registry import Registry -from . import OutboundMessageSender, WorkerHandle + from ..util.stderr_logger import Logger +from . import OutboundMessageSender, WorkerHandle class SimpleWorker(WorkerHandle): @@ -30,10 +33,27 @@ def dispatch(self, message: InboundMessage): elif message.op == "updateWorld": world = cast(World, message.body) ContextImpl.INSTANCE.set_world(world) - + elif message.op == "invokeScript": + try: + modulename, function = message.body.event.rsplit(".", 1) + module = importlib.import_module(modulename) + res = getattr(module, function)(*message.body.body) + for action in res or []: + self._sender.send(action.__dict__) + except KeyboardInterrupt as e: + raise e + except Exception as e: + _logger.error( + f"Unhandled exception while invoking script '{message.body.event}'. {e}" + ) elif message.op == "gameEvent": _enrich_message(ContextImpl.INSTANCE, message.body) # type: ignore handlers = Registry.INSTANCE.get_handlers(_event_key(message.body)) + if not handlers: + default_handler_key = _default_event_handler_key(message.body) + if default_handler_key: + handlers = Registry.INSTANCE.get_handlers( + default_handler_key) for h in handlers: try: sig = signature(h) @@ -43,12 +63,14 @@ def dispatch(self, message: InboundMessage): res = h(message.body.body) else: res = h() - for action in res: + for action in res or []: self._sender.send(action.__dict__) except KeyboardInterrupt as e: raise e except Exception as e: - _logger.error(f"Unhandled exception while running script {message.body.event}. {e}") + _logger.error( + f"Unhandled exception while invoking script {message.body.event}. {e}" + ) def _event_key(payload) -> Tuple[Optional[Union[str, int]], ...]: @@ -56,10 +78,14 @@ def _event_key(payload) -> Tuple[Optional[Union[str, int]], ...]: return GameEvent.ServerInitEvent, if payload.event == GameEvent.ObjectInteractionEvent: return GameEvent.ObjectInteractionEvent, payload.body.interaction.target.objectId + if payload.event == GameEvent.ItemOnObjectInteractionEvent: + return GameEvent.ItemOnObjectInteractionEvent, payload.body.interaction.target.objectId if payload.event == GameEvent.NpcInteractionEvent: return GameEvent.NpcInteractionEvent, payload.body.interaction.target.npcId if payload.event == GameEvent.NpcAttackInteractionEvent: return GameEvent.NpcAttackInteractionEvent, payload.body.interaction.target.npcId + if payload.event == GameEvent.PickupItemInteractionEvent: + return GameEvent.PickupItemInteractionEvent, payload.body.interaction.target.itemId if payload.event == GameEvent.NpcAttackEvent: return GameEvent.NpcAttackEvent, payload.body.npcId if payload.event == GameEvent.PlayerAttackEvent: @@ -70,18 +96,50 @@ def _event_key(payload) -> Tuple[Optional[Union[str, int]], ...]: return GameEvent.NpcEntityTickEvent, payload.body.npcId if payload.event == GameEvent.PlayerCommandEvent: return GameEvent.PlayerCommandEvent, payload.body.command + if payload.event == GameEvent.DropItemEvent: + return GameEvent.DropItemEvent, payload.body.itemId - _logger.warning(f"Got event '{payload.event}' with an unconfigured key. No script will be invoked.") + _logger.warning( + f"Got event '{payload.event}' with an unconfigured key. No script will be invoked." + ) return "unassined", +def _default_event_handler_key( + payload) -> Optional[Tuple[Optional[Union[str, int]], ...]]: + if payload.event == GameEvent.ObjectInteractionEvent: + return GameEvent.ObjectInteractionEvent, "default" + if payload.event == GameEvent.ItemOnObjectInteractionEvent: + return GameEvent.ItemOnObjectInteractionEvent, "default" + if payload.event == GameEvent.NpcInteractionEvent: + return GameEvent.NpcInteractionEvent, "default" + if payload.event == GameEvent.NpcAttackInteractionEvent: + return GameEvent.NpcAttackInteractionEvent, "default" + if payload.event == GameEvent.PickupItemInteractionEvent: + return GameEvent.PickupItemInteractionEvent, "default" + if payload.event == GameEvent.NpcAttackEvent: + return GameEvent.NpcAttackEvent, "default" + if payload.event == GameEvent.NpcDeadEvent: + return GameEvent.NpcDeadEvent, "default" + if payload.event == GameEvent.NpcEntityTickEvent: + return GameEvent.NpcEntityTickEvent, "default" + if payload.event == GameEvent.PlayerCommandEvent: + return GameEvent.PlayerCommandEvent, "default" + if payload.event == GameEvent.DropItemEvent: + return GameEvent.DropItemEvent, "default" + + return None + + def _enrich_message(context: ContextImpl, payload): if payload.event == GameEvent.NpcInteractionEvent: # payload.body: NpcInteractionEventPayload - payload.body.interaction.target["npcId"] = _find_npc_id(context, payload.body.interaction.target.npcIndex) + payload.body.interaction.target["npcId"] = _find_npc_id( + context, payload.body.interaction.target.npcIndex) if payload.event == GameEvent.NpcAttackInteractionEvent: # payload.body: NpcAttackInteractionEventPayload - payload.body.interaction.target["npcId"] = _find_npc_id(context, payload.body.interaction.target.npcIndex) + payload.body.interaction.target["npcId"] = _find_npc_id( + context, payload.body.interaction.target.npcIndex) if payload.event == GameEvent.NpcAttackEvent: # payload.body: NpcAttackEventPayload payload.body["npcId"] = _find_npc_id(context, payload.body.npcIndex) @@ -99,4 +157,5 @@ def _find_npc_id(context: ContextImpl, npc_index: int) -> int: return npc.definitionId return 0 + _logger = Logger("ScriptWorker") diff --git a/scripts/builtin/commands/animation_command.py b/scripts/builtin/commands/animation_command.py new file mode 100644 index 0000000..f744fe5 --- /dev/null +++ b/scripts/builtin/commands/animation_command.py @@ -0,0 +1,15 @@ +from potato_cactus import Context, EventHandler, GameEvent +from potato_cactus.api.actions import SendMessage, SetPlayerAnimation +from potato_cactus.api.events import PlayerCommandEventPayload + + +@EventHandler(GameEvent.PlayerCommandEvent, command="animation") +@EventHandler(GameEvent.PlayerCommandEvent, command="anim") +def animation_command(e: PlayerCommandEventPayload, context: Context): + player = context.find_player_by_index(e.playerIndex) + if player is None: + return [] + if len(e.args) < 1: + return [SendMessage(e.playerIndex, f"Usage: ::{e.command} ")] + return [SetPlayerAnimation(e.playerIndex, int(e.args[0]))] + diff --git a/scripts/builtin/commands/item_command.py b/scripts/builtin/commands/item_command.py new file mode 100644 index 0000000..fc73760 --- /dev/null +++ b/scripts/builtin/commands/item_command.py @@ -0,0 +1,19 @@ +from potato_cactus import Context, EventHandler, GameEvent +from potato_cactus.api.actions import GiveItem, SendMessage +from potato_cactus.api.events import PlayerCommandEventPayload + + +@EventHandler(GameEvent.PlayerCommandEvent, command="item") +def on_command(e: PlayerCommandEventPayload, context: Context): + player = context.find_player_by_index(e.playerIndex) + if player is None: + return [] + if len(e.args) < 1: + return [SendMessage(e.playerIndex, f"Usage: ::{e.command} [quantity]")] + quantity = 1 + item_id = int(e.args[0]) + if len(e.args) >= 2: + quantity = int(e.args[1]) + return [ + GiveItem(e.playerIndex, item_id, quantity) + ] diff --git a/scripts/builtin/commands/npc_command.py b/scripts/builtin/commands/npc_command.py new file mode 100644 index 0000000..d24ea62 --- /dev/null +++ b/scripts/builtin/commands/npc_command.py @@ -0,0 +1,14 @@ +from potato_cactus import Context, EventHandler, GameEvent +from potato_cactus.api.actions import SendMessage, SpawnNpc +from potato_cactus.api.events import PlayerCommandEventPayload + + +@EventHandler(GameEvent.PlayerCommandEvent, command="npc") +@EventHandler(GameEvent.PlayerCommandEvent, command="spawnNpc") +def on_command(e: PlayerCommandEventPayload, context: Context): + player = context.find_player_by_index(e.playerIndex) + if player is None: + return [] + if len(e.args) < 1: + return [SendMessage(e.playerIndex, f"Usage: ::{e.command} ")] + return [SpawnNpc(int(e.args[0]), player.movement.position)] diff --git a/scripts/builtin/commands/position_command.py b/scripts/builtin/commands/position_command.py index d5ad896..82f0661 100644 --- a/scripts/builtin/commands/position_command.py +++ b/scripts/builtin/commands/position_command.py @@ -1,6 +1,6 @@ -from potato_cactus import EventHandler, Context +from potato_cactus import Context, EventHandler from potato_cactus.api.actions import SendMessage -from potato_cactus.api.events import PlayerCommandEventPayload, GameEvent +from potato_cactus.api.events import GameEvent, PlayerCommandEventPayload @EventHandler(GameEvent.PlayerCommandEvent, command="position") @@ -11,5 +11,8 @@ def on_command(e: PlayerCommandEventPayload, context: Context): return [] return [ - SendMessage(e.playerIndex, - f"Position [{player.movement.position.x}, {player.movement.position.y}, {player.movement.position.z}]")] + SendMessage( + e.playerIndex, + f"Position [{player.movement.position.x}, {player.movement.position.y}, {player.movement.position.z}]" + ) + ] diff --git a/scripts/builtin/environment/__init__.py b/scripts/builtin/environment/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/scripts/builtin/environment/ladder.py b/scripts/builtin/environment/ladder.py new file mode 100644 index 0000000..80f2ed3 --- /dev/null +++ b/scripts/builtin/environment/ladder.py @@ -0,0 +1,69 @@ +from potato_cactus import EventHandler, GameEvent, get_context +from potato_cactus.api.actions import (ClearPlayerInteraction, InvokeScript, + SendMessage, SetPlayerAnimation, + SetPlayerPosition) +from potato_cactus.api.dto.script_invocation import ScriptInvocation +from potato_cactus.api.events import ObjectInteractionEventPayload +from potato_cactus.helper.dialogue import (DialogueCallbackRef, DialogueNode, + OptionsDialogueScreen, + start_dialogue) + + +@EventHandler(GameEvent.ObjectInteractionEvent, objectId=1747) +def on_interact_ladder_up(e: ObjectInteractionEventPayload): + return go_up(e.playerIndex) + + +@EventHandler(GameEvent.ObjectInteractionEvent, objectId=1746) +def on_interact_ladder_down(e: ObjectInteractionEventPayload): + return go_down(e.playerIndex) + + +@EventHandler(GameEvent.ObjectInteractionEvent, objectId=1748) +def on_interact_ladder_up_down(e: ObjectInteractionEventPayload): + if not e.interaction.target: + pass + elif e.interaction.target.actionIndex == 1: + return start_dialogue(ladder_dialogue.ref, e.playerIndex) + elif e.interaction.target.actionIndex == 2: + return go_up(e.playerIndex) + elif e.interaction.target.actionIndex == 3: + return go_down(e.playerIndex) + + return [ClearPlayerInteraction(e.playerIndex)] + + +def go_up(playerIndex: int): + player = get_context().find_player_by_index(playerIndex) + if player is None: + return [] + pos = player.movement.position + return [ + InvokeScript(ScriptInvocation(set_position_delayed, + (playerIndex, pos.x, pos.y, pos.z + 1)), + delay=1), + SetPlayerAnimation(playerIndex, 828), + ClearPlayerInteraction(playerIndex) + ] + + +def go_down(playerIndex: int): + player = get_context().find_player_by_index(playerIndex) + if player is None: + return [] + pos = player.movement.position + return [ + InvokeScript(ScriptInvocation(set_position_delayed, + (playerIndex, pos.x, pos.y, pos.z - 1)), + delay=1), + SetPlayerAnimation(playerIndex, 828), + ClearPlayerInteraction(playerIndex) + ] + + +def set_position_delayed(playerIndex: int, x: int, y: int, z: int): + return [SetPlayerPosition(playerIndex, (x, y, z))] + +ladder_dialogue = DialogueNode(__name__, "ladder_dialogue") \ + .add(OptionsDialogueScreen([("Climb up", DialogueCallbackRef(go_up)), + ("Climb down", DialogueCallbackRef(go_down))])) diff --git a/scripts/builtin/environment/persistent_door.py b/scripts/builtin/environment/persistent_door.py new file mode 100644 index 0000000..92df3a2 --- /dev/null +++ b/scripts/builtin/environment/persistent_door.py @@ -0,0 +1,39 @@ +from potato_cactus import Context, EventHandler, GameEvent +from potato_cactus.api.actions import ClearPlayerInteraction, SpawnGameObject +from potato_cactus.api.events import ObjectInteractionEventPayload + + +@EventHandler(GameEvent.ObjectInteractionEvent, objectId=1519) # Large door right +@EventHandler(GameEvent.ObjectInteractionEvent, objectId=1516) # Large door left +@EventHandler(GameEvent.ObjectInteractionEvent, objectId=1551) # Gate right # TODO - handle both parts as one - keotl 2023-05-13 +@EventHandler(GameEvent.ObjectInteractionEvent, objectId=1553) # Gate left +@EventHandler(GameEvent.ObjectInteractionEvent, objectId=1536) +@EventHandler(GameEvent.ObjectInteractionEvent, objectId=1530) +def on_door_open(e: ObjectInteractionEventPayload, context: Context): + if (e.interaction.target is None): + return [ClearPlayerInteraction(e.playerIndex)] + + return [ + SpawnGameObject( + e.interaction.target.objectId + 1, e.interaction.target.position, + 0, 2 + ), # TODO - Calculate facing from static object set - keotl 2023-05-13 + ClearPlayerInteraction(e.playerIndex) + ] + + +@EventHandler(GameEvent.ObjectInteractionEvent, objectId=1520) +@EventHandler(GameEvent.ObjectInteractionEvent, objectId=1517) +@EventHandler(GameEvent.ObjectInteractionEvent, objectId=1552) # Gate right +@EventHandler(GameEvent.ObjectInteractionEvent, objectId=1554) # Gate left +def on_door_close(e: ObjectInteractionEventPayload, context: Context): + if (e.interaction.target is None): + return [ClearPlayerInteraction(e.playerIndex)] + + return [ + SpawnGameObject( + e.interaction.target.objectId - 1, e.interaction.target.position, + 0, 1 + ), # TODO - Calculate facing from static object set - keotl 2023-05-13 + ClearPlayerInteraction(e.playerIndex) + ] diff --git a/scripts/builtin/interaction/__init__.py b/scripts/builtin/interaction/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/scripts/builtin/interaction/drop_item_handler.py b/scripts/builtin/interaction/drop_item_handler.py new file mode 100644 index 0000000..8cfdd2d --- /dev/null +++ b/scripts/builtin/interaction/drop_item_handler.py @@ -0,0 +1,25 @@ +from potato_cactus import Context, EventHandler, GameEvent +from potato_cactus.api.actions import (RemoveItemStack, SendMessage, + SpawnGroundItem) +from potato_cactus.api.events import DropItemEventPayload + + +@EventHandler(GameEvent.DropItemEvent, itemId="default") +def drop_item_default_handler(e: DropItemEventPayload, ctx: Context): + player = ctx.find_player_by_index(e.playerIndex) + if player is None: + return [] + if e.widgetId != 3214: + return [ + SendMessage(e.playerIndex, + "Got an illegal widgetId for a drop packet.") + ] + stack = player.inventory[e.index] + if stack is None: + return [] + + return [ + RemoveItemStack(e.playerIndex, e.itemId, e.index), + SpawnGroundItem(e.itemId, stack.quantity, player.movement.position, + player.username) + ] diff --git a/scripts/builtin/interaction/pickup_item_handler.py b/scripts/builtin/interaction/pickup_item_handler.py new file mode 100644 index 0000000..e7d1bc3 --- /dev/null +++ b/scripts/builtin/interaction/pickup_item_handler.py @@ -0,0 +1,21 @@ +from potato_cactus.api.actions import ClearPlayerInteraction, RemoveGroundItem +from potato_cactus.api.context import Context +from potato_cactus.api.events import (GameEvent, + PickupItemInteractionEventPayload) +from potato_cactus.internal.registry import EventHandler + + +@EventHandler(GameEvent.PickupItemInteractionEvent, itemId="default") +def on_pickup_item(e: PickupItemInteractionEventPayload, ctx: Context): + player = ctx.find_player_by_index(e.playerIndex) + + if player is None or e.interaction.target is None: + return [ClearPlayerInteraction(e.playerIndex)] + + return [ + RemoveGroundItem(e.interaction.target.itemId, + e.interaction.target.quantity, + e.interaction.target.position, + removedByPlayer=e.playerIndex), + ClearPlayerInteraction(e.playerIndex) + ] diff --git a/scripts/example/cooks_assistant/__init__.py b/scripts/example/cooks_assistant/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/scripts/example/cooks_assistant/chicken.py b/scripts/example/cooks_assistant/chicken.py new file mode 100644 index 0000000..17d06d0 --- /dev/null +++ b/scripts/example/cooks_assistant/chicken.py @@ -0,0 +1,54 @@ +from example.cooks_assistant.dairy_cow import SendMessage +from potato_cactus import Context, EventHandler, GameEvent +from potato_cactus.api.actions import NpcQueueWalk, SpawnGroundItem, SpawnNpc +from potato_cactus.api.dto.position import Position +from potato_cactus.api.events import (NpcDeadEventPayload, + NpcEntityTickEventPayload) +from potato_cactus.helper.dialogue import random + +BONES = 526 +FEATHER = 314 +EGG = 1944 +CHICKEN = 41 +WANDER_AREA = [Position(3185, 3276, 0), Position(3191, 3278, 0)] + + +@EventHandler(GameEvent.ServerInitEvent) +def spawn_chicken(): + return [SpawnNpc(CHICKEN, (3190, 3277, 0), 100)] + + +@EventHandler(GameEvent.NpcEntityTickEvent, npcId=CHICKEN) +def chicken_wander(e: NpcEntityTickEventPayload, context: Context): + npc = context.find_npc_by_index(e.npcIndex) + if npc is None: + return [] + + if npc.combat.target is None and _should_wander(): + return [NpcQueueWalk(e.npcIndex, _within_wander_radius())] + + return [] + + +@EventHandler(GameEvent.NpcDeadEvent, npcId=CHICKEN) +def on_death(e: NpcDeadEventPayload, context: Context): + npc = context.find_npc_by_index(e.npcIndex) + if npc is None: + return [] + + return [ + # TODO - Set NPC animation - keotl 2023-06-12 + SpawnGroundItem(EGG, 1, npc.movement.position), + SpawnGroundItem(BONES, 1, npc.movement.position), + SpawnGroundItem(FEATHER, 5, npc.movement.position), + ] + + +def _should_wander(): + return random.random() > 0.9 + + +def _within_wander_radius(): + return (random.randint(WANDER_AREA[0].x, WANDER_AREA[1].x), + random.randint(WANDER_AREA[0].y, + WANDER_AREA[1].y), WANDER_AREA[0].z) diff --git a/scripts/example/cooks_assistant/cook.py b/scripts/example/cooks_assistant/cook.py new file mode 100644 index 0000000..443de4b --- /dev/null +++ b/scripts/example/cooks_assistant/cook.py @@ -0,0 +1,105 @@ +from potato_cactus import Context, EventHandler, GameEvent, get_context +from potato_cactus.api.actions import (GiveItem, SendMessage, + SetPlayerEntityData, SpawnNpc, + SubtractItem) +from potato_cactus.api.dto.player import Player +from potato_cactus.api.events import NpcInteractionEventPayload +from potato_cactus.helper.dialogue import (DialogueCallbackRef, DialogueNode, + NpcDialogueScreen, + PlayerDialogueScreen, + start_dialogue) + +NPC_ID = 278 +BUCKET_OF_MILK = 1927 +EGG = 1944 +POT_OF_FLOUR = 1933 +CAKE = 1895 +COINS = 617 + + +def start_quest(playerIndex: int): + player = get_context().find_player_by_index(playerIndex) + if player is None: + return [] + + return [SetPlayerEntityData(playerIndex, "cooks_assistant.started", True)] + + +def subtract_ingredients(playerIndex: int): + return [ + SubtractItem(playerIndex, BUCKET_OF_MILK), + SubtractItem(playerIndex, EGG), + SubtractItem(playerIndex, POT_OF_FLOUR) + ] + + +def complete_quest(playerIndex: int): + return [ + SetPlayerEntityData(playerIndex, "cooks_assistant.completed", True), + GiveItem(playerIndex, CAKE), + GiveItem(playerIndex, COINS, 100) + ] + + +dialogue_root = DialogueNode(__name__, "dialogue_root") +dialogue_root.add(NpcDialogueScreen(NPC_ID, "Cook", ["Oh no! What am I supposed to do?"], "worried")) \ + .add(PlayerDialogueScreen(["What's wrong?"])) \ + .add(NpcDialogueScreen(NPC_ID, "Cook", ["I need to bake a cake for the Duke's ", "birthday but I haven't got any ingredients!"])) \ + .add(PlayerDialogueScreen(["Maybe I could help just this one time..."], "skeptical")) \ + .add(NpcDialogueScreen(NPC_ID, "Cook", ["That you so much adventurer!", + "Bring me a pot of flour, an egg and a bucket of milk", + "and I shall reward you handsomely."], "default", onContinue=DialogueCallbackRef(start_quest))) + +in_progress_dialogue = DialogueNode(__name__, "in_progress_dialogue") +in_progress_dialogue.add( + NpcDialogueScreen(NPC_ID, "Cook", [ + "Have you got the ingredients I requested?", + "Come back to me when you have a pot of flour", + "an egg and a bucket of milk." + ])) + +completion_dialogue = DialogueNode(__name__, "completion_dialogue") +completion_dialogue.add( + PlayerDialogueScreen( + ["Here are the ingredients you requested."], + onContinue=DialogueCallbackRef(subtract_ingredients))).add( + NpcDialogueScreen(NPC_ID, + "Cook", [ + "Thank you so much adventurer!", + "Please take this as a reward." + ], + onContinue=DialogueCallbackRef(complete_quest))) + +post_completion_dialogue = DialogueNode(__name__, "post_completion_dialogue") +post_completion_dialogue.add( + NpcDialogueScreen(NPC_ID, "Cook", [ + "Thank you for your help adventurer.", + "One day you might also become a cook yourself!" + ])) + + +@EventHandler(GameEvent.NpcInteractionEvent, npcId=NPC_ID) +def on_talk(e: NpcInteractionEventPayload, context: Context): + player = context.find_player_by_index(e.playerIndex) + if player is None: + return [] + if player.entityData.get("cooks_assistant.completed"): + return start_dialogue(post_completion_dialogue.ref, e.playerIndex) + if not player.entityData.get("cooks_assistant.started"): + return start_dialogue(dialogue_root.ref, e.playerIndex) + + if has_item(BUCKET_OF_MILK, player) and has_item( + POT_OF_FLOUR, player) and has_item(EGG, player): + return start_dialogue(completion_dialogue.ref, e.playerIndex) + + return start_dialogue(in_progress_dialogue.ref, e.playerIndex) + + +def has_item(item_id: int, player: Player) -> bool: + return any(stack is not None and stack.itemId == item_id + for stack in player.inventory) + + +@EventHandler(GameEvent.ServerInitEvent) +def onServerInit(e): + return [SpawnNpc(NPC_ID, (3208, 3213, 0))] diff --git a/scripts/example/cooks_assistant/dairy_cow.py b/scripts/example/cooks_assistant/dairy_cow.py new file mode 100644 index 0000000..ea8fe7a --- /dev/null +++ b/scripts/example/cooks_assistant/dairy_cow.py @@ -0,0 +1,68 @@ +from example.windmill.miller import GameEvent +from potato_cactus import Context, EventHandler +from potato_cactus.api.actions import (ClearPlayerInteraction, GiveItem, + InvokeScript, SendMessage, + SetPlayerAnimation, SpawnGroundItem, + SubtractItem) +from potato_cactus.api.dto.player import Player +from potato_cactus.api.events import (ItemOnObjectInteractionEventPayload, + ObjectInteractionEventPayload) + +DAIRY_COW = 8689 +EMPTY_BUCKET = 1925 +BUCKET_OF_MILK = 1927 +ANIMATION_ID = 645 + + +@EventHandler(GameEvent.ItemOnObjectInteractionEvent, objectId=DAIRY_COW) +def on_use_item_interaction(e: ItemOnObjectInteractionEventPayload, + context: Context): + player = context.find_player_by_index(e.playerIndex) + if player is None or e.interaction.target is None: + return [] + + if e.interaction.target.itemId != EMPTY_BUCKET: + return [ + SendMessage(e.playerIndex, "Nothing interesting happens."), + ClearPlayerInteraction(e.playerIndex) + ] + + return _do_interaction(player) + + +@EventHandler(GameEvent.ObjectInteractionEvent, objectId=DAIRY_COW) +def on_object_interaction(e: ObjectInteractionEventPayload, context: Context): + player = context.find_player_by_index(e.playerIndex) + if player is None or e.interaction.target is None: + return [] + return _do_interaction(player) + + +def _do_interaction(player: Player): + if not _has_empty_bucket(player): + return [ + ClearPlayerInteraction(player.serverIndex), + SetPlayerAnimation(player.serverIndex, ANIMATION_ID), + SendMessage(player.serverIndex, + "You need an empty bucket to gather the milk.") + ] + + return [ + SubtractItem(player.serverIndex, EMPTY_BUCKET, 1), + GiveItem(player.serverIndex, BUCKET_OF_MILK, 1), + SendMessage(player.serverIndex, "You gather milk in the bucket."), + ClearPlayerInteraction(player.serverIndex) + ] + + +def _has_empty_bucket(player: Player) -> bool: + return any(stack is not None and stack.itemId == EMPTY_BUCKET + for stack in player.inventory) + + +@EventHandler(GameEvent.ServerInitEvent) +def bucket_spawn(): + return [ + SpawnGroundItem(EMPTY_BUCKET, 1, (3168, 3319, 0)), + InvokeScript(bucket_spawn, delay=100) + ] diff --git a/scripts/example/hans.py b/scripts/example/hans.py index 1546cec..31822e5 100644 --- a/scripts/example/hans.py +++ b/scripts/example/hans.py @@ -1,19 +1,31 @@ -from potato_cactus import EventHandler, GameEvent, Context -from potato_cactus.api.actions import SpawnNpc, NpcSetForcedChat, ClearPlayerInteraction +from potato_cactus import Context, EventHandler, GameEvent +from potato_cactus.api.actions import SpawnNpc from potato_cactus.api.dto.position import Position -from potato_cactus.api.events import NpcEntityTickEventPayload, NpcInteractionEventPayload +from potato_cactus.api.events import NpcInteractionEventPayload +from potato_cactus.helper.dialogue import (DialogueNode, NpcDialogueScreen, + OptionsDialogueScreen, + PlayerDialogueScreen, + start_dialogue) +intimidation_dialogue_node = DialogueNode( + __name__, "intimidation_dialogue_node", [ + PlayerDialogueScreen(["I'm here to kill everyone in this castle!"], + "angry_laughing"), + NpcDialogueScreen(0, "Hans", ["Oh no!"], "worried") + ]) -@EventHandler(GameEvent.NpcEntityTickEvent, npcId=0) -def onNpcTick(e: NpcEntityTickEventPayload): - return [] +dialogue_root = DialogueNode(__name__, "dialogue_root") +dialogue_root.add(NpcDialogueScreen(0, "Hans", ["Hello there!"])) \ + .add(PlayerDialogueScreen(["Hello!"])) \ + .add(NpcDialogueScreen(0, "Hans", ["What can I do for you?"])) \ + .add(OptionsDialogueScreen([ + ("I'm here to kill everyone in this castle!", intimidation_dialogue_node.ref), + ("Could you say that again?", dialogue_root.ref)])) @EventHandler(GameEvent.NpcInteractionEvent, npcId=0) def onNpcInteraction(e: NpcInteractionEventPayload, context: Context): - player = context.find_player_by_index(e.playerIndex) - return [NpcSetForcedChat(e.interaction.target.npcIndex, f"Hello {player.username}!"), - ClearPlayerInteraction(e.playerIndex)] + return start_dialogue(dialogue_root.ref, e.playerIndex) @EventHandler(GameEvent.ServerInitEvent) diff --git a/scripts/example/windmill/__init__.py b/scripts/example/windmill/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/scripts/example/windmill/hopper.py b/scripts/example/windmill/hopper.py new file mode 100644 index 0000000..5bf1371 --- /dev/null +++ b/scripts/example/windmill/hopper.py @@ -0,0 +1,92 @@ +from potato_cactus import Context, EventHandler, GameEvent +from potato_cactus.api.actions import (ClearPlayerInteraction, GiveItem, + SendMessage, SetPlayerAnimation, + SetPlayerEntityData, SpawnGameObject, + SubtractItem) +from potato_cactus.api.dto.player import Player +from potato_cactus.api.events import (ItemOnObjectInteractionEventPayload, + ObjectInteractionEventPayload) + + +@EventHandler(GameEvent.ItemOnObjectInteractionEvent, objectId=2714) +def on_put_item(e: ItemOnObjectInteractionEventPayload, context: Context): + player = context.find_player_by_index(e.playerIndex) + + if player is None: + return [] + + current_grain = player.entityData.get("windmill.grain", 0) + + return [ + SubtractItem(e.playerIndex, 1947, 1), + SetPlayerEntityData(e.playerIndex, "windmill.grain", + current_grain + 1), + SendMessage(e.playerIndex, f"Current grain: {current_grain + 1}"), + SetPlayerAnimation(e.playerIndex, 832), + ClearPlayerInteraction(e.playerIndex) + ] + + +@EventHandler(GameEvent.ObjectInteractionEvent, objectId=2718) +def on_interact_controls(e: ObjectInteractionEventPayload, context: Context): + player = context.find_player_by_index(e.playerIndex) + if player is None or e.interaction.target is None: + return [] + + current_grain = player.entityData.get("windmill.grain", 0) + current_flour = player.entityData.get("windmill.flour", 0) + controls_pos = e.interaction.target.position + if current_grain + current_flour > 0: + return [ + SetPlayerEntityData(e.playerIndex, "windmill.grain", 0), + SetPlayerEntityData(e.playerIndex, "windmill.flour", + current_grain + current_flour), + SpawnGameObject( + 1782, (controls_pos.x, controls_pos.y + 1, 0), 10, 0 + ), # TODO - This object should be instanced - keotl 2023-05-13 + SendMessage(e.playerIndex, + f"Current flour: {current_grain + current_flour}"), + SetPlayerAnimation(e.playerIndex, 832), + ClearPlayerInteraction(e.playerIndex) + ] + return [ + ClearPlayerInteraction(e.playerIndex), + SetPlayerAnimation(e.playerIndex, 832) + ] + + +@EventHandler(GameEvent.ObjectInteractionEvent, objectId=1782) +def on_interact_flour_bin(e: ObjectInteractionEventPayload, context: Context): + player = context.find_player_by_index(e.playerIndex) + if player is None or e.interaction.target is None: + return [] + + current_flour = player.entityData.get("windmill.flour", 0) + if current_flour == 0: + return [ClearPlayerInteraction(e.playerIndex)] + if not _has_empty_pot(player): + return [ + ClearPlayerInteraction(e.playerIndex), + SendMessage(e.playerIndex, + "You need an empty pot to hold the flour in.") + ] + + pos = e.interaction.target.position + + return [ + SetPlayerEntityData(e.playerIndex, "windmill.flour", + current_flour - 1), + SubtractItem(e.playerIndex, 1931), + GiveItem(e.playerIndex, 1933), + SendMessage(e.playerIndex, f"Current flour: {current_flour - 1}"), + SetPlayerAnimation(e.playerIndex, 832) + ] + ([ + SpawnGameObject( + 1781, (pos.x, pos.y, pos.z), 10, + 0) # TODO - This object should be instanced - keotl 2023-05-13 + ] if current_flour == 1 else []) + + +def _has_empty_pot(player: Player) -> bool: + return any(stack is not None and stack.itemId == 1931 + for stack in player.inventory) diff --git a/scripts/example/windmill/miller.py b/scripts/example/windmill/miller.py new file mode 100644 index 0000000..9f24433 --- /dev/null +++ b/scripts/example/windmill/miller.py @@ -0,0 +1,30 @@ +from potato_cactus import EventHandler, GameEvent +from potato_cactus.api.actions import GiveItem, SpawnNpc +from potato_cactus.api.events import NpcInteractionEventPayload +from potato_cactus.helper.dialogue import (DialogueCallbackRef, DialogueNode, + NpcDialogueScreen, + PlayerDialogueScreen, + start_dialogue) + +NPC_ID = 531 + + +def give_empty_pot(playerIndex: int): + return [GiveItem(playerIndex, 1931)] + + +dialogue_root = DialogueNode(__name__, "dialogue_root") +dialogue_root.add(NpcDialogueScreen(NPC_ID, "Miller", ["Welcome to the mill."])) \ + .add(PlayerDialogueScreen(["Do you have any empty pots lying around?"])) \ + .add(NpcDialogueScreen(NPC_ID, "Miller", ["Of course. Here you go."], + onContinue=DialogueCallbackRef(give_empty_pot))) + + +@EventHandler(GameEvent.NpcInteractionEvent, npcId=NPC_ID) +def on_talk(e: NpcInteractionEventPayload): + return start_dialogue(dialogue_root.ref, e.playerIndex) + + +@EventHandler(GameEvent.ServerInitEvent) +def onServerInit(e): + return [SpawnNpc(NPC_ID, (3168, 3305, 0))] diff --git a/scripts/example/windmill/wheat.py b/scripts/example/windmill/wheat.py new file mode 100644 index 0000000..0a9ccf7 --- /dev/null +++ b/scripts/example/windmill/wheat.py @@ -0,0 +1,33 @@ +from potato_cactus import EventHandler, GameEvent +from potato_cactus.api.actions import (ClearPlayerInteraction, GiveItem, + InvokeScript, RemoveGameObject, + SetPlayerAnimation, SpawnGameObject) +from potato_cactus.api.dto.script_invocation import ScriptInvocation +from potato_cactus.api.events import ObjectInteractionEventPayload + + +@EventHandler(GameEvent.ObjectInteractionEvent, objectId=313) +@EventHandler(GameEvent.ObjectInteractionEvent, objectId=5585) +@EventHandler(GameEvent.ObjectInteractionEvent, objectId=5584) +def on_interaction(e: ObjectInteractionEventPayload): + if e.interaction.target is None: + return [ClearPlayerInteraction(e.playerIndex)] + + target = e.interaction.target + return [ + GiveItem(e.playerIndex, 1947, 1), + SetPlayerAnimation(e.playerIndex, 827), + RemoveGameObject(target.objectId, target.position, 10), + ClearPlayerInteraction(e.playerIndex), + InvokeScript( + ScriptInvocation(respawn, (target.objectId, target.position.x, + target.position.y, target.position.z)), + RESPAWN_DELAY) + ] + + +RESPAWN_DELAY = 10 + + +def respawn(objectId: int, x: int, y: int, z: int): + return [SpawnGameObject(objectId, (x, y, z), 10, 0)] diff --git a/tests/Game/GroudItemCollectionTests.hs b/tests/Game/GroudItemCollectionTests.hs new file mode 100644 index 0000000..7a89aeb --- /dev/null +++ b/tests/Game/GroudItemCollectionTests.hs @@ -0,0 +1,180 @@ +module Game.GroudItemCollectionTests where + +import PotatoCactus.Config.Constants (groundItemGlobalDespawnDelay) +import PotatoCactus.Game.Entity.GroundItem.GroundItem (GroundItem) +import qualified PotatoCactus.Game.Entity.GroundItem.GroundItem as GroundItem +import PotatoCactus.Game.Entity.GroundItem.GroundItemCollection (GroundItemCollection) +import qualified PotatoCactus.Game.Entity.GroundItem.GroundItemCollection as GroundItemCollection +import qualified PotatoCactus.Game.ItemContainer as ItemStack +import PotatoCactus.Game.Position (Position (Position), chunkX, chunkY) +import PotatoCactus.Utils.Flow ((|>)) +import Test.HUnit + +testGroundItemCollection = + TestList + [ TestCase + ( assertEqual + "findByChunk" + [item] + ( GroundItemCollection.findByChunkXYForPlayer + collection + "some player" + (chunkX itemPos, chunkY itemPos, 0) + ) + ), + TestCase + ( assertEqual + "findByChunk with player visibility includes both global and scoped items" + [scopedItem, item] + ( GroundItemCollection.findByChunkXYForPlayer + collection + "the doctor" + (chunkX itemPos, chunkY itemPos, 0) + ) + ), + TestCase + ( assertEqual + "advanceTime removes expired and transitions expired scoped items to global scope" + [transitionedScopedItem] + ( GroundItemCollection.advanceTime + collection + expiryTime + |> \c -> + GroundItemCollection.findByChunkXYForPlayer + c + "some player" + (chunkX itemPos, chunkY itemPos, 0) + ) + ), + TestCase + ( assertEqual + "advanceTime keeps non expired" + [item] + ( GroundItemCollection.advanceTime + collection + (expiryTime - 1) + |> \c -> + GroundItemCollection.findByChunkXYForPlayer + c + "some player" + (chunkX itemPos, chunkY itemPos, 0) + ) + ), + TestCase + ( assertEqual + "remove global item returns the ground item as an item stack" + (GroundItem.toItemStack item, []) + ( GroundItemCollection.remove + collection + (1234, 1, itemPos, Nothing) + |> \(i, c) -> + ( i, + GroundItemCollection.findByChunkXYForPlayer + c + "some player" + (chunkX itemPos, chunkY itemPos, 0) + ) + ) + ), + TestCase + ( assertEqual + "remove matching local items before global items" + (GroundItem.toItemStack scopedItem, [item]) + ( GroundItemCollection.remove + collection + (123, 1, itemPos, Just "the doctor") + |> \(i, c) -> + ( i, + GroundItemCollection.findByChunkXYForPlayer + c + "the doctor" + (chunkX itemPos, chunkY itemPos, 0) + ) + ) + ), + TestCase + ( assertEqual + "remove ignores out of scope items" + (ItemStack.Empty, [scopedItem, item]) + ( GroundItemCollection.remove + collection + (123, 1, itemPos, Just "some player") + |> \(i, c) -> + ( i, + GroundItemCollection.findByChunkXYForPlayer + c + "the doctor" + (chunkX itemPos, chunkY itemPos, 0) + ) + ) + ), + TestCase + ( assertEqual + "findMatchingItem finds a stack matching the user command" + (Just item) + ( GroundItemCollection.findMatchingItem + (1234, itemPos, "some player") + collection + ) + ), + TestCase + ( assertEqual + "findMatchingItem finds a local item" + (Just scopedItem) + ( GroundItemCollection.findMatchingItem + (123, itemPos, "the doctor") + collection + ) + ), + TestCase + ( assertEqual + "findMatchingItem does not find an item only visible to another player" + (Just scopedItem) + ( GroundItemCollection.findMatchingItem + (123, itemPos, "the master") + collection + ) + ) + ] + +itemPos = Position 100 100 0 + +collection :: GroundItemCollection +collection = + foldl + GroundItemCollection.insert + GroundItemCollection.create + [item, scopedItem] + +expiryTime :: Int +expiryTime = 100 + +item :: GroundItem +item = + GroundItem.GroundItem + { GroundItem.itemId = 1234, + GroundItem.quantity = 1, + GroundItem.position = itemPos, + GroundItem.player = Nothing, + GroundItem.despawnTime = expiryTime + } + +scopedItem :: GroundItem +scopedItem = + GroundItem.GroundItem + { GroundItem.itemId = 123, + GroundItem.quantity = 1, + GroundItem.position = itemPos, + GroundItem.player = Just "the doctor", + GroundItem.despawnTime = expiryTime + } + +transitionedScopedItem :: GroundItem +transitionedScopedItem = + GroundItem.GroundItem + { GroundItem.itemId = 123, + GroundItem.quantity = 1, + GroundItem.position = itemPos, + GroundItem.player = Nothing, + GroundItem.despawnTime = expiryTime + groundItemGlobalDespawnDelay + } diff --git a/tests/Game/GroundItemsUpdateDiffTests.hs b/tests/Game/GroundItemsUpdateDiffTests.hs new file mode 100644 index 0000000..4152567 --- /dev/null +++ b/tests/Game/GroundItemsUpdateDiffTests.hs @@ -0,0 +1,31 @@ +module Game.GroundItemsUpdateDiffTests where + +import PotatoCactus.Client.GroundItemsUpdate.GroundItemsUpdateDiff (GroundItemClientView (GroundItemClientView), GroundItemDiff (Added, Removed, Retained), computeDiff) +import PotatoCactus.Game.Position (Position (Position)) +import Test.HUnit + +groundItemsUpdateDiffTests :: Test +groundItemsUpdateDiffTests = + TestList + [ TestCase + ( assertEqual + "added item" + [Added item] + (computeDiff [] [item]) + ), + TestCase + ( assertEqual + "removed item" + [Removed item] + (computeDiff [item] []) + ), + TestCase + ( assertEqual + "retained item" + [Retained item] + (computeDiff [item] [item]) + ) + ] + +item :: GroundItemClientView +item = GroundItemClientView 123 1 (Position 0 0 0) diff --git a/tests/Main.hs b/tests/Main.hs index 05eef5d..21716e5 100644 --- a/tests/Main.hs +++ b/tests/Main.hs @@ -1,14 +1,16 @@ module Main where import Base37Test (testDecode, testEncode) -import BinaryTest (testByte, testByteNegate, testIntME, testMixedBitMode, testPack, testShortBE, testShortLE, testShortAdd) -import GetMonadTests (getTests) -import Test.HUnit +import BinaryTest (testByte, testByteNegate, testIntME, testMixedBitMode, testPack, testShortAdd, testShortBE, testShortLE) +import DecodeChatTests (decodeChatTests, encodeChatTests, testNibbles) +import Game.GameObjectUpdateDiffTests (testObjectDiff) +import Game.GroudItemCollectionTests (testGroundItemCollection) +import Game.GroundItemsUpdateDiffTests (groundItemsUpdateDiffTests) import Game.InterpolatePathTests (interpolatePathTests) -import DecodeChatTests (decodeChatTests, testNibbles, encodeChatTests) +import GetMonadTests (getTests) import IterableTests (testReplaceAt) import MobListTests (testMobList) -import Game.GameObjectUpdateDiffTests (testObjectDiff) +import Test.HUnit tests = TestList @@ -29,7 +31,9 @@ tests = testNibbles, testReplaceAt, testMobList, - testObjectDiff + testObjectDiff, + testGroundItemCollection, + groundItemsUpdateDiffTests ] main :: IO ()