Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
import com.gauchoracing.warden.WardenPlugin;
import com.gauchoracing.warden.staff.VanishManager;
import java.lang.management.ManagementFactory;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import org.bukkit.Server;
import org.bukkit.entity.Player;
Expand All @@ -28,8 +30,8 @@ public final class ServerStatusReporter implements Listener {
private final long startedAt = ManagementFactory.getRuntimeMXBean().getStartTime();
private final AtomicBoolean pending = new AtomicBoolean();

record ServerStatus(int online, int maxPlayers, int uniquePlayers, double tps, double mspt,
long startedAt) {}
record ServerStatus(int online, List<String> players, int maxPlayers, int uniquePlayers,
double tps, double mspt, long startedAt) {}

private record Stopping(boolean stopping) {}

Expand All @@ -41,13 +43,13 @@ public ServerStatusReporter(WardenPlugin plugin, VanishManager vanish) {
/** Call from the main thread. Sampling is main-thread; the POST is not. */
public void report() {
Server server = plugin.getServer();
int online = 0;
List<String> players = new ArrayList<>();
for (Player player : server.getOnlinePlayers()) {
if (!vanish.isVanished(player.getUniqueId())) {
online++;
players.add(player.getUniqueId().toString());
}
}
ServerStatus status = new ServerStatus(online, server.getMaxPlayers(),
ServerStatus status = new ServerStatus(players.size(), players, server.getMaxPlayers(),
server.getOfflinePlayers().length, server.getTPS()[0], server.getAverageTickTime(),
startedAt);
plugin.runAsync(() -> {
Expand Down
21 changes: 16 additions & 5 deletions warden/api/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -150,11 +150,12 @@ func normalizePlayer(rawUUID string, rawUsername string) (string, string, error)
}

type serverStatusReport struct {
Online int `json:"online"`
MaxPlayers int `json:"max_players"`
UniquePlayers int `json:"unique_players"`
TPS float64 `json:"tps"`
MSPT float64 `json:"mspt"`
Online int `json:"online"`
Players []string `json:"players"`
MaxPlayers int `json:"max_players"`
UniquePlayers int `json:"unique_players"`
TPS float64 `json:"tps"`
MSPT float64 `json:"mspt"`
// Epoch milliseconds, as the JVM reports its own start time.
StartedAt int64 `json:"started_at"`
// Sent once from onDisable in place of a sample.
Expand All @@ -175,9 +176,19 @@ func ReportServerStatus(c *gin.Context) {
c.Status(http.StatusNoContent)
return
}
players := make([]string, 0, len(report.Players))
for _, raw := range report.Players {
uuid, err := service.NormalizeUUID(raw)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
players = append(players, uuid)
}
status := model.ServerStatus{
RecordedAt: time.Now(),
Online: report.Online,
OnlinePlayers: players,
MaxPlayers: report.MaxPlayers,
UniquePlayers: report.UniquePlayers,
TPS: report.TPS,
Expand Down
4 changes: 3 additions & 1 deletion warden/api/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
type serverStatusResponse struct {
State string `json:"state"`
Online int `json:"online"`
OnlinePlayers []string `json:"online_players"`
MaxPlayers int `json:"max_players"`
UniquePlayers int `json:"unique_players"`
UptimeMinutes int `json:"uptime_minutes"`
Expand All @@ -24,14 +25,15 @@ type serverStatusResponse struct {
func GetServerStatus(c *gin.Context) {
Require(c, RequestTokenExists(c))
state, status := service.CurrentServerState()
response := serverStatusResponse{State: state}
response := serverStatusResponse{State: state, OnlinePlayers: []string{}}
if status != nil {
response.Online = status.Online
response.MaxPlayers = status.MaxPlayers
response.UniquePlayers = status.UniquePlayers
response.TPS = status.TPS
response.RecordedAt = &status.RecordedAt
if state != service.ServerOffline {
response.OnlinePlayers = status.OnlinePlayers
response.UptimeMinutes = int(time.Since(status.StartedAt).Minutes())
}
}
Expand Down
3 changes: 3 additions & 0 deletions warden/model/server_status.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ type ServerStatus struct {
MSPT float64 `json:"mspt"`
// When the game server process started; uptime is RecordedAt minus this.
StartedAt time.Time `json:"started_at"`
// Visible players' UUIDs. Only the latest sample's matters, so it is
// held in memory rather than stored with the history.
OnlinePlayers []string `json:"online_players" gorm:"-"`
}

func (ServerStatus) TableName() string {
Expand Down
1 change: 1 addition & 0 deletions web/src/lib/warden.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ export type ServerState = "active" | "empty" | "offline"
export type ServerStatus = {
state: ServerState
online: number
online_players: string[]
max_players: number
unique_players: number
uptime_minutes: number
Expand Down
14 changes: 12 additions & 2 deletions web/src/pages/PlayersPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@ import { Link } from "react-router-dom"

import { PageContainer, PageHeader } from "@/components/PageContainer"
import { SkinFrame } from "@/components/SkinFrame"
import { Badge } from "@/components/ui/badge"
import { Card, CardContent } from "@/components/ui/card"
import { Input } from "@/components/ui/input"
import { Skeleton } from "@/components/ui/skeleton"
import { errorMessage, type MinecraftAccount, useAccounts } from "@/lib/warden"
import { errorMessage, type MinecraftAccount, useAccounts, useServerStatus } from "@/lib/warden"

function formatPlaytime(minutes: number) {
const hours = Math.floor(minutes / 60)
Expand Down Expand Up @@ -41,6 +42,8 @@ function RosterStat({ label, value }: { label: string; value: string }) {

export default function PlayersPage() {
const accounts = useAccounts()
const serverStatus = useServerStatus()
const onlinePlayers = new Set(serverStatus.data?.online_players ?? [])
const [filter, setFilter] = useState("")

const term = filter.trim().toLowerCase()
Expand Down Expand Up @@ -116,7 +119,14 @@ export default function PlayersPage() {
label="Sessions"
value={account.stats ? account.stats.sessions.toLocaleString() : "—"}
/>
<div className="text-xs text-muted-foreground">{formatLastSeen(seenAt(account))}</div>
{onlinePlayers.has(account.uuid) ? (
<Badge>
<span className="size-1.5 rounded-full bg-current" />
Online
</Badge>
) : (
<div className="text-xs text-muted-foreground">{formatLastSeen(seenAt(account))}</div>
)}
</CardContent>
</Card>
</Link>
Expand Down
Loading