Files
2026-05-26 23:20:08 -04:00

8.7 KiB

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 Plugin entry. Reads NetworkCore services, bootstraps schema, registers everything.
SocialPermissionsNodes.java Constants for every networksocial.* node.
config/SocialConfig.java Default config shape.
model/ Records and enums: Friendship, Guild, GuildRank, GuildMember, GuildInvite, GuildPermission (bitmask), PrivacyKey, PrivacyValue, PrivacySettings.
db/SchemaBootstrap.java CREATE TABLE IF NOT EXISTS for the six Social tables.
db/FriendRepository.java SQL for friend requests + accepted friendships.
db/GuildRepository.java SQL for guilds themselves.
db/GuildRankRepository.java SQL for the per-guild rank ladder.
db/GuildMemberRepository.java SQL for guild membership (one row per player).
db/GuildInviteRepository.java SQL for pending guild invites.
db/PrivacyRepository.java SQL for per-player privacy settings.
service/FriendService.java All friend operations + cross-server announcements. Returns a Result enum.
service/GuildService.java All guild operations + per-guild rank seeding + bus publishes.
service/GuildChatService.java Subscribes to guild chat channel; delivers locally to members of the originating guild.
service/DmRoutingService.java Publishes DM payloads + per-server delivery + last-sender tracking for /r.
service/PrivacyService.java Reads policies and applies them in canContact(from, to, key).
service/ChatTargetService.java In-memory per-player chat mode (NORMAL / GUILD / DM).
service/NotificationDispatcher.java Subscribes to friend + guild event channels; surfaces messages to local players.
service/payload/ Wire payloads: FriendEventPayload, GuildEventPayload, GuildChatPayload, DmPayload.
listener/SocialChatRouter.java Hooks PlayerChatEvent; when sender's mode is GUILD or DM, cancels and reroutes.
listener/SocialDisconnectListener.java Clears chat-target + last-sender state on disconnect.
command/SocialCommand.java /social opens the HyUI hub.
command/friend/FriendCommand.java /friend add|accept|reject|remove|list|pending.
command/guild/GuildCommand.java /guild create|disband|invite|accept|leave|kick|promote|demote|motd|info|roster|ranks|chat.
command/dm/ /msg, /r, /chat.
command/privacy/PrivacyCommand.java /privacy show|set.
ui/ HyUI: SocialHubPage, FriendsListPage, GuildRosterPage, PrivacySettingsPage.
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 <name> <body> isn't possible. /msg <name> 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.