panel v0.9.1 — open-source game server manager
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,526 @@
|
||||
package dispatch
|
||||
|
||||
// Per-player POI discovery query against the empyrion server's
|
||||
// global.db SQLite store. Spawns an alpine+sqlite sidecar with the
|
||||
// instance's saves volume mounted read-only, runs a parameterized
|
||||
// join query, returns the rows.
|
||||
//
|
||||
// Schema reference (DiscoveredPOIs / Entities / Playfields / SolarSystems
|
||||
// / LoginLogoff) was reverse-engineered from a live empyrion install on
|
||||
// 2026-04-30. Eleon's mod API does NOT expose per-player discovery —
|
||||
// Request_GlobalStructure_List returns the server-wide aggregate that
|
||||
// EAH shows. The DB has the finer per-player data the in-game map uses.
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
|
||||
panelv1 "github.com/dbledeez/panel/proto/panel/v1"
|
||||
)
|
||||
|
||||
// sqlite3's CLI doesn't support `?` parameter binding (that's a
|
||||
// prepared-statement-only feature on the C API). We string-interpolate
|
||||
// the SteamID64 — already validated as 17 digits server-side, but we
|
||||
// also belt-and-suspenders re-validate at the shell layer (digits only).
|
||||
//
|
||||
// Two query modes:
|
||||
// "personal" — POIs the player personally first-walked-into
|
||||
// "faction" — POIs anyone in the player's faction has discovered
|
||||
// (matches the in-game shared map)
|
||||
const sqlPersonalTpl = `
|
||||
SELECT
|
||||
COALESCE(ss.name, '') AS solar_system,
|
||||
COALESCE(pf.name, '') AS playfield,
|
||||
COALESCE(poi.name, '') AS poi_name,
|
||||
COALESCE(poi.etype, 0) AS poi_type,
|
||||
COALESCE(dp.gametime, 0) AS game_time,
|
||||
COALESCE(ll.playername, '') AS discoverer_name,
|
||||
COALESCE(ll.playerid, '') AS discoverer_steam_id,
|
||||
COALESCE(dp.facgroup, 0) AS facgroup,
|
||||
COALESCE(dp.facid, 0) AS facid
|
||||
FROM DiscoveredPOIs dp
|
||||
JOIN Entities poi ON dp.poiid = poi.entityid
|
||||
LEFT JOIN Playfields pf ON dp.pfid = pf.pfid
|
||||
LEFT JOIN SolarSystems ss ON pf.ssid = ss.ssid
|
||||
LEFT JOIN LoginLogoff ll ON dp.entityid = ll.entityid
|
||||
WHERE dp.entityid IN (
|
||||
SELECT DISTINCT entityid FROM LoginLogoff WHERE playerid='%SID%'
|
||||
)
|
||||
ORDER BY dp.gametime DESC
|
||||
LIMIT 5000;
|
||||
`
|
||||
|
||||
const sqlFactionTpl = `
|
||||
SELECT
|
||||
COALESCE(ss.name, '') AS solar_system,
|
||||
COALESCE(pf.name, '') AS playfield,
|
||||
COALESCE(poi.name, '') AS poi_name,
|
||||
COALESCE(poi.etype, 0) AS poi_type,
|
||||
COALESCE(dp.gametime, 0) AS game_time,
|
||||
COALESCE(ll.playername, '') AS discoverer_name,
|
||||
COALESCE(ll.playerid, '') AS discoverer_steam_id,
|
||||
COALESCE(dp.facgroup, 0) AS facgroup,
|
||||
COALESCE(dp.facid, 0) AS facid
|
||||
FROM DiscoveredPOIs dp
|
||||
JOIN Entities poi ON dp.poiid = poi.entityid
|
||||
LEFT JOIN Playfields pf ON dp.pfid = pf.pfid
|
||||
LEFT JOIN SolarSystems ss ON pf.ssid = ss.ssid
|
||||
LEFT JOIN LoginLogoff ll ON dp.entityid = ll.entityid
|
||||
WHERE (dp.facgroup, dp.facid) IN (
|
||||
SELECT DISTINCT pe.facgroup, pe.facid
|
||||
FROM Entities pe
|
||||
WHERE pe.entityid IN (
|
||||
SELECT DISTINCT entityid FROM LoginLogoff WHERE playerid='%SID%'
|
||||
)
|
||||
)
|
||||
ORDER BY dp.gametime DESC
|
||||
LIMIT 5000;
|
||||
`
|
||||
|
||||
const sqlPlayerNameTpl = `
|
||||
SELECT playername FROM LoginLogoff
|
||||
WHERE playerid='%SID%'
|
||||
ORDER BY loginticks DESC
|
||||
LIMIT 1;
|
||||
`
|
||||
|
||||
func (d *Dispatcher) handleEmpyrionDiscoveries(corrID string, req *panelv1.EmpyrionDiscoveriesRequest) {
|
||||
if req == nil || req.InstanceId == "" || req.SteamId == "" {
|
||||
d.sendEmpyrionDiscoveriesResult(corrID, &panelv1.EmpyrionDiscoveriesResult{
|
||||
Ok: false, Error: "instance_id and steam_id required",
|
||||
})
|
||||
return
|
||||
}
|
||||
go d.runEmpyrionDiscoveriesJob(corrID, req)
|
||||
}
|
||||
|
||||
func (d *Dispatcher) runEmpyrionDiscoveriesJob(corrID string, req *panelv1.EmpyrionDiscoveriesRequest) {
|
||||
mode := strings.ToLower(strings.TrimSpace(req.Mode))
|
||||
if mode == "" || mode == "personal" {
|
||||
mode = "personal"
|
||||
} else if mode != "faction" {
|
||||
d.sendEmpyrionDiscoveriesResult(corrID, &panelv1.EmpyrionDiscoveriesResult{
|
||||
Ok: false, Error: "mode must be 'personal' or 'faction'", Mode: req.Mode, SteamId: req.SteamId,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
dockerBin, err := exec.LookPath("docker")
|
||||
if err != nil {
|
||||
d.sendEmpyrionDiscoveriesResult(corrID, &panelv1.EmpyrionDiscoveriesResult{
|
||||
Ok: false, Error: "docker binary not found on agent host", Mode: mode, SteamId: req.SteamId,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Saves volume name follows the panel convention: panel-<id>-saves.
|
||||
savesVol := "panel-" + req.InstanceId + "-saves"
|
||||
// Pre-flight: volume must exist (instance exists on this agent).
|
||||
if err := exec.Command(dockerBin, "volume", "inspect", savesVol).Run(); err != nil {
|
||||
d.sendEmpyrionDiscoveriesResult(corrID, &panelv1.EmpyrionDiscoveriesResult{
|
||||
Ok: false, Error: fmt.Sprintf("instance saves volume %q not found on this agent", savesVol),
|
||||
Mode: mode, SteamId: req.SteamId,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Validate steam_id is digits-only — defense in depth, controller
|
||||
// already checks 17-digit format. Belt-and-suspenders against any
|
||||
// future caller that bypasses the controller's validation.
|
||||
for _, c := range req.SteamId {
|
||||
if c < '0' || c > '9' {
|
||||
d.sendEmpyrionDiscoveriesResult(corrID, &panelv1.EmpyrionDiscoveriesResult{
|
||||
Ok: false, Error: "steam_id must be digits only", Mode: mode, SteamId: req.SteamId,
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
tpl := sqlPersonalTpl
|
||||
if mode == "faction" {
|
||||
tpl = sqlFactionTpl
|
||||
}
|
||||
q := strings.ReplaceAll(tpl, "%SID%", req.SteamId)
|
||||
nameQ := strings.ReplaceAll(sqlPlayerNameTpl, "%SID%", req.SteamId)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// alpine+sqlite sidecar. We don't need to discover the savegame
|
||||
// folder name dynamically — the entrypoint creates exactly one
|
||||
// (via dedicated.yaml.SaveDirectory which defaults to "DediGame";
|
||||
// the panel's empyrion module pins this). We pick `*/global.db`
|
||||
// at the shell layer so renames don't break the query.
|
||||
//
|
||||
// Note: -2 disables stdin which avoids the `command timed out`
|
||||
// path that empty stdin can hit on some Alpine builds. We feed the
|
||||
// SQL via the third positional arg.
|
||||
rowsScript := `set -e
|
||||
apk add --no-cache sqlite >/dev/null 2>&1
|
||||
DB=$(ls /saves/Saves/Games/*/global.db 2>/dev/null | head -1)
|
||||
if [ -z "$DB" ]; then echo '{"err":"no global.db found in saves volume"}' >&2; exit 2; fi
|
||||
NAME=$(sqlite3 "$DB" "$1" 2>/dev/null || echo "")
|
||||
ROWS=$(sqlite3 -json "$DB" "$2" 2>&1)
|
||||
if [ -z "$ROWS" ]; then ROWS='[]'; fi
|
||||
NAME_JSON=$(printf '%s' "$NAME" | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g')
|
||||
printf '{"player_name":"%s","rows":%s}\n' "$NAME_JSON" "$ROWS"
|
||||
`
|
||||
|
||||
out, err := exec.CommandContext(ctx, dockerBin,
|
||||
"run", "--rm",
|
||||
"-v", savesVol+":/saves:ro",
|
||||
"--entrypoint", "sh",
|
||||
"alpine:3.20", "-c", rowsScript, "_", nameQ, q,
|
||||
).Output()
|
||||
if err != nil {
|
||||
d.sendEmpyrionDiscoveriesResult(corrID, &panelv1.EmpyrionDiscoveriesResult{
|
||||
Ok: false, Error: "sqlite query failed: " + err.Error(),
|
||||
Mode: mode, SteamId: req.SteamId,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Parse the wrapper json {"player_name": "...", "rows": [...]}.
|
||||
type wireRow struct {
|
||||
SolarSystem string `json:"solar_system"`
|
||||
Playfield string `json:"playfield"`
|
||||
PoiName string `json:"poi_name"`
|
||||
PoiType int32 `json:"poi_type"`
|
||||
GameTime int64 `json:"game_time"`
|
||||
DiscovererName string `json:"discoverer_name"`
|
||||
DiscovererSteamID string `json:"discoverer_steam_id"`
|
||||
Facgroup int32 `json:"facgroup"`
|
||||
Facid int32 `json:"facid"`
|
||||
}
|
||||
var wrap struct {
|
||||
PlayerName string `json:"player_name"`
|
||||
Rows []wireRow `json:"rows"`
|
||||
}
|
||||
// Strip leading whitespace; sqlite3 -json sometimes prepends a BOM-ish blank line.
|
||||
if err := json.Unmarshal([]byte(strings.TrimSpace(string(out))), &wrap); err != nil {
|
||||
// Tolerate "no rows" — sqlite3 -json prints `[]` (good) or empty
|
||||
// string when no DB (bad — caught above) or `null` for some
|
||||
// builds. Fall back to empty-result on parse failure.
|
||||
var rowsOnly []wireRow
|
||||
if err2 := json.Unmarshal([]byte(strings.TrimSpace(string(out))), &rowsOnly); err2 == nil {
|
||||
wrap.Rows = rowsOnly
|
||||
} else {
|
||||
d.sendEmpyrionDiscoveriesResult(corrID, &panelv1.EmpyrionDiscoveriesResult{
|
||||
Ok: false, Error: "parse sqlite output: " + err.Error(),
|
||||
Mode: mode, SteamId: req.SteamId,
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
pois := make([]*panelv1.DiscoveredPOI, 0, len(wrap.Rows))
|
||||
for _, r := range wrap.Rows {
|
||||
pois = append(pois, &panelv1.DiscoveredPOI{
|
||||
SolarSystem: r.SolarSystem,
|
||||
Playfield: r.Playfield,
|
||||
PoiName: r.PoiName,
|
||||
PoiType: r.PoiType,
|
||||
GameTime: r.GameTime,
|
||||
DiscovererName: r.DiscovererName,
|
||||
DiscovererSteamId: r.DiscovererSteamID,
|
||||
FactionGroup: r.Facgroup,
|
||||
FactionId: r.Facid,
|
||||
})
|
||||
}
|
||||
d.sendEmpyrionDiscoveriesResult(corrID, &panelv1.EmpyrionDiscoveriesResult{
|
||||
Ok: true,
|
||||
Pois: pois,
|
||||
Mode: mode,
|
||||
SteamId: req.SteamId,
|
||||
PlayerName: strings.Trim(wrap.PlayerName, "\""),
|
||||
})
|
||||
_ = errors.New
|
||||
}
|
||||
|
||||
func (d *Dispatcher) sendEmpyrionDiscoveriesResult(corrID string, res *panelv1.EmpyrionDiscoveriesResult) {
|
||||
d.sendEnv(&panelv1.AgentEnvelope{
|
||||
CorrelationId: corrID,
|
||||
SentAt: timestamppb.Now(),
|
||||
Payload: &panelv1.AgentEnvelope_EmpyrionDiscoveriesResult{
|
||||
EmpyrionDiscoveriesResult: res,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Player Summary — stats + sessions + inventory in one query
|
||||
// ============================================================================
|
||||
|
||||
// playerSummaryScript is one big sh script run inside an alpine sidecar.
|
||||
// It opens global.db, resolves the player's entityid via LoginLogoff,
|
||||
// then runs four sqlite queries with -json output. The output is one
|
||||
// JSON object with named keys for each query — we marshal-decode it
|
||||
// directly into the proto result.
|
||||
//
|
||||
// The 4 queries:
|
||||
// 1. summary — Entities row + LoginLogoff name lookup + last position
|
||||
// 2. stats — PlayerStatistics row
|
||||
// 3. sessions — recent LoginLogoff rows
|
||||
// 4. inv — most recent PlayerInventory snapshot + items
|
||||
//
|
||||
// We accept that some of these may return [] (player has no stats row
|
||||
// yet, never logged in this savegame, etc.) — JSON parser handles it.
|
||||
func playerSummaryScript() string {
|
||||
return `set -e
|
||||
apk add --no-cache sqlite >/dev/null 2>&1
|
||||
DB=$(ls /saves/Saves/Games/*/global.db 2>/dev/null | head -1)
|
||||
if [ -z "$DB" ]; then echo '{"err":"no global.db found in saves volume"}' >&2; exit 2; fi
|
||||
SID="$1"
|
||||
LIMIT="${2:-50}"
|
||||
|
||||
# Resolve entityid for this Steam ID. Use the most recent LoginLogoff
|
||||
# row to handle character resets that re-bind the same Steam ID to a
|
||||
# new entityid.
|
||||
EID=$(sqlite3 "$DB" "SELECT entityid FROM LoginLogoff WHERE playerid='${SID}' ORDER BY loginticks DESC LIMIT 1;")
|
||||
if [ -z "$EID" ]; then
|
||||
printf '{"steam_id":"%s","entity_id":0,"player_name":"","stats":null,"sessions":[],"inventory":[]}\n' "$SID"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
NAME=$(sqlite3 "$DB" "SELECT playername FROM LoginLogoff WHERE entityid=${EID} ORDER BY loginticks DESC LIMIT 1;")
|
||||
FG=$(sqlite3 "$DB" "SELECT facgroup FROM Entities WHERE entityid=${EID};")
|
||||
FI=$(sqlite3 "$DB" "SELECT facid FROM Entities WHERE entityid=${EID};")
|
||||
|
||||
# Last position from PlayerData.
|
||||
PD=$(sqlite3 -json "$DB" "
|
||||
SELECT
|
||||
COALESCE(pf.name, '') AS last_playfield,
|
||||
COALESCE(pd.posx, 0) AS last_x,
|
||||
COALESCE(pd.posy, 0) AS last_y,
|
||||
COALESCE(pd.posz, 0) AS last_z,
|
||||
COALESCE(pd.credits, 0) AS credits
|
||||
FROM PlayerData pd
|
||||
LEFT JOIN Playfields pf ON pd.pfid = pf.pfid
|
||||
WHERE pd.entityid=${EID};
|
||||
")
|
||||
|
||||
STATS=$(sqlite3 -json "$DB" "
|
||||
SELECT
|
||||
COALESCE(killedenemies, 0) AS killed_enemies,
|
||||
COALESCE(killedallied, 0) AS killed_allied,
|
||||
COALESCE(killedanimals, 0) AS killed_animals,
|
||||
COALESCE(killeddrones, 0) AS killed_drones,
|
||||
COALESCE(killedplayers, 0) AS killed_players,
|
||||
COALESCE(killedalliedplayers, 0) AS killed_allied_players,
|
||||
COALESCE(died, 0) AS died,
|
||||
COALESCE(score, 0) AS score,
|
||||
COALESCE(blocksplaced, 0) AS blocks_placed,
|
||||
COALESCE(blocksdigged, 0) AS blocks_digged,
|
||||
COALESCE(walkedmeters, 0) AS walked_meters,
|
||||
COALESCE(jetpackmeters, 0) AS jetpack_meters,
|
||||
COALESCE(hvmeters, 0) AS hv_meters,
|
||||
COALESCE(svmeters, 0) AS sv_meters,
|
||||
COALESCE(cvmeters, 0) AS cv_meters,
|
||||
COALESCE(playtime, 0.0) AS playtime,
|
||||
COALESCE(travly, 0.0) AS light_years,
|
||||
COALESCE(travau, 0.0) AS astronomical_units
|
||||
FROM PlayerStatistics WHERE entityid=${EID};
|
||||
")
|
||||
|
||||
SESS=$(sqlite3 -json "$DB" "
|
||||
SELECT
|
||||
COALESCE(loginticks, 0) AS login_ticks,
|
||||
COALESCE(logoffticks, 0) AS logoff_ticks,
|
||||
COALESCE(playername, '') AS player_name,
|
||||
COALESCE(buildnr, 0) AS build_nr,
|
||||
COALESCE(ip, '') AS ip,
|
||||
COALESCE(os, '') AS os,
|
||||
COALESCE(gfx, '') AS gfx,
|
||||
COALESCE(cpu, '') AS cpu
|
||||
FROM LoginLogoff WHERE entityid=${EID}
|
||||
ORDER BY loginticks DESC LIMIT ${LIMIT};
|
||||
")
|
||||
|
||||
INV_PIID=$(sqlite3 "$DB" "SELECT piid FROM PlayerInventory WHERE entityid=${EID} ORDER BY gametime DESC LIMIT 1;")
|
||||
INV_GT=0
|
||||
INV='[]'
|
||||
if [ -n "$INV_PIID" ]; then
|
||||
INV_GT=$(sqlite3 "$DB" "SELECT gametime FROM PlayerInventory WHERE piid=${INV_PIID};")
|
||||
INV=$(sqlite3 -json "$DB" "
|
||||
SELECT item AS item_id, count
|
||||
FROM PlayerInventoryItems WHERE piid=${INV_PIID};
|
||||
")
|
||||
if [ -z "$INV" ]; then INV='[]'; fi
|
||||
fi
|
||||
|
||||
if [ -z "$STATS" ]; then STATS='[]'; fi
|
||||
if [ -z "$SESS" ]; then SESS='[]'; fi
|
||||
if [ -z "$PD" ]; then PD='[]'; fi
|
||||
|
||||
printf '{"steam_id":"%s","player_name":%s,"entity_id":%s,"faction_group":%s,"faction_id":%s,"position":%s,"stats":%s,"sessions":%s,"inventory_gametime":%s,"inventory":%s}\n' \
|
||||
"$SID" \
|
||||
"$(printf '%%s' "$NAME" | sqlite3 ':memory:' 'select json_quote(?1);' 2>/dev/null || echo '""')" \
|
||||
"${EID:-0}" "${FG:-0}" "${FI:-0}" \
|
||||
"$PD" "$STATS" "$SESS" "${INV_GT:-0}" "$INV"
|
||||
`
|
||||
}
|
||||
|
||||
func (d *Dispatcher) handleEmpyrionPlayerSummary(corrID string, req *panelv1.EmpyrionPlayerSummaryRequest) {
|
||||
if req == nil || req.InstanceId == "" || req.SteamId == "" {
|
||||
d.sendEmpyrionPlayerSummaryResult(corrID, &panelv1.EmpyrionPlayerSummaryResult{
|
||||
Ok: false, Error: "instance_id and steam_id required",
|
||||
})
|
||||
return
|
||||
}
|
||||
go d.runEmpyrionPlayerSummaryJob(corrID, req)
|
||||
}
|
||||
|
||||
func (d *Dispatcher) runEmpyrionPlayerSummaryJob(corrID string, req *panelv1.EmpyrionPlayerSummaryRequest) {
|
||||
dockerBin, err := exec.LookPath("docker")
|
||||
if err != nil {
|
||||
d.sendEmpyrionPlayerSummaryResult(corrID, &panelv1.EmpyrionPlayerSummaryResult{
|
||||
Ok: false, Error: "docker binary not found on agent host", SteamId: req.SteamId,
|
||||
})
|
||||
return
|
||||
}
|
||||
savesVol := "panel-" + req.InstanceId + "-saves"
|
||||
if err := exec.Command(dockerBin, "volume", "inspect", savesVol).Run(); err != nil {
|
||||
d.sendEmpyrionPlayerSummaryResult(corrID, &panelv1.EmpyrionPlayerSummaryResult{
|
||||
Ok: false, Error: fmt.Sprintf("instance saves volume %q not found on this agent", savesVol), SteamId: req.SteamId,
|
||||
})
|
||||
return
|
||||
}
|
||||
limit := req.MaxLoginRows
|
||||
if limit <= 0 || limit > 500 {
|
||||
limit = 50
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
||||
defer cancel()
|
||||
out, err := exec.CommandContext(ctx, dockerBin,
|
||||
"run", "--rm",
|
||||
"-v", savesVol+":/saves:ro",
|
||||
"--entrypoint", "sh",
|
||||
"alpine:3.20", "-c", playerSummaryScript(), "_",
|
||||
req.SteamId, fmt.Sprintf("%d", limit),
|
||||
).Output()
|
||||
if err != nil {
|
||||
d.sendEmpyrionPlayerSummaryResult(corrID, &panelv1.EmpyrionPlayerSummaryResult{
|
||||
Ok: false, Error: "sqlite query failed: " + err.Error(), SteamId: req.SteamId,
|
||||
})
|
||||
return
|
||||
}
|
||||
type wireStats struct {
|
||||
KilledEnemies int64 `json:"killed_enemies"`
|
||||
KilledAllied int64 `json:"killed_allied"`
|
||||
KilledAnimals int64 `json:"killed_animals"`
|
||||
KilledDrones int64 `json:"killed_drones"`
|
||||
KilledPlayers int64 `json:"killed_players"`
|
||||
KilledAlliedPlayers int64 `json:"killed_allied_players"`
|
||||
Died int64 `json:"died"`
|
||||
Score int64 `json:"score"`
|
||||
BlocksPlaced int64 `json:"blocks_placed"`
|
||||
BlocksDigged int64 `json:"blocks_digged"`
|
||||
WalkedMeters int64 `json:"walked_meters"`
|
||||
JetpackMeters int64 `json:"jetpack_meters"`
|
||||
HvMeters int64 `json:"hv_meters"`
|
||||
SvMeters int64 `json:"sv_meters"`
|
||||
CvMeters int64 `json:"cv_meters"`
|
||||
Playtime float64 `json:"playtime"`
|
||||
LightYears float64 `json:"light_years"`
|
||||
AstroUnits float64 `json:"astronomical_units"`
|
||||
}
|
||||
type wireSession struct {
|
||||
LoginTicks int64 `json:"login_ticks"`
|
||||
LogoffTicks int64 `json:"logoff_ticks"`
|
||||
PlayerName string `json:"player_name"`
|
||||
BuildNr int64 `json:"build_nr"`
|
||||
IP string `json:"ip"`
|
||||
OS string `json:"os"`
|
||||
GFX string `json:"gfx"`
|
||||
CPU string `json:"cpu"`
|
||||
}
|
||||
type wireInvItem struct {
|
||||
ItemID int32 `json:"item_id"`
|
||||
Count int32 `json:"count"`
|
||||
}
|
||||
type wirePos struct {
|
||||
LastPlayfield string `json:"last_playfield"`
|
||||
LastX float64 `json:"last_x"`
|
||||
LastY float64 `json:"last_y"`
|
||||
LastZ float64 `json:"last_z"`
|
||||
Credits int64 `json:"credits"`
|
||||
}
|
||||
type wire struct {
|
||||
SteamID string `json:"steam_id"`
|
||||
PlayerName string `json:"player_name"`
|
||||
EntityID int64 `json:"entity_id"`
|
||||
FactionGroup int32 `json:"faction_group"`
|
||||
FactionID int32 `json:"faction_id"`
|
||||
Position []wirePos `json:"position"`
|
||||
Stats []wireStats `json:"stats"`
|
||||
Sessions []wireSession `json:"sessions"`
|
||||
InventoryGametime int64 `json:"inventory_gametime"`
|
||||
Inventory []wireInvItem `json:"inventory"`
|
||||
}
|
||||
var w wire
|
||||
if err := json.Unmarshal([]byte(strings.TrimSpace(string(out))), &w); err != nil {
|
||||
d.sendEmpyrionPlayerSummaryResult(corrID, &panelv1.EmpyrionPlayerSummaryResult{
|
||||
Ok: false, Error: "parse sqlite output: " + err.Error(), SteamId: req.SteamId,
|
||||
})
|
||||
return
|
||||
}
|
||||
res := &panelv1.EmpyrionPlayerSummaryResult{
|
||||
Ok: true,
|
||||
SteamId: w.SteamID,
|
||||
PlayerName: strings.Trim(w.PlayerName, "\""),
|
||||
EntityId: w.EntityID,
|
||||
FactionGroup: w.FactionGroup,
|
||||
FactionId: w.FactionID,
|
||||
InventoryGametime: w.InventoryGametime,
|
||||
}
|
||||
if len(w.Position) > 0 {
|
||||
res.LastPlayfield = w.Position[0].LastPlayfield
|
||||
res.LastX = w.Position[0].LastX
|
||||
res.LastY = w.Position[0].LastY
|
||||
res.LastZ = w.Position[0].LastZ
|
||||
res.Credits = w.Position[0].Credits
|
||||
}
|
||||
if len(w.Stats) > 0 {
|
||||
s := w.Stats[0]
|
||||
res.Stats = &panelv1.PlayerStatsRow{
|
||||
KilledEnemies: s.KilledEnemies, KilledAllied: s.KilledAllied,
|
||||
KilledAnimals: s.KilledAnimals, KilledDrones: s.KilledDrones,
|
||||
KilledPlayers: s.KilledPlayers, KilledAlliedPlayers: s.KilledAlliedPlayers,
|
||||
Died: s.Died, Score: s.Score,
|
||||
BlocksPlaced: s.BlocksPlaced, BlocksDigged: s.BlocksDigged,
|
||||
WalkedMeters: s.WalkedMeters, JetpackMeters: s.JetpackMeters,
|
||||
HvMeters: s.HvMeters, SvMeters: s.SvMeters, CvMeters: s.CvMeters,
|
||||
Playtime: s.Playtime, LightYears: s.LightYears, AstronomicalUnits: s.AstroUnits,
|
||||
}
|
||||
}
|
||||
for _, s := range w.Sessions {
|
||||
res.Sessions = append(res.Sessions, &panelv1.PlayerLoginRow{
|
||||
LoginTicks: s.LoginTicks, LogoffTicks: s.LogoffTicks,
|
||||
PlayerName: s.PlayerName, BuildNr: s.BuildNr,
|
||||
Ip: s.IP, Os: s.OS, Gfx: s.GFX, Cpu: s.CPU,
|
||||
})
|
||||
}
|
||||
for _, it := range w.Inventory {
|
||||
res.Inventory = append(res.Inventory, &panelv1.PlayerInventoryItem{
|
||||
ItemId: it.ItemID, Count: it.Count,
|
||||
})
|
||||
}
|
||||
d.sendEmpyrionPlayerSummaryResult(corrID, res)
|
||||
}
|
||||
|
||||
func (d *Dispatcher) sendEmpyrionPlayerSummaryResult(corrID string, res *panelv1.EmpyrionPlayerSummaryResult) {
|
||||
d.sendEnv(&panelv1.AgentEnvelope{
|
||||
CorrelationId: corrID,
|
||||
SentAt: timestamppb.Now(),
|
||||
Payload: &panelv1.AgentEnvelope_EmpyrionPlayerSummaryResult{
|
||||
EmpyrionPlayerSummaryResult: res,
|
||||
},
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user