# Overview ## What this plugin owns Four player-facing features that talk to each other but stay logically independent: - **Friends** - send/accept/reject friend requests, remove friends, see incoming/outgoing pending. - **Guilds** - create/disband, invite/accept/kick, leave, MOTD, fully configurable per-guild rank ladder with bitmask permissions, guild chat across the network. - **Direct messages** - cross-server `/msg` routing with `/r` reply-to-last-sender. - **Privacy** - per-player policy (`EVERYONE` / `FRIENDS` / `NOBODY`) for who can DM you, send you a friend request, or invite you to a guild. DM history is **not** persisted in v1. Messages route through the MessageBus and surface in the recipient's chat; nothing is stored. ## Architecture in one diagram ``` SocialPlugin (entry) +-- FriendService DB + bus + privacy enforcement | +-- FriendRepository +-- GuildService create/disband/invite/accept/leave/kick/promote/demote/MOTD/ranks | +-- GuildRepository | +-- GuildRankRepository per-guild rank ladder | +-- GuildMemberRepository | +-- GuildInviteRepository +-- GuildChatService cross-server guild chat, scoped per server to local members +-- DmRoutingService cross-server DMs + /r tracking +-- PrivacyService EVERYONE/FRIENDS/NOBODY policy lookup | +-- PrivacyRepository +-- ChatTargetService per-player chat-mode state (NORMAL / GUILD / DM) +-- NotificationDispatcher subscribes to friend/guild events, surfaces in chat to local players +-- SocialChatRouter intercepts PlayerChatEvent, routes per ChatTargetService +-- SocialDisconnectListener clears toggles + /r state on disconnect ``` External dependencies (all from NetworkCore): - **`NetworkCore.getDatabase()`** - MySQL pool (HikariCP). **Required.** - **`NetworkCore.getMessageBus()`** - Redis pub/sub. **Required.** - **`NetworkCore.getPlayerPresence()`** - network-wide who-is-where lookup. **Required** for name->UUID resolution and DM routing. - **`NetworkCore.getServerId()`** - identifies the server an action was taken on. If any of these is missing at `start()`, Social logs SEVERE and returns. The server keeps running; this plugin just does nothing. ## What lives where | File | Responsibility | |---|---| | [SocialPlugin.java](../src/main/java/net/kewwbec/social/SocialPlugin.java) | Plugin entry. Reads NetworkCore services, bootstraps schema, registers everything. | | [SocialPermissionsNodes.java](../src/main/java/net/kewwbec/social/SocialPermissionsNodes.java) | Constants for every `networksocial.*` node. | | [config/SocialConfig.java](../src/main/java/net/kewwbec/social/config/SocialConfig.java) | Default config shape. | | [model/](../src/main/java/net/kewwbec/social/model/) | Records and enums: `Friendship`, `Guild`, `GuildRank`, `GuildMember`, `GuildInvite`, `GuildPermission` (bitmask), `PrivacyKey`, `PrivacyValue`, `PrivacySettings`. | | [db/SchemaBootstrap.java](../src/main/java/net/kewwbec/social/db/SchemaBootstrap.java) | `CREATE TABLE IF NOT EXISTS` for the six Social tables. | | [db/FriendRepository.java](../src/main/java/net/kewwbec/social/db/FriendRepository.java) | SQL for friend requests + accepted friendships. | | [db/GuildRepository.java](../src/main/java/net/kewwbec/social/db/GuildRepository.java) | SQL for guilds themselves. | | [db/GuildRankRepository.java](../src/main/java/net/kewwbec/social/db/GuildRankRepository.java) | SQL for the per-guild rank ladder. | | [db/GuildMemberRepository.java](../src/main/java/net/kewwbec/social/db/GuildMemberRepository.java) | SQL for guild membership (one row per player). | | [db/GuildInviteRepository.java](../src/main/java/net/kewwbec/social/db/GuildInviteRepository.java) | SQL for pending guild invites. | | [db/PrivacyRepository.java](../src/main/java/net/kewwbec/social/db/PrivacyRepository.java) | SQL for per-player privacy settings. | | [service/FriendService.java](../src/main/java/net/kewwbec/social/service/FriendService.java) | All friend operations + cross-server announcements. Returns a `Result` enum. | | [service/GuildService.java](../src/main/java/net/kewwbec/social/service/GuildService.java) | All guild operations + per-guild rank seeding + bus publishes. | | [service/GuildChatService.java](../src/main/java/net/kewwbec/social/service/GuildChatService.java) | Subscribes to guild chat channel; delivers locally to members of the originating guild. | | [service/DmRoutingService.java](../src/main/java/net/kewwbec/social/service/DmRoutingService.java) | Publishes DM payloads + per-server delivery + last-sender tracking for `/r`. | | [service/PrivacyService.java](../src/main/java/net/kewwbec/social/service/PrivacyService.java) | Reads policies and applies them in `canContact(from, to, key)`. | | [service/ChatTargetService.java](../src/main/java/net/kewwbec/social/service/ChatTargetService.java) | In-memory per-player chat mode (NORMAL / GUILD / DM). | | [service/NotificationDispatcher.java](../src/main/java/net/kewwbec/social/service/NotificationDispatcher.java) | Subscribes to friend + guild event channels; surfaces messages to local players. | | [service/payload/](../src/main/java/net/kewwbec/social/service/payload/) | Wire payloads: `FriendEventPayload`, `GuildEventPayload`, `GuildChatPayload`, `DmPayload`. | | [listener/SocialChatRouter.java](../src/main/java/net/kewwbec/social/listener/SocialChatRouter.java) | Hooks `PlayerChatEvent`; when sender's mode is GUILD or DM, cancels and reroutes. | | [listener/SocialDisconnectListener.java](../src/main/java/net/kewwbec/social/listener/SocialDisconnectListener.java) | Clears chat-target + last-sender state on disconnect. | | [command/SocialCommand.java](../src/main/java/net/kewwbec/social/command/SocialCommand.java) | `/social` opens the HyUI hub. | | [command/friend/FriendCommand.java](../src/main/java/net/kewwbec/social/command/friend/FriendCommand.java) | `/friend add\|accept\|reject\|remove\|list\|pending`. | | [command/guild/GuildCommand.java](../src/main/java/net/kewwbec/social/command/guild/GuildCommand.java) | `/guild create\|disband\|invite\|accept\|leave\|kick\|promote\|demote\|motd\|info\|roster\|ranks\|chat`. | | [command/dm/](../src/main/java/net/kewwbec/social/command/dm/) | `/msg`, `/r`, `/chat`. | | [command/privacy/PrivacyCommand.java](../src/main/java/net/kewwbec/social/command/privacy/PrivacyCommand.java) | `/privacy show\|set`. | | [ui/](../src/main/java/net/kewwbec/social/ui/) | HyUI: `SocialHubPage`, `FriendsListPage`, `GuildRosterPage`, `PrivacySettingsPage`. | | [util/PlayerResolver.java](../src/main/java/net/kewwbec/social/util/PlayerResolver.java) | Name -> `PlayerLocation` via `PlayerPresence`. Online players only. | ## Why the design is this shape - **One service per concern.** Friends, guilds, DMs, and privacy are independent services that don't know about each other except through their public types. Each owns its own bus subscription. You can change DM routing without touching guild code. - **Repositories return models, services orchestrate.** Repos never call services. Services own consistency (cascade on disband, privacy gating, etc). - **Privacy is checked in the sender's process before publishing.** A NOBODY setting blocks the publish, so we don't ship rejection logic to every receiver. - **Per-guild rank ladder.** Each guild gets its own `social_guild_ranks` rows seeded on creation (Member / Officer / Owner). Owners with `MANAGE_RANKS` add or edit. Permissions are a bitmask of `GuildPermission` enum values - cheap to evaluate, easy to extend. - **The owner rank is special.** It's flagged `is_owner_rank=1`, can't be deleted, and members can't be promoted into or out of it via `setRank`. Ownership transfer is a future operation that updates both `guilds.owner_uuid` and the member's rank atomically. - **DM routing is "publish + filter locally".** Every server subscribes to `social.dm`; on receive, if the recipient's UUID is on the local Universe, deliver. This sidesteps having to ask "which server is X on?" - we let the bus fan out and each server self-selects. - **Chat-target toggle, not multi-arg commands.** Hytale args are single-token, so `/msg ` isn't possible. `/msg ` enters one-shot DM mode; the next chat goes to them and routing snaps back. `/guild chat` is sticky until `/chat` (or disconnect) flips it off. ## Lifecycle Standard Hytale plugin lifecycle: - **`setup()`** - load config, build `ChatTargetService`. - **`start()`** - require NetworkCore's database + bus + presence, bootstrap schema, build services, subscribe to bus channels, register events and commands. - **`shutdown()`** - stop service subscriptions. The database pool is owned by NetworkCore and closes on its own shutdown. If NetworkCore's MySQL is unhealthy at `start()`, Social logs SEVERE and returns. Restart once MySQL is back.