init
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
# 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 <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.
|
||||
@@ -0,0 +1,107 @@
|
||||
# Configuration
|
||||
|
||||
## Location
|
||||
|
||||
`<plugin data dir>/config.json`. Written with defaults on first boot.
|
||||
|
||||
## Full config
|
||||
|
||||
```json
|
||||
{
|
||||
"tables": {
|
||||
"friends": "social_friends",
|
||||
"guilds": "social_guilds",
|
||||
"guild_ranks": "social_guild_ranks",
|
||||
"guild_members": "social_guild_members",
|
||||
"guild_invites": "social_guild_invites",
|
||||
"privacy": "social_privacy"
|
||||
},
|
||||
"channels": {
|
||||
"friend_event": "social.friend",
|
||||
"guild_event": "social.guild",
|
||||
"guild_chat": "social.guild.chat",
|
||||
"dm": "social.dm"
|
||||
},
|
||||
"guilds": {
|
||||
"name_min_length": 3,
|
||||
"name_max_length": 24,
|
||||
"tag_max_length": 6,
|
||||
"max_members": 50,
|
||||
"chat_prefix": "[G]"
|
||||
},
|
||||
"friends": {
|
||||
"max_friends": 100,
|
||||
"max_pending_requests": 25
|
||||
},
|
||||
"dms": {
|
||||
"prefix_to": "[to {name}]",
|
||||
"prefix_from": "[from {name}]"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Permission gating doesn't live here. Each command checks a permission node (`networksocial.<command>`) via Hytale's `PermissionsModule`. Configure those in the Hytale server's own permission config - see [03_Commands.md](03_Commands.md#permissions).
|
||||
|
||||
## Fields
|
||||
|
||||
### tables
|
||||
|
||||
| Field | Default | Meaning |
|
||||
|---|---|---|
|
||||
| `friends` | `social_friends` | Friend requests + accepted friendships. |
|
||||
| `guilds` | `social_guilds` | One row per guild. |
|
||||
| `guild_ranks` | `social_guild_ranks` | Per-guild rank ladder (each guild has its own ranks). |
|
||||
| `guild_members` | `social_guild_members` | One row per player, points to their guild and rank. |
|
||||
| `guild_invites` | `social_guild_invites` | Pending invites. |
|
||||
| `privacy` | `social_privacy` | Per-player privacy settings (DM, friend-request, guild-invite policies). |
|
||||
|
||||
Change a name only if you really need to (e.g. table collision with another plugin) and you're prepared to migrate data.
|
||||
|
||||
### channels
|
||||
|
||||
| Field | Default | Meaning |
|
||||
|---|---|---|
|
||||
| `friend_event` | `social.friend` | Bus channel for friend-request / accept / remove notifications. |
|
||||
| `guild_event` | `social.guild` | Bus channel for guild lifecycle events (invite, member changes, ranks, MOTD, disband). |
|
||||
| `guild_chat` | `social.guild.chat` | Bus channel for guild chat messages. |
|
||||
| `dm` | `social.dm` | Bus channel for DMs. |
|
||||
|
||||
Channels are plain Redis pub/sub. Don't share them with other plugins. If you run multiple Social plugin clusters against the same Redis (different networks), use different channel names so they don't cross-talk.
|
||||
|
||||
### guilds
|
||||
|
||||
| Field | Default | Meaning |
|
||||
|---|---|---|
|
||||
| `name_min_length` | `3` | Minimum characters in a guild name. |
|
||||
| `name_max_length` | `24` | Maximum characters in a guild name. Stored as `VARCHAR(32)` so don't push above 32. |
|
||||
| `tag_max_length` | `6` | Maximum characters in the optional guild tag (currently informational; not surfaced in chat). |
|
||||
| `max_members` | `50` | Hard cap on members per guild. `acceptInvite` and `invite` enforce this. |
|
||||
| `chat_prefix` | `[G]` | Visual prefix for every guild chat line. |
|
||||
|
||||
Names must be alphanumeric plus `_` and `-`. Spaces and punctuation are rejected. This matches the single-token nature of Hytale commands - if you allow spaces, `/guild invite My Cool Guild` is unparseable.
|
||||
|
||||
### friends
|
||||
|
||||
| Field | Default | Meaning |
|
||||
|---|---|---|
|
||||
| `max_friends` | `100` | Cap on accepted friendships per player. Send fails with `LIMIT_REACHED` past this. |
|
||||
| `max_pending_requests` | `25` | Cap on outgoing-pending requests per player. Stops one player from spamming the system. |
|
||||
|
||||
### dms
|
||||
|
||||
| Field | Default | Meaning |
|
||||
|---|---|---|
|
||||
| `prefix_to` | `[to {name}]` | Format for the sender's local echo. `{name}` substitutes the recipient's username. |
|
||||
| `prefix_from` | `[from {name}]` | Format for what the recipient sees. `{name}` substitutes the sender's username. |
|
||||
|
||||
## Environment variables
|
||||
|
||||
None specific to Social. Database + bus secrets live in NetworkCore's env (`MYSQL_PASSWORD`, `NETWORK_AUTH_TOKEN`).
|
||||
|
||||
## Setting up permissions
|
||||
|
||||
1. In your Hytale server's permission config, grant the relevant `networksocial.*` nodes to whatever groups you want.
|
||||
2. Default groups (e.g. `Adventurer`) should typically get `networksocial.friend.use`, `networksocial.dm.use`, `networksocial.privacy.use`, `networksocial.guild.use`.
|
||||
3. `networksocial.guild.create` is the gate for *founding* a guild - you might restrict that to a paid rank or to specific roles.
|
||||
4. `networksocial.admin` is reserved for future server-side moderation tools (force-disband, force-leave, etc); not consumed in v1.
|
||||
5. Players need to reconnect for newly-granted permissions to apply.
|
||||
@@ -0,0 +1,105 @@
|
||||
# Commands
|
||||
|
||||
Every command is single-token-only (Hytale arg restriction). Multi-word inputs (DM bodies, guild chat lines, MOTDs longer than one word) use the **chat-target toggle** pattern: a command sets your chat mode, then your next chat message carries the body.
|
||||
|
||||
## Friends - `/friend`
|
||||
|
||||
| Subcommand | Args | Permission | Behavior |
|
||||
|---|---|---|---|
|
||||
| `/friend add <name>` | name | `networksocial.friend.use` | Send a friend request to an online player. Fails if recipient has `FRIEND_REQUEST` privacy set to `NOBODY` (or `FRIENDS` and you're not one already). |
|
||||
| `/friend accept <name>` | sender name | `networksocial.friend.use` | Accept an incoming pending request. |
|
||||
| `/friend reject <name>` | sender name | `networksocial.friend.use` | Reject an incoming pending request (no notification sent to sender). |
|
||||
| `/friend remove <name>` | friend name | `networksocial.friend.use` | Remove someone from your friends list. Bidirectional. |
|
||||
| `/friend list` | (none) | `networksocial.friend.use` | Show all accepted friends. |
|
||||
| `/friend pending` | (none) | `networksocial.friend.use` | Show incoming + outgoing pending requests. |
|
||||
|
||||
Notes:
|
||||
- `add`/`accept`/`reject`/`remove` need a username. If the target is online network-wide, `PlayerPresence` resolves them. If not, `accept`/`reject`/`remove` fall back to scanning your own friendship rows. `add` requires the target to be online.
|
||||
|
||||
## Guilds - `/guild`
|
||||
|
||||
| Subcommand | Args | Permission | Behavior |
|
||||
|---|---|---|---|
|
||||
| `/guild create <name>` | name | `networksocial.guild.create` | Create a new guild with you as owner. Seeds default ranks: Member / Officer / Owner. |
|
||||
| `/guild disband` | (none) | `networksocial.guild.use` | Owner only. Deletes guild, ranks, members, and pending invites in one shot. |
|
||||
| `/guild invite <name>` | invitee name | `networksocial.guild.use` | Requires `INVITE` permission on your rank. Sends an invite to an online player. Subject to their `GUILD_INVITE` privacy. |
|
||||
| `/guild accept <guild>` | guild name | `networksocial.guild.use` | Accept a pending invite. You join at the guild's lowest-priority non-owner rank. |
|
||||
| `/guild leave` | (none) | `networksocial.guild.use` | Leave your guild. Owners can't leave - use `/guild disband` instead. |
|
||||
| `/guild kick <name>` | member name | `networksocial.guild.use` | Requires `KICK` permission, must outrank target by `priority`. Can't kick the owner. |
|
||||
| `/guild promote <name> <rankId>` | name + rank | `networksocial.guild.use` | Requires `PROMOTE` permission, both source and destination ranks must be below yours. |
|
||||
| `/guild demote <name> <rankId>` | name + rank | `networksocial.guild.use` | Same constraints, but uses `DEMOTE` permission. |
|
||||
| `/guild motd <text>` | one-token text | `networksocial.guild.use` | Requires `MOTD` permission. Use `/guild chat` for multi-word MOTDs? No - MOTD is one token. For long MOTDs, edit via SQL or extend the plugin. |
|
||||
| `/guild info` | (none) | `networksocial.guild.use` | Print your guild's name, owner, MOTD, and your rank. |
|
||||
| `/guild roster` | (none) | `networksocial.guild.use` | List every member of your guild and their rank id. |
|
||||
| `/guild ranks` | (none) | `networksocial.guild.use` | List every rank in your guild with id, name, priority, owner-flag, and bitmask permissions decoded. |
|
||||
| `/guild chat` | (none) | `networksocial.guild.use` | Toggle guild-chat mode. While ON, your chat goes to your guild instead of public. Requires `CHAT` permission on your rank. |
|
||||
|
||||
### Guild permission bits
|
||||
|
||||
A rank's `permissions` is a bitmask of [`GuildPermission`](../src/main/java/net/kewwbec/social/model/GuildPermission.java):
|
||||
|
||||
| Bit | Name | What it allows |
|
||||
|---|---|---|
|
||||
| 1 | `INVITE` | `/guild invite` |
|
||||
| 2 | `KICK` | `/guild kick` |
|
||||
| 4 | `PROMOTE` | `/guild promote` |
|
||||
| 8 | `DEMOTE` | `/guild demote` |
|
||||
| 16 | `MOTD` | `/guild motd` |
|
||||
| 32 | `MANAGE_RANKS` | Edit the rank ladder (no CLI in v1; service-only API) |
|
||||
| 64 | `DISBAND` | (reserved; today only the owner can disband) |
|
||||
| 128 | `CHAT` | `/guild chat` + guild chat send |
|
||||
|
||||
Default rank seeding on `/guild create`:
|
||||
- **Member** (priority 10, perms = `CHAT`)
|
||||
- **Officer** (priority 50, perms = `CHAT | INVITE | KICK | MOTD`)
|
||||
- **Owner** (priority 100, all perms, `is_owner_rank=1`)
|
||||
|
||||
## Direct messages
|
||||
|
||||
| Command | Args | Permission | Behavior |
|
||||
|---|---|---|---|
|
||||
| `/msg <name>` | recipient name | `networksocial.dm.use` | Enter one-shot DM mode for the next chat message. Your next chat is delivered as a DM and your mode reverts to NORMAL. |
|
||||
| `/r` | (none) | `networksocial.dm.use` | Enter DM mode targeting whoever DMed you last. Same one-shot behavior as `/msg`. |
|
||||
| `/chat` | (none) | (none) | Clear any DM-mode / guild-chat-mode state so your chat is public again. |
|
||||
|
||||
Why one-shot DMs: Hytale args are single-token, so `/msg name body of message` is impossible. The toggle pattern is the same one Staff uses for `/sc`.
|
||||
|
||||
## Privacy - `/privacy`
|
||||
|
||||
| Subcommand | Args | Permission | Behavior |
|
||||
|---|---|---|---|
|
||||
| `/privacy show` | (none) | `networksocial.privacy.use` | Print your current DM / friend-request / guild-invite policies. |
|
||||
| `/privacy set <key> <value>` | key + value | `networksocial.privacy.use` | Update one policy. `key` is `DM`, `FRIEND_REQUEST`, or `GUILD_INVITE`. `value` is `EVERYONE`, `FRIENDS`, or `NOBODY`. |
|
||||
|
||||
Privacy is enforced **at the sender's server**: when you `/friend add X`, your server calls `PrivacyService.canContact(you, X, FRIEND_REQUEST)` before publishing on the bus. If X has set their friend-request policy to NOBODY, the request never travels. Same for DM and guild-invite.
|
||||
|
||||
## Hub - `/social`
|
||||
|
||||
| Command | Args | Permission | Behavior |
|
||||
|---|---|---|---|
|
||||
| `/social` | (none) | (none) | Open the HyUI hub. Buttons go to Friends, Guild Roster, and Privacy Settings panels. |
|
||||
|
||||
See [07_UI.md](07_UI.md).
|
||||
|
||||
## Permissions index
|
||||
|
||||
```
|
||||
networksocial.friend.use
|
||||
networksocial.guild.use
|
||||
networksocial.guild.create
|
||||
networksocial.dm.use
|
||||
networksocial.privacy.use
|
||||
networksocial.admin # reserved for future server-side moderation tools
|
||||
```
|
||||
|
||||
Each command checks one specific node via `requirePermission(...)` + `canGeneratePermission() = false`. We don't rely on Hytale's auto-generated permission names - those proved unreliable in practice. See [Ranks docs / Provider_Integration](../../Ranks/docs/05_Provider_Integration.md) for the broader rationale.
|
||||
|
||||
## Error/result responses
|
||||
|
||||
Every operation returns a `Result` enum from its service. Commands map these to user-facing chat text. The full set per service:
|
||||
|
||||
**FriendService.Result**: `OK`, `SELF`, `ALREADY_PENDING`, `ALREADY_FRIENDS`, `BLOCKED_BY_PRIVACY`, `LIMIT_REACHED`, `NO_REQUEST`, `DB_ERROR`.
|
||||
|
||||
**GuildService.Result**: `OK`, `NAME_INVALID`, `NAME_TAKEN`, `ALREADY_IN_GUILD`, `NOT_IN_GUILD`, `NOT_FOUND`, `NO_PERMISSION`, `SELF`, `BLOCKED_BY_PRIVACY`, `AT_CAPACITY`, `NO_INVITE`, `RANK_INVALID`, `OWNER_RANK_LOCKED`, `OWNER_ONLY`, `DB_ERROR`.
|
||||
|
||||
**DmRoutingService.Result**: `OK`, `SELF`, `TARGET_OFFLINE`, `BLOCKED_BY_PRIVACY`, `DB_ERROR`.
|
||||
@@ -0,0 +1,9 @@
|
||||
# Social Docs
|
||||
|
||||
1. [01_Overview.md](01_Overview.md) - architecture and pieces
|
||||
2. [02_Configuration.md](02_Configuration.md) - config.json
|
||||
3. [03_Commands.md](03_Commands.md) - every command, syntax, behavior
|
||||
4. [04_Schema.md](04_Schema.md) - MySQL tables
|
||||
5. [05_Cross_Server_Propagation.md](05_Cross_Server_Propagation.md) - friends, guilds, DMs over the bus
|
||||
6. [06_Operations.md](06_Operations.md) - ops scenarios, debugging
|
||||
7. [07_UI.md](07_UI.md) - the /social panel
|
||||
Reference in New Issue
Block a user