Skip to content

Commit

Permalink
Merge pull request 'questing-interactions' (#3) from questing-interac…
Browse files Browse the repository at this point in the history
…tions into master
  • Loading branch information
keotl committed Jun 14, 2023
2 parents caaf240 + 57a5244 commit 69fadf4
Show file tree
Hide file tree
Showing 101 changed files with 5,916 additions and 222 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -81,4 +81,5 @@ cabal.project.local~

**/.idea
.pyc
__pycache__
__pycache__
venv
2 changes: 2 additions & 0 deletions .style.yapf
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[style]
INDENT_DICTIONARY_VALUE = true
12 changes: 10 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,30 @@ 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
- [x] Players can chat to each other
- [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
Expand Down
4 changes: 2 additions & 2 deletions app/PotatoCactus/Boot/GameThreadMain.hs
Original file line number Diff line number Diff line change
@@ -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)
Expand Down Expand Up @@ -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
Expand Down
33 changes: 22 additions & 11 deletions app/PotatoCactus/Client/ClientUpdate.hs
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,19 @@ 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))
import PotatoCactus.Game.Message.ObjectClickPayload (ObjectClickPayload (ObjectClickPayload))
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))
Expand All @@ -42,6 +46,7 @@ data ClientLocalState_ = ClientLocalState_
{ localPlayers :: LocalEntityList Player,
localNpcs :: LocalEntityList Npc,
gameObjects :: [DynamicObject],
groundItems :: [GroundItemClientView],
localPlayerIndex :: Int
}

Expand All @@ -50,6 +55,7 @@ defaultState =
{ localPlayers = [],
localNpcs = [],
gameObjects = [],
groundItems = [],
localPlayerIndex = -1
}

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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]
]
Original file line number Diff line number Diff line change
@@ -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]
]
44 changes: 44 additions & 0 deletions app/PotatoCactus/Client/GroundItemsUpdate/GroundItemsUpdateDiff.hs
Original file line number Diff line number Diff line change
@@ -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
40 changes: 40 additions & 0 deletions app/PotatoCactus/Client/Interface/EncodeInterfaceUpdate.hs
Original file line number Diff line number Diff line change
@@ -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)
3 changes: 3 additions & 0 deletions app/PotatoCactus/Config/Constants.hs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ tickInterval = 600 * 1000
entityViewingDistance :: Int
entityViewingDistance = 15

groundItemGlobalDespawnDelay :: Int
groundItemGlobalDespawnDelay = 100

maxPlayers :: Int
maxPlayers = 2000

Expand Down
1 change: 1 addition & 0 deletions app/PotatoCactus/Game/Definitions/ItemDefinitions.hs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
13 changes: 13 additions & 0 deletions app/PotatoCactus/Game/Entity/EntityData.hs
Original file line number Diff line number Diff line change
@@ -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
36 changes: 36 additions & 0 deletions app/PotatoCactus/Game/Entity/GroundItem/GroundItem.hs
Original file line number Diff line number Diff line change
@@ -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)
Loading

0 comments on commit 69fadf4

Please sign in to comment.