Skip to content

Commit

Permalink
Merge pull request #310 from ethanmoffat/boards
Browse files Browse the repository at this point in the history
Implement town board handling (Board packet family + dialog)
  • Loading branch information
ethanmoffat committed May 18, 2023
2 parents 10491f9 + d77ee12 commit d5befa2
Show file tree
Hide file tree
Showing 28 changed files with 910 additions and 127 deletions.
19 changes: 19 additions & 0 deletions EOLib.IO/Extensions/TileSpecExtensions.cs
@@ -0,0 +1,19 @@
using EOLib.IO.Map;

namespace EOLib.IO.Extensions
{
public static class TileSpecExtensions
{
public static bool IsBoard(this TileSpec tileSpec)
{
return tileSpec == TileSpec.Board1 ||
tileSpec == TileSpec.Board2 ||
tileSpec == TileSpec.Board3 ||
tileSpec == TileSpec.Board4 ||
tileSpec == TileSpec.Board5 ||
tileSpec == TileSpec.Board6 ||
tileSpec == TileSpec.Board7 ||
tileSpec == TileSpec.Board8;
}
}
}
10 changes: 4 additions & 6 deletions EOLib.Localization/DialogResourceID.cs
Expand Up @@ -58,14 +58,12 @@ public enum DialogResourceID
BANK_ACCOUNT_UNABLE_TO_DEPOSIT = 102,
SHOP_NOTHING_IS_FOR_SALE = 104,
//106: confirmed (AFAIK) that the client does not show this message separately from NOT_BUYING_YOUR_ITEMS message
// ReSharper disable once UnusedMember.Global
SHOP_NOT_BUYING_USED_ITEMS = 106,
SHOP_NOT_BUYING_YOUR_ITEMS = 108,
WARNING_YOU_HAVE_NOT_ENOUGH = 110,
SHOP_DOES_NOT_BUY = 112,
LOCKER_FULL_SINGLE_ITEM_MAX = 114,
//116: confirmed (AFAIK) that the client does not show a message when you hit the max (unless the server is missing a message for this)
// ReSharper disable once UnusedMember.Global
LOCKER_FULL_DIFF_ITEMS_MAX = 116,
LOCKER_DEPOSIT_GOLD_ERROR = 118,
DROP_MANY_GOLD_ON_GROUND = 120,
Expand Down Expand Up @@ -108,10 +106,10 @@ public enum DialogResourceID
GUILD_DEPOSIT_NEW_BALANCE = 194,
ITEM_IS_LORE_ITEM = 196,
ITEM_IS_CURSED_ITEM = 198,
BOARD_ERROR_NO_SUBJECT = 200,
BOARD_ERROR_NO_MESSAGE = 202,
BOARD_ERROR_MSG_NO_SUBJECT = 204,
BOARD_ERROR_SUBJECT_NO_MSG = 206,
DEPRECATED_BOARD_ERROR_NO_SUBJECT = 200,
DEPRECATED_BOARD_ERROR_NO_MESSAGE = 202,
BOARD_ERROR_NO_SUBJECT = 204,
BOARD_ERROR_NO_MESSAGE = 206,
BOARD_ERROR_TOO_MANY_MESSAGES = 208,
ITEM_CURSE_REMOVE_PROMPT = 210,
JUKEBOX_REQUESTED_RECENTLY = 212,
Expand Down
5 changes: 5 additions & 0 deletions EOLib.Localization/EOResourceID.cs
Expand Up @@ -161,6 +161,11 @@ public enum EOResourceID
SETTING_KEYBOARD_SWEDISH = 255,
SETTING_KEYBOARD_AZERTY = 256,

BOARD_TOWN_BOARD = 271,
BOARD_TOWN_BOARD_NOW_VIEWED = 272,
BOARD_LOADING_MESSAGE = 273,
BOARD_POSTING_NEW_MESSAGE = 274,

JAIL_WARNING_CANNOT_DROP_ITEMS = 275,
JAIL_WARNING_CANNOT_TRADE = 276,
JAIL_WARNING_CANNOT_USE_GLOBAL = 277,
Expand Down
70 changes: 70 additions & 0 deletions EOLib/Domain/Interact/Board/BoardActions.cs
@@ -0,0 +1,70 @@
using AutomaticTypeMapper;
using EOLib.Net;
using EOLib.Net.Communication;

namespace EOLib.Domain.Interact.Board
{
[AutoMappedType]
public class BoardActions : IBoardActions
{
private readonly IPacketSendService _packetSendService;
private readonly IBoardProvider _boardProvider;

public BoardActions(IPacketSendService packetSendService,
IBoardProvider boardProvider)
{
_packetSendService = packetSendService;
_boardProvider = boardProvider;
}

public void AddPost(string subject, string body)
{
_boardProvider.BoardId.MatchSome(boardId =>
{
var packet = new PacketBuilder(PacketFamily.Board, PacketAction.Create)
.AddShort(boardId)
.AddByte(255)
.AddBreakString(subject.Replace('y', (char)255)) // this is in EOSERV for some reason. Probably due to chunking (see Sanitization here: https://github.com/Cirras/eo-protocol/blob/master/docs/chunks.md)
.AddBreakString(body.Replace('\n', '\r')) // original EO client uses \r as newline separator. XNAControls uses \n.
.Build();
_packetSendService.SendPacket(packet);
});
}

public void DeletePost(int postId)
{
_boardProvider.BoardId.MatchSome(boardId =>
{
var packet = new PacketBuilder(PacketFamily.Board, PacketAction.Remove)
.AddShort(boardId)
.AddShort(postId)
.Build();
_packetSendService.SendPacket(packet);
});
}

public void ViewPost(int postId)
{
_boardProvider.BoardId.MatchSome(boardId =>
{
var packet = new PacketBuilder(PacketFamily.Board, PacketAction.Take)
.AddShort(boardId)
.AddShort(postId)
.Build();
_packetSendService.SendPacket(packet);
});
}
}

public interface IBoardActions
{
void AddPost(string subject, string body);

void ViewPost(int postId);

void DeletePost(int postId);
}
}
55 changes: 55 additions & 0 deletions EOLib/Domain/Interact/Board/BoardDataRepository.cs
@@ -0,0 +1,55 @@
using AutomaticTypeMapper;
using Optional;
using System.Collections.Generic;

namespace EOLib.Domain.Interact.Board
{
public interface IBoardRepository
{
Option<int> BoardId { get; set; }

HashSet<BoardPostInfo> Posts { get; set; }

Option<BoardPostInfo> ActivePost { get; set; }

Option<string> ActivePostMessage { get; set; }
}

public interface IBoardProvider
{
Option<int> BoardId { get; }

IReadOnlyCollection<BoardPostInfo> Posts { get; }

Option<BoardPostInfo> ActivePost { get; }

Option<string> ActivePostMessage { get; }
}

[AutoMappedType(IsSingleton = true)]
public class BoardDataRepository : IBoardRepository, IBoardProvider, IResettable
{
public Option<int> BoardId { get; set; }

public HashSet<BoardPostInfo> Posts { get; set; }

IReadOnlyCollection<BoardPostInfo> IBoardProvider.Posts => Posts;

public Option<BoardPostInfo> ActivePost { get; set; }

public Option<string> ActivePostMessage { get; set; }

public BoardDataRepository()
{
ResetState();
}

public void ResetState()
{
BoardId = Option.None<int>();
Posts = new HashSet<BoardPostInfo>();
ActivePost = Option.None<BoardPostInfo>();
ActivePostMessage = Option.None<string>();
}
}
}
@@ -1,7 +1,7 @@
using Amadevus.RecordGenerator;
using EOLib.IO.Map;

namespace EOLib.Domain.Board
namespace EOLib.Domain.Interact.Board
{
[Record]
public sealed partial class BoardMapEntity : IMapEntity
Expand Down
14 changes: 14 additions & 0 deletions EOLib/Domain/Interact/Board/BoardPostInfo.cs
@@ -0,0 +1,14 @@
using Amadevus.RecordGenerator;

namespace EOLib.Domain.Interact.Board
{
[Record(Features.Default | Features.ObjectEquals | Features.EquatableEquals)]
public sealed partial class BoardPostInfo
{
public int PostId { get; }

public string Author { get; }

public string Subject { get; }
}
}
2 changes: 2 additions & 0 deletions EOLib/Domain/Login/LoginActions.cs
Expand Up @@ -113,6 +113,8 @@ public async Task<int> RequestCharacterLogin(Character.Character character)
.WithStats(data.CharacterStats);

_playerInfoRepository.IsFirstTimePlayer = data.FirstTimePlayer;
_playerInfoRepository.PlayerHasAdminCharacter = _characterSelectorRepository.Characters.Any(x => x.AdminLevel > 0);

_currentMapStateRepository.CurrentMapID = data.MapID;
_currentMapStateRepository.JailMapID = data.JailMap;

Expand Down
7 changes: 7 additions & 0 deletions EOLib/Domain/Login/PlayerInfoRepository.cs
Expand Up @@ -13,6 +13,8 @@ public interface IPlayerInfoRepository
bool IsFirstTimePlayer { get; set; }

bool PlayerIsInGame { get; set; }

bool PlayerHasAdminCharacter { get; set; }
}

public interface IPlayerInfoProvider
Expand All @@ -26,6 +28,8 @@ public interface IPlayerInfoProvider
bool IsFirstTimePlayer { get; }

bool PlayerIsInGame { get; }

bool PlayerHasAdminCharacter { get; }
}

[AutoMappedType(IsSingleton = true)]
Expand All @@ -41,13 +45,16 @@ public sealed class PlayerInfoRepository : IPlayerInfoRepository, IPlayerInfoPro

public bool PlayerIsInGame { get; set; }

public bool PlayerHasAdminCharacter { get; set; }

public void ResetState()
{
LoggedInAccountName = "";
PlayerPassword = "";
PlayerID = 0;
IsFirstTimePlayer = false;
PlayerIsInGame = false;
PlayerHasAdminCharacter = false;
}
}
}
17 changes: 13 additions & 4 deletions EOLib/Domain/Map/MapActions.cs
@@ -1,6 +1,7 @@
using AutomaticTypeMapper;
using EOLib.Domain.Character;
using EOLib.Domain.Item;
using EOLib.IO.Map;
using EOLib.Net;
using EOLib.Net.Communication;

Expand All @@ -13,19 +14,16 @@ public class MapActions : IMapActions
private readonly IItemPickupValidator _itemPickupValidator;
private readonly ICharacterProvider _characterProvider;
private readonly ICurrentMapStateRepository _currentMapStateRepository;
private readonly IChestDataProvider _chestDataProvider;

public MapActions(IPacketSendService packetSendService,
IItemPickupValidator itemPickupValidator,
ICharacterProvider characterProvider,
ICurrentMapStateRepository currentMapStateRepository,
IChestDataProvider chestDataProvider)
ICurrentMapStateRepository currentMapStateRepository)
{
_packetSendService = packetSendService;
_itemPickupValidator = itemPickupValidator;
_characterProvider = characterProvider;
_currentMapStateRepository = currentMapStateRepository;
_chestDataProvider = chestDataProvider;
}

public void RequestRefresh()
Expand Down Expand Up @@ -84,6 +82,15 @@ public void OpenLocker(MapCoordinate location)

_packetSendService.SendPacket(packet);
}

public void OpenBoard(TileSpec boardSpec)
{
var packet = new PacketBuilder(PacketFamily.Board, PacketAction.Open)
.AddShort(boardSpec - TileSpec.Board1)
.Build();

_packetSendService.SendPacket(packet);
}
}

public interface IMapActions
Expand All @@ -97,5 +104,7 @@ public interface IMapActions
void OpenChest(MapCoordinate location);

void OpenLocker(MapCoordinate location);

void OpenBoard(TileSpec boardSpec);
}
}
74 changes: 74 additions & 0 deletions EOLib/PacketHandlers/Board/BoardOpenHandler.cs
@@ -0,0 +1,74 @@
using AutomaticTypeMapper;
using EOLib.Domain.Interact.Board;
using EOLib.Domain.Login;
using EOLib.Domain.Notifiers;
using EOLib.Net;
using EOLib.Net.Handlers;
using Optional;
using System.Collections.Generic;

namespace EOLib.PacketHandlers.Board
{
/// <summary>
/// Sent by the server when a board should be opened
/// </summary>
[AutoMappedType]
public class BoardOpenHandler : InGameOnlyPacketHandler
{
private readonly IBoardRepository _boardRepository;
private readonly IEnumerable<IUserInterfaceNotifier> _userInterfaceNotifiers;

public override PacketFamily Family => PacketFamily.Board;

public override PacketAction Action => PacketAction.Open;

public BoardOpenHandler(IPlayerInfoProvider playerInfoProvider,
IBoardRepository boardRepository,
IEnumerable<IUserInterfaceNotifier> userInterfaceNotifiers)
: base(playerInfoProvider)
{
_boardRepository = boardRepository;
_userInterfaceNotifiers = userInterfaceNotifiers;
}

public override bool HandlePacket(IPacket packet)
{
_boardRepository.BoardId = Option.Some(packet.ReadChar());

var numPosts = packet.ReadChar();
_boardRepository.Posts = new HashSet<BoardPostInfo>();

var chunks = new List<IPacket>();
while (packet.ReadPosition < packet.Length)
{
var chunkData = new List<byte> { (byte)PacketFamily.Board, (byte)PacketAction.Open };
while (packet.ReadPosition < packet.Length && packet.PeekByte() != 255)
chunkData.Add(packet.ReadByte());

if (packet.ReadPosition < packet.Length)
packet.ReadByte();

chunks.Add(new Packet(chunkData));
}

if (chunks.Count % 3 != 0 || chunks.Count / 3 != numPosts)
throw new MalformedPacketException("Unexpected number of elements in BOARD_OPEN packet", packet);

for (int i = 0; i < chunks.Count; i += 3)
{
var postId = chunks[i].ReadShort();
var author = chunks[i + 1].ReadEndString();
var subject = chunks[i + 2].ReadEndString();
_boardRepository.Posts.Add(new BoardPostInfo(postId, author, subject));
}

_boardRepository.ActivePost = Option.None<BoardPostInfo>();
_boardRepository.ActivePostMessage = Option.None<string>();

foreach (var notifier in _userInterfaceNotifiers)
notifier.NotifyPacketDialog(PacketFamily.Board);

return true;
}
}
}

0 comments on commit d5befa2

Please sign in to comment.