5232609719
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
527 lines
19 KiB
Go
527 lines
19 KiB
Go
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,
|
|
},
|
|
})
|
|
}
|