actually bruh

This commit is contained in:
2026-05-26 13:31:21 -04:00
parent 4f4f731bc7
commit ade5f069e3
31 changed files with 1109 additions and 179 deletions
+1 -1
View File
@@ -13,7 +13,7 @@ Staff tools for KweebecNet: punishments, history, cross-server staff chat, playe
- **NetworkCore** with `mysql.enabled: true` and a healthy MySQL connection
- A working `network.auth_token` configured the same way as for NetworkCore (since this plugin uses the cross-server bus)
- The built-in Hytale `PermissionsModule` populated with at least one of the configured staff groups (defaults: `OP`, `ADMIN`, `MOD`, `HELPER`)
- Hytale permission nodes assigned to your staff group(s). Grant `networkstaff.*` for full access, or pick specific nodes - see [docs/03_Commands.md](docs/03_Commands.md#permissions).
If MySQL isn't configured in NetworkCore, Staff refuses to start and logs a clear error. Fix NetworkCore first, then restart.
+9 -6
View File
@@ -9,10 +9,11 @@ StaffPlugin (entry)
| +-- PunishmentRepository SQL CRUD
| +-- MuteCache in-memory map of active mutes for online players
+-- StaffChatService cross-server staff chat over MessageBus
+-- PlayerSeenTracker writes to players_seen on join/leave
+-- StaffChatToggleService in-memory toggle of which players have chat-to-staff mode on
+-- PlayerSeenTracker writes to players_seen on join/leave; clears toggle on disconnect
| +-- PlayerSeenRepository SQL CRUD
+-- BanEnforcer cancels PlayerSetupConnectEvent if banned
+-- ChatEnforcer cancels PlayerChatEvent if muted
+-- ChatEnforcer cancels PlayerChatEvent if muted; routes to staff chat if toggle is on
```
External dependencies:
@@ -28,7 +29,8 @@ External dependencies:
|---|---|
| [StaffPlugin.java](../src/main/java/net/kewwbec/staff/StaffPlugin.java) | Plugin entry. Reads NetworkCore services, bootstraps schema, registers everything. |
| [config/StaffConfig.java](../src/main/java/net/kewwbec/staff/config/StaffConfig.java) | Default config shape. |
| [util/StaffPermissions.java](../src/main/java/net/kewwbec/staff/util/StaffPermissions.java) | `isStaff(uuid)` / `isAdmin(uuid)` checks. |
| [util/StaffPermissions.java](../src/main/java/net/kewwbec/staff/util/StaffPermissions.java) | Thin wrapper for `PermissionsModule.get().hasPermission(uuid, node)`. Just `has(ref, node)`. |
| [StaffPermissionsNodes.java](../src/main/java/net/kewwbec/staff/StaffPermissionsNodes.java) | String constants for every `networkstaff.*` node. |
| [util/PlayerResolver.java](../src/main/java/net/kewwbec/staff/util/PlayerResolver.java) | Name -> UUID, looks at online players first, then players_seen. |
| [util/DurationParser.java](../src/main/java/net/kewwbec/staff/util/DurationParser.java) | Parse "30m", "2h" etc.; format milliseconds back to human strings. |
| [model/Punishment.java](../src/main/java/net/kewwbec/staff/model/Punishment.java), [PunishmentType.java](../src/main/java/net/kewwbec/staff/model/PunishmentType.java), [PlayerSeen.java](../src/main/java/net/kewwbec/staff/model/PlayerSeen.java) | Record types. |
@@ -38,12 +40,13 @@ External dependencies:
| [service/PunishmentService.java](../src/main/java/net/kewwbec/staff/service/PunishmentService.java) | Issuing/revoking + cross-server broadcast + applying remote actions locally. |
| [service/MuteCache.java](../src/main/java/net/kewwbec/staff/service/MuteCache.java) | Per-server hot cache of active mutes for online players. |
| [service/StaffChatService.java](../src/main/java/net/kewwbec/staff/service/StaffChatService.java) | Cross-server staff chat over MessageBus. |
| [service/StaffChatToggleService.java](../src/main/java/net/kewwbec/staff/service/StaffChatToggleService.java) | In-memory set of players whose chat is currently being routed to staff chat. |
| [service/PunishmentBroadcast.java](../src/main/java/net/kewwbec/staff/service/PunishmentBroadcast.java), [StaffChatPayload.java](../src/main/java/net/kewwbec/staff/service/StaffChatPayload.java) | Wire payloads. |
| [listener/BanEnforcer.java](../src/main/java/net/kewwbec/staff/listener/BanEnforcer.java) | `PlayerSetupConnectEvent` -> cancel + set reason if banned. |
| [listener/ChatEnforcer.java](../src/main/java/net/kewwbec/staff/listener/ChatEnforcer.java) | `PlayerChatEvent` -> cancel if muted. |
| [listener/PlayerSeenTracker.java](../src/main/java/net/kewwbec/staff/listener/PlayerSeenTracker.java) | Connect/disconnect -> update players_seen, load/clear mute cache. |
| [listener/ChatEnforcer.java](../src/main/java/net/kewwbec/staff/listener/ChatEnforcer.java) | `PlayerChatEvent` -> cancel if muted; otherwise route to staff chat if toggle is on. |
| [listener/PlayerSeenTracker.java](../src/main/java/net/kewwbec/staff/listener/PlayerSeenTracker.java) | Connect/disconnect -> update players_seen, load/clear mute cache, clear staff-chat toggle. |
| [command/punish/PunishmentCommands.java](../src/main/java/net/kewwbec/staff/command/punish/PunishmentCommands.java) | All 8 punishment commands as nested classes. |
| [command/chat/StaffChatCommand.java](../src/main/java/net/kewwbec/staff/command/chat/StaffChatCommand.java) | `/sc`. |
| [command/chat/StaffChatCommand.java](../src/main/java/net/kewwbec/staff/command/chat/StaffChatCommand.java) | `/sc` - toggles staff-chat-mode for the caller. |
| [command/lookup/LookupCommand.java](../src/main/java/net/kewwbec/staff/command/lookup/LookupCommand.java), [HistoryCommand.java](../src/main/java/net/kewwbec/staff/command/lookup/HistoryCommand.java) | `/lookup`, `/history`. |
## Why the design is this shape
+8 -19
View File
@@ -8,10 +8,6 @@
```json
{
"permissions": {
"staff_groups": ["OP", "ADMIN", "MOD", "HELPER"],
"admin_groups": ["OP", "ADMIN"]
},
"chat": {
"channel": "staff.chat",
"prefix": "[Staff]"
@@ -23,17 +19,10 @@
}
```
Permission gating no longer lives here. Each command checks a permission node (`networkstaff.<command>`) via Hytale's `PermissionsModule`. Configure those in the Hytale server's own permission config - see [03_Commands.md](03_Commands.md#permissions) for the full list of nodes.
## Fields
### permissions
| Field | Default | Meaning |
|---|---|---|
| `staff_groups` | `["OP", "ADMIN", "MOD", "HELPER"]` | Any Hytale group name in this list grants access to staff commands. |
| `admin_groups` | `["OP", "ADMIN"]` | Subset that the plugin treats as "admin". v1 doesn't gate anything on this, but it's available for future features that want to require admin (e.g. unban after a permaban). |
Group names come from `PermissionsModule.get().getGroupsForUser(uuid)`. Set up your groups in the Hytale server's own configuration first; this plugin just reads them.
### chat
| Field | Default | Meaning |
@@ -48,13 +37,13 @@ Group names come from `PermissionsModule.get().getGroupsForUser(uuid)`. Set up y
| `punishments` | `"staff_punishments"` | Table name. Change only if you really need to and you're prepared to migrate data. |
| `players_seen` | `"staff_players_seen"` | Same. |
## Setting up staff groups
## Setting up staff permissions
In your Hytale server config, define groups and assign players:
1. Make sure the group names you assign match one of `staff_groups` exactly. The check is case-sensitive (groups in Hytale tend to be uppercase, e.g. `OP`).
2. After assigning, the player needs to relog for the change to take effect (`PermissionsModule.get().getGroupsForUser` is queried on each command, but Hytale generally requires a reconnect for group changes to apply on the player side).
3. Test with `/lookup <staff-member-name>` from a staff account. If you see the lookup output, you have staff perms.
1. In your Hytale server's permission config, create a `staff` group (or whatever you want to call it).
2. Grant it `networkstaff.*` (or pick specific nodes - see [03_Commands.md](03_Commands.md#permissions) for the list).
3. Assign your staff members to that group.
4. Players need to reconnect for newly-granted permissions to apply.
5. Test with `/lookup <staff-member-name>` from a staff account. If you see the lookup output, the perms are wired.
## Environment variables
+43 -5
View File
@@ -1,6 +1,29 @@
# Commands
All commands require the caller to be in one of the `staff_groups` from config. Player-only (no console support yet).
Player-only (no console support yet). Every command is gated by a Hytale permission node.
## Permissions
| Command | Required permission |
|---|---|
| `/ban` | `networkstaff.ban` |
| `/tempban` | `networkstaff.tempban` |
| `/mute` | `networkstaff.mute` |
| `/tempmute` | `networkstaff.tempmute` |
| `/kick` | `networkstaff.kick` |
| `/warn` | `networkstaff.warn` |
| `/unban` | `networkstaff.unban` |
| `/unmute` | `networkstaff.unmute` |
| `/sc` | `networkstaff.chat` (also required to *receive* staff chat) |
| `/lookup` | `networkstaff.lookup` |
| `/history` | `networkstaff.history` |
| `/staffui` | `networkstaff.ui` |
Grant `networkstaff.*` to give a group everything. Without the relevant node, Hytale's command system blocks the call before our handler runs - the user sees the standard `You do not have permission for this command.` message.
Permissions are wired via `AbstractCommand.requirePermission(node)` in each command's constructor. We also override `canGeneratePermission()` to return false so Hytale doesn't auto-generate a different permission name and gate on that instead.
Receiving staff chat also requires `networkstaff.chat`, so a moderator who can read it can also send to it. If you want separate read/send permissions later, split the check in [StaffChatService.java](../src/main/java/net/kewwbec/staff/service/StaffChatService.java) into two nodes.
## Punishments
@@ -38,11 +61,20 @@ Same as unban but for mutes. Also clears the entry from the mute cache on any se
## Staff chat
### /sc &lt;message&gt;
### /sc
Broadcasts on the cross-server staff chat channel. Visible only to other players whose `getGroupsForUser` overlaps with `staff_groups`.
Toggles **staff chat mode** for you. No arguments. (Hytale command args are single-token, so a multi-word `/sc <message>` doesn't work; the toggle is the workaround.)
System events (a ban being issued, an unban) also post a line on this channel automatically.
When ON:
- Anything you type into world chat is intercepted, the regular chat event is cancelled, and the message is broadcast on the cross-server staff chat channel.
- Other players who have `networkstaff.chat` see it; nobody else does.
- Run `/sc` again to turn it off.
When OFF: regular chat behaves normally.
System events (a ban issued, an unban) also post a line on this channel automatically.
The toggle state lives in memory on this server only and is cleared when you disconnect. If you log into a different server, you'll need to re-toggle.
## Lookup
@@ -61,11 +93,17 @@ If the player has never connected, only the name/UUID lookup happens via online
Last 20 punishment entries for the player, most recent first. Each entry shows id, state (`ACTIVE` / `EXPIRED` / `REVOKED`), type, who issued it, when, and the reason.
## UI
### /staffui
Opens the Staff Hub page. From there: Player Lookup -> select player -> Detail page with action buttons. See [07_UI.md](07_UI.md) for the page flow and how to add new screens.
## Common error cases
| Error message | Cause |
|---|---|
| "You do not have permission to use this command." | Caller isn't in any of `staff_groups`. |
| "You do not have permission for this command." | Caller lacks the relevant `networkstaff.<command>` node. Add it (or `networkstaff.*`) to their group. Hytale's command system enforces this before our handler runs. |
| "No player found with that name (online or in history)." | Name doesn't match any online player or any name in `staff_players_seen`. |
| "Database error: ..." | MySQL query failed. Check NetworkCore connection (`/networkcore status`). |
| "Bad duration. Use 30s, 5m, 2h, 7d, 1w." | Duration string didn't parse. Last char must be `s`/`m`/`h`/`d`/`w`, prefix must be a positive integer. |
+4 -4
View File
@@ -5,7 +5,7 @@
1. `/networkcore status` - confirm "Database: ok" and "Network: connected".
2. Connect a staff alt. Run `/lookup <your-main>`. You should see your own UUID and online status.
3. Connect a non-staff alt. Have a staff member run `/warn <alt> test`. The alt receives a chat message; nothing crashes.
4. `/sc test` from staff. Every other online staff member sees it. No non-staff sees it.
4. `/sc` from a staff alt to toggle staff-chat-mode ON, then type a message in chat. Every other online staff member sees it; no non-staff sees it. Run `/sc` again to turn off.
5. Look in MySQL:
```sql
SELECT * FROM staff_punishments ORDER BY issued_at DESC LIMIT 5;
@@ -14,9 +14,9 @@
## A staff member can't run staff commands
1. Check what groups Hytale shows for them. There's no direct command for this in Staff; have them run `/lookup <theirname>` - if it works, they're staff. If it doesn't, the group is wrong.
2. `PermissionsModule.get().getGroupsForUser(uuid)` is the source. Confirm the group name they're assigned matches one in `permissions.staff_groups` in `config.json` (case-sensitive).
3. Group changes typically require the player to reconnect.
1. The plugin checks `PermissionsModule.get().hasPermission(uuid, "networkstaff.<command>")`. The error message includes the exact node that was checked - read it.
2. Confirm their group has either the specific node or `networkstaff.*`. See [03_Commands.md](03_Commands.md#permissions) for the full list.
3. Permission changes typically require the player to reconnect.
## A ban isn't propagating
+132
View File
@@ -0,0 +1,132 @@
# UI
Built on top of [HyUI](https://github.com/Elliesaur/HyUI), a third-party library that wraps Hytale's native Custom UI API in an HTML-like markup (HYUIML). HyUI is bundled into the Staff jar via the shade plugin so you don't need it installed separately.
## Reading the source
Each page lives in [src/main/java/net/kewwbec/staff/ui/](../src/main/java/net/kewwbec/staff/ui/). Pages don't extend the SDK's `InteractiveCustomUIPage` anymore - they're plain classes that build a `PageBuilder` and expose an `open(store)` method. Navigation between pages is `new OtherPage(...).open(store)`.
## Page flow
```
/staffui
|
v
[StaffHubPage]
|
+-> Player Lookup -> [PlayerLookupPage] (text search, click result for detail)
|
v
[PlayerDetailPage]
|
+-> Ban / TempBan / Mute / TempMute / Kick / Warn --> [PunishFormPage]
+-> Unban / Unmute (applied inline, page re-opens)
+-> History --> [HistoryPage]
```
Every non-hub page has a Back button. ESC always closes (lifetime is `CanDismiss`).
## Page reference
| Page | Purpose |
|---|---|
| [StaffHubPage](../src/main/java/net/kewwbec/staff/ui/StaffHubPage.java) | Top-level menu. Currently just routes to Player Lookup; add buttons here for new sections. |
| [PlayerLookupPage](../src/main/java/net/kewwbec/staff/ui/PlayerLookupPage.java) | Text input + Search button. Online players are listed first; if you type a name not online, the players_seen table is consulted. Click a result to open detail. |
| [PlayerDetailPage](../src/main/java/net/kewwbec/staff/ui/PlayerDetailPage.java) | Player info + active ban/mute lines + action buttons. Unban / Unmute fire immediately (no form); the page reopens with refreshed data. |
| [PunishFormPage](../src/main/java/net/kewwbec/staff/ui/PunishFormPage.java) | Reason field, plus duration field for timed types. Validation errors re-open the same page with the entered values and a red error line. |
| [HistoryPage](../src/main/java/net/kewwbec/staff/ui/HistoryPage.java) | Last 50 entries for the target. Read-only. |
## HyUI patterns we use
All pages follow the same skeleton:
```java
public final class SomePage {
private final PlayerRef playerRef;
private final StaffUIContext sCtx;
private final PageBuilder page;
public SomePage(PlayerRef playerRef, StaffUIContext sCtx, ...args) {
this.playerRef = playerRef;
this.sCtx = sCtx;
// Build the HYUIML based on constructor state
String html = """
<div class="page-overlay">
<div class="container" data-hyui-title="Title">
<div class="container-contents">
<button id="someBtn">Do thing</button>
</div>
</div>
</div>
""";
this.page = PageBuilder.pageForPlayer(playerRef)
.withLifetime(CustomPageLifetime.CanDismiss)
.fromHtml(html);
// Wire listeners by element id
page.addEventListener("someBtn", CustomUIEventBindingType.Activating, (ignored, pCtx) -> {
// pCtx.getValue("inputId", String.class).orElse("") to read input values
// Navigate by opening a new page instance
});
}
public void open(Store<EntityStore> store) { page.open(store); }
}
```
### Reading input values
Inside an event listener, the second argument is the HyUI page context, which exposes `getValue(id, type)`:
```java
page.addEventListener("submitBtn", CustomUIEventBindingType.Activating, (ignored, pCtx) -> {
String reason = pCtx.getValue("reasonInput", String.class).orElse("").trim();
...
});
```
### Re-rendering after state changes
We do *not* mutate the page in place. Instead, we open a fresh page instance with new state:
```java
new PunishFormPage(playerRef, sCtx, uuid, name, type, durationFromForm, reasonFromForm, "Error: ...").open(store);
```
This is simpler than HyUI's `updatePage(true)` re-render dance and keeps each page render side-effect-free. The cost is rebuilding the HTML each navigation - cheap compared to anything else we do.
### HYUIML class reference (the bits we use)
- `<div class="page-overlay">` - root, dims background, captures input
- `<div class="container" data-hyui-title="...">` - main framed window with header
- `<div class="container-contents">` - inner content area
- `<button id="...">Text</button>` - clickable button (use `class="back-button"` for back-themed)
- `<input type="text" id="..." value="..." placeholder="...">` - text input
- `<p>` / `<label>` - text
The full HYUIML reference is in `extras to look at/HyUI-Docs/hyuiml-htmlish-in-hytale.md`.
## Adding a new page
1. Create `src/main/java/net/kewwbec/staff/ui/MyPage.java` following the skeleton above.
2. Use HTMLish in the `fromHtml` block. Stick to alphanumeric IDs (HyUI sanitizes them, but bugs hide in non-trivial chars).
3. Wire listeners with `page.addEventListener(id, eventType, handler)`.
4. To navigate to it, call `new MyPage(...).open(store)` from another page's handler.
5. To open it from a command, do the same thing in the command's `execute` method.
## Things to be aware of
- **HyUI must be on the classpath at runtime.** This is handled by the shade plugin in [pom.xml](../pom.xml), which packs HyUI into the Staff jar. Don't drop the shade plugin without a replacement.
- **Sanitized IDs.** Use plain alphanumeric ids in HYUIML. Use the same string in `addEventListener` - HyUI translates internally.
- **Escape user input** when embedding into HTML strings. We have a tiny `escape(String)` helper in each page; use it for any value that came from a player.
- **`value` attribute is what carries text-field state across re-renders.** When you rebuild a form page with an error, set `value="..."` on each input so the user doesn't lose what they typed.
- **No `<script>`, no full CSS.** Logic is in Java; layout is via Hytale's `LayoutMode` and `flex-weight` via `style="..."` on `<div>`s if needed.
## What this UI deliberately doesn't do
- **Network-wide active punishments view.** Could be added as a Hub button if needed.
- **Bulk actions.** No "ban everyone in this list" - intentional.
- **In-place updates.** Every state change rebuilds the page. Simpler reasoning, slightly more network chatter.
- **Custom themes.** We rely on HyUI's default Container/Button styling. Theming via `<style>` blocks is possible later.
+1
View File
@@ -6,3 +6,4 @@
4. [04_Schema.md](04_Schema.md) - MySQL tables
5. [05_Cross_Server_Propagation.md](05_Cross_Server_Propagation.md) - how bans/mutes propagate
6. [06_Operations.md](06_Operations.md) - ops scenarios, debugging
7. [07_UI.md](07_UI.md) - the /staffui hub and the page system
+50
View File
@@ -20,6 +20,13 @@
<networkcore.jar>${project.basedir}/../Core/target/NetworkCore-0.1.0.jar</networkcore.jar>
</properties>
<repositories>
<repository>
<id>cursemaven</id>
<url>https://www.cursemaven.com</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>com.hypixel.hytale</groupId>
@@ -37,6 +44,12 @@
<systemPath>${networkcore.jar}</systemPath>
</dependency>
<dependency>
<groupId>curse.maven</groupId>
<artifactId>hyui-1431415</artifactId>
<version>7511121</version>
</dependency>
<dependency>
<groupId>com.google.code.findbugs</groupId>
<artifactId>jsr305</artifactId>
@@ -47,5 +60,42 @@
<build>
<finalName>${project.artifactId}-${project.version}</finalName>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.5.1</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<createDependencyReducedPom>false</createDependencyReducedPom>
<minimizeJar>false</minimizeJar>
<artifactSet>
<excludes>
<exclude>com.hypixel.hytale:*</exclude>
<exclude>net.kewwbec:networkcore</exclude>
<exclude>com.google.code.findbugs:*</exclude>
</excludes>
</artifactSet>
<filters>
<filter>
<artifact>*:*</artifact>
<excludes>
<exclude>META-INF/*.SF</exclude>
<exclude>META-INF/*.DSA</exclude>
<exclude>META-INF/*.RSA</exclude>
<exclude>module-info.class</exclude>
</excludes>
</filter>
</filters>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
@@ -0,0 +1,23 @@
package net.kewwbec.staff;
public final class StaffPermissionsNodes {
public static final String ROOT = "networkstaff";
public static final String BAN = ROOT + ".ban";
public static final String TEMPBAN = ROOT + ".tempban";
public static final String MUTE = ROOT + ".mute";
public static final String TEMPMUTE = ROOT + ".tempmute";
public static final String KICK = ROOT + ".kick";
public static final String WARN = ROOT + ".warn";
public static final String UNBAN = ROOT + ".unban";
public static final String UNMUTE = ROOT + ".unmute";
public static final String CHAT = ROOT + ".chat";
public static final String LOOKUP = ROOT + ".lookup";
public static final String HISTORY = ROOT + ".history";
public static final String UI = ROOT + ".ui";
private StaffPermissionsNodes() {
}
}
@@ -10,10 +10,12 @@ import com.hypixel.hytale.server.core.plugin.JavaPluginInit;
import net.kewwbec.networkcore.NetworkCore;
import net.kewwbec.networkcore.api.DatabaseService;
import net.kewwbec.networkcore.api.MessageBus;
import net.kewwbec.staff.command.StaffUICommand;
import net.kewwbec.staff.command.chat.StaffChatCommand;
import net.kewwbec.staff.command.lookup.HistoryCommand;
import net.kewwbec.staff.command.lookup.LookupCommand;
import net.kewwbec.staff.command.punish.PunishmentCommands;
import net.kewwbec.staff.ui.StaffUIContext;
import net.kewwbec.staff.config.StaffConfig;
import net.kewwbec.staff.config.StaffConfigLoader;
import net.kewwbec.staff.db.PlayerSeenRepository;
@@ -25,6 +27,7 @@ import net.kewwbec.staff.listener.PlayerSeenTracker;
import net.kewwbec.staff.service.MuteCache;
import net.kewwbec.staff.service.PunishmentService;
import net.kewwbec.staff.service.StaffChatService;
import net.kewwbec.staff.service.StaffChatToggleService;
import net.kewwbec.staff.util.PlayerResolver;
import net.kewwbec.staff.util.StaffPermissions;
@@ -40,6 +43,7 @@ public final class StaffPlugin extends JavaPlugin {
private StaffConfig config;
private StaffPermissions permissions;
private MuteCache muteCache;
private StaffChatToggleService chatToggle;
private PunishmentService punishmentService;
private StaffChatService staffChat;
@@ -55,8 +59,9 @@ public final class StaffPlugin extends JavaPlugin {
((HytaleLogger.Api) LOGGER.at(Level.SEVERE).withCause(e)).log("Failed to load Staff config");
this.config = new StaffConfig();
}
this.permissions = new StaffPermissions(config.permissions);
this.permissions = new StaffPermissions();
this.muteCache = new MuteCache();
this.chatToggle = new StaffChatToggleService();
LOGGER.at(Level.INFO).log("Staff setup complete");
}
@@ -103,25 +108,28 @@ public final class StaffPlugin extends JavaPlugin {
punishmentService.start();
BanEnforcer banEnforcer = new BanEnforcer(punishmentService);
ChatEnforcer chatEnforcer = new ChatEnforcer(muteCache);
PlayerSeenTracker seenTracker = new PlayerSeenTracker(seenRepo, muteCache, punishmentService, core.getServerId());
ChatEnforcer chatEnforcer = new ChatEnforcer(muteCache, chatToggle, staffChat);
PlayerSeenTracker seenTracker = new PlayerSeenTracker(seenRepo, muteCache, punishmentService, chatToggle, core.getServerId());
getEventRegistry().registerGlobal(PlayerSetupConnectEvent.class, banEnforcer::onSetupConnect);
getEventRegistry().registerGlobal(PlayerChatEvent.class, chatEnforcer::onChat);
getEventRegistry().registerGlobal(PlayerConnectEvent.class, seenTracker::onConnect);
getEventRegistry().registerGlobal(PlayerDisconnectEvent.class, seenTracker::onDisconnect);
getCommandRegistry().registerCommand(new PunishmentCommands.Ban(punishmentService, staffChat, resolver, permissions));
getCommandRegistry().registerCommand(new PunishmentCommands.TempBan(punishmentService, staffChat, resolver, permissions));
getCommandRegistry().registerCommand(new PunishmentCommands.Mute(punishmentService, staffChat, resolver, permissions));
getCommandRegistry().registerCommand(new PunishmentCommands.TempMute(punishmentService, staffChat, resolver, permissions));
getCommandRegistry().registerCommand(new PunishmentCommands.Kick(punishmentService, staffChat, resolver, permissions));
getCommandRegistry().registerCommand(new PunishmentCommands.Warn(punishmentService, staffChat, resolver, permissions));
getCommandRegistry().registerCommand(new PunishmentCommands.Unban(punishmentService, staffChat, resolver, permissions));
getCommandRegistry().registerCommand(new PunishmentCommands.Unmute(punishmentService, staffChat, resolver, permissions));
getCommandRegistry().registerCommand(new StaffChatCommand(staffChat, permissions));
getCommandRegistry().registerCommand(new LookupCommand(resolver, seenRepo, punishmentService, permissions));
getCommandRegistry().registerCommand(new HistoryCommand(resolver, punishmentRepo, permissions));
getCommandRegistry().registerCommand(new PunishmentCommands.Ban(punishmentService, staffChat, resolver));
getCommandRegistry().registerCommand(new PunishmentCommands.TempBan(punishmentService, staffChat, resolver));
getCommandRegistry().registerCommand(new PunishmentCommands.Mute(punishmentService, staffChat, resolver));
getCommandRegistry().registerCommand(new PunishmentCommands.TempMute(punishmentService, staffChat, resolver));
getCommandRegistry().registerCommand(new PunishmentCommands.Kick(punishmentService, staffChat, resolver));
getCommandRegistry().registerCommand(new PunishmentCommands.Warn(punishmentService, staffChat, resolver));
getCommandRegistry().registerCommand(new PunishmentCommands.Unban(punishmentService, staffChat, resolver));
getCommandRegistry().registerCommand(new PunishmentCommands.Unmute(punishmentService, staffChat, resolver));
getCommandRegistry().registerCommand(new StaffChatCommand(chatToggle));
getCommandRegistry().registerCommand(new LookupCommand(resolver, seenRepo, punishmentService));
getCommandRegistry().registerCommand(new HistoryCommand(resolver, punishmentRepo));
StaffUIContext uiContext = new StaffUIContext(permissions, punishmentService, staffChat, resolver, punishmentRepo, seenRepo);
getCommandRegistry().registerCommand(new StaffUICommand(uiContext));
LOGGER.at(Level.INFO).log("Staff started");
}
@@ -0,0 +1,36 @@
package net.kewwbec.staff.command;
import com.hypixel.hytale.component.Ref;
import com.hypixel.hytale.component.Store;
import com.hypixel.hytale.server.core.command.system.CommandContext;
import com.hypixel.hytale.server.core.command.system.basecommands.AbstractPlayerCommand;
import com.hypixel.hytale.server.core.universe.PlayerRef;
import com.hypixel.hytale.server.core.universe.world.World;
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
import net.kewwbec.staff.StaffPermissionsNodes;
import net.kewwbec.staff.ui.StaffHubPage;
import net.kewwbec.staff.ui.StaffUIContext;
import javax.annotation.Nonnull;
public final class StaffUICommand extends AbstractPlayerCommand {
private final StaffUIContext ctx;
public StaffUICommand(@Nonnull StaffUIContext ctx) {
super("staffui", "Open the Staff hub UI");
this.ctx = ctx;
requirePermission(StaffPermissionsNodes.UI);
}
@Override protected boolean canGeneratePermission() { return false; }
@Override
protected void execute(@Nonnull CommandContext context,
@Nonnull Store<EntityStore> store,
@Nonnull Ref<EntityStore> ref,
@Nonnull PlayerRef playerRef,
@Nonnull World world) {
new StaffHubPage(playerRef, ctx).open(store);
}
}
@@ -4,45 +4,39 @@ import com.hypixel.hytale.component.Ref;
import com.hypixel.hytale.component.Store;
import com.hypixel.hytale.server.core.Message;
import com.hypixel.hytale.server.core.command.system.CommandContext;
import com.hypixel.hytale.server.core.command.system.arguments.system.RequiredArg;
import com.hypixel.hytale.server.core.command.system.arguments.types.ArgTypes;
import com.hypixel.hytale.server.core.command.system.basecommands.AbstractPlayerCommand;
import com.hypixel.hytale.server.core.universe.PlayerRef;
import com.hypixel.hytale.server.core.universe.world.World;
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
import net.kewwbec.staff.service.StaffChatService;
import net.kewwbec.staff.util.StaffPermissions;
import net.kewwbec.staff.StaffPermissionsNodes;
import net.kewwbec.staff.service.StaffChatToggleService;
import javax.annotation.Nonnull;
import java.awt.Color;
public final class StaffChatCommand extends AbstractPlayerCommand {
private final StaffChatService chat;
private final StaffPermissions perms;
private final RequiredArg<String> messageArg = withRequiredArg("message", "Message to send", ArgTypes.STRING);
private final StaffChatToggleService toggle;
public StaffChatCommand(@Nonnull StaffChatService chat, @Nonnull StaffPermissions perms) {
super("sc", "Send a message to staff chat (cross-server)");
this.chat = chat;
this.perms = perms;
public StaffChatCommand(@Nonnull StaffChatToggleService toggle) {
super("sc", "Toggle staff chat mode (your chat goes to staff instead of world)");
this.toggle = toggle;
requirePermission(StaffPermissionsNodes.CHAT);
}
@Override protected boolean canGeneratePermission() { return false; }
@Override
protected void execute(@Nonnull CommandContext ctx,
@Nonnull Store<EntityStore> store,
@Nonnull Ref<EntityStore> ref,
@Nonnull PlayerRef sender,
@Nonnull World world) {
if (!perms.isStaff(sender)) {
ctx.sendMessage(Message.raw("You do not have permission to use staff chat.").color(Color.RED));
return;
}
String message = messageArg.get(ctx);
if (message == null || message.isBlank()) {
ctx.sendMessage(Message.raw("Usage: /sc <message>").color(Color.YELLOW));
return;
}
chat.send(sender, message);
boolean nowOn = toggle.toggle(sender.getUuid());
if (nowOn) {
ctx.sendMessage(Message.raw("Staff chat ON. Your chat now broadcasts to staff. Run /sc again to turn off.").color(Color.GREEN));
} else {
ctx.sendMessage(Message.raw("Staff chat OFF. Your chat is back to normal.").color(Color.YELLOW));
}
}
}
@@ -10,10 +10,10 @@ import com.hypixel.hytale.server.core.command.system.basecommands.AbstractPlayer
import com.hypixel.hytale.server.core.universe.PlayerRef;
import com.hypixel.hytale.server.core.universe.world.World;
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
import net.kewwbec.staff.StaffPermissionsNodes;
import net.kewwbec.staff.db.PunishmentRepository;
import net.kewwbec.staff.model.Punishment;
import net.kewwbec.staff.util.PlayerResolver;
import net.kewwbec.staff.util.StaffPermissions;
import javax.annotation.Nonnull;
import java.awt.Color;
@@ -27,28 +27,24 @@ public final class HistoryCommand extends AbstractPlayerCommand {
private final PlayerResolver resolver;
private final PunishmentRepository repo;
private final StaffPermissions perms;
private final RequiredArg<String> targetArg = withRequiredArg("player", "Player name", ArgTypes.STRING);
public HistoryCommand(@Nonnull PlayerResolver resolver,
@Nonnull PunishmentRepository repo,
@Nonnull StaffPermissions perms) {
@Nonnull PunishmentRepository repo) {
super("history", "Show a player's recent punishment history");
this.resolver = resolver;
this.repo = repo;
this.perms = perms;
requirePermission(StaffPermissionsNodes.HISTORY);
}
@Override protected boolean canGeneratePermission() { return false; }
@Override
protected void execute(@Nonnull CommandContext ctx,
@Nonnull Store<EntityStore> store,
@Nonnull Ref<EntityStore> ref,
@Nonnull PlayerRef issuer,
@Nonnull World world) {
if (!perms.isStaff(issuer)) {
ctx.sendMessage(Message.raw("You do not have permission to use this command.").color(Color.RED));
return;
}
String name = targetArg.get(ctx);
if (name == null || name.isBlank()) {
ctx.sendMessage(Message.raw("Usage: /history <player>").color(Color.YELLOW));
@@ -10,13 +10,13 @@ import com.hypixel.hytale.server.core.command.system.basecommands.AbstractPlayer
import com.hypixel.hytale.server.core.universe.PlayerRef;
import com.hypixel.hytale.server.core.universe.world.World;
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
import net.kewwbec.staff.StaffPermissionsNodes;
import net.kewwbec.staff.db.PlayerSeenRepository;
import net.kewwbec.staff.model.PlayerSeen;
import net.kewwbec.staff.model.Punishment;
import net.kewwbec.staff.service.PunishmentService;
import net.kewwbec.staff.util.DurationParser;
import net.kewwbec.staff.util.PlayerResolver;
import net.kewwbec.staff.util.StaffPermissions;
import javax.annotation.Nonnull;
import java.awt.Color;
@@ -29,30 +29,26 @@ public final class LookupCommand extends AbstractPlayerCommand {
private final PlayerResolver resolver;
private final PlayerSeenRepository seen;
private final PunishmentService punishments;
private final StaffPermissions perms;
private final RequiredArg<String> targetArg = withRequiredArg("player", "Player name or UUID", ArgTypes.STRING);
public LookupCommand(@Nonnull PlayerResolver resolver,
@Nonnull PlayerSeenRepository seen,
@Nonnull PunishmentService punishments,
@Nonnull StaffPermissions perms) {
@Nonnull PunishmentService punishments) {
super("lookup", "Look up a player");
this.resolver = resolver;
this.seen = seen;
this.punishments = punishments;
this.perms = perms;
requirePermission(StaffPermissionsNodes.LOOKUP);
}
@Override protected boolean canGeneratePermission() { return false; }
@Override
protected void execute(@Nonnull CommandContext ctx,
@Nonnull Store<EntityStore> store,
@Nonnull Ref<EntityStore> ref,
@Nonnull PlayerRef issuer,
@Nonnull World world) {
if (!perms.isStaff(issuer)) {
ctx.sendMessage(Message.raw("You do not have permission to use this command.").color(Color.RED));
return;
}
String name = targetArg.get(ctx);
if (name == null || name.isBlank()) {
ctx.sendMessage(Message.raw("Usage: /lookup <player>").color(Color.YELLOW));
@@ -10,13 +10,13 @@ import com.hypixel.hytale.server.core.command.system.basecommands.AbstractPlayer
import com.hypixel.hytale.server.core.universe.PlayerRef;
import com.hypixel.hytale.server.core.universe.world.World;
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
import net.kewwbec.staff.StaffPermissionsNodes;
import net.kewwbec.staff.model.Punishment;
import net.kewwbec.staff.model.PunishmentType;
import net.kewwbec.staff.service.PunishmentService;
import net.kewwbec.staff.service.StaffChatService;
import net.kewwbec.staff.util.DurationParser;
import net.kewwbec.staff.util.PlayerResolver;
import net.kewwbec.staff.util.StaffPermissions;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
@@ -28,12 +28,6 @@ public final class PunishmentCommands {
private PunishmentCommands() {
}
static boolean requireStaff(@Nonnull CommandContext ctx, @Nonnull PlayerRef ref, @Nonnull StaffPermissions perms) {
if (perms.isStaff(ref)) return true;
ctx.sendMessage(Message.raw("You do not have permission to use this command.").color(Color.RED));
return false;
}
@Nullable
static PlayerResolver.Resolved resolve(@Nonnull CommandContext ctx,
@Nonnull PlayerResolver resolver,
@@ -74,18 +68,19 @@ public final class PunishmentCommands {
private final PunishmentService punishments;
private final StaffChatService staffChat;
private final PlayerResolver resolver;
private final StaffPermissions perms;
private final RequiredArg<String> targetArg = withRequiredArg("player", "Target player", ArgTypes.STRING);
private final RequiredArg<String> reasonArg = withRequiredArg("reason", "Reason", ArgTypes.STRING);
public Ban(@Nonnull PunishmentService p, @Nonnull StaffChatService c, @Nonnull PlayerResolver r, @Nonnull StaffPermissions perms) {
public Ban(@Nonnull PunishmentService p, @Nonnull StaffChatService c, @Nonnull PlayerResolver r) {
super("ban", "Permanently ban a player");
this.punishments = p; this.staffChat = c; this.resolver = r; this.perms = perms;
this.punishments = p; this.staffChat = c; this.resolver = r;
requirePermission(StaffPermissionsNodes.BAN);
}
@Override protected boolean canGeneratePermission() { return false; }
@Override
protected void execute(@Nonnull CommandContext ctx, @Nonnull Store<EntityStore> store, @Nonnull Ref<EntityStore> ref, @Nonnull PlayerRef issuer, @Nonnull World world) {
if (!requireStaff(ctx, issuer, perms)) return;
String name = targetArg.get(ctx);
String reason = reasonArg.get(ctx);
if (name == null || reason == null) {
@@ -102,19 +97,20 @@ public final class PunishmentCommands {
private final PunishmentService punishments;
private final StaffChatService staffChat;
private final PlayerResolver resolver;
private final StaffPermissions perms;
private final RequiredArg<String> targetArg = withRequiredArg("player", "Target player", ArgTypes.STRING);
private final RequiredArg<String> durationArg = withRequiredArg("duration", "e.g. 30m, 2h, 7d", ArgTypes.STRING);
private final RequiredArg<String> reasonArg = withRequiredArg("reason", "Reason", ArgTypes.STRING);
public TempBan(@Nonnull PunishmentService p, @Nonnull StaffChatService c, @Nonnull PlayerResolver r, @Nonnull StaffPermissions perms) {
public TempBan(@Nonnull PunishmentService p, @Nonnull StaffChatService c, @Nonnull PlayerResolver r) {
super("tempban", "Temporarily ban a player");
this.punishments = p; this.staffChat = c; this.resolver = r; this.perms = perms;
this.punishments = p; this.staffChat = c; this.resolver = r;
requirePermission(StaffPermissionsNodes.TEMPBAN);
}
@Override protected boolean canGeneratePermission() { return false; }
@Override
protected void execute(@Nonnull CommandContext ctx, @Nonnull Store<EntityStore> store, @Nonnull Ref<EntityStore> ref, @Nonnull PlayerRef issuer, @Nonnull World world) {
if (!requireStaff(ctx, issuer, perms)) return;
String name = targetArg.get(ctx);
String durationStr = durationArg.get(ctx);
String reason = reasonArg.get(ctx);
@@ -137,18 +133,19 @@ public final class PunishmentCommands {
private final PunishmentService punishments;
private final StaffChatService staffChat;
private final PlayerResolver resolver;
private final StaffPermissions perms;
private final RequiredArg<String> targetArg = withRequiredArg("player", "Target player", ArgTypes.STRING);
private final RequiredArg<String> reasonArg = withRequiredArg("reason", "Reason", ArgTypes.STRING);
public Mute(@Nonnull PunishmentService p, @Nonnull StaffChatService c, @Nonnull PlayerResolver r, @Nonnull StaffPermissions perms) {
public Mute(@Nonnull PunishmentService p, @Nonnull StaffChatService c, @Nonnull PlayerResolver r) {
super("mute", "Permanently mute a player");
this.punishments = p; this.staffChat = c; this.resolver = r; this.perms = perms;
this.punishments = p; this.staffChat = c; this.resolver = r;
requirePermission(StaffPermissionsNodes.MUTE);
}
@Override protected boolean canGeneratePermission() { return false; }
@Override
protected void execute(@Nonnull CommandContext ctx, @Nonnull Store<EntityStore> store, @Nonnull Ref<EntityStore> ref, @Nonnull PlayerRef issuer, @Nonnull World world) {
if (!requireStaff(ctx, issuer, perms)) return;
String name = targetArg.get(ctx);
String reason = reasonArg.get(ctx);
if (name == null || reason == null) {
@@ -165,19 +162,20 @@ public final class PunishmentCommands {
private final PunishmentService punishments;
private final StaffChatService staffChat;
private final PlayerResolver resolver;
private final StaffPermissions perms;
private final RequiredArg<String> targetArg = withRequiredArg("player", "Target player", ArgTypes.STRING);
private final RequiredArg<String> durationArg = withRequiredArg("duration", "e.g. 30m, 2h", ArgTypes.STRING);
private final RequiredArg<String> reasonArg = withRequiredArg("reason", "Reason", ArgTypes.STRING);
public TempMute(@Nonnull PunishmentService p, @Nonnull StaffChatService c, @Nonnull PlayerResolver r, @Nonnull StaffPermissions perms) {
public TempMute(@Nonnull PunishmentService p, @Nonnull StaffChatService c, @Nonnull PlayerResolver r) {
super("tempmute", "Temporarily mute a player");
this.punishments = p; this.staffChat = c; this.resolver = r; this.perms = perms;
this.punishments = p; this.staffChat = c; this.resolver = r;
requirePermission(StaffPermissionsNodes.TEMPMUTE);
}
@Override protected boolean canGeneratePermission() { return false; }
@Override
protected void execute(@Nonnull CommandContext ctx, @Nonnull Store<EntityStore> store, @Nonnull Ref<EntityStore> ref, @Nonnull PlayerRef issuer, @Nonnull World world) {
if (!requireStaff(ctx, issuer, perms)) return;
String name = targetArg.get(ctx);
String durationStr = durationArg.get(ctx);
String reason = reasonArg.get(ctx);
@@ -200,18 +198,19 @@ public final class PunishmentCommands {
private final PunishmentService punishments;
private final StaffChatService staffChat;
private final PlayerResolver resolver;
private final StaffPermissions perms;
private final RequiredArg<String> targetArg = withRequiredArg("player", "Target player", ArgTypes.STRING);
private final RequiredArg<String> reasonArg = withRequiredArg("reason", "Reason", ArgTypes.STRING);
public Kick(@Nonnull PunishmentService p, @Nonnull StaffChatService c, @Nonnull PlayerResolver r, @Nonnull StaffPermissions perms) {
public Kick(@Nonnull PunishmentService p, @Nonnull StaffChatService c, @Nonnull PlayerResolver r) {
super("kick", "Kick a player from the network");
this.punishments = p; this.staffChat = c; this.resolver = r; this.perms = perms;
this.punishments = p; this.staffChat = c; this.resolver = r;
requirePermission(StaffPermissionsNodes.KICK);
}
@Override protected boolean canGeneratePermission() { return false; }
@Override
protected void execute(@Nonnull CommandContext ctx, @Nonnull Store<EntityStore> store, @Nonnull Ref<EntityStore> ref, @Nonnull PlayerRef issuer, @Nonnull World world) {
if (!requireStaff(ctx, issuer, perms)) return;
String name = targetArg.get(ctx);
String reason = reasonArg.get(ctx);
if (name == null || reason == null) {
@@ -228,18 +227,19 @@ public final class PunishmentCommands {
private final PunishmentService punishments;
private final StaffChatService staffChat;
private final PlayerResolver resolver;
private final StaffPermissions perms;
private final RequiredArg<String> targetArg = withRequiredArg("player", "Target player", ArgTypes.STRING);
private final RequiredArg<String> reasonArg = withRequiredArg("reason", "Reason", ArgTypes.STRING);
public Warn(@Nonnull PunishmentService p, @Nonnull StaffChatService c, @Nonnull PlayerResolver r, @Nonnull StaffPermissions perms) {
public Warn(@Nonnull PunishmentService p, @Nonnull StaffChatService c, @Nonnull PlayerResolver r) {
super("warn", "Warn a player");
this.punishments = p; this.staffChat = c; this.resolver = r; this.perms = perms;
this.punishments = p; this.staffChat = c; this.resolver = r;
requirePermission(StaffPermissionsNodes.WARN);
}
@Override protected boolean canGeneratePermission() { return false; }
@Override
protected void execute(@Nonnull CommandContext ctx, @Nonnull Store<EntityStore> store, @Nonnull Ref<EntityStore> ref, @Nonnull PlayerRef issuer, @Nonnull World world) {
if (!requireStaff(ctx, issuer, perms)) return;
String name = targetArg.get(ctx);
String reason = reasonArg.get(ctx);
if (name == null || reason == null) {
@@ -256,17 +256,18 @@ public final class PunishmentCommands {
private final PunishmentService punishments;
private final StaffChatService staffChat;
private final PlayerResolver resolver;
private final StaffPermissions perms;
private final RequiredArg<String> targetArg = withRequiredArg("player", "Target player", ArgTypes.STRING);
public Unban(@Nonnull PunishmentService p, @Nonnull StaffChatService c, @Nonnull PlayerResolver r, @Nonnull StaffPermissions perms) {
public Unban(@Nonnull PunishmentService p, @Nonnull StaffChatService c, @Nonnull PlayerResolver r) {
super("unban", "Lift a player's ban");
this.punishments = p; this.staffChat = c; this.resolver = r; this.perms = perms;
this.punishments = p; this.staffChat = c; this.resolver = r;
requirePermission(StaffPermissionsNodes.UNBAN);
}
@Override protected boolean canGeneratePermission() { return false; }
@Override
protected void execute(@Nonnull CommandContext ctx, @Nonnull Store<EntityStore> store, @Nonnull Ref<EntityStore> ref, @Nonnull PlayerRef issuer, @Nonnull World world) {
if (!requireStaff(ctx, issuer, perms)) return;
String name = targetArg.get(ctx);
if (name == null) {
ctx.sendMessage(Message.raw("Usage: /unban <player>").color(Color.YELLOW));
@@ -293,17 +294,18 @@ public final class PunishmentCommands {
private final PunishmentService punishments;
private final StaffChatService staffChat;
private final PlayerResolver resolver;
private final StaffPermissions perms;
private final RequiredArg<String> targetArg = withRequiredArg("player", "Target player", ArgTypes.STRING);
public Unmute(@Nonnull PunishmentService p, @Nonnull StaffChatService c, @Nonnull PlayerResolver r, @Nonnull StaffPermissions perms) {
public Unmute(@Nonnull PunishmentService p, @Nonnull StaffChatService c, @Nonnull PlayerResolver r) {
super("unmute", "Lift a player's mute");
this.punishments = p; this.staffChat = c; this.resolver = r; this.perms = perms;
this.punishments = p; this.staffChat = c; this.resolver = r;
requirePermission(StaffPermissionsNodes.UNMUTE);
}
@Override protected boolean canGeneratePermission() { return false; }
@Override
protected void execute(@Nonnull CommandContext ctx, @Nonnull Store<EntityStore> store, @Nonnull Ref<EntityStore> ref, @Nonnull PlayerRef issuer, @Nonnull World world) {
if (!requireStaff(ctx, issuer, perms)) return;
String name = targetArg.get(ctx);
if (name == null) {
ctx.sendMessage(Message.raw("Usage: /unmute <player>").color(Color.YELLOW));
@@ -1,19 +1,10 @@
package net.kewwbec.staff.config;
import java.util.ArrayList;
import java.util.List;
public final class StaffConfig {
public PermissionsSection permissions = new PermissionsSection();
public ChatSection chat = new ChatSection();
public TableSection tables = new TableSection();
public static final class PermissionsSection {
public List<String> staff_groups = new ArrayList<>(List.of("OP", "ADMIN", "MOD", "HELPER"));
public List<String> admin_groups = new ArrayList<>(List.of("OP", "ADMIN"));
}
public static final class ChatSection {
public String channel = "staff.chat";
public String prefix = "[Staff]";
@@ -28,7 +28,6 @@ public final class StaffConfigLoader {
String json = Files.readString(file, StandardCharsets.UTF_8);
StaffConfig parsed = GSON.fromJson(json, StaffConfig.class);
if (parsed == null) parsed = new StaffConfig();
if (parsed.permissions == null) parsed.permissions = new StaffConfig.PermissionsSection();
if (parsed.chat == null) parsed.chat = new StaffConfig.ChatSection();
if (parsed.tables == null) parsed.tables = new StaffConfig.TableSection();
return parsed;
@@ -1,6 +1,7 @@
package net.kewwbec.staff.listener;
import com.hypixel.hytale.logger.HytaleLogger;
import com.hypixel.hytale.server.core.Message;
import com.hypixel.hytale.server.core.event.events.player.PlayerSetupConnectEvent;
import net.kewwbec.staff.model.Punishment;
import net.kewwbec.staff.service.PunishmentService;
@@ -25,7 +26,7 @@ public final class BanEnforcer {
Optional<Punishment> ban = punishments.getActiveBan(event.getUuid());
if (ban.isPresent()) {
event.setCancelled(true);
event.setReason(PunishmentService.disconnectMessage(ban.get()));
event.setReason(Message.raw(PunishmentService.disconnectMessage(ban.get())));
}
} catch (SQLException e) {
((HytaleLogger.Api) LOGGER.at(Level.WARNING).withCause(e))
@@ -5,6 +5,8 @@ import com.hypixel.hytale.server.core.event.events.player.PlayerChatEvent;
import com.hypixel.hytale.server.core.universe.PlayerRef;
import net.kewwbec.staff.model.Punishment;
import net.kewwbec.staff.service.MuteCache;
import net.kewwbec.staff.service.StaffChatService;
import net.kewwbec.staff.service.StaffChatToggleService;
import net.kewwbec.staff.util.DurationParser;
import javax.annotation.Nonnull;
@@ -12,21 +14,35 @@ import java.awt.Color;
public final class ChatEnforcer {
private final MuteCache cache;
private final MuteCache muteCache;
private final StaffChatToggleService toggle;
private final StaffChatService staffChat;
public ChatEnforcer(@Nonnull MuteCache cache) {
this.cache = cache;
public ChatEnforcer(@Nonnull MuteCache muteCache,
@Nonnull StaffChatToggleService toggle,
@Nonnull StaffChatService staffChat) {
this.muteCache = muteCache;
this.toggle = toggle;
this.staffChat = staffChat;
}
public void onChat(@Nonnull PlayerChatEvent event) {
PlayerRef sender = event.getSender();
if (sender == null) return;
Punishment mute = cache.get(sender.getUuid());
if (mute == null) return;
Punishment mute = muteCache.get(sender.getUuid());
if (mute != null) {
event.setCancelled(true);
String suffix = (mute.expiresAt() == null)
? "permanently"
: "for " + DurationParser.formatRemaining(mute.expiresAt() - System.currentTimeMillis());
sender.sendMessage(Message.raw("You are muted " + suffix + ": " + mute.reason()).color(Color.RED));
return;
}
if (toggle.isOn(sender.getUuid())) {
event.setCancelled(true);
staffChat.send(sender, event.getContent());
}
}
}
@@ -7,6 +7,7 @@ import com.hypixel.hytale.server.core.universe.PlayerRef;
import net.kewwbec.staff.db.PlayerSeenRepository;
import net.kewwbec.staff.service.MuteCache;
import net.kewwbec.staff.service.PunishmentService;
import net.kewwbec.staff.service.StaffChatToggleService;
import javax.annotation.Nonnull;
import java.sql.SQLException;
@@ -19,15 +20,18 @@ public final class PlayerSeenTracker {
private final PlayerSeenRepository seen;
private final MuteCache muteCache;
private final PunishmentService punishments;
private final StaffChatToggleService toggle;
private final String serverId;
public PlayerSeenTracker(@Nonnull PlayerSeenRepository seen,
@Nonnull MuteCache muteCache,
@Nonnull PunishmentService punishments,
@Nonnull StaffChatToggleService toggle,
@Nonnull String serverId) {
this.seen = seen;
this.muteCache = muteCache;
this.punishments = punishments;
this.toggle = toggle;
this.serverId = serverId;
}
@@ -53,5 +57,6 @@ public final class PlayerSeenTracker {
.log("Failed to record leave for %s", ref.getUuid());
}
muteCache.remove(ref.getUuid());
toggle.clear(ref.getUuid());
}
}
@@ -131,7 +131,7 @@ public final class PunishmentService {
if (p.type().isBan() || p.type() == PunishmentType.KICK) {
PlayerRef target = findOnline(p.targetUuid());
if (target != null) {
target.getPacketHandler().disconnect(disconnectMessage(p));
target.getPacketHandler().disconnect(Message.raw(disconnectMessage(p)));
}
return;
}
@@ -4,6 +4,7 @@ import com.hypixel.hytale.server.core.Message;
import com.hypixel.hytale.server.core.universe.PlayerRef;
import com.hypixel.hytale.server.core.universe.Universe;
import net.kewwbec.networkcore.api.MessageBus;
import net.kewwbec.staff.StaffPermissionsNodes;
import net.kewwbec.staff.config.StaffConfig;
import net.kewwbec.staff.util.StaffPermissions;
@@ -71,7 +72,7 @@ public final class StaffChatService {
String formatted = formatLine(p);
for (PlayerRef ref : universe.getPlayers()) {
if (!perms.isStaff(ref)) continue;
if (!perms.has(ref, StaffPermissionsNodes.CHAT)) continue;
ref.sendMessage(Message.raw(formatted).color(p.systemAnnouncement ? Color.YELLOW : Color.LIGHT_GRAY));
}
}
@@ -0,0 +1,34 @@
package net.kewwbec.staff.service;
import javax.annotation.Nonnull;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
/**
* Tracks which players have "staff chat mode" toggled on. When on, the player's regular chat
* messages are intercepted in {@link net.kewwbec.staff.listener.ChatEnforcer} and routed
* through {@link StaffChatService} instead.
*
* State is per-server and in-memory only. Cleared on disconnect.
*/
public final class StaffChatToggleService {
private final Set<UUID> enabled = ConcurrentHashMap.newKeySet();
public boolean toggle(@Nonnull UUID uuid) {
if (enabled.remove(uuid)) {
return false;
}
enabled.add(uuid);
return true;
}
public boolean isOn(@Nonnull UUID uuid) {
return enabled.contains(uuid);
}
public void clear(@Nonnull UUID uuid) {
enabled.remove(uuid);
}
}
@@ -0,0 +1,105 @@
package net.kewwbec.staff.ui;
import au.ellie.hyui.builders.PageBuilder;
import com.hypixel.hytale.component.Ref;
import com.hypixel.hytale.component.Store;
import com.hypixel.hytale.logger.HytaleLogger;
import com.hypixel.hytale.protocol.packets.interface_.CustomPageLifetime;
import com.hypixel.hytale.protocol.packets.interface_.CustomUIEventBindingType;
import com.hypixel.hytale.server.core.universe.PlayerRef;
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
import net.kewwbec.staff.model.Punishment;
import javax.annotation.Nonnull;
import java.sql.SQLException;
import java.time.Instant;
import java.util.List;
import java.util.UUID;
import java.util.function.Consumer;
import java.util.logging.Level;
public final class HistoryPage {
private static final HytaleLogger LOGGER = HytaleLogger.forEnclosingClass();
private static final int PAGE_SIZE = 50;
private final PlayerRef playerRef;
private final StaffUIContext sCtx;
private final UUID targetUuid;
private final String targetName;
private final PageBuilder page;
public HistoryPage(@Nonnull PlayerRef playerRef,
@Nonnull StaffUIContext sCtx,
@Nonnull UUID targetUuid,
@Nonnull String targetName) {
this.playerRef = playerRef;
this.sCtx = sCtx;
this.targetUuid = targetUuid;
this.targetName = targetName;
StringBuilder body = new StringBuilder();
try {
List<Punishment> history = sCtx.punishmentRepo.findHistory(targetUuid, PAGE_SIZE);
if (history.isEmpty()) {
body.append("<p>No punishments on record.</p>");
} else {
long now = System.currentTimeMillis();
for (Punishment p : history) {
String state;
if (!p.active() || p.revokedAt() != null) state = "REVOKED";
else if (p.expiresAt() != null && p.expiresAt() <= now) state = "EXPIRED";
else state = "ACTIVE";
body.append("<p>#")
.append(p.id()).append(" ")
.append(state).append(" ")
.append(p.type()).append(" by ")
.append(escape(p.issuedByName())).append(" @ ")
.append(Instant.ofEpochMilli(p.issuedAt()))
.append(" | ")
.append(escape(p.reason()))
.append("</p>");
}
}
} catch (SQLException e) {
((HytaleLogger.Api) LOGGER.at(Level.WARNING).withCause(e))
.log("History query failed for %s", targetUuid);
body.append("<p>DB error: ").append(escape(e.getMessage())).append("</p>");
}
String html = """
<div class="page-overlay">
<div class="container" data-hyui-title="History: %s">
<div class="container-contents">
%s
<button class="back-button" id="backBtn">Back</button>
</div>
</div>
</div>
""".formatted(escape(targetName), body.toString());
this.page = PageBuilder.pageForPlayer(playerRef)
.withLifetime(CustomPageLifetime.CanDismiss)
.fromHtml(html);
page.addEventListener("backBtn", CustomUIEventBindingType.Activating, (ignored, pCtx) ->
openStore(s -> new PlayerDetailPage(playerRef, sCtx, targetUuid, targetName).open(s))
);
}
public void open(@Nonnull Store<EntityStore> store) {
page.open(store);
}
private void openStore(@Nonnull Consumer<Store<EntityStore>> action) {
Ref<EntityStore> ref = playerRef.getReference();
if (ref == null) return;
action.accept(ref.getStore());
}
@Nonnull
private static String escape(@Nonnull String s) {
return s.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;").replace("\"", "&quot;");
}
}
@@ -0,0 +1,160 @@
package net.kewwbec.staff.ui;
import au.ellie.hyui.builders.PageBuilder;
import com.hypixel.hytale.component.Ref;
import com.hypixel.hytale.component.Store;
import com.hypixel.hytale.logger.HytaleLogger;
import com.hypixel.hytale.protocol.packets.interface_.CustomPageLifetime;
import com.hypixel.hytale.protocol.packets.interface_.CustomUIEventBindingType;
import com.hypixel.hytale.server.core.universe.PlayerRef;
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
import net.kewwbec.staff.model.PlayerSeen;
import net.kewwbec.staff.model.Punishment;
import net.kewwbec.staff.model.PunishmentType;
import net.kewwbec.staff.util.DurationParser;
import javax.annotation.Nonnull;
import java.sql.SQLException;
import java.time.Instant;
import java.util.Optional;
import java.util.UUID;
import java.util.function.Consumer;
import java.util.logging.Level;
public final class PlayerDetailPage {
private static final HytaleLogger LOGGER = HytaleLogger.forEnclosingClass();
private final PlayerRef playerRef;
private final StaffUIContext sCtx;
private final UUID targetUuid;
private final String targetName;
private final PageBuilder page;
public PlayerDetailPage(@Nonnull PlayerRef playerRef,
@Nonnull StaffUIContext sCtx,
@Nonnull UUID targetUuid,
@Nonnull String targetName) {
this.playerRef = playerRef;
this.sCtx = sCtx;
this.targetUuid = targetUuid;
this.targetName = targetName;
String statusLine = "";
String lastSeenLine = "";
String banLine = "";
String muteLine = "";
try {
Optional<PlayerSeen> seen = sCtx.seenRepo.findByUuid(targetUuid);
if (seen.isPresent()) {
PlayerSeen s = seen.get();
statusLine = s.online()
? "Status: ONLINE on " + (s.lastServerId() == null ? "?" : s.lastServerId())
: "Status: offline";
lastSeenLine = "Last seen: " + Instant.ofEpochMilli(s.lastSeen());
} else {
statusLine = "Status: no presence record";
}
Optional<Punishment> ban = sCtx.punishments.getActiveBan(targetUuid);
if (ban.isPresent()) banLine = "BANNED: " + escape(ban.get().reason()) + " (" + describeExpiry(ban.get()) + ")";
Optional<Punishment> mute = sCtx.punishments.getActiveMute(targetUuid);
if (mute.isPresent()) muteLine = "MUTED: " + escape(mute.get().reason()) + " (" + describeExpiry(mute.get()) + ")";
} catch (SQLException e) {
statusLine = "DB error: " + escape(e.getMessage());
}
String html = """
<div class="page-overlay">
<div class="container" data-hyui-title="%s">
<div class="container-contents">
<p>UUID: %s</p>
<p>%s</p>
<p>%s</p>
<p>%s</p>
<p>%s</p>
<button id="banBtn">Ban</button>
<button id="tempbanBtn">TempBan</button>
<button id="muteBtn">Mute</button>
<button id="tempmuteBtn">TempMute</button>
<button id="kickBtn">Kick</button>
<button id="warnBtn">Warn</button>
<button id="unbanBtn">Unban</button>
<button id="unmuteBtn">Unmute</button>
<button id="historyBtn">History</button>
<button class="back-button" id="backBtn">Back</button>
</div>
</div>
</div>
""".formatted(escape(targetName), targetUuid, statusLine, lastSeenLine, banLine, muteLine);
this.page = PageBuilder.pageForPlayer(playerRef)
.withLifetime(CustomPageLifetime.CanDismiss)
.fromHtml(html);
wirePunish("banBtn", PunishmentType.BAN);
wirePunish("tempbanBtn", PunishmentType.TEMPBAN);
wirePunish("muteBtn", PunishmentType.MUTE);
wirePunish("tempmuteBtn", PunishmentType.TEMPMUTE);
wirePunish("kickBtn", PunishmentType.KICK);
wirePunish("warnBtn", PunishmentType.WARN);
page.addEventListener("unbanBtn", CustomUIEventBindingType.Activating, (ignored, pCtx) ->
revoke(PunishmentType.BAN, PunishmentType.TEMPBAN, "Unbanned")
);
page.addEventListener("unmuteBtn", CustomUIEventBindingType.Activating, (ignored, pCtx) ->
revoke(PunishmentType.MUTE, PunishmentType.TEMPMUTE, "Unmuted")
);
page.addEventListener("historyBtn", CustomUIEventBindingType.Activating, (ignored, pCtx) ->
openStore(s -> new HistoryPage(playerRef, sCtx, targetUuid, targetName).open(s))
);
page.addEventListener("backBtn", CustomUIEventBindingType.Activating, (ignored, pCtx) ->
openStore(s -> new PlayerLookupPage(playerRef, sCtx).open(s))
);
}
private void wirePunish(@Nonnull String id, @Nonnull PunishmentType type) {
page.addEventListener(id, CustomUIEventBindingType.Activating, (ignored, pCtx) ->
openStore(s -> new PunishFormPage(playerRef, sCtx, targetUuid, targetName, type).open(s))
);
}
private void revoke(@Nonnull PunishmentType primary, @Nonnull PunishmentType timed, @Nonnull String pastTense) {
try {
int n = sCtx.punishments.revoke(targetUuid, primary, playerRef.getUuid(), playerRef.getUsername(), null);
n += sCtx.punishments.revoke(targetUuid, timed, playerRef.getUuid(), playerRef.getUsername(), null);
if (n > 0) {
sCtx.staffChat.announce("[" + pastTense.toUpperCase() + "] " + playerRef.getUsername() + " -> " + targetName);
}
} catch (SQLException e) {
((HytaleLogger.Api) LOGGER.at(Level.WARNING).withCause(e))
.log("Revoke failed for %s", targetUuid);
}
openStore(s -> new PlayerDetailPage(playerRef, sCtx, targetUuid, targetName).open(s));
}
public void open(@Nonnull Store<EntityStore> store) {
page.open(store);
}
private void openStore(@Nonnull Consumer<Store<EntityStore>> action) {
Ref<EntityStore> ref = playerRef.getReference();
if (ref == null) return;
action.accept(ref.getStore());
}
@Nonnull
private static String describeExpiry(@Nonnull Punishment p) {
if (p.expiresAt() == null) return "permanent";
long ms = p.expiresAt() - System.currentTimeMillis();
return ms <= 0 ? "expired" : DurationParser.formatRemaining(ms) + " left";
}
@Nonnull
private static String escape(@Nonnull String s) {
return s.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;").replace("\"", "&quot;");
}
}
@@ -0,0 +1,145 @@
package net.kewwbec.staff.ui;
import au.ellie.hyui.builders.PageBuilder;
import com.hypixel.hytale.component.Ref;
import com.hypixel.hytale.component.Store;
import com.hypixel.hytale.logger.HytaleLogger;
import com.hypixel.hytale.protocol.packets.interface_.CustomPageLifetime;
import com.hypixel.hytale.protocol.packets.interface_.CustomUIEventBindingType;
import com.hypixel.hytale.server.core.universe.PlayerRef;
import com.hypixel.hytale.server.core.universe.Universe;
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
import net.kewwbec.staff.util.PlayerResolver;
import javax.annotation.Nonnull;
import java.sql.SQLException;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.UUID;
import java.util.logging.Level;
public final class PlayerLookupPage {
private static final HytaleLogger LOGGER = HytaleLogger.forEnclosingClass();
private static final int MAX_ROWS = 25;
private final PlayerRef playerRef;
private final StaffUIContext sCtx;
private final PageBuilder page;
public PlayerLookupPage(@Nonnull PlayerRef playerRef, @Nonnull StaffUIContext sCtx) {
this(playerRef, sCtx, "");
}
public PlayerLookupPage(@Nonnull PlayerRef playerRef, @Nonnull StaffUIContext sCtx, @Nonnull String query) {
this.playerRef = playerRef;
this.sCtx = sCtx;
Map<UUID, String> hits = buildHits(query);
StringBuilder rows = new StringBuilder();
int i = 0;
for (Map.Entry<UUID, String> e : hits.entrySet()) {
rows.append("<button id=\"row")
.append(i)
.append("\">")
.append(escape(e.getValue()))
.append("</button>");
i++;
}
String resultsHtml = rows.length() == 0
? "<p>" + (query.isBlank() ? "Type a name and click Search." : "No matches for '" + escape(query) + "'.") + "</p>"
: rows.toString();
String html = """
<div class="page-overlay">
<div class="container" data-hyui-title="Player Lookup">
<div class="container-contents">
<input type="text" id="searchInput" placeholder="Player name" value="%s" />
<button id="searchBtn">Search</button>
<button class="back-button" id="backBtn">Back</button>
<p>%d result(s)</p>
<div>%s</div>
</div>
</div>
</div>
""".formatted(escape(query), hits.size(), resultsHtml);
this.page = PageBuilder.pageForPlayer(playerRef)
.withLifetime(CustomPageLifetime.CanDismiss)
.fromHtml(html);
page.addEventListener("searchBtn", CustomUIEventBindingType.Activating, (ignored, pCtx) -> {
String q = pCtx.getValue("searchInput", String.class).orElse("");
openStore(s -> new PlayerLookupPage(playerRef, sCtx, q).open(s));
});
page.addEventListener("backBtn", CustomUIEventBindingType.Activating, (ignored, pCtx) ->
openStore(s -> new StaffHubPage(playerRef, sCtx).open(s))
);
int idx = 0;
for (Map.Entry<UUID, String> e : hits.entrySet()) {
UUID uuid = e.getKey();
String name = stripSuffix(e.getValue());
page.addEventListener("row" + idx, CustomUIEventBindingType.Activating, (ignored, pCtx) ->
openStore(s -> new PlayerDetailPage(playerRef, sCtx, uuid, name).open(s))
);
idx++;
}
}
public void open(@Nonnull Store<EntityStore> store) {
page.open(store);
}
private void openStore(@Nonnull java.util.function.Consumer<Store<EntityStore>> action) {
Ref<EntityStore> ref = playerRef.getReference();
if (ref == null) return;
action.accept(ref.getStore());
}
@Nonnull
private Map<UUID, String> buildHits(@Nonnull String q) {
Map<UUID, String> out = new LinkedHashMap<>();
String norm = q.trim().toLowerCase();
Universe universe = Universe.get();
if (universe != null) {
for (PlayerRef ref : universe.getPlayers()) {
if (norm.isEmpty() || ref.getUsername().toLowerCase().contains(norm)) {
out.put(ref.getUuid(), ref.getUsername() + " (online)");
if (out.size() >= MAX_ROWS) return out;
}
}
}
if (out.size() < MAX_ROWS && !norm.isEmpty()) {
try {
PlayerResolver.Resolved hit = sCtx.resolver.byName(norm);
if (hit != null && !out.containsKey(hit.uuid())) {
String suffix = hit.isOnline() ? " (online)" : " (offline)";
out.put(hit.uuid(), hit.name() + suffix);
} else if (hit == null) {
sCtx.seenRepo.findByName(norm).ifPresent(s ->
out.put(s.uuid(), s.lastName() + " (offline)")
);
}
} catch (SQLException e) {
LOGGER.at(Level.WARNING).log("Lookup query failed: %s", e.getMessage());
}
}
return out;
}
@Nonnull
private static String stripSuffix(@Nonnull String labelled) {
int i = labelled.lastIndexOf(" (");
return i < 0 ? labelled : labelled.substring(0, i);
}
@Nonnull
private static String escape(@Nonnull String s) {
return s.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;").replace("\"", "&quot;");
}
}
@@ -0,0 +1,144 @@
package net.kewwbec.staff.ui;
import au.ellie.hyui.builders.PageBuilder;
import com.hypixel.hytale.component.Ref;
import com.hypixel.hytale.component.Store;
import com.hypixel.hytale.logger.HytaleLogger;
import com.hypixel.hytale.protocol.packets.interface_.CustomPageLifetime;
import com.hypixel.hytale.protocol.packets.interface_.CustomUIEventBindingType;
import com.hypixel.hytale.server.core.universe.PlayerRef;
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
import net.kewwbec.staff.model.PunishmentType;
import net.kewwbec.staff.util.DurationParser;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.sql.SQLException;
import java.util.UUID;
import java.util.function.Consumer;
import java.util.logging.Level;
public final class PunishFormPage {
private static final HytaleLogger LOGGER = HytaleLogger.forEnclosingClass();
private final PlayerRef playerRef;
private final StaffUIContext sCtx;
private final UUID targetUuid;
private final String targetName;
private final PunishmentType type;
private final PageBuilder page;
public PunishFormPage(@Nonnull PlayerRef playerRef,
@Nonnull StaffUIContext sCtx,
@Nonnull UUID targetUuid,
@Nonnull String targetName,
@Nonnull PunishmentType type) {
this(playerRef, sCtx, targetUuid, targetName, type, "", "", "");
}
public PunishFormPage(@Nonnull PlayerRef playerRef,
@Nonnull StaffUIContext sCtx,
@Nonnull UUID targetUuid,
@Nonnull String targetName,
@Nonnull PunishmentType type,
@Nonnull String duration,
@Nonnull String reason,
@Nonnull String error) {
this.playerRef = playerRef;
this.sCtx = sCtx;
this.targetUuid = targetUuid;
this.targetName = targetName;
this.type = type;
String durationField = type.isTimed()
? """
<label>Duration (e.g. 30m, 2h, 7d):</label>
<input type="text" id="durationInput" placeholder="Duration" value="%s" />
""".formatted(escape(duration))
: "";
String errorLine = error.isBlank() ? "" : "<p>" + escape(error) + "</p>";
String html = """
<div class="page-overlay">
<div class="container" data-hyui-title="Issue %s">
<div class="container-contents">
<p>Target: %s</p>
%s
<label>Reason:</label>
<input type="text" id="reasonInput" placeholder="Reason" value="%s" />
%s
<button id="submitBtn">Confirm</button>
<button class="back-button" id="cancelBtn">Cancel</button>
</div>
</div>
</div>
""".formatted(type.name(), escape(targetName), durationField, escape(reason), errorLine);
this.page = PageBuilder.pageForPlayer(playerRef)
.withLifetime(CustomPageLifetime.CanDismiss)
.fromHtml(html);
page.addEventListener("submitBtn", CustomUIEventBindingType.Activating, (ignored, pCtx) -> {
String r = pCtx.getValue("reasonInput", String.class).orElse("").trim();
String d = type.isTimed() ? pCtx.getValue("durationInput", String.class).orElse("").trim() : "";
if (r.isEmpty()) {
rerender(d, r, "Reason is required.");
return;
}
Long durationMs = null;
if (type.isTimed()) {
if (d.isEmpty()) {
rerender(d, r, "Duration is required for " + type.name() + ".");
return;
}
durationMs = DurationParser.parseMillis(d);
if (durationMs == null) {
rerender(d, r, "Bad duration. Use 30s, 5m, 2h, 7d, 1w.");
return;
}
}
submit(r, durationMs);
});
page.addEventListener("cancelBtn", CustomUIEventBindingType.Activating, (ignored, pCtx) ->
openStore(s -> new PlayerDetailPage(playerRef, sCtx, targetUuid, targetName).open(s))
);
}
private void submit(@Nonnull String reason, @Nullable Long durationMs) {
try {
sCtx.punishments.issue(targetUuid, targetName, type, reason,
playerRef.getUuid(), playerRef.getUsername(), durationMs);
String durStr = (durationMs == null) ? "permanent" : DurationParser.formatRemaining(durationMs);
sCtx.staffChat.announce("[" + type.name() + "] " + playerRef.getUsername()
+ " -> " + targetName + " (" + durStr + ") " + reason);
openStore(s -> new PlayerDetailPage(playerRef, sCtx, targetUuid, targetName).open(s));
} catch (SQLException e) {
((HytaleLogger.Api) LOGGER.at(Level.WARNING).withCause(e))
.log("Failed to issue punishment from UI");
rerender("", reason, "DB error: " + e.getMessage());
}
}
private void rerender(@Nonnull String d, @Nonnull String r, @Nonnull String error) {
openStore(s -> new PunishFormPage(playerRef, sCtx, targetUuid, targetName, type, d, r, error).open(s));
}
public void open(@Nonnull Store<EntityStore> store) {
page.open(store);
}
private void openStore(@Nonnull Consumer<Store<EntityStore>> action) {
Ref<EntityStore> ref = playerRef.getReference();
if (ref == null) return;
action.accept(ref.getStore());
}
@Nonnull
private static String escape(@Nonnull String s) {
return s.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;").replace("\"", "&quot;");
}
}
@@ -0,0 +1,51 @@
package net.kewwbec.staff.ui;
import au.ellie.hyui.builders.ButtonBuilder;
import au.ellie.hyui.builders.PageBuilder;
import com.hypixel.hytale.component.Store;
import com.hypixel.hytale.protocol.packets.interface_.CustomPageLifetime;
import com.hypixel.hytale.protocol.packets.interface_.CustomUIEventBindingType;
import com.hypixel.hytale.server.core.universe.PlayerRef;
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
import javax.annotation.Nonnull;
public final class StaffHubPage {
private final PlayerRef playerRef;
private final StaffUIContext ctx;
private final PageBuilder page;
public StaffHubPage(@Nonnull PlayerRef playerRef, @Nonnull StaffUIContext ctx) {
this.playerRef = playerRef;
this.ctx = ctx;
this.page = PageBuilder.detachedPage()
.withLifetime(CustomPageLifetime.CanDismiss)
.fromHtml("""
<div class="page-overlay">
<div class="container" data-hyui-title="Staff Hub">
<div class="container-contents">
<p>Pick an action. Press ESC to close.</p>
<button id="lookupBtn">Player Lookup</button>
</div>
</div>
</div>
""");
wire();
}
private void wire() {
page.getById("lookupBtn", ButtonBuilder.class).ifPresent(b ->
b.addEventListener(CustomUIEventBindingType.Activating, e -> openLookup()));
}
public void open(@Nonnull Store<EntityStore> store) {
page.open(playerRef, store);
}
private void openLookup() {
var ref = playerRef.getReference();
if (ref == null) return;
new PlayerLookupPage(playerRef, ctx).open(ref.getStore());
}
}
@@ -0,0 +1,34 @@
package net.kewwbec.staff.ui;
import net.kewwbec.staff.db.PlayerSeenRepository;
import net.kewwbec.staff.db.PunishmentRepository;
import net.kewwbec.staff.service.PunishmentService;
import net.kewwbec.staff.service.StaffChatService;
import net.kewwbec.staff.util.PlayerResolver;
import net.kewwbec.staff.util.StaffPermissions;
import javax.annotation.Nonnull;
public final class StaffUIContext {
public final StaffPermissions permissions;
public final PunishmentService punishments;
public final StaffChatService staffChat;
public final PlayerResolver resolver;
public final PunishmentRepository punishmentRepo;
public final PlayerSeenRepository seenRepo;
public StaffUIContext(@Nonnull StaffPermissions permissions,
@Nonnull PunishmentService punishments,
@Nonnull StaffChatService staffChat,
@Nonnull PlayerResolver resolver,
@Nonnull PunishmentRepository punishmentRepo,
@Nonnull PlayerSeenRepository seenRepo) {
this.permissions = permissions;
this.punishments = punishments;
this.staffChat = staffChat;
this.resolver = resolver;
this.punishmentRepo = punishmentRepo;
this.seenRepo = seenRepo;
}
}
@@ -2,45 +2,21 @@ package net.kewwbec.staff.util;
import com.hypixel.hytale.server.core.permissions.PermissionsModule;
import com.hypixel.hytale.server.core.universe.PlayerRef;
import net.kewwbec.staff.config.StaffConfig;
import javax.annotation.Nonnull;
import java.util.List;
import java.util.Set;
import java.util.UUID;
public final class StaffPermissions {
private final StaffConfig.PermissionsSection cfg;
public StaffPermissions(@Nonnull StaffConfig.PermissionsSection cfg) {
this.cfg = cfg;
public StaffPermissions() {
}
public boolean isStaff(@Nonnull PlayerRef player) {
return isStaff(player.getUuid());
public boolean has(@Nonnull PlayerRef ref, @Nonnull String node) {
return has(ref.getUuid(), node);
}
public boolean isStaff(@Nonnull UUID uuid) {
return matchesAny(uuid, cfg.staff_groups);
}
public boolean isAdmin(@Nonnull PlayerRef player) {
return isAdmin(player.getUuid());
}
public boolean isAdmin(@Nonnull UUID uuid) {
return matchesAny(uuid, cfg.admin_groups);
}
private boolean matchesAny(@Nonnull UUID uuid, @Nonnull List<String> groups) {
public boolean has(@Nonnull UUID uuid, @Nonnull String node) {
PermissionsModule module = PermissionsModule.get();
if (module == null) return false;
Set<String> playerGroups = module.getGroupsForUser(uuid);
if (playerGroups == null || playerGroups.isEmpty()) return false;
for (String allowed : groups) {
if (playerGroups.contains(allowed)) return true;
}
return false;
return module != null && module.hasPermission(uuid, node);
}
}
+1 -1
View File
@@ -3,7 +3,7 @@
"Name": "Staff",
"Version": "0.1.0",
"Description": "Staff tools: punishments, history, staff chat, player lookup",
"IncludesAssetsPack": false,
"IncludesAssetPack": false,
"Authors": [
{"Name": "kewwbec"}
],