From 589d4c0b9db5ef6f95eadd3da679918ba385d6d1 Mon Sep 17 00:00:00 2001
From: Marek Grzyb <marek@grzyb.dev>
Date: Sun, 22 Sep 2024 15:42:10 +0200
Subject: [PATCH] Added message system and all known plasma transactions

---
 Dockerfile                                    |   2 +
 bfbc2_masterserver/database.py                | 191 ++++++++++++-
 bfbc2_masterserver/dataclasses/Manager.py     |   1 +
 .../dataclasses/plasma/Service.py             |   2 +
 .../enumerators/fesl/FESLTransaction.py       |  41 +++
 .../plasma/AssocationUpdateOperation.py       |   6 +
 .../enumerators/plasma/ListFullBehavior.py    |   6 +
 .../enumerators/plasma/StatUpdateType.py      |   7 +
 .../messages/plasma/account/GameSpyPreAuth.py |   9 +
 .../plasma/account/NuCreateEncryptedToken.py  |  12 +
 .../messages/plasma/account/NuGetAccount.py   |  28 ++
 .../plasma/account/NuGetAccountByNuid.py      |  13 +
 .../plasma/account/NuGetAccountByPS3Ticket.py |  13 +
 .../plasma/account/NuGetEntitlementCount.py   |  26 ++
 .../plasma/account/NuPS3AddAccount.py         |  12 +
 .../messages/plasma/account/NuPS3Login.py     |  14 +
 .../messages/plasma/account/NuSearchOwners.py |  11 +
 .../plasma/account/NuSuggestPersonas.py       |  12 +
 .../plasma/account/NuUpdateAccount.py         |  29 ++
 .../plasma/account/NuUpdatePassword.py        |  11 +
 .../plasma/account/NuXBL360AddAccount.py      |  12 +
 .../messages/plasma/account/NuXBL360Login.py  |  13 +
 .../plasma/assocation/AddAssocations.py       |  15 +-
 .../plasma/assocation/DeleteAssociations.py   |  23 ++
 .../plasma/assocation/GetAssociationsCount.py |  18 ++
 .../assocation/NotifyAssociationUpdate.py     |  16 ++
 .../messages/plasma/connect/Suicide.py        |   9 +
 .../plasma/message/AsyncMessageEvent.py       |   6 +
 .../plasma/message/AsyncPurgedEvent.py        |   5 +
 .../messages/plasma/message/DeleteMessages.py |  10 +
 .../plasma/message/GetMessageAttachments.py   |  11 +
 .../messages/plasma/message/PurgeMessages.py  |  10 +
 .../messages/plasma/message/SendMessage.py    |  17 ++
 .../presence/AsyncPresenceStatusEvent.py      |   8 +
 .../plasma/presence/PresenceSubscribe.py      |  10 +
 .../plasma/presence/PresenceUnsubscribe.py    |  10 +
 .../messages/plasma/ranking/GetDateRange.py   |  14 +
 .../plasma/ranking/GetStatsForOwners.py       |  12 +
 .../messages/plasma/ranking/GetTopN.py        |  12 +
 .../messages/plasma/ranking/GetTopNAndMe.py   |  12 +
 .../messages/plasma/ranking/UpdateStats.py    |  10 +
 .../messages/plasma/record/AddRecord.py       |  12 +
 .../messages/plasma/record/AddRecordAsMap.py  |  13 +
 .../messages/plasma/record/UpdateRecord.py    |  12 +
 .../plasma/record/UpdateRecordAsMap.py        |  13 +
 .../models/plasma/Association.py              |  17 +-
 .../models/plasma/Attachment.py               |   7 +
 bfbc2_masterserver/models/plasma/Attribute.py |   6 +
 bfbc2_masterserver/models/plasma/Message.py   |   2 +-
 bfbc2_masterserver/models/plasma/Presence.py  |  12 +
 bfbc2_masterserver/models/plasma/Stats.py     |  13 +
 bfbc2_masterserver/models/plasma/Status.py    |   6 +
 .../models/plasma/database/Message.py         |  16 +-
 .../plasma/database/MessageAttachment.py      |   2 +-
 bfbc2_masterserver/plasma.py                  |  11 +-
 bfbc2_masterserver/services/plasma/account.py | 219 +++++++++++++++
 .../services/plasma/association.py            | 250 +++++++++++++++++-
 bfbc2_masterserver/services/plasma/connect.py |  27 +-
 bfbc2_masterserver/services/plasma/message.py | 237 +++++++++++++++--
 bfbc2_masterserver/services/plasma/playnow.py |   6 +-
 .../services/plasma/presence.py               | 117 +++++++-
 bfbc2_masterserver/services/plasma/ranking.py |  70 +++++
 bfbc2_masterserver/services/plasma/record.py  |  88 ++++++
 docker-compose.yml                            |   5 +
 64 files changed, 1793 insertions(+), 67 deletions(-)
 create mode 100644 bfbc2_masterserver/enumerators/plasma/AssocationUpdateOperation.py
 create mode 100644 bfbc2_masterserver/enumerators/plasma/ListFullBehavior.py
 create mode 100644 bfbc2_masterserver/enumerators/plasma/StatUpdateType.py
 create mode 100644 bfbc2_masterserver/messages/plasma/account/GameSpyPreAuth.py
 create mode 100644 bfbc2_masterserver/messages/plasma/account/NuCreateEncryptedToken.py
 create mode 100644 bfbc2_masterserver/messages/plasma/account/NuGetAccount.py
 create mode 100644 bfbc2_masterserver/messages/plasma/account/NuGetAccountByNuid.py
 create mode 100644 bfbc2_masterserver/messages/plasma/account/NuGetAccountByPS3Ticket.py
 create mode 100644 bfbc2_masterserver/messages/plasma/account/NuGetEntitlementCount.py
 create mode 100644 bfbc2_masterserver/messages/plasma/account/NuPS3AddAccount.py
 create mode 100644 bfbc2_masterserver/messages/plasma/account/NuPS3Login.py
 create mode 100644 bfbc2_masterserver/messages/plasma/account/NuSearchOwners.py
 create mode 100644 bfbc2_masterserver/messages/plasma/account/NuSuggestPersonas.py
 create mode 100644 bfbc2_masterserver/messages/plasma/account/NuUpdateAccount.py
 create mode 100644 bfbc2_masterserver/messages/plasma/account/NuUpdatePassword.py
 create mode 100644 bfbc2_masterserver/messages/plasma/account/NuXBL360AddAccount.py
 create mode 100644 bfbc2_masterserver/messages/plasma/account/NuXBL360Login.py
 create mode 100644 bfbc2_masterserver/messages/plasma/assocation/DeleteAssociations.py
 create mode 100644 bfbc2_masterserver/messages/plasma/assocation/GetAssociationsCount.py
 create mode 100644 bfbc2_masterserver/messages/plasma/assocation/NotifyAssociationUpdate.py
 create mode 100644 bfbc2_masterserver/messages/plasma/connect/Suicide.py
 create mode 100644 bfbc2_masterserver/messages/plasma/message/AsyncMessageEvent.py
 create mode 100644 bfbc2_masterserver/messages/plasma/message/AsyncPurgedEvent.py
 create mode 100644 bfbc2_masterserver/messages/plasma/message/DeleteMessages.py
 create mode 100644 bfbc2_masterserver/messages/plasma/message/GetMessageAttachments.py
 create mode 100644 bfbc2_masterserver/messages/plasma/message/PurgeMessages.py
 create mode 100644 bfbc2_masterserver/messages/plasma/message/SendMessage.py
 create mode 100644 bfbc2_masterserver/messages/plasma/presence/AsyncPresenceStatusEvent.py
 create mode 100644 bfbc2_masterserver/messages/plasma/presence/PresenceSubscribe.py
 create mode 100644 bfbc2_masterserver/messages/plasma/presence/PresenceUnsubscribe.py
 create mode 100644 bfbc2_masterserver/messages/plasma/ranking/GetDateRange.py
 create mode 100644 bfbc2_masterserver/messages/plasma/ranking/GetStatsForOwners.py
 create mode 100644 bfbc2_masterserver/messages/plasma/ranking/GetTopN.py
 create mode 100644 bfbc2_masterserver/messages/plasma/ranking/GetTopNAndMe.py
 create mode 100644 bfbc2_masterserver/messages/plasma/ranking/UpdateStats.py
 create mode 100644 bfbc2_masterserver/messages/plasma/record/AddRecord.py
 create mode 100644 bfbc2_masterserver/messages/plasma/record/AddRecordAsMap.py
 create mode 100644 bfbc2_masterserver/messages/plasma/record/UpdateRecord.py
 create mode 100644 bfbc2_masterserver/messages/plasma/record/UpdateRecordAsMap.py
 create mode 100644 bfbc2_masterserver/models/plasma/Attachment.py
 create mode 100644 bfbc2_masterserver/models/plasma/Attribute.py
 create mode 100644 bfbc2_masterserver/models/plasma/Presence.py
 create mode 100644 bfbc2_masterserver/models/plasma/Status.py

diff --git a/Dockerfile b/Dockerfile
index af90526..69ecc0b 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -21,6 +21,8 @@ RUN poetry install --no-root
 # Production image, copy all the files and run next
 FROM base AS runner
 
+RUN apk add libpq --no-cache
+
 WORKDIR /app
 
 RUN addgroup --system --gid 1001 bfbc2emu
diff --git a/bfbc2_masterserver/database.py b/bfbc2_masterserver/database.py
index f4d7cec..2b58ef5 100644
--- a/bfbc2_masterserver/database.py
+++ b/bfbc2_masterserver/database.py
@@ -2,6 +2,7 @@
 import secrets
 import uuid
 from datetime import datetime
+from decimal import Decimal
 from typing import Optional
 from uu import Error
 
@@ -14,6 +15,7 @@
 from bfbc2_masterserver.enumerators.ErrorCode import ErrorCode
 from bfbc2_masterserver.enumerators.plasma.AssocationType import AssocationType
 from bfbc2_masterserver.enumerators.plasma.RecordName import RecordName
+from bfbc2_masterserver.enumerators.plasma.StatUpdateType import StatUpdateType
 from bfbc2_masterserver.enumerators.theater.GameType import GameType
 from bfbc2_masterserver.enumerators.theater.JoinMode import JoinMode
 from bfbc2_masterserver.messages.plasma.account.NuGrantEntitlement import (
@@ -29,6 +31,9 @@
 from bfbc2_masterserver.models.plasma.database.Association import Association
 from bfbc2_masterserver.models.plasma.database.Entitlement import Entitlement
 from bfbc2_masterserver.models.plasma.database.Message import Message
+from bfbc2_masterserver.models.plasma.database.MessageAttachment import (
+    MessageAttachment,
+)
 from bfbc2_masterserver.models.plasma.database.Persona import Persona
 from bfbc2_masterserver.models.plasma.database.Ranking import Ranking
 from bfbc2_masterserver.models.plasma.database.Record import Record
@@ -376,6 +381,16 @@ def persona_get_by_name(self, persona_name: str) -> Persona | ErrorCode:
 
             return persona
 
+    def persona_get_owner_id(self, persona_id: int) -> int | ErrorCode:
+        with Session(self._engine) as session:
+            persona_query = select(Persona).where(Persona.id == persona_id)
+            persona = session.exec(persona_query).one_or_none()
+
+            if not persona:
+                return ErrorCode.USER_NOT_FOUND
+
+            return persona.owner.id
+
     def persona_add(self, account_id: int, persona: Persona) -> bool | ErrorCode:
         with Session(self._engine) as session:
             account_query = select(Account).where(Account.id == account_id)
@@ -407,7 +422,26 @@ def persona_delete(self, account_id: int, persona_id: int) -> bool | ErrorCode:
 
             return True
 
-    def assocation_get(
+    def persona_search(self, search: str) -> list[Persona]:
+        with Session(self._engine) as session:
+            search = search.replace("*", "%").replace("_", "")
+            personas_query = select(Persona).filter(Persona.name.ilike(search))  # type: ignore
+            personas = session.exec(personas_query).all()
+
+            return list(personas)
+
+    def association_get(self, persona_id, target_id, association_type: AssocationType):
+        with Session(self._engine) as session:
+            association_query = select(Association).where(
+                Association.owner_id == persona_id,
+                Association.target_id == target_id,
+                Association.type == association_type,
+            )
+            association = session.exec(association_query).one_or_none()
+
+            return association
+
+    def association_get_all(
         self, persona_id: int, assocation_type: AssocationType
     ) -> list[Association] | ErrorCode:
         with Session(self._engine) as session:
@@ -422,9 +456,52 @@ def assocation_get(
             for association in persona.associations:
                 if association.type == assocation_type:
                     assocations_list.append(association)
+                    # Preload the target persona
+                    association.target
 
             return assocations_list
 
+    def association_delete(self, association_id: int) -> bool | ErrorCode:
+        with Session(self._engine) as session:
+            association_query = select(Association).where(
+                Association.id == association_id
+            )
+            association = session.exec(association_query).one_or_none()
+
+            if not association:
+                return ErrorCode.SYSTEM_ERROR
+
+            session.delete(association)
+            session.commit()
+
+            return True
+
+    def association_add(
+        self, owner_id: int, target_id: int, type: AssocationType
+    ) -> Association | ErrorCode:
+        with Session(self._engine) as session:
+            association_query = select(Association).where(
+                Association.owner_id == owner_id,
+                Association.target_id == target_id,
+                Association.type == type,
+            )
+            existing_association = session.exec(association_query).one_or_none()
+
+            if existing_association:
+                return ErrorCode.SYSTEM_ERROR
+
+            association = Association(
+                owner_id=owner_id,
+                target_id=target_id,
+                type=type,
+            )
+
+            session.add(association)
+            session.commit()
+            session.refresh(association)
+
+            return association
+
     def ranking_get(self, persona_id: int, keys: list[str]) -> list[Ranking]:
         with Session(self._engine) as session:
             stats_query = select(Ranking).where(Ranking.owner_id == persona_id)
@@ -483,6 +560,28 @@ def ranking_leaderboard_get(
             stats = session.exec(stats_query).all()
             return list(stats)
 
+    def ranking_set(
+        self, persona_id: int, key: str, value: Decimal, update_type: StatUpdateType
+    ) -> bool:
+        with Session(self._engine) as session:
+            stat_query = select(Ranking).where(
+                Ranking.owner_id == persona_id, Ranking.key == key
+            )
+            stat = session.exec(stat_query).one_or_none()
+
+            if not stat:
+                stat = Ranking(owner_id=persona_id, key=key, value=value)
+            else:
+                if update_type == StatUpdateType.RelativeValue:
+                    stat.value += value
+                else:
+                    stat.value = value
+
+            session.add(stat)
+            session.commit()
+
+            return True
+
     def record_get(self, persona_id: int, type: RecordName) -> list[Record]:
         with Session(self._engine) as session:
             record_query = select(Record).where(Record.owner_id == persona_id)
@@ -492,13 +591,92 @@ def record_get(self, persona_id: int, type: RecordName) -> list[Record]:
 
             return list(records)
 
-    def message_get(self, persona_id: int) -> list[Message]:
+    def record_add(
+        self, persona_id: int, type: RecordName, key: str, value: str
+    ) -> bool:
+        with Session(self._engine) as session:
+            record = Record(owner_id=persona_id, type=type, key=key, value=value)
+            session.add(record)
+            session.commit()
+
+            return True
+
+    def record_update(
+        self, persona_id: int, type: RecordName, key: str, value: str
+    ) -> bool:
+        with Session(self._engine) as session:
+            record_query = select(Record).where(
+                Record.owner_id == persona_id, Record.type == type, Record.key == key
+            )
+            record = session.exec(record_query).one_or_none()
+
+            if not record:
+                return False
+
+            record.value = value
+
+            session.add(record)
+            session.commit()
+
+            return True
+
+    def message_get(self, message_id: int) -> Message | None:
+        with Session(self._engine) as session:
+            message_query = select(Message).where(Message.id == message_id)
+            message = session.exec(message_query).one_or_none()
+
+            return message
+
+    def message_get_all(self, persona_id: int) -> list[Message]:
         with Session(self._engine) as session:
             message_query = select(Message).where(Message.recipient_id == persona_id)
             messages = session.exec(message_query).all()
 
             return list(messages)
 
+    def message_add(self, messageData: Message) -> Message:
+        with Session(self._engine) as session:
+            attachments = []
+
+            for attachment in messageData.attachments:
+                attachment = MessageAttachment(
+                    key=attachment.key, type=attachment.type, data=attachment.data
+                )
+
+                session.add(attachment)
+                session.commit()
+
+                attachments.append(attachment)
+
+            message = Message(
+                sender_id=messageData.sender_id,
+                recipient_id=messageData.recipient.id,
+                messageType=messageData.messageType,
+                deliveryType=messageData.deliveryType,
+                purgeStrategy=messageData.purgeStrategy,
+                expiration=messageData.expiration,
+                attachments=attachments,
+            )
+
+            session.add(message)
+            session.commit()
+            session.refresh(message)
+
+            return message
+
+    def message_delete(self, message_id: int) -> bool:
+        with Session(self._engine) as session:
+            message_query = select(Message).where(Message.id == message_id)
+            message = session.exec(message_query).one_or_none()
+
+            if not message:
+                return False
+
+            session.delete(message)
+            session.commit()
+
+            return True
+
     def lobby_get(self, lobby_id: int) -> Lobby | None:
         with Session(self._engine) as session:
             lobby_query = select(Lobby).where(Lobby.id == lobby_id)
@@ -709,3 +887,12 @@ def game_get(self, lobby_id: int, game_id: int) -> Game | None:
             game = session.exec(game_query).one_or_none()
 
             return game
+
+    def game_find(self, gameType: GameType, level: int) -> Game | None:
+        with Session(self._engine) as session:
+            game_query = select(Game).where(
+                Game.gameType == gameType, Game.gameLevel == level
+            )
+            game = session.exec(game_query).one_or_none()
+
+            return game
diff --git a/bfbc2_masterserver/dataclasses/Manager.py b/bfbc2_masterserver/dataclasses/Manager.py
index d82e81f..6c1f507 100644
--- a/bfbc2_masterserver/dataclasses/Manager.py
+++ b/bfbc2_masterserver/dataclasses/Manager.py
@@ -1,4 +1,5 @@
 from redis import Redis
+
 from bfbc2_masterserver.database import DatabaseAPI
 from bfbc2_masterserver.dataclasses.Client import Client
 
diff --git a/bfbc2_masterserver/dataclasses/plasma/Service.py b/bfbc2_masterserver/dataclasses/plasma/Service.py
index a031efb..8acb573 100644
--- a/bfbc2_masterserver/dataclasses/plasma/Service.py
+++ b/bfbc2_masterserver/dataclasses/plasma/Service.py
@@ -31,6 +31,8 @@ def __init__(self, plasma: BasePlasmaHandler) -> None:
 
         self.connection = plasma.client.connection
         self.database = plasma.manager.database
+        self.manager = plasma.manager
+        self.redis = plasma.manager.redis
         self.plasma = plasma
 
     @abstractmethod
diff --git a/bfbc2_masterserver/enumerators/fesl/FESLTransaction.py b/bfbc2_masterserver/enumerators/fesl/FESLTransaction.py
index 5ceaa45..32d3199 100644
--- a/bfbc2_masterserver/enumerators/fesl/FESLTransaction.py
+++ b/bfbc2_masterserver/enumerators/fesl/FESLTransaction.py
@@ -2,12 +2,16 @@
 
 
 class FESLTransaction(Enum):
+    # Universal
+    TransactionException = "TransactionException"
+
     # Plasma (Connect)
     Hello = "Hello"
     MemCheck = "MemCheck"
     GetPingSites = "GetPingSites"
     Ping = "Ping"
     Goodbye = "Goodbye"
+    # Never called during my tests, but implemented in game code
     Suicide = "Suicide"
 
     # Plasma (Account)
@@ -26,14 +30,38 @@ class FESLTransaction(Enum):
     NuEntitleUser = "NuEntitleUser"
     NuLookupUserInfo = "NuLookupUserInfo"
     NuGrantEntitlement = "NuGrantEntitlement"
+    NuSearchOwners = "NuSearchOwners"
+    # Never called during my tests, but implemented in game code
+    NuCreateEncryptedToken = "NuCreateEncryptedToken"
+    NuSuggestPersonas = "NuSuggestPersonas"
+    NuUpdatePassword = "NuUpdatePassword"
+    NuGetAccount = "NuGetAccount"
+    NuGetAccountByNuid = "NuGetAccountByNuid"
+    NuGetAccountByPS3Ticket = "NuGetAccountByPS3Ticket"  # PS3 Only?
+    NuUpdateAccount = "NuUpdateAccount"
+    GameSpyPreAuth = "GameSpyPreAuth"  # GameSpy leftover?
+    NuXBL360Login = "NuXBL360Login"  # Xbox 360 Only?
+    NuXBL360AddAccount = "NuXBL360AddAccount"  # Xbox 360 Only?
+    NuPS3Login = "NuPS3Login"  # PS3 Only?
+    NuPS3AddAccount = "NuPS3AddAccount"  # PS3 Only?
+    NuGetEntitlementCount = "NuGetEntitlementCount"
 
     # Plasma (Assocation)
     GetAssociations = "GetAssociations"
     AddAssociations = "AddAssociations"
+    NotifyAssociationUpdate = "NotifyAssociationUpdate"
+    DeleteAssociations = "DeleteAssociations"
+    GetAssociationsCount = "GetAssociationsCount"
 
     # Plasma (Extensible Message)
     ModifySettings = "ModifySettings"
     GetMessages = "GetMessages"
+    SendMessage = "SendMessage"
+    AsyncMessageEvent = "AsyncMessageEvent"
+    GetMessageAttachments = "GetMessageAttachments"
+    DeleteMessages = "DeleteMessages"
+    PurgeMessages = "PurgeMessages"
+    AsyncPurgedEvent = "AsyncPurgedEvent"
 
     # Plasma (PlayNow)
     Start = "Start"
@@ -41,13 +69,26 @@ class FESLTransaction(Enum):
 
     # Plasma (Presence)
     SetPresenceStatus = "SetPresenceStatus"
+    PresenceSubscribe = "PresenceSubscribe"
+    PresenceUnsubscribe = "PresenceUnsubscribe"
+    AsyncPresenceStatusEvent = "AsyncPresenceStatusEvent"
 
     # Plasma (Ranking)
     GetStats = "GetStats"
     GetRankedStatsForOwners = "GetRankedStatsForOwners"
     GetRankedStats = "GetRankedStats"
     GetTopNAndStats = "GetTopNAndStats"
+    UpdateStats = "UpdateStats"
+    # Never called during my tests, but implemented in game code
+    GetStatsForOwners = "GetStatsForOwners"
+    GetTopN = "GetTopN"
+    GetTopNAndMe = "GetTopNAndMe"
+    GetDateRange = "GetDateRange"
 
     # Plasma (Record)
     GetRecordAsMap = "GetRecordAsMap"
     GetRecord = "GetRecord"
+    AddRecord = "AddRecord"
+    UpdateRecord = "UpdateRecord"
+    AddRecordAsMap = "AddRecordAsMap"
+    UpdateRecordAsMap = "UpdateRecordAsMap"
diff --git a/bfbc2_masterserver/enumerators/plasma/AssocationUpdateOperation.py b/bfbc2_masterserver/enumerators/plasma/AssocationUpdateOperation.py
new file mode 100644
index 0000000..35ee788
--- /dev/null
+++ b/bfbc2_masterserver/enumerators/plasma/AssocationUpdateOperation.py
@@ -0,0 +1,6 @@
+from enum import Enum
+
+
+class AssocationUpdateOperation(Enum):
+    ADD = "add"
+    DEL = "del"
diff --git a/bfbc2_masterserver/enumerators/plasma/ListFullBehavior.py b/bfbc2_masterserver/enumerators/plasma/ListFullBehavior.py
new file mode 100644
index 0000000..481564f
--- /dev/null
+++ b/bfbc2_masterserver/enumerators/plasma/ListFullBehavior.py
@@ -0,0 +1,6 @@
+from enum import Enum
+
+
+class ListFullBehavior(Enum):
+    ReturnError = "ReturnError"
+    RollLeastRecentlyModified = "RollLeastRecentlyModified"
diff --git a/bfbc2_masterserver/enumerators/plasma/StatUpdateType.py b/bfbc2_masterserver/enumerators/plasma/StatUpdateType.py
new file mode 100644
index 0000000..58da931
--- /dev/null
+++ b/bfbc2_masterserver/enumerators/plasma/StatUpdateType.py
@@ -0,0 +1,7 @@
+from enum import Enum
+
+
+class StatUpdateType(Enum):
+    AbsoluteValueRounded = 0
+    AbsoluteValue = 1
+    RelativeValue = 3
diff --git a/bfbc2_masterserver/messages/plasma/account/GameSpyPreAuth.py b/bfbc2_masterserver/messages/plasma/account/GameSpyPreAuth.py
new file mode 100644
index 0000000..ea272f9
--- /dev/null
+++ b/bfbc2_masterserver/messages/plasma/account/GameSpyPreAuth.py
@@ -0,0 +1,9 @@
+from bfbc2_masterserver.models.general.PlasmaTransaction import PlasmaTransaction
+
+
+class GameSpyPreAuthRequest(PlasmaTransaction):
+    pass
+
+
+class GameSpyPreAuthResponse(PlasmaTransaction):
+    pass
diff --git a/bfbc2_masterserver/messages/plasma/account/NuCreateEncryptedToken.py b/bfbc2_masterserver/messages/plasma/account/NuCreateEncryptedToken.py
new file mode 100644
index 0000000..2e1affb
--- /dev/null
+++ b/bfbc2_masterserver/messages/plasma/account/NuCreateEncryptedToken.py
@@ -0,0 +1,12 @@
+from bfbc2_masterserver.models.general.PlasmaTransaction import PlasmaTransaction
+from bfbc2_masterserver.models.plasma.Attribute import Attribute
+
+
+class NuCreateEncryptedTokenRequest(PlasmaTransaction):
+    expires: int
+    attributes: list[Attribute]
+
+
+class NuCreateEncryptedTokenResponse(PlasmaTransaction):
+    # Couldn't find any response for this transaction in my poor RE effort
+    pass
diff --git a/bfbc2_masterserver/messages/plasma/account/NuGetAccount.py b/bfbc2_masterserver/messages/plasma/account/NuGetAccount.py
new file mode 100644
index 0000000..b77e624
--- /dev/null
+++ b/bfbc2_masterserver/messages/plasma/account/NuGetAccount.py
@@ -0,0 +1,28 @@
+from typing import Optional
+
+from bfbc2_masterserver.models.general.PlasmaTransaction import PlasmaTransaction
+from bfbc2_masterserver.models.plasma.Entitlement import Entitlement
+
+
+class NuGetAccountRequest(PlasmaTransaction):
+    nuid: Optional[str]
+
+
+class NuGetAccountResponse(PlasmaTransaction):
+    nuid: str
+    parentalEmail: Optional[str]
+    firstName: Optional[str]
+    lastName: Optional[str]
+    street: Optional[str]
+    street2: Optional[str]
+    city: Optional[str]
+    state: Optional[str]
+    zip: Optional[str]
+    country: Optional[str]
+    language: Optional[str]
+    userId: int
+    dOBMonth: Optional[int]
+    dOBDay: Optional[int]
+    dOBYear: Optional[int]
+    globalCommOptin: Optional[str]
+    thirdPartyMailFlag: Optional[str]
diff --git a/bfbc2_masterserver/messages/plasma/account/NuGetAccountByNuid.py b/bfbc2_masterserver/messages/plasma/account/NuGetAccountByNuid.py
new file mode 100644
index 0000000..d53f637
--- /dev/null
+++ b/bfbc2_masterserver/messages/plasma/account/NuGetAccountByNuid.py
@@ -0,0 +1,13 @@
+from typing import Optional
+
+from bfbc2_masterserver.messages.plasma.account.NuGetAccount import NuGetAccountResponse
+from bfbc2_masterserver.models.general.PlasmaTransaction import PlasmaTransaction
+from bfbc2_masterserver.models.plasma.Entitlement import Entitlement
+
+
+class NuGetAccountByNuidRequest(PlasmaTransaction):
+    nuid: str
+
+
+class NuGetAccountByNuidResponse(NuGetAccountResponse):
+    pass
diff --git a/bfbc2_masterserver/messages/plasma/account/NuGetAccountByPS3Ticket.py b/bfbc2_masterserver/messages/plasma/account/NuGetAccountByPS3Ticket.py
new file mode 100644
index 0000000..5d383f5
--- /dev/null
+++ b/bfbc2_masterserver/messages/plasma/account/NuGetAccountByPS3Ticket.py
@@ -0,0 +1,13 @@
+from typing import Optional
+
+from bfbc2_masterserver.messages.plasma.account.NuGetAccount import NuGetAccountResponse
+from bfbc2_masterserver.models.general.PlasmaTransaction import PlasmaTransaction
+from bfbc2_masterserver.models.plasma.Entitlement import Entitlement
+
+
+class NuGetAccountByPS3TicketRequest(PlasmaTransaction):
+    ticket: bytes
+
+
+class NuGetAccountByPS3TicketResponse(NuGetAccountResponse):
+    pass
diff --git a/bfbc2_masterserver/messages/plasma/account/NuGetEntitlementCount.py b/bfbc2_masterserver/messages/plasma/account/NuGetEntitlementCount.py
new file mode 100644
index 0000000..742dd6f
--- /dev/null
+++ b/bfbc2_masterserver/messages/plasma/account/NuGetEntitlementCount.py
@@ -0,0 +1,26 @@
+from typing import Optional
+
+from pydantic import Field
+
+from bfbc2_masterserver.models.general.PlasmaTransaction import PlasmaTransaction
+
+
+class NuGetEntitlementCountRequest(PlasmaTransaction):
+    entitlementId: Optional[str]
+    entitlementTag: Optional[str]
+    masterUserId: Optional[int]
+    userId: Optional[int]
+    global_: Optional[str] = Field(alias="global")
+    status: Optional[str]
+    groupName: Optional[str]
+    productCatalog: Optional[str]
+    productId: Optional[str]
+    grantStartDate: Optional[str]
+    grantEndDate: Optional[str]
+    projectId: Optional[str]
+    entitlementType: Optional[str]
+    devicePhysicalId: Optional[str]
+
+
+class NuGetEntitlementCountResponse(PlasmaTransaction):
+    entitlementCount: int
diff --git a/bfbc2_masterserver/messages/plasma/account/NuPS3AddAccount.py b/bfbc2_masterserver/messages/plasma/account/NuPS3AddAccount.py
new file mode 100644
index 0000000..c17d421
--- /dev/null
+++ b/bfbc2_masterserver/messages/plasma/account/NuPS3AddAccount.py
@@ -0,0 +1,12 @@
+from bfbc2_masterserver.messages.plasma.account.NuAddAccount import (
+    NuAddAccountRequest,
+    NuAddAccountResponse,
+)
+
+
+class NuPS3AddAccountRequest(NuAddAccountRequest):
+    pass
+
+
+class NuPS3AddAccountResponse(NuAddAccountResponse):
+    pass
diff --git a/bfbc2_masterserver/messages/plasma/account/NuPS3Login.py b/bfbc2_masterserver/messages/plasma/account/NuPS3Login.py
new file mode 100644
index 0000000..55cf829
--- /dev/null
+++ b/bfbc2_masterserver/messages/plasma/account/NuPS3Login.py
@@ -0,0 +1,14 @@
+from typing import Optional
+
+from bfbc2_masterserver.models.general.PlasmaTransaction import PlasmaTransaction
+
+
+class NuPS3LoginRequest(PlasmaTransaction):
+    ticket: bytes
+    macAddr: str
+    consoleId: str
+    tosVersion: Optional[str]
+
+
+class NuPS3LoginResponse(PlasmaTransaction):
+    pass
diff --git a/bfbc2_masterserver/messages/plasma/account/NuSearchOwners.py b/bfbc2_masterserver/messages/plasma/account/NuSearchOwners.py
new file mode 100644
index 0000000..190baf1
--- /dev/null
+++ b/bfbc2_masterserver/messages/plasma/account/NuSearchOwners.py
@@ -0,0 +1,11 @@
+from bfbc2_masterserver.models.general.PlasmaTransaction import PlasmaTransaction
+from bfbc2_masterserver.models.plasma.Owner import Owner
+
+
+class NuSearchOwnersRequest(PlasmaTransaction):
+    screenName: str
+
+
+class NuSearchOwnersResponse(PlasmaTransaction):
+    users: list[Owner]
+    nameSpaceId: str  # "battlefield"
diff --git a/bfbc2_masterserver/messages/plasma/account/NuSuggestPersonas.py b/bfbc2_masterserver/messages/plasma/account/NuSuggestPersonas.py
new file mode 100644
index 0000000..ce7fccd
--- /dev/null
+++ b/bfbc2_masterserver/messages/plasma/account/NuSuggestPersonas.py
@@ -0,0 +1,12 @@
+from bfbc2_masterserver.models.general.PlasmaTransaction import PlasmaTransaction
+from bfbc2_masterserver.models.plasma.UserInfo import UserInfo, UserInfoRequest
+
+
+class NuSuggestPersonasRequest(PlasmaTransaction):
+    name: str
+    maxSuggestions: int
+    keywords: list[str]
+
+
+class NuSuggestPersonasResponse(PlasmaTransaction):
+    names: list[str]
diff --git a/bfbc2_masterserver/messages/plasma/account/NuUpdateAccount.py b/bfbc2_masterserver/messages/plasma/account/NuUpdateAccount.py
new file mode 100644
index 0000000..a0c74bd
--- /dev/null
+++ b/bfbc2_masterserver/messages/plasma/account/NuUpdateAccount.py
@@ -0,0 +1,29 @@
+from typing import Optional
+
+from bfbc2_masterserver.models.general.PlasmaTransaction import PlasmaTransaction
+
+
+class NuUpdateAccountRequest(PlasmaTransaction):
+    nuid: str
+    password: str
+    globalOptin: str
+    thirdPartyOptin: str
+
+    parentalEmail: Optional[str]
+    DOBDay: Optional[int]
+    DOBMonth: Optional[int]
+    DOBYear: Optional[int]
+    first_Name: Optional[str]
+    last_Name: Optional[str]
+    gender: Optional[str]
+    street: Optional[str]
+    street2: Optional[str]
+    city: Optional[str]
+    state: Optional[str]
+    zipCode: Optional[str]
+    country: Optional[str]
+    language: Optional[str]
+
+
+class NuUpdateAccountResponse(PlasmaTransaction):
+    pass
diff --git a/bfbc2_masterserver/messages/plasma/account/NuUpdatePassword.py b/bfbc2_masterserver/messages/plasma/account/NuUpdatePassword.py
new file mode 100644
index 0000000..6fb70ac
--- /dev/null
+++ b/bfbc2_masterserver/messages/plasma/account/NuUpdatePassword.py
@@ -0,0 +1,11 @@
+from typing import Optional
+
+from bfbc2_masterserver.models.general.PlasmaTransaction import PlasmaTransaction
+
+
+class NuUpdatePasswordRequest(PlasmaTransaction):
+    newPassword: str
+
+
+class NuUpdatePasswordResponse(PlasmaTransaction):
+    encryptedLoginInfo: Optional[str]
diff --git a/bfbc2_masterserver/messages/plasma/account/NuXBL360AddAccount.py b/bfbc2_masterserver/messages/plasma/account/NuXBL360AddAccount.py
new file mode 100644
index 0000000..3dedb43
--- /dev/null
+++ b/bfbc2_masterserver/messages/plasma/account/NuXBL360AddAccount.py
@@ -0,0 +1,12 @@
+from bfbc2_masterserver.messages.plasma.account.NuAddAccount import (
+    NuAddAccountRequest,
+    NuAddAccountResponse,
+)
+
+
+class NuXBL360AddAccountRequest(NuAddAccountRequest):
+    pass
+
+
+class NuXBL360AddAccountResponse(NuAddAccountResponse):
+    pass
diff --git a/bfbc2_masterserver/messages/plasma/account/NuXBL360Login.py b/bfbc2_masterserver/messages/plasma/account/NuXBL360Login.py
new file mode 100644
index 0000000..f2fb6af
--- /dev/null
+++ b/bfbc2_masterserver/messages/plasma/account/NuXBL360Login.py
@@ -0,0 +1,13 @@
+from typing import Optional
+
+from bfbc2_masterserver.models.general.PlasmaTransaction import PlasmaTransaction
+
+
+class NuXBL360LoginRequest(PlasmaTransaction):
+    macAddr: str
+    consoleId: str
+    tosVersion: Optional[str]
+
+
+class NuXBL360LoginResponse(PlasmaTransaction):
+    pass
diff --git a/bfbc2_masterserver/messages/plasma/assocation/AddAssocations.py b/bfbc2_masterserver/messages/plasma/assocation/AddAssocations.py
index acbbd17..d217db7 100644
--- a/bfbc2_masterserver/messages/plasma/assocation/AddAssocations.py
+++ b/bfbc2_masterserver/messages/plasma/assocation/AddAssocations.py
@@ -1,11 +1,22 @@
 from bfbc2_masterserver.enumerators.plasma.AssocationType import AssocationType
+from bfbc2_masterserver.enumerators.plasma.ListFullBehavior import ListFullBehavior
 from bfbc2_masterserver.models.general.PlasmaTransaction import PlasmaTransaction
-from bfbc2_masterserver.models.plasma.Association import AssociationRequest
+from bfbc2_masterserver.models.plasma.Association import (
+    AssociationRequest,
+    AssociationResult,
+)
 from bfbc2_masterserver.models.plasma.DomainPartition import DomainPartition
 
 
 class AddAssociationsRequest(PlasmaTransaction):
     domainPartition: DomainPartition
     type: AssocationType
-    listFullBehavior: str
+    listFullBehavior: ListFullBehavior
     addRequests: list[AssociationRequest]
+
+
+class AddAssociationsResponse(PlasmaTransaction):
+    domainPartition: DomainPartition
+    maxListSize: int
+    result: list[AssociationResult]
+    type: AssocationType
diff --git a/bfbc2_masterserver/messages/plasma/assocation/DeleteAssociations.py b/bfbc2_masterserver/messages/plasma/assocation/DeleteAssociations.py
new file mode 100644
index 0000000..e937a6a
--- /dev/null
+++ b/bfbc2_masterserver/messages/plasma/assocation/DeleteAssociations.py
@@ -0,0 +1,23 @@
+from bfbc2_masterserver.enumerators.plasma.AssocationType import AssocationType
+from bfbc2_masterserver.enumerators.plasma.ListFullBehavior import ListFullBehavior
+from bfbc2_masterserver.models.general.PlasmaTransaction import PlasmaTransaction
+from bfbc2_masterserver.models.plasma.Association import (
+    AssociationRequest,
+    AssociationResult,
+    AssociationReturn,
+)
+from bfbc2_masterserver.models.plasma.DomainPartition import DomainPartition
+from bfbc2_masterserver.models.plasma.Owner import Owner
+
+
+class DeleteAssociationsRequest(PlasmaTransaction):
+    domainPartition: DomainPartition
+    type: AssocationType
+    deleteRequests: list[AssociationRequest]
+
+
+class DeleteAssociationsResponse(PlasmaTransaction):
+    domainPartition: DomainPartition
+    maxListSize: int
+    result: list[AssociationResult]
+    type: AssocationType
diff --git a/bfbc2_masterserver/messages/plasma/assocation/GetAssociationsCount.py b/bfbc2_masterserver/messages/plasma/assocation/GetAssociationsCount.py
new file mode 100644
index 0000000..4faf564
--- /dev/null
+++ b/bfbc2_masterserver/messages/plasma/assocation/GetAssociationsCount.py
@@ -0,0 +1,18 @@
+from bfbc2_masterserver.enumerators.plasma.AssocationType import AssocationType
+from bfbc2_masterserver.models.general.PlasmaTransaction import PlasmaTransaction
+from bfbc2_masterserver.models.plasma.Association import AssociationReturn
+from bfbc2_masterserver.models.plasma.DomainPartition import DomainPartition
+from bfbc2_masterserver.models.plasma.Owner import Owner
+
+
+class GetAssociationsCountRequest(PlasmaTransaction):
+    domainPartition: DomainPartition
+    type: AssocationType
+    owner: Owner
+
+
+class GetAssociationsCountResponse(PlasmaTransaction):
+    domainPartition: DomainPartition
+    maxListSize: int
+    count: int
+    owner: Owner
diff --git a/bfbc2_masterserver/messages/plasma/assocation/NotifyAssociationUpdate.py b/bfbc2_masterserver/messages/plasma/assocation/NotifyAssociationUpdate.py
new file mode 100644
index 0000000..f5dbe5a
--- /dev/null
+++ b/bfbc2_masterserver/messages/plasma/assocation/NotifyAssociationUpdate.py
@@ -0,0 +1,16 @@
+from bfbc2_masterserver.enumerators.plasma.AssocationUpdateOperation import (
+    AssocationUpdateOperation,
+)
+from bfbc2_masterserver.models.general.PlasmaTransaction import PlasmaTransaction
+from bfbc2_masterserver.models.plasma.Association import AssociationReturn
+from bfbc2_masterserver.models.plasma.DomainPartition import DomainPartition
+from bfbc2_masterserver.models.plasma.Owner import Owner
+
+
+class NotifyAssociationUpdate(PlasmaTransaction):
+    domainPartition: DomainPartition
+    listSize: int
+    member: AssociationReturn
+    operation: AssocationUpdateOperation
+    owner: Owner
+    type: str
diff --git a/bfbc2_masterserver/messages/plasma/connect/Suicide.py b/bfbc2_masterserver/messages/plasma/connect/Suicide.py
new file mode 100644
index 0000000..add1b71
--- /dev/null
+++ b/bfbc2_masterserver/messages/plasma/connect/Suicide.py
@@ -0,0 +1,9 @@
+from bfbc2_masterserver.models.general.PlasmaTransaction import PlasmaTransaction
+
+
+class SuicideRequest(PlasmaTransaction):
+    pass
+
+
+class SuicideResponse(PlasmaTransaction):
+    pass
diff --git a/bfbc2_masterserver/messages/plasma/message/AsyncMessageEvent.py b/bfbc2_masterserver/messages/plasma/message/AsyncMessageEvent.py
new file mode 100644
index 0000000..3cfc83b
--- /dev/null
+++ b/bfbc2_masterserver/messages/plasma/message/AsyncMessageEvent.py
@@ -0,0 +1,6 @@
+from bfbc2_masterserver.models.general.PlasmaTransaction import PlasmaTransaction
+from bfbc2_masterserver.models.plasma.Message import MessageResponse
+
+
+class AsyncMessageEvent(PlasmaTransaction, MessageResponse):
+    pass
diff --git a/bfbc2_masterserver/messages/plasma/message/AsyncPurgedEvent.py b/bfbc2_masterserver/messages/plasma/message/AsyncPurgedEvent.py
new file mode 100644
index 0000000..a9f8890
--- /dev/null
+++ b/bfbc2_masterserver/messages/plasma/message/AsyncPurgedEvent.py
@@ -0,0 +1,5 @@
+from bfbc2_masterserver.models.general.PlasmaTransaction import PlasmaTransaction
+
+
+class AsyncPurgedEvent(PlasmaTransaction):
+    messageIds: list[int]
diff --git a/bfbc2_masterserver/messages/plasma/message/DeleteMessages.py b/bfbc2_masterserver/messages/plasma/message/DeleteMessages.py
new file mode 100644
index 0000000..687c7f2
--- /dev/null
+++ b/bfbc2_masterserver/messages/plasma/message/DeleteMessages.py
@@ -0,0 +1,10 @@
+from bfbc2_masterserver.models.general.PlasmaTransaction import PlasmaTransaction
+from bfbc2_masterserver.models.plasma.Message import MessageResponse
+
+
+class DeleteMessagesRequest(PlasmaTransaction):
+    messageIds: list[int]
+
+
+class DeleteMessagesResponse(PlasmaTransaction):
+    pass
diff --git a/bfbc2_masterserver/messages/plasma/message/GetMessageAttachments.py b/bfbc2_masterserver/messages/plasma/message/GetMessageAttachments.py
new file mode 100644
index 0000000..a174ccf
--- /dev/null
+++ b/bfbc2_masterserver/messages/plasma/message/GetMessageAttachments.py
@@ -0,0 +1,11 @@
+from bfbc2_masterserver.models.general.PlasmaTransaction import PlasmaTransaction
+from bfbc2_masterserver.models.plasma.Message import MessageResponse
+
+
+class GetMessageAttachmentsRequest(PlasmaTransaction):
+    messageId: int
+    keys: list[str]
+
+
+class GetMessageAttachmentsResponse(PlasmaTransaction):
+    pass
diff --git a/bfbc2_masterserver/messages/plasma/message/PurgeMessages.py b/bfbc2_masterserver/messages/plasma/message/PurgeMessages.py
new file mode 100644
index 0000000..6161da9
--- /dev/null
+++ b/bfbc2_masterserver/messages/plasma/message/PurgeMessages.py
@@ -0,0 +1,10 @@
+from bfbc2_masterserver.models.general.PlasmaTransaction import PlasmaTransaction
+from bfbc2_masterserver.models.plasma.Message import MessageResponse
+
+
+class PurgeMessagesRequest(PlasmaTransaction):
+    messageIds: list[int]
+
+
+class PurgeMessagesResponse(PlasmaTransaction):
+    pass
diff --git a/bfbc2_masterserver/messages/plasma/message/SendMessage.py b/bfbc2_masterserver/messages/plasma/message/SendMessage.py
new file mode 100644
index 0000000..d6198a3
--- /dev/null
+++ b/bfbc2_masterserver/messages/plasma/message/SendMessage.py
@@ -0,0 +1,17 @@
+from bfbc2_masterserver.models.general.PlasmaTransaction import PlasmaTransaction
+from bfbc2_masterserver.models.plasma.Attachment import Attachment
+from bfbc2_masterserver.models.plasma.Status import Status
+
+
+class SendMessageRequest(PlasmaTransaction):
+    attachments: list[Attachment]
+    to: list[int]
+    expires: int
+    deliveryType: str
+    messageType: str
+    purgeStrategy: str
+
+
+class SendMessageResponse(PlasmaTransaction):
+    messageId: int
+    status: list[Status]
diff --git a/bfbc2_masterserver/messages/plasma/presence/AsyncPresenceStatusEvent.py b/bfbc2_masterserver/messages/plasma/presence/AsyncPresenceStatusEvent.py
new file mode 100644
index 0000000..fef41f6
--- /dev/null
+++ b/bfbc2_masterserver/messages/plasma/presence/AsyncPresenceStatusEvent.py
@@ -0,0 +1,8 @@
+from bfbc2_masterserver.models.general.PlasmaTransaction import PlasmaTransaction
+from bfbc2_masterserver.models.plasma.Owner import Owner
+
+
+class AsyncPresenceStatusEvent(PlasmaTransaction):
+    initial: bool
+    owner: Owner
+    status: dict
diff --git a/bfbc2_masterserver/messages/plasma/presence/PresenceSubscribe.py b/bfbc2_masterserver/messages/plasma/presence/PresenceSubscribe.py
new file mode 100644
index 0000000..462207d
--- /dev/null
+++ b/bfbc2_masterserver/messages/plasma/presence/PresenceSubscribe.py
@@ -0,0 +1,10 @@
+from bfbc2_masterserver.models.general.PlasmaTransaction import PlasmaTransaction
+from bfbc2_masterserver.models.plasma.Presence import PresenceRequest, PresenceResponse
+
+
+class PresenceSubscribeRequest(PlasmaTransaction):
+    requests: list[PresenceRequest]
+
+
+class PresenceSubscribeResponse(PlasmaTransaction):
+    responses: list[PresenceResponse]
diff --git a/bfbc2_masterserver/messages/plasma/presence/PresenceUnsubscribe.py b/bfbc2_masterserver/messages/plasma/presence/PresenceUnsubscribe.py
new file mode 100644
index 0000000..3ed5ac9
--- /dev/null
+++ b/bfbc2_masterserver/messages/plasma/presence/PresenceUnsubscribe.py
@@ -0,0 +1,10 @@
+from bfbc2_masterserver.models.general.PlasmaTransaction import PlasmaTransaction
+from bfbc2_masterserver.models.plasma.Presence import PresenceRequest, PresenceResponse
+
+
+class PresenceUnsubscribeRequest(PlasmaTransaction):
+    requests: list[PresenceRequest]
+
+
+class PresenceUnsubscribeResponse(PlasmaTransaction):
+    responses: list[PresenceResponse]
diff --git a/bfbc2_masterserver/messages/plasma/ranking/GetDateRange.py b/bfbc2_masterserver/messages/plasma/ranking/GetDateRange.py
new file mode 100644
index 0000000..af7f7a5
--- /dev/null
+++ b/bfbc2_masterserver/messages/plasma/ranking/GetDateRange.py
@@ -0,0 +1,14 @@
+from datetime import datetime
+
+from bfbc2_masterserver.models.general.PlasmaTransaction import PlasmaTransaction
+from bfbc2_masterserver.models.plasma.Leaderboard import Leaderboard
+
+
+class GetDateRangeRequest(PlasmaTransaction):
+    key: str
+    periodId: int
+
+
+class GetDateRangeResponse(PlasmaTransaction):
+    startDate: datetime
+    endDate: datetime
diff --git a/bfbc2_masterserver/messages/plasma/ranking/GetStatsForOwners.py b/bfbc2_masterserver/messages/plasma/ranking/GetStatsForOwners.py
new file mode 100644
index 0000000..d404cf5
--- /dev/null
+++ b/bfbc2_masterserver/messages/plasma/ranking/GetStatsForOwners.py
@@ -0,0 +1,12 @@
+from bfbc2_masterserver.models.general.PlasmaTransaction import PlasmaTransaction
+from bfbc2_masterserver.models.plasma.Owner import RankedOwner
+from bfbc2_masterserver.models.plasma.Stats import Stat
+
+
+class GetStatsForOwnersRequest(PlasmaTransaction):
+    owners: list[RankedOwner]
+    keys: list[str]
+
+
+class GetStatsForOwnersResponse(PlasmaTransaction):
+    stats: list[Stat]
diff --git a/bfbc2_masterserver/messages/plasma/ranking/GetTopN.py b/bfbc2_masterserver/messages/plasma/ranking/GetTopN.py
new file mode 100644
index 0000000..8b8056c
--- /dev/null
+++ b/bfbc2_masterserver/messages/plasma/ranking/GetTopN.py
@@ -0,0 +1,12 @@
+from bfbc2_masterserver.models.general.PlasmaTransaction import PlasmaTransaction
+from bfbc2_masterserver.models.plasma.Leaderboard import Leaderboard
+
+
+class GetTopNRequest(PlasmaTransaction):
+    key: str
+    minRank: int
+    maxRank: int
+
+
+class GetTopNResponse(PlasmaTransaction):
+    stats: list[Leaderboard]
diff --git a/bfbc2_masterserver/messages/plasma/ranking/GetTopNAndMe.py b/bfbc2_masterserver/messages/plasma/ranking/GetTopNAndMe.py
new file mode 100644
index 0000000..eddc754
--- /dev/null
+++ b/bfbc2_masterserver/messages/plasma/ranking/GetTopNAndMe.py
@@ -0,0 +1,12 @@
+from bfbc2_masterserver.models.general.PlasmaTransaction import PlasmaTransaction
+from bfbc2_masterserver.models.plasma.Leaderboard import Leaderboard
+
+
+class GetTopNAndMeRequest(PlasmaTransaction):
+    key: str
+    minRank: int
+    maxRank: int
+
+
+class GetTopNAndMeResponse(PlasmaTransaction):
+    stats: list[Leaderboard]
diff --git a/bfbc2_masterserver/messages/plasma/ranking/UpdateStats.py b/bfbc2_masterserver/messages/plasma/ranking/UpdateStats.py
new file mode 100644
index 0000000..67964d9
--- /dev/null
+++ b/bfbc2_masterserver/messages/plasma/ranking/UpdateStats.py
@@ -0,0 +1,10 @@
+from bfbc2_masterserver.models.general.PlasmaTransaction import PlasmaTransaction
+from bfbc2_masterserver.models.plasma.Stats import RankedOwnerStat, UserUpdateRequest
+
+
+class UpdateStatsRequest(PlasmaTransaction):
+    u: list[UserUpdateRequest]
+
+
+class UpdateStatsResponse(PlasmaTransaction):
+    pass
diff --git a/bfbc2_masterserver/messages/plasma/record/AddRecord.py b/bfbc2_masterserver/messages/plasma/record/AddRecord.py
new file mode 100644
index 0000000..66f818d
--- /dev/null
+++ b/bfbc2_masterserver/messages/plasma/record/AddRecord.py
@@ -0,0 +1,12 @@
+from bfbc2_masterserver.enumerators.plasma.RecordName import RecordName
+from bfbc2_masterserver.models.general.PlasmaTransaction import PlasmaTransaction
+from bfbc2_masterserver.models.plasma.Record import Record
+
+
+class AddRecordRequest(PlasmaTransaction):
+    recordName: RecordName
+    values: list[Record]
+
+
+class AddRecordResponse(PlasmaTransaction):
+    pass
diff --git a/bfbc2_masterserver/messages/plasma/record/AddRecordAsMap.py b/bfbc2_masterserver/messages/plasma/record/AddRecordAsMap.py
new file mode 100644
index 0000000..dc352c3
--- /dev/null
+++ b/bfbc2_masterserver/messages/plasma/record/AddRecordAsMap.py
@@ -0,0 +1,13 @@
+from datetime import datetime
+
+from bfbc2_masterserver.enumerators.plasma.RecordName import RecordName
+from bfbc2_masterserver.models.general.PlasmaTransaction import PlasmaTransaction
+
+
+class AddRecordAsMapRequest(PlasmaTransaction):
+    recordName: RecordName
+    values: dict[str, str]
+
+
+class AddRecordAsMapResponse(PlasmaTransaction):
+    pass
diff --git a/bfbc2_masterserver/messages/plasma/record/UpdateRecord.py b/bfbc2_masterserver/messages/plasma/record/UpdateRecord.py
new file mode 100644
index 0000000..915df93
--- /dev/null
+++ b/bfbc2_masterserver/messages/plasma/record/UpdateRecord.py
@@ -0,0 +1,12 @@
+from bfbc2_masterserver.enumerators.plasma.RecordName import RecordName
+from bfbc2_masterserver.models.general.PlasmaTransaction import PlasmaTransaction
+from bfbc2_masterserver.models.plasma.Record import Record
+
+
+class UpdateRecordRequest(PlasmaTransaction):
+    recordName: RecordName
+    values: list[Record]
+
+
+class UpdateRecordResponse(PlasmaTransaction):
+    pass
diff --git a/bfbc2_masterserver/messages/plasma/record/UpdateRecordAsMap.py b/bfbc2_masterserver/messages/plasma/record/UpdateRecordAsMap.py
new file mode 100644
index 0000000..cc895c8
--- /dev/null
+++ b/bfbc2_masterserver/messages/plasma/record/UpdateRecordAsMap.py
@@ -0,0 +1,13 @@
+from datetime import datetime
+
+from bfbc2_masterserver.enumerators.plasma.RecordName import RecordName
+from bfbc2_masterserver.models.general.PlasmaTransaction import PlasmaTransaction
+
+
+class UpdateRecordAsMapRequest(PlasmaTransaction):
+    recordName: RecordName
+    values: dict[str, str]
+
+
+class UpdateRecordAsMapResponse(PlasmaTransaction):
+    pass
diff --git a/bfbc2_masterserver/models/plasma/Association.py b/bfbc2_masterserver/models/plasma/Association.py
index 7340491..3a20834 100644
--- a/bfbc2_masterserver/models/plasma/Association.py
+++ b/bfbc2_masterserver/models/plasma/Association.py
@@ -1,19 +1,28 @@
 from datetime import datetime
+from typing import Optional
 
 from pydantic import BaseModel
 
 from bfbc2_masterserver.models.plasma.Owner import Owner
 
 
+class AssociationRequest(BaseModel):
+    owner: Owner
+    member: Owner
+    mutual: bool
+
+
 class AssociationReturn(BaseModel):
     id: int
     name: str
     type: int
-    created: datetime
-    modified: datetime
+    created: Optional[datetime]
+    modified: Optional[datetime]
 
 
-class AssociationRequest(BaseModel):
+class AssociationResult(BaseModel):
+    member: AssociationReturn
     owner: Owner
-    member: Owner
     mutual: bool
+    outcome: int
+    listSize: int
diff --git a/bfbc2_masterserver/models/plasma/Attachment.py b/bfbc2_masterserver/models/plasma/Attachment.py
new file mode 100644
index 0000000..46839ec
--- /dev/null
+++ b/bfbc2_masterserver/models/plasma/Attachment.py
@@ -0,0 +1,7 @@
+from pydantic import BaseModel
+
+
+class Attachment(BaseModel):
+    key: str
+    type: str
+    data: str
diff --git a/bfbc2_masterserver/models/plasma/Attribute.py b/bfbc2_masterserver/models/plasma/Attribute.py
new file mode 100644
index 0000000..74ef625
--- /dev/null
+++ b/bfbc2_masterserver/models/plasma/Attribute.py
@@ -0,0 +1,6 @@
+from pydantic import BaseModel
+
+
+class Attribute(BaseModel):
+    name: str
+    value: str
diff --git a/bfbc2_masterserver/models/plasma/Message.py b/bfbc2_masterserver/models/plasma/Message.py
index 373948a..fd90727 100644
--- a/bfbc2_masterserver/models/plasma/Message.py
+++ b/bfbc2_masterserver/models/plasma/Message.py
@@ -21,7 +21,7 @@ class MessageResponse(BaseModel):
     messageId: int
     messageType: str
     purgeStrategy: str
-    from_: Target = Field(validation_alias="from")
+    from_: Target = Field(alias="from")
     to: list[Target]
     timeSent: datetime
     expiration: datetime
diff --git a/bfbc2_masterserver/models/plasma/Presence.py b/bfbc2_masterserver/models/plasma/Presence.py
new file mode 100644
index 0000000..1231754
--- /dev/null
+++ b/bfbc2_masterserver/models/plasma/Presence.py
@@ -0,0 +1,12 @@
+from pydantic import BaseModel
+
+from bfbc2_masterserver.models.plasma.Owner import Owner
+
+
+class PresenceRequest(BaseModel):
+    userId: int
+
+
+class PresenceResponse(BaseModel):
+    owner: Owner
+    outcome: int
diff --git a/bfbc2_masterserver/models/plasma/Stats.py b/bfbc2_masterserver/models/plasma/Stats.py
index 16ee58a..ff0e22c 100644
--- a/bfbc2_masterserver/models/plasma/Stats.py
+++ b/bfbc2_masterserver/models/plasma/Stats.py
@@ -2,6 +2,8 @@
 
 from pydantic import BaseModel
 
+from bfbc2_masterserver.enumerators.plasma.StatUpdateType import StatUpdateType
+
 
 class Stat(BaseModel):
     key: str
@@ -16,3 +18,14 @@ class RankedOwnerStat(BaseModel):
     rankedStats: list[RankedStat]
     ownerId: int
     ownerType: int
+
+
+class StatUpdate(BaseModel):
+    ut: StatUpdateType
+    k: str
+    v: Decimal
+
+
+class UserUpdateRequest(BaseModel):
+    o: int
+    s: list[StatUpdate]
diff --git a/bfbc2_masterserver/models/plasma/Status.py b/bfbc2_masterserver/models/plasma/Status.py
new file mode 100644
index 0000000..5b16676
--- /dev/null
+++ b/bfbc2_masterserver/models/plasma/Status.py
@@ -0,0 +1,6 @@
+from pydantic import BaseModel
+
+
+class Status(BaseModel):
+    userid: int
+    status: int
diff --git a/bfbc2_masterserver/models/plasma/database/Message.py b/bfbc2_masterserver/models/plasma/database/Message.py
index df97316..c60c1cc 100644
--- a/bfbc2_masterserver/models/plasma/database/Message.py
+++ b/bfbc2_masterserver/models/plasma/database/Message.py
@@ -17,17 +17,23 @@ class Message(SQLModel, table=True):
 
     sender: "Persona" = Relationship(
         back_populates="messagesSent",
-        sa_relationship_kwargs=dict(foreign_keys="[Message.sender_id]"),
+        sa_relationship_kwargs=dict(
+            foreign_keys="[Message.sender_id]", lazy="selectin"
+        ),
     )
-    sender_id: int = Field(default=None, foreign_key="persona.id")
+    sender_id: int | None = Field(default=None, foreign_key="persona.id")
 
     recipient: "Persona" = Relationship(
         back_populates="messages",
-        sa_relationship_kwargs=dict(foreign_keys="[Message.recipient_id]"),
+        sa_relationship_kwargs=dict(
+            foreign_keys="[Message.recipient_id]", lazy="selectin"
+        ),
     )
-    recipient_id: int = Field(default=None, foreign_key="persona.id")
+    recipient_id: int | None = Field(default=None, foreign_key="persona.id")
 
-    attachments: list["MessageAttachment"] = Relationship(back_populates="message")
+    attachments: list["MessageAttachment"] = Relationship(
+        back_populates="message", sa_relationship_kwargs=dict(lazy="selectin")
+    )
 
     deliveryType: str
     messageType: str
diff --git a/bfbc2_masterserver/models/plasma/database/MessageAttachment.py b/bfbc2_masterserver/models/plasma/database/MessageAttachment.py
index 5e531df..cca8b31 100644
--- a/bfbc2_masterserver/models/plasma/database/MessageAttachment.py
+++ b/bfbc2_masterserver/models/plasma/database/MessageAttachment.py
@@ -10,7 +10,7 @@ class MessageAttachment(SQLModel, table=True):
     id: int = Field(default=None, primary_key=True)
 
     message: "Message" = Relationship(back_populates="attachments")
-    message_id: int = Field(default=None, foreign_key="message.id")
+    message_id: int | None = Field(default=None, foreign_key="message.id")
 
     key: str
     type: str
diff --git a/bfbc2_masterserver/plasma.py b/bfbc2_masterserver/plasma.py
index 7851de9..63cfc0f 100644
--- a/bfbc2_masterserver/plasma.py
+++ b/bfbc2_masterserver/plasma.py
@@ -4,6 +4,7 @@
 from pydoc import resolve
 
 from fastapi import WebSocket
+from fastapi.websockets import WebSocketState
 from pydantic import ValidationError
 
 from bfbc2_masterserver.dataclasses.Client import Client
@@ -308,10 +309,16 @@ async def __send(self, message: Message) -> None:
                 )
 
                 # Send the fragment
-                await self.websocket.send_bytes(message_fragment.compile())
+                try:
+                    await self.websocket.send_bytes(message_fragment.compile())
+                except RuntimeError as e:
+                    self.on_disconnect(str(e), None)
         else:
             # If the response fits into one message, send it
-            await self.websocket.send_bytes(response_bytes)
+            try:
+                await self.websocket.send_bytes(response_bytes)
+            except RuntimeError as e:
+                self.on_disconnect(str(e), None)
 
     def disconnect(self, reason: int) -> None:
         self.start_transaction(
diff --git a/bfbc2_masterserver/services/plasma/account.py b/bfbc2_masterserver/services/plasma/account.py
index c064f7e..5753f40 100644
--- a/bfbc2_masterserver/services/plasma/account.py
+++ b/bfbc2_masterserver/services/plasma/account.py
@@ -13,6 +13,10 @@
 from bfbc2_masterserver.enumerators.ErrorCode import ErrorCode
 from bfbc2_masterserver.enumerators.fesl.FESLTransaction import FESLTransaction
 from bfbc2_masterserver.error import TransactionError
+from bfbc2_masterserver.messages.plasma.account.GameSpyPreAuth import (
+    GameSpyPreAuthRequest,
+    GameSpyPreAuthResponse,
+)
 from bfbc2_masterserver.messages.plasma.account.GetCountryList import (
     GetCountryListRequest,
     GetCountryListResponse,
@@ -33,6 +37,10 @@
     NuAddPersonaRequest,
     NuAddPersonaResponse,
 )
+from bfbc2_masterserver.messages.plasma.account.NuCreateEncryptedToken import (
+    NuCreateEncryptedTokenRequest,
+    NuCreateEncryptedTokenResponse,
+)
 from bfbc2_masterserver.messages.plasma.account.NuDisablePersona import (
     NuDisablePersonaRequest,
     NuDisablePersonaResponse,
@@ -45,6 +53,22 @@
     NuEntitleUserRequest,
     NuEntitleUserResponse,
 )
+from bfbc2_masterserver.messages.plasma.account.NuGetAccount import (
+    NuGetAccountRequest,
+    NuGetAccountResponse,
+)
+from bfbc2_masterserver.messages.plasma.account.NuGetAccountByNuid import (
+    NuGetAccountByNuidRequest,
+    NuGetAccountByNuidResponse,
+)
+from bfbc2_masterserver.messages.plasma.account.NuGetAccountByPS3Ticket import (
+    NuGetAccountByPS3TicketRequest,
+    NuGetAccountByPS3TicketResponse,
+)
+from bfbc2_masterserver.messages.plasma.account.NuGetEntitlementCount import (
+    NuGetEntitlementCountRequest,
+    NuGetEntitlementCountResponse,
+)
 from bfbc2_masterserver.messages.plasma.account.NuGetEntitlements import (
     NuGetEntitlementsRequest,
     NuGetEntitlementsResponse,
@@ -73,9 +97,42 @@
     NuLookupUserInfoRequest,
     NuLookupUserInfoResponse,
 )
+from bfbc2_masterserver.messages.plasma.account.NuPS3AddAccount import (
+    NuPS3AddAccountRequest,
+    NuPS3AddAccountResponse,
+)
+from bfbc2_masterserver.messages.plasma.account.NuPS3Login import (
+    NuPS3LoginRequest,
+    NuPS3LoginResponse,
+)
+from bfbc2_masterserver.messages.plasma.account.NuSearchOwners import (
+    NuSearchOwnersRequest,
+    NuSearchOwnersResponse,
+)
+from bfbc2_masterserver.messages.plasma.account.NuSuggestPersonas import (
+    NuSuggestPersonasRequest,
+    NuSuggestPersonasResponse,
+)
+from bfbc2_masterserver.messages.plasma.account.NuUpdateAccount import (
+    NuUpdateAccountRequest,
+    NuUpdateAccountResponse,
+)
+from bfbc2_masterserver.messages.plasma.account.NuUpdatePassword import (
+    NuUpdatePasswordRequest,
+    NuUpdatePasswordResponse,
+)
+from bfbc2_masterserver.messages.plasma.account.NuXBL360AddAccount import (
+    NuXBL360AddAccountRequest,
+    NuXBL360AddAccountResponse,
+)
+from bfbc2_masterserver.messages.plasma.account.NuXBL360Login import (
+    NuXBL360LoginRequest,
+    NuXBL360LoginResponse,
+)
 from bfbc2_masterserver.models.plasma.database.Account import Account
 from bfbc2_masterserver.models.plasma.database.Persona import Persona
 from bfbc2_masterserver.models.plasma.Entitlement import Entitlement
+from bfbc2_masterserver.models.plasma.Owner import Owner
 from bfbc2_masterserver.models.plasma.UserInfo import UserInfo
 from bfbc2_masterserver.tools.country_list import COUNTRY_LIST, getLocalizedCountryList
 from bfbc2_masterserver.tools.terms_of_service import getLocalizedTOS
@@ -159,6 +216,71 @@ def __init__(self, plasma) -> None:
             NuGrantEntitlementRequest,
         )
 
+        self.resolvers[FESLTransaction.NuCreateEncryptedToken] = (
+            self.__handle_nu_create_encrypted_token,
+            NuCreateEncryptedTokenRequest,
+        )
+
+        self.resolvers[FESLTransaction.NuUpdatePassword] = (
+            self.__handle_nu_update_password,
+            NuUpdatePasswordRequest,
+        )
+
+        self.resolvers[FESLTransaction.NuGetAccount] = (
+            self.__handle_nu_get_account,
+            NuGetAccountRequest,
+        )
+
+        self.resolvers[FESLTransaction.NuGetAccountByNuid] = (
+            self.__handle_nu_get_account_by_nuid,
+            NuGetAccountByNuidRequest,
+        )
+
+        self.resolvers[FESLTransaction.NuGetAccountByPS3Ticket] = (
+            self.__handle_nu_get_account_by_ps3_ticket,
+            NuGetAccountByPS3TicketRequest,
+        )
+
+        self.resolvers[FESLTransaction.NuUpdateAccount] = (
+            self.__handle_nu_update_account,
+            NuUpdateAccountRequest,
+        )
+
+        self.resolvers[FESLTransaction.GameSpyPreAuth] = (
+            self.__handle_gamespy_pre_auth,
+            GameSpyPreAuthRequest,
+        )
+
+        self.resolvers[FESLTransaction.NuXBL360Login] = (
+            self.__handle_nu_xbl360_login,
+            NuXBL360LoginRequest,
+        )
+
+        self.resolvers[FESLTransaction.NuXBL360AddAccount] = (
+            self.__handle_nu_xbl360_add_account,
+            NuXBL360AddAccountRequest,
+        )
+
+        self.resolvers[FESLTransaction.NuPS3Login] = (
+            self.__handle_nu_ps3_login,
+            NuPS3LoginRequest,
+        )
+
+        self.resolvers[FESLTransaction.NuPS3AddAccount] = (
+            self.__handle_nu_ps3_add_account,
+            NuPS3AddAccountRequest,
+        )
+
+        self.resolvers[FESLTransaction.NuSearchOwners] = (
+            self.__handle_nu_search_owners,
+            NuSearchOwnersRequest,
+        )
+
+        self.resolvers[FESLTransaction.NuGetEntitlementCount] = (
+            self.__handle_nu_get_entitlement_count,
+            NuGetEntitlementCountRequest,
+        )
+
     def _get_resolver(self, txn):
         """
         Gets the resolver for a given transaction.
@@ -641,3 +763,100 @@ def __handle_nu_grant_entitlement(
     ) -> NuGrantEntitlementResponse | TransactionError:
         self.database.account_grant_entitlement(data)
         return NuGrantEntitlementResponse()
+
+    def __handle_nu_create_encrypted_token(
+        self, data: NuCreateEncryptedTokenRequest
+    ) -> NuCreateEncryptedTokenResponse | TransactionError:
+        # I don't know what is the expected response for this transaction
+        # Name suggest that it should create new session? But I don't think it's ever called by the client
+        raise NotImplementedError("NuCreateEncryptedToken is not implemented")
+
+    def __handle_nu_suggest_personas(
+        self, data: NuSuggestPersonasRequest
+    ) -> NuSuggestPersonasResponse | TransactionError:
+        # Is this ever called from the client?
+
+        # Not sure what "name" is here, but I assume it's the name of the currently logged persona
+        if not self.connection.persona:
+            return TransactionError(ErrorCode.SESSION_NOT_AUTHORIZED)
+
+        if self.connection.persona.name != data.name:
+            return TransactionError(ErrorCode.SYSTEM_ERROR)
+
+        # I don't know what is the expected response for this transaction
+        raise NotImplementedError("NuSuggestPersonas is not implemented")
+
+    def __handle_nu_update_password(
+        self, data: NuUpdatePasswordRequest
+    ) -> NuUpdatePasswordResponse | TransactionError:
+        # Is this ever called from the client?
+        raise NotImplementedError("NuUpdatePassword is not implemented")
+
+    def __handle_nu_get_account(
+        self, data: NuGetAccountRequest
+    ) -> NuGetAccountResponse | TransactionError:
+        # Is this ever called from the client?
+        raise NotImplementedError("NuGetAccount is not implemented")
+
+    def __handle_nu_get_account_by_nuid(
+        self, data: NuGetAccountByNuidRequest
+    ) -> NuGetAccountByNuidResponse | TransactionError:
+        # Is this ever called from the client?
+        raise NotImplementedError("NuGetAccountByNuid is not implemented")
+
+    def __handle_nu_get_account_by_ps3_ticket(
+        self, data: NuGetAccountByPS3TicketRequest
+    ) -> NuGetAccountByPS3TicketResponse | TransactionError:
+        # Is this ever called from the client?
+        raise NotImplementedError("NuGetAccountByPS3Ticket is not implemented")
+
+    def __handle_nu_update_account(
+        self, data: NuUpdateAccountRequest
+    ) -> NuUpdateAccountResponse | TransactionError:
+        # Is this ever called from the client?
+        raise NotImplementedError("NuUpdateAccount is not implemented")
+
+    def __handle_gamespy_pre_auth(
+        self, data: GameSpyPreAuthRequest
+    ) -> GameSpyPreAuthResponse | TransactionError:
+        # Is this ever called from the client?
+        raise NotImplementedError("GameSpyPreAuth is not implemented")
+
+    def __handle_nu_xbl360_login(
+        self, data: NuXBL360LoginRequest
+    ) -> NuXBL360LoginResponse | TransactionError:
+        # Is this ever called from the client?
+        raise NotImplementedError("NuXBL360Login is not implemented")
+
+    def __handle_nu_xbl360_add_account(
+        self, data: NuXBL360AddAccountRequest
+    ) -> NuXBL360AddAccountResponse | TransactionError:
+        # Is this ever called from the client?
+        raise NotImplementedError("NuXBL360AddAccount is not implemented")
+
+    def __handle_nu_ps3_login(
+        self, data: NuPS3LoginRequest
+    ) -> NuPS3LoginResponse | TransactionError:
+        # Is this ever called from the client?
+        raise NotImplementedError("NuPS3Login is not implemented")
+
+    def __handle_nu_ps3_add_account(
+        self, data: NuPS3AddAccountRequest
+    ) -> NuPS3AddAccountResponse | TransactionError:
+        # Is this ever called from the client?
+        raise NotImplementedError("NuPS3AddAccount is not implemented")
+
+    def __handle_nu_search_owners(
+        self, data: NuSearchOwnersRequest
+    ) -> NuSearchOwnersResponse | TransactionError:
+        owners = self.database.persona_search(data.screenName)
+        return NuSearchOwnersResponse(
+            users=[Owner(id=owner.id, name=owner.name, type=1) for owner in owners],
+            nameSpaceId="battlefield",
+        )
+
+    def __handle_nu_get_entitlement_count(
+        self, data: NuGetEntitlementCountRequest
+    ) -> NuGetEntitlementCountResponse | TransactionError:
+        # Is this ever called from the client?
+        raise NotImplementedError("NuGetEntitlementCount is not implemented")
diff --git a/bfbc2_masterserver/services/plasma/association.py b/bfbc2_masterserver/services/plasma/association.py
index d5a4e1d..a3b8db6 100644
--- a/bfbc2_masterserver/services/plasma/association.py
+++ b/bfbc2_masterserver/services/plasma/association.py
@@ -1,20 +1,35 @@
-from typing import Tuple
+from ast import Delete
 
 from bfbc2_masterserver.dataclasses.plasma.Service import PlasmaService
 from bfbc2_masterserver.enumerators.ErrorCode import ErrorCode
+from bfbc2_masterserver.enumerators.fesl.FESLService import FESLService
 from bfbc2_masterserver.enumerators.fesl.FESLTransaction import FESLTransaction
 from bfbc2_masterserver.enumerators.plasma.AssocationType import AssocationType
+from bfbc2_masterserver.enumerators.plasma.AssocationUpdateOperation import (
+    AssocationUpdateOperation,
+)
+from bfbc2_masterserver.enumerators.plasma.ListFullBehavior import ListFullBehavior
 from bfbc2_masterserver.error import TransactionError
 from bfbc2_masterserver.messages.plasma.assocation.AddAssocations import (
     AddAssociationsRequest,
+    AddAssociationsResponse,
+)
+from bfbc2_masterserver.messages.plasma.assocation.DeleteAssociations import (
+    DeleteAssociationsRequest,
+    DeleteAssociationsResponse,
 )
 from bfbc2_masterserver.messages.plasma.assocation.GetAssociations import (
     GetAssociationsRequest,
     GetAssociationsResponse,
 )
-from bfbc2_masterserver.models.general.PlasmaTransaction import PlasmaTransaction
+from bfbc2_masterserver.messages.plasma.assocation.GetAssociationsCount import (
+    GetAssociationsCountRequest,
+)
+from bfbc2_masterserver.messages.plasma.assocation.NotifyAssociationUpdate import (
+    NotifyAssociationUpdate,
+)
 from bfbc2_masterserver.models.plasma.Association import (
-    AssociationRequest,
+    AssociationResult,
     AssociationReturn,
 )
 from bfbc2_masterserver.models.plasma.database.Association import Association
@@ -36,6 +51,20 @@ def __init__(self, plasma) -> None:
             AddAssociationsRequest,
         )
 
+        self.resolvers[FESLTransaction.DeleteAssociations] = (
+            self.__handle_delete_associations,
+            DeleteAssociationsRequest,
+        )
+
+        self.resolvers[FESLTransaction.GetAssociationsCount] = (
+            self.__handle_get_associations_count,
+            GetAssociationsCountRequest,
+        )
+
+        self.generators[FESLTransaction.NotifyAssociationUpdate] = (
+            self.__notify_association_update
+        )
+
     def _get_resolver(self, txn):
         """
         Gets the resolver for a given transaction.
@@ -63,7 +92,7 @@ def _get_generator(self, txn):
     def __handle_get_associations(
         self, data: GetAssociationsRequest
     ) -> GetAssociationsResponse | TransactionError:
-        assocations: list[Association] | ErrorCode = self.database.assocation_get(
+        assocations: list[Association] | ErrorCode = self.database.association_get_all(
             data.owner.id, data.type
         )
 
@@ -96,5 +125,214 @@ def __handle_get_associations(
         return response
 
     def __handle_add_associations(self, data: AddAssociationsRequest):
-        # TODO
-        return PlasmaTransaction()
+        if self.connection.persona is None:
+            return TransactionError(ErrorCode.SESSION_NOT_AUTHORIZED)
+
+        maxAssociations = 20 if data.type != AssocationType.PlasmaRecentPlayers else 100
+        result = []
+
+        for addRequest in data.addRequests:
+            outcome = 0
+
+            associations = self.database.association_get_all(
+                addRequest.owner.id, data.type
+            )
+
+            if isinstance(associations, ErrorCode):
+                return TransactionError(ErrorCode.TRANSACTION_DATA_NOT_FOUND)
+
+            isListFull = len(associations) >= maxAssociations
+
+            if isListFull:
+                if data.listFullBehavior == ListFullBehavior.ReturnError:
+                    outcome = 23005
+                elif (
+                    data.listFullBehavior == ListFullBehavior.RollLeastRecentlyModified
+                ):
+                    # Order by oldest first
+                    associations.sort(key=lambda x: x.updatedAt)
+                    self.database.association_delete(associations[0].id)
+
+            # Add new member
+            assoResult = self.database.association_add(
+                owner_id=addRequest.owner.id,
+                target_id=addRequest.member.id,
+                type=data.type,
+            )
+
+            if isinstance(assoResult, ErrorCode):
+                outcome = 23005
+
+            memberAccountId = self.database.persona_get_owner_id(addRequest.member.id)
+            memberPersona = self.database.persona_get_by_id(addRequest.member.id)
+
+            if isinstance(memberAccountId, ErrorCode) or isinstance(
+                memberPersona, ErrorCode
+            ):
+                continue
+
+            externalClient = self.plasma.manager.CLIENTS.get(memberAccountId)
+
+            if externalClient:
+                externalClient.plasma.start_transaction(
+                    FESLService.AssociationService,
+                    FESLTransaction.NotifyAssociationUpdate,
+                    NotifyAssociationUpdate(
+                        domainPartition=data.domainPartition,
+                        listSize=len(associations) + 1,
+                        member=AssociationReturn(
+                            id=addRequest.owner.id,
+                            name=self.connection.persona.name,
+                            type=1,
+                            created=(
+                                assoResult.createdAt
+                                if not isinstance(assoResult, ErrorCode)
+                                else None
+                            ),
+                            modified=(
+                                assoResult.updatedAt
+                                if not isinstance(assoResult, ErrorCode)
+                                else None
+                            ),
+                        ),
+                        operation=AssocationUpdateOperation.ADD,
+                        owner=Owner(
+                            id=addRequest.member.id,
+                            name=addRequest.member.name,
+                            type=addRequest.member.type,
+                        ),
+                        type=data.type.value,
+                    ),
+                )
+
+            result.append(
+                AssociationResult(
+                    member=AssociationReturn(
+                        id=addRequest.owner.id,
+                        name=self.connection.persona.name,
+                        type=1,
+                        created=(
+                            assoResult.createdAt
+                            if not isinstance(assoResult, ErrorCode)
+                            else None
+                        ),
+                        modified=(
+                            assoResult.updatedAt
+                            if not isinstance(assoResult, ErrorCode)
+                            else None
+                        ),
+                    ),
+                    owner=addRequest.owner,
+                    mutual=data.type != AssocationType.PlasmaRecentPlayers,
+                    outcome=outcome,
+                    listSize=len(associations) + 1,
+                )
+            )
+
+        response = AddAssociationsResponse(
+            domainPartition=data.domainPartition,
+            maxListSize=maxAssociations,
+            result=result,
+            type=data.type,
+        )
+
+        return response
+
+    def __handle_delete_associations(self, data: DeleteAssociationsRequest):
+        if self.connection.persona is None:
+            return TransactionError(ErrorCode.SESSION_NOT_AUTHORIZED)
+
+        maxAssociations = 20 if data.type != AssocationType.PlasmaRecentPlayers else 100
+        result = []
+
+        for deleteRequest in data.deleteRequests:
+            outcome = 0
+
+            association = self.database.association_get(
+                deleteRequest.owner.id, deleteRequest.member.id, data.type
+            )
+
+            associations = self.database.association_get_all(
+                deleteRequest.owner.id, data.type
+            )
+
+            if isinstance(associations, ErrorCode):
+                return TransactionError(ErrorCode.TRANSACTION_DATA_NOT_FOUND)
+
+            if not association:
+                outcome = 23005
+            else:
+                self.database.association_delete(association.id)
+
+            memberAccountId = self.database.persona_get_owner_id(
+                deleteRequest.member.id
+            )
+            memberPersona = self.database.persona_get_by_id(deleteRequest.member.id)
+
+            if isinstance(memberAccountId, ErrorCode) or isinstance(
+                memberPersona, ErrorCode
+            ):
+                continue
+
+            externalClient = self.plasma.manager.CLIENTS.get(memberAccountId)
+
+            if externalClient and association:
+                externalClient.plasma.start_transaction(
+                    FESLService.AssociationService,
+                    FESLTransaction.NotifyAssociationUpdate,
+                    NotifyAssociationUpdate(
+                        domainPartition=data.domainPartition,
+                        listSize=len(associations),
+                        member=AssociationReturn(
+                            id=deleteRequest.owner.id,
+                            name=self.connection.persona.name,
+                            type=1,
+                            created=association.createdAt,
+                            modified=association.updatedAt,
+                        ),
+                        operation=AssocationUpdateOperation.DEL,
+                        owner=Owner(
+                            id=deleteRequest.member.id,
+                            name=deleteRequest.member.name,
+                            type=deleteRequest.member.type,
+                        ),
+                        type=data.type.value,
+                    ),
+                )
+
+            if association:
+                result.append(
+                    AssociationResult(
+                        member=AssociationReturn(
+                            id=deleteRequest.member.id,
+                            name=memberPersona.name,
+                            type=1,
+                            created=association.createdAt,
+                            modified=association.updatedAt,
+                        ),
+                        owner=deleteRequest.owner,
+                        mutual=data.type != AssocationType.PlasmaRecentPlayers,
+                        outcome=outcome,
+                        listSize=len(associations),
+                    )
+                )
+
+        response = DeleteAssociationsResponse(
+            domainPartition=data.domainPartition,
+            maxListSize=maxAssociations,
+            result=result,
+            type=data.type,
+        )
+
+        return response
+
+    def __notify_association_update(self, data: NotifyAssociationUpdate):
+        return data
+
+    def __handle_get_associations_count(self, data: GetAssociationsCountRequest):
+        associations = self.database.association_get_all(data.owner.id, data.type)
+
+        if isinstance(associations, ErrorCode):
+            return TransactionError(ErrorCode.TRANSACTION_DATA_NOT_FOUND)
+
+        return len(associations)
diff --git a/bfbc2_masterserver/services/plasma/connect.py b/bfbc2_masterserver/services/plasma/connect.py
index fbb1aac..55f5999 100644
--- a/bfbc2_masterserver/services/plasma/connect.py
+++ b/bfbc2_masterserver/services/plasma/connect.py
@@ -21,6 +21,10 @@
     MemCheckResult,
 )
 from bfbc2_masterserver.messages.plasma.connect.Ping import PingRequest, PingResponse
+from bfbc2_masterserver.messages.plasma.connect.Suicide import (
+    SuicideRequest,
+    SuicideResponse,
+)
 from bfbc2_masterserver.models.general.PlasmaTransaction import PlasmaTransaction
 from bfbc2_masterserver.models.plasma.DomainPartition import DomainPartition
 from bfbc2_masterserver.models.plasma.MemCheck import MemCheck
@@ -167,15 +171,10 @@ def __handle_hello(self, data: HelloRequest) -> HelloResponse | TransactionError
         self.__make_memcheck()
         return response
 
-    def __create_memcheck(self, data: dict) -> MemCheckRequest:
+    def __create_memcheck(self, request: MemCheckRequest) -> MemCheckRequest:
         """
         Creates a memcheck request
         """
-        request = MemCheckRequest(
-            memcheck=MemCheck(),
-            type=0,
-            salt="".join(random.choice(string.digits) for _ in range(10)),
-        )
 
         return request
 
@@ -188,7 +187,7 @@ def __handle_memcheck(self, data: MemCheckResult) -> TransactionSkip:
 
     def __make_ping(self) -> None:
         self.plasma.start_transaction(
-            FESLService.ConnectService, FESLTransaction.Ping, PlasmaTransaction()
+            FESLService.ConnectService, FESLTransaction.Ping, PingRequest()
         )
 
         loop = asyncio.get_event_loop()
@@ -203,7 +202,13 @@ def __make_ping(self) -> None:
 
     def __make_memcheck(self) -> None:
         self.plasma.start_transaction(
-            FESLService.ConnectService, FESLTransaction.MemCheck, PlasmaTransaction()
+            FESLService.ConnectService,
+            FESLTransaction.MemCheck,
+            MemCheckRequest(
+                memcheck=MemCheck(),
+                type=0,
+                salt="".join(random.choice(string.digits) for _ in range(10)),
+            ),
         )
 
         loop = asyncio.get_event_loop()
@@ -283,13 +288,13 @@ def __handle_goodbye(self, data: GoodbyeRequest) -> TransactionSkip:
 
         return TransactionSkip()
 
-    def __handle_suicide(self, data: dict):
+    def __handle_suicide(self, data: SuicideRequest) -> SuicideResponse:
         """
         Client support this message, but I'm not sure what this Transaction is supposed to do,
         we ignore it - never captured this packet from original master server so I suppose it's another leftover.
         """
 
-        raise NotImplementedError()
+        raise NotImplementedError("Suicide not implemented")
 
-    def __create_goodbye(self, data: GoodbyeRequest):
+    def __create_goodbye(self, data: GoodbyeRequest) -> GoodbyeRequest:
         return data
diff --git a/bfbc2_masterserver/services/plasma/message.py b/bfbc2_masterserver/services/plasma/message.py
index 71748b0..dd18c96 100644
--- a/bfbc2_masterserver/services/plasma/message.py
+++ b/bfbc2_masterserver/services/plasma/message.py
@@ -1,7 +1,22 @@
+from datetime import datetime, timedelta
+
 from bfbc2_masterserver.dataclasses.plasma.Service import PlasmaService
 from bfbc2_masterserver.enumerators.ErrorCode import ErrorCode
+from bfbc2_masterserver.enumerators.fesl.FESLService import FESLService
 from bfbc2_masterserver.enumerators.fesl.FESLTransaction import FESLTransaction
 from bfbc2_masterserver.error import TransactionError
+from bfbc2_masterserver.messages.plasma.message.AsyncMessageEvent import (
+    AsyncMessageEvent,
+)
+from bfbc2_masterserver.messages.plasma.message.AsyncPurgedEvent import AsyncPurgedEvent
+from bfbc2_masterserver.messages.plasma.message.DeleteMessages import (
+    DeleteMessagesRequest,
+    DeleteMessagesResponse,
+)
+from bfbc2_masterserver.messages.plasma.message.GetMessageAttachments import (
+    GetMessageAttachmentsRequest,
+    GetMessageAttachmentsResponse,
+)
 from bfbc2_masterserver.messages.plasma.message.GetMessages import (
     GetMessagesRequest,
     GetMessagesResponse,
@@ -10,8 +25,20 @@
     ModifySettingsRequest,
     ModifySettingsResponse,
 )
+from bfbc2_masterserver.messages.plasma.message.PurgeMessages import (
+    PurgeMessagesRequest,
+    PurgeMessagesResponse,
+)
+from bfbc2_masterserver.messages.plasma.message.SendMessage import (
+    SendMessageRequest,
+    SendMessageResponse,
+)
 from bfbc2_masterserver.models.plasma.database.Message import Message
+from bfbc2_masterserver.models.plasma.database.MessageAttachment import (
+    MessageAttachment,
+)
 from bfbc2_masterserver.models.plasma.Message import Attachment, MessageResponse, Target
+from bfbc2_masterserver.models.plasma.Status import Status
 
 
 class ExtensibleMessageService(PlasmaService):
@@ -29,6 +56,33 @@ def __init__(self, plasma) -> None:
             GetMessagesRequest,
         )
 
+        self.resolvers[FESLTransaction.SendMessage] = (
+            self.__handle_send_message,
+            SendMessageRequest,
+        )
+
+        self.resolvers[FESLTransaction.GetMessageAttachments] = (
+            self.__handle_get_message_attachments,
+            GetMessageAttachmentsRequest,
+        )
+
+        self.resolvers[FESLTransaction.DeleteMessages] = (
+            self.__handle_delete_messages,
+            DeleteMessagesRequest,
+        )
+
+        self.resolvers[FESLTransaction.PurgeMessages] = (
+            self.__handle_purge_messages,
+            PurgeMessagesRequest,
+        )
+
+        self.generators[FESLTransaction.AsyncMessageEvent] = (
+            self.__create_async_message_event
+        )
+        self.generators[FESLTransaction.AsyncPurgedEvent] = (
+            self.__create_async_purged_event
+        )
+
     def _get_resolver(self, txn):
         """
         Gets the resolver for a given transaction.
@@ -64,38 +118,171 @@ def __handle_get_messages(
         if self.connection.persona is None:
             return TransactionError(ErrorCode.SYSTEM_ERROR)
 
-        messages_db: list[Message] = self.database.message_get(
+        messages_db: list[Message] = self.database.message_get_all(
             self.connection.persona.id
         )
 
         messages: list[MessageResponse] = [
-            MessageResponse(
-                attachments=[
-                    Attachment(
-                        key=attachment.key, type=attachment.type, data=attachment.data
-                    )
-                    for attachment in message.attachments
-                ],
-                deliveryType=message.deliveryType,
-                messageId=message.id,
-                messageType=message.messageType,
-                purgeStrategy=message.purgeStrategy,
-                from_=Target(
-                    name=message.sender.name,
-                    id=message.sender.id,
-                    type=1,
-                ),
-                to=[
-                    Target(
-                        name=message.recipient.name,
-                        id=message.recipient.id,
+            MessageResponse.model_validate(
+                {
+                    "attachments": [
+                        Attachment(
+                            key=attachment.key,
+                            type=attachment.type,
+                            data=attachment.data,
+                        )
+                        for attachment in message.attachments
+                    ],
+                    "deliveryType": message.deliveryType,
+                    "messageId": message.id,
+                    "messageType": message.messageType,
+                    "purgeStrategy": message.purgeStrategy,
+                    "from": Target(
+                        name=message.sender.name,
+                        id=message.sender.id,
                         type=1,
-                    )
-                ],
-                timeSent=message.timeSent,
-                expiration=message.expiration,
+                    ),
+                    "to": [
+                        Target(
+                            name=message.recipient.name,
+                            id=message.recipient.id,
+                            type=1,
+                        )
+                    ],
+                    "timeSent": message.timeSent,
+                    "expiration": message.expiration,
+                }
             )
             for message in messages_db
         ]
 
         return GetMessagesResponse(messages=messages)
+
+    def __handle_send_message(
+        self, data: SendMessageRequest
+    ) -> SendMessageResponse | TransactionError:
+        if not self.connection.persona:
+            return TransactionError(ErrorCode.SESSION_NOT_AUTHORIZED)
+
+        expiration_date = datetime.now() + timedelta(seconds=data.expires)
+
+        # While the game code only allows one recipient, the message schema allows for multiple recipients
+        targetPersona = self.database.persona_get_by_id(data.to[0])
+
+        if isinstance(targetPersona, ErrorCode):
+            return TransactionError(targetPersona)
+
+        message = Message(
+            attachments=[
+                MessageAttachment(
+                    key=attachment.key,
+                    type=attachment.type,
+                    data=attachment.data,
+                )
+                for attachment in data.attachments
+            ],
+            deliveryType=data.deliveryType,
+            messageType=data.messageType,
+            purgeStrategy=data.purgeStrategy,
+            sender_id=self.connection.persona.id,
+            recipient=targetPersona,
+            expiration=expiration_date,
+        )
+
+        result = self.database.message_add(message)
+
+        accountId = self.database.persona_get_owner_id(targetPersona.id)
+
+        if isinstance(accountId, ErrorCode):
+            return TransactionError(accountId)
+
+        receiverSession = self.plasma.manager.CLIENTS.get(accountId)
+
+        if receiverSession:
+            receiverSession.plasma.start_transaction(
+                FESLService.MessageService,
+                FESLTransaction.AsyncMessageEvent,
+                AsyncMessageEvent.model_validate(
+                    {
+                        "attachments": [
+                            Attachment(
+                                key=attachment.key,
+                                type=attachment.type,
+                                data=attachment.data,
+                            )
+                            for attachment in message.attachments
+                        ],
+                        "deliveryType": message.deliveryType,
+                        "messageId": result.id,
+                        "messageType": message.messageType,
+                        "purgeStrategy": message.purgeStrategy,
+                        "from": Target(
+                            name=self.connection.persona.name,
+                            id=self.connection.persona.id,
+                            type=1,
+                        ),
+                        "to": [
+                            Target(
+                                name=targetPersona.name,
+                                id=targetPersona.id,
+                                type=1,
+                            )
+                        ],
+                        "timeSent": result.timeSent,
+                        "expiration": message.expiration,
+                    }
+                ),
+            ),
+
+        return SendMessageResponse(
+            messageId=result.id, status=[Status(userid=data.to[0], status=0)]
+        )
+
+    def __handle_get_message_attachments(
+        self, data: GetMessageAttachmentsRequest
+    ) -> GetMessageAttachmentsResponse | TransactionError:
+        # Is this ever called from the client?
+        raise NotImplementedError("GetMessageAttachments is not implemented.")
+
+    def __handle_delete_messages(
+        self, data: DeleteMessagesRequest
+    ) -> DeleteMessagesResponse | TransactionError:
+        for messageId in data.messageIds:
+            message = self.database.message_get(messageId)
+            self.database.message_delete(messageId)
+
+            if message and (message.sender_id and message.recipient_id):
+                senderSession = self.plasma.manager.CLIENTS.get(message.sender_id)
+                receiverSession = self.plasma.manager.CLIENTS.get(message.recipient_id)
+
+                if senderSession:
+                    senderSession.plasma.start_transaction(
+                        FESLService.MessageService,
+                        FESLTransaction.AsyncPurgedEvent,
+                        AsyncPurgedEvent(messageIds=[messageId]),
+                    )
+
+                if receiverSession:
+                    receiverSession.plasma.start_transaction(
+                        FESLService.MessageService,
+                        FESLTransaction.AsyncPurgedEvent,
+                        AsyncPurgedEvent(messageIds=[messageId]),
+                    )
+
+        return DeleteMessagesResponse()
+
+    def __handle_purge_messages(
+        self, data: PurgeMessagesRequest
+    ) -> PurgeMessagesResponse:
+        # Is this ever called from the client?
+        raise NotImplementedError("PurgeMessages is not implemented.")
+
+    def __create_async_message_event(
+        self, request: AsyncMessageEvent
+    ) -> AsyncMessageEvent:
+        return request
+
+    def __create_async_purged_event(
+        self, request: AsyncPurgedEvent
+    ) -> AsyncPurgedEvent:
+        return request
diff --git a/bfbc2_masterserver/services/plasma/playnow.py b/bfbc2_masterserver/services/plasma/playnow.py
index 8b1e61f..4bb53b6 100644
--- a/bfbc2_masterserver/services/plasma/playnow.py
+++ b/bfbc2_masterserver/services/plasma/playnow.py
@@ -73,7 +73,7 @@ async def __matchmaking(self, matchmakingId, params):
         prefGamemode = params.get("prefGamemode")
         prefLevel = params.get("prefLevel")
 
-        game = self.database.find_game(prefGamemode, prefLevel)
+        game = self.database.game_find(prefGamemode, prefLevel)
 
         if game:
             self.plasma.start_transaction(
@@ -81,8 +81,8 @@ async def __matchmaking(self, matchmakingId, params):
                 FESLTransaction.Status,
                 StatusRequest(
                     id=matchmakingId,
-                    gid=game.GID,
-                    lid=game.LID,
+                    gid=game.id,
+                    lid=game.lobbyId,
                 ),
             )
         else:
diff --git a/bfbc2_masterserver/services/plasma/presence.py b/bfbc2_masterserver/services/plasma/presence.py
index d7d3559..4ad9911 100644
--- a/bfbc2_masterserver/services/plasma/presence.py
+++ b/bfbc2_masterserver/services/plasma/presence.py
@@ -1,14 +1,28 @@
 import json
-from base64 import b64encode
+from base64 import b64decode, b64encode
 
 from bfbc2_masterserver.dataclasses.plasma.Service import PlasmaService
 from bfbc2_masterserver.enumerators.ErrorCode import ErrorCode
+from bfbc2_masterserver.enumerators.fesl.FESLService import FESLService
 from bfbc2_masterserver.enumerators.fesl.FESLTransaction import FESLTransaction
 from bfbc2_masterserver.error import TransactionError
+from bfbc2_masterserver.messages.plasma.presence.AsyncPresenceStatusEvent import (
+    AsyncPresenceStatusEvent,
+)
+from bfbc2_masterserver.messages.plasma.presence.PresenceSubscribe import (
+    PresenceSubscribeRequest,
+    PresenceSubscribeResponse,
+)
+from bfbc2_masterserver.messages.plasma.presence.PresenceUnsubscribe import (
+    PresenceUnsubscribeRequest,
+    PresenceUnsubscribeResponse,
+)
 from bfbc2_masterserver.messages.plasma.presence.SetPresenceStatus import (
     SetPresenceStatusRequest,
     SetPresenceStatusResponse,
 )
+from bfbc2_masterserver.models.plasma.Owner import Owner
+from bfbc2_masterserver.models.plasma.Presence import PresenceResponse
 
 
 class PresenceService(PlasmaService):
@@ -21,6 +35,20 @@ def __init__(self, plasma) -> None:
             SetPresenceStatusRequest,
         )
 
+        self.resolvers[FESLTransaction.PresenceSubscribe] = (
+            self.__handle_presence_subscribe,
+            PresenceSubscribeRequest,
+        )
+
+        self.resolvers[FESLTransaction.PresenceUnsubscribe] = (
+            self.__handle_presence_unsubscribe,
+            PresenceUnsubscribeRequest,
+        )
+
+        self.generators[FESLTransaction.AsyncPresenceStatusEvent] = (
+            self.__create_async_presence_status_event
+        )
+
     def _get_resolver(self, txn):
         """
         Gets the resolver for a given transaction.
@@ -54,8 +82,87 @@ def __handle_set_presence_status(
         if not self.connection.persona:
             return TransactionError(ErrorCode.SYSTEM_ERROR)
 
-        self.plasma.manager.redis.set(
-            f"presence:{self.connection.persona.id}", statusEncoded
-        )
-
+        self.redis.set(f"presence:{self.connection.persona.id}", statusEncoded)
         return SetPresenceStatusResponse()
+
+    def __handle_presence_subscribe(
+        self, data: PresenceSubscribeRequest
+    ) -> PresenceSubscribeResponse | TransactionError:
+        if self.connection.persona is None:
+            return TransactionError(ErrorCode.SESSION_NOT_AUTHORIZED)
+
+        responses = []
+
+        for request in data.requests:
+            owner = self.database.persona_get_by_id(request.userId)
+
+            if isinstance(owner, ErrorCode):
+                return TransactionError(owner)
+
+            responses.append(
+                PresenceResponse(
+                    owner=Owner(id=owner.id, name=owner.name, type=0), outcome=0
+                )
+            )
+
+            targetUserStatus = self.redis.get(f"presence:{request.userId}")
+            accountId = self.database.persona_get_owner_id(owner.id)
+
+            if isinstance(accountId, ErrorCode):
+                return TransactionError(accountId)
+
+            receiverSession = self.plasma.manager.CLIENTS.get(accountId)
+
+            if targetUserStatus and receiverSession:
+                self.plasma.start_transaction(
+                    FESLService.PresenceService,
+                    FESLTransaction.AsyncPresenceStatusEvent,
+                    AsyncPresenceStatusEvent(
+                        initial=True,
+                        owner=Owner(id=owner.id, name=owner.name, type=0),
+                        status=json.loads(str(b64decode(targetUserStatus).decode("utf-8"))),  # type: ignore
+                    ),
+                )
+
+                receiverSession.plasma.start_transaction(
+                    FESLService.PresenceService,
+                    FESLTransaction.AsyncPresenceStatusEvent,
+                    AsyncPresenceStatusEvent(
+                        initial=True,
+                        owner=Owner(
+                            id=self.connection.persona.id,
+                            name=self.connection.persona.name,
+                            type=0,
+                        ),
+                        status=json.loads(str(b64decode(targetUserStatus).decode("utf-8"))),  # type: ignore
+                    ),
+                )
+
+        return PresenceSubscribeResponse(responses=responses)
+
+    def __handle_presence_unsubscribe(
+        self, data: PresenceUnsubscribeRequest
+    ) -> PresenceUnsubscribeResponse | TransactionError:
+        if self.connection.persona is None:
+            return TransactionError(ErrorCode.SESSION_NOT_AUTHORIZED)
+
+        responses = []
+
+        for request in data.requests:
+            owner = self.database.persona_get_by_id(request.userId)
+
+            if isinstance(owner, ErrorCode):
+                return TransactionError(owner)
+
+            responses.append(
+                PresenceResponse(
+                    owner=Owner(id=owner.id, name=owner.name, type=0), outcome=0
+                )
+            )
+
+        return PresenceUnsubscribeResponse(responses=responses)
+
+    def __create_async_presence_status_event(
+        self, request: AsyncPresenceStatusEvent
+    ) -> AsyncPresenceStatusEvent:
+        return request
diff --git a/bfbc2_masterserver/services/plasma/ranking.py b/bfbc2_masterserver/services/plasma/ranking.py
index 1ec29c8..06985cf 100644
--- a/bfbc2_masterserver/services/plasma/ranking.py
+++ b/bfbc2_masterserver/services/plasma/ranking.py
@@ -4,6 +4,10 @@
 from bfbc2_masterserver.enumerators.ErrorCode import ErrorCode
 from bfbc2_masterserver.enumerators.fesl.FESLTransaction import FESLTransaction
 from bfbc2_masterserver.error import TransactionError
+from bfbc2_masterserver.messages.plasma.ranking.GetDateRange import (
+    GetDateRangeRequest,
+    GetDateRangeResponse,
+)
 from bfbc2_masterserver.messages.plasma.ranking.GetRankedStatsForOwners import (
     GetRankedStatsForOwnersRequest,
     GetRankedStatsForOwnersResponse,
@@ -16,11 +20,27 @@
     GetStatsRequest,
     GetStatsResponse,
 )
+from bfbc2_masterserver.messages.plasma.ranking.GetStatsForOwners import (
+    GetStatsForOwnersRequest,
+    GetStatsForOwnersResponse,
+)
+from bfbc2_masterserver.messages.plasma.ranking.GetTopN import (
+    GetTopNRequest,
+    GetTopNResponse,
+)
+from bfbc2_masterserver.messages.plasma.ranking.GetTopNAndMe import (
+    GetTopNAndMeRequest,
+    GetTopNAndMeResponse,
+)
 from bfbc2_masterserver.messages.plasma.ranking.GetTopNAndStats import (
     GetTopNAndStatsRequest,
     GetTopNAndStatsResponse,
     Leaderboard,
 )
+from bfbc2_masterserver.messages.plasma.ranking.UpdateStats import (
+    UpdateStatsRequest,
+    UpdateStatsResponse,
+)
 from bfbc2_masterserver.models.plasma.database.Ranking import Ranking
 from bfbc2_masterserver.models.plasma.Stats import RankedOwnerStat, RankedStat, Stat
 
@@ -50,6 +70,31 @@ def __init__(self, plasma) -> None:
             GetTopNAndStatsRequest,
         )
 
+        self.resolvers[FESLTransaction.UpdateStats] = (
+            self.__handle_update_stats,
+            UpdateStatsRequest,
+        )
+
+        self.resolvers[FESLTransaction.GetStatsForOwners] = (
+            self.__handle_get_stats_for_owners,
+            GetStatsForOwnersRequest,
+        )
+
+        self.resolvers[FESLTransaction.GetTopN] = (
+            self.__handle_get_top_n,
+            GetTopNRequest,
+        )
+
+        self.resolvers[FESLTransaction.GetTopNAndMe] = (
+            self.__handle_get_top_n_and_me,
+            GetTopNAndMeRequest,
+        )
+
+        self.resolvers[FESLTransaction.GetDateRange] = (
+            self.__handle_get_date_range,
+            GetDateRangeRequest,
+        )
+
     def _get_resolver(self, txn):
         """
         Gets the resolver for a given transaction.
@@ -157,3 +202,28 @@ def __handle_get_top_n_and_stats(
             )
 
         return GetTopNAndStatsResponse(stats=leaderboard)
+
+    def __handle_update_stats(self, data: UpdateStatsRequest) -> UpdateStatsResponse:
+        for request in data.u:
+            for stat in request.s:
+                self.database.ranking_set(request.o, stat.k, stat.v, stat.ut)
+
+        return UpdateStatsResponse()
+
+    def __handle_get_stats_for_owners(
+        self, data: GetStatsForOwnersRequest
+    ) -> GetStatsForOwnersResponse:
+        raise NotImplementedError("GetStatsForOwners is not implemented")
+
+    def __handle_get_top_n(self, data: GetTopNRequest) -> GetTopNResponse:
+        raise NotImplementedError("GetTopN is not implemented")
+
+    def __handle_get_top_n_and_me(
+        self, data: GetTopNAndMeRequest
+    ) -> GetTopNAndMeResponse:
+        raise NotImplementedError("GetTopNAndMe is not implemented")
+
+    def __handle_get_date_range(
+        self, data: GetDateRangeRequest
+    ) -> GetDateRangeResponse:
+        raise NotImplementedError("GetDateRange is not implemented")
diff --git a/bfbc2_masterserver/services/plasma/record.py b/bfbc2_masterserver/services/plasma/record.py
index c7a0167..2abe784 100644
--- a/bfbc2_masterserver/services/plasma/record.py
+++ b/bfbc2_masterserver/services/plasma/record.py
@@ -2,6 +2,14 @@
 from bfbc2_masterserver.enumerators.ErrorCode import ErrorCode
 from bfbc2_masterserver.enumerators.fesl.FESLTransaction import FESLTransaction
 from bfbc2_masterserver.error import TransactionError
+from bfbc2_masterserver.messages.plasma.record.AddRecord import (
+    AddRecordRequest,
+    AddRecordResponse,
+)
+from bfbc2_masterserver.messages.plasma.record.AddRecordAsMap import (
+    AddRecordAsMapRequest,
+    AddRecordAsMapResponse,
+)
 from bfbc2_masterserver.messages.plasma.record.GetRecord import (
     GetRecordRequest,
     GetRecordResponse,
@@ -10,6 +18,14 @@
     GetRecordAsMapRequest,
     GetRecordAsMapResponse,
 )
+from bfbc2_masterserver.messages.plasma.record.UpdateRecord import (
+    UpdateRecordRequest,
+    UpdateRecordResponse,
+)
+from bfbc2_masterserver.messages.plasma.record.UpdateRecordAsMap import (
+    UpdateRecordAsMapRequest,
+    UpdateRecordAsMapResponse,
+)
 from bfbc2_masterserver.models.plasma.Record import Record
 
 
@@ -28,6 +44,26 @@ def __init__(self, plasma) -> None:
             GetRecordRequest,
         )
 
+        self.resolvers[FESLTransaction.AddRecord] = (
+            self.__handle_add_record,
+            AddRecordRequest,
+        )
+
+        self.resolvers[FESLTransaction.UpdateRecord] = (
+            self.__handle_update_record,
+            UpdateRecordRequest,
+        )
+
+        self.resolvers[FESLTransaction.AddRecordAsMap] = (
+            self.__handle_add_record_as_map,
+            AddRecordAsMapRequest,
+        )
+
+        self.resolvers[FESLTransaction.UpdateRecordAsMap] = (
+            self.__handle_update_record_as_map,
+            UpdateRecordAsMapRequest,
+        )
+
     def _get_resolver(self, txn):
         """
         Gets the resolver for a given transaction.
@@ -84,3 +120,55 @@ def __handle_get_record(
 
         values = [Record(key=record.key, value=record.value) for record in records]
         return GetRecordResponse(values=values)
+
+    def __handle_add_record(
+        self, data: AddRecordRequest
+    ) -> AddRecordResponse | TransactionError:
+        if self.connection.persona is None:
+            return TransactionError(ErrorCode.SYSTEM_ERROR)
+
+        for record in data.values:
+            self.database.record_add(
+                self.connection.persona.id, data.recordName, record.key, record.value
+            )
+
+        return AddRecordResponse()
+
+    def __handle_update_record(
+        self, data: UpdateRecordRequest
+    ) -> UpdateRecordResponse | TransactionError:
+        if self.connection.persona is None:
+            return TransactionError(ErrorCode.SYSTEM_ERROR)
+
+        for record in data.values:
+            self.database.record_update(
+                self.connection.persona.id, data.recordName, record.key, record.value
+            )
+
+        return UpdateRecordResponse()
+
+    def __handle_add_record_as_map(
+        self, data: AddRecordAsMapRequest
+    ) -> AddRecordAsMapResponse | TransactionError:
+        if self.connection.persona is None:
+            return TransactionError(ErrorCode.SYSTEM_ERROR)
+
+        for key, value in data.values.items():
+            self.database.record_add(
+                self.connection.persona.id, data.recordName, key, value
+            )
+
+        return AddRecordAsMapResponse()
+
+    def __handle_update_record_as_map(
+        self, data: UpdateRecordAsMapRequest
+    ) -> UpdateRecordAsMapResponse | TransactionError:
+        if self.connection.persona is None:
+            return TransactionError(ErrorCode.SYSTEM_ERROR)
+
+        for key, value in data.values.items():
+            self.database.record_update(
+                self.connection.persona.id, data.recordName, key, value
+            )
+
+        return UpdateRecordAsMapResponse()
diff --git a/docker-compose.yml b/docker-compose.yml
index e76e6b2..d1c22f7 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -16,3 +16,8 @@ services:
       REDIS_HOST: redis
     ports: 
       - "8000:8000"
+    depends_on:
+      - database
+      - redis
+    volumes:
+      - ./static:/app/static