295eb22826
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
786 lines
27 KiB
Protocol Buffer
786 lines
27 KiB
Protocol Buffer
syntax = "proto3";
|
|
|
|
package panel.v1;
|
|
|
|
option go_package = "github.com/dbledeez/panel/proto/panel/v1;panelv1";
|
|
|
|
import "google/protobuf/timestamp.proto";
|
|
import "panel/v1/common.proto";
|
|
|
|
// Agent is the gRPC service a Target agent dials on the Controller.
|
|
// A single persistent bidirectional stream carries all control-plane
|
|
// traffic between Controller and Target: commands downstream, state
|
|
// and events upstream.
|
|
service Agent {
|
|
rpc Connect(stream AgentEnvelope) returns (stream ControllerEnvelope);
|
|
}
|
|
|
|
// ControllerEnvelope — messages from Controller to Target.
|
|
message ControllerEnvelope {
|
|
string correlation_id = 1;
|
|
google.protobuf.Timestamp sent_at = 2;
|
|
oneof payload {
|
|
ControllerHello hello = 10;
|
|
Ping ping = 11;
|
|
ExecRequest exec = 20;
|
|
InstanceCreate instance_create = 30;
|
|
InstanceStart instance_start = 31;
|
|
InstanceStop instance_stop = 32;
|
|
InstanceDelete instance_delete = 33;
|
|
RCONCommand rcon = 40;
|
|
FsListRequest fs_list = 50;
|
|
FsReadRequest fs_read = 51;
|
|
FsWriteRequest fs_write = 52;
|
|
FsDeleteRequest fs_delete = 53;
|
|
FsSymlinkRequest fs_symlink = 54;
|
|
FsExtractRequest fs_extract = 55;
|
|
FsCompressRequest fs_compress = 56;
|
|
FsRenameRequest fs_rename = 57;
|
|
FsWriteChunkRequest fs_write_chunk = 58;
|
|
ArkSaveRestoreRequest ark_save_restore = 59;
|
|
DayzModInstallRequest dayz_mod_install = 90;
|
|
DayzModUninstallRequest dayz_mod_uninstall = 91;
|
|
UpdateRequest update = 60;
|
|
BackupRequest backup = 70;
|
|
RestoreRequest restore = 71;
|
|
BackupListEntriesRequest backup_list_entries = 72;
|
|
BackupReadFileRequest backup_read_file = 73;
|
|
EmpyrionScenarioInstallRequest empyrion_scenario_install = 100;
|
|
EmpyrionDiscoveriesRequest empyrion_discoveries = 101;
|
|
EmpyrionPlayerSummaryRequest empyrion_player_summary = 102;
|
|
RegionScanRequest region_scan = 110;
|
|
RegionHealRequest region_heal = 111;
|
|
InstanceRenderConfigRequest instance_render_config = 112;
|
|
SeedModsRequest seed_mods = 113;
|
|
UpdateCheckRequest update_check = 114;
|
|
}
|
|
}
|
|
|
|
// AgentEnvelope — messages from Target to Controller.
|
|
message AgentEnvelope {
|
|
string correlation_id = 1;
|
|
google.protobuf.Timestamp sent_at = 2;
|
|
oneof payload {
|
|
AgentHello hello = 10;
|
|
Heartbeat heartbeat = 11;
|
|
Pong pong = 12;
|
|
CommandResult result = 20;
|
|
InstanceStateUpdate instance_state = 30;
|
|
AppStateUpdate app_state = 31;
|
|
LogLine log = 32;
|
|
PlayerEvent player = 33;
|
|
RCONResult rcon_result = 40;
|
|
FsListResult fs_list_result = 50;
|
|
FsReadResult fs_read_result = 51;
|
|
FsWriteResult fs_write_result = 52;
|
|
FsDeleteResult fs_delete_result = 53;
|
|
FsSymlinkResult fs_symlink_result = 54;
|
|
FsExtractResult fs_extract_result = 55;
|
|
FsCompressResult fs_compress_result = 56;
|
|
FsRenameResult fs_rename_result = 57;
|
|
FsWriteChunkResult fs_write_chunk_result = 58;
|
|
ArkSaveRestoreResult ark_save_restore_result = 59;
|
|
DayzModInstallResult dayz_mod_install_result = 90;
|
|
DayzModUninstallResult dayz_mod_uninstall_result = 91;
|
|
UpdateResult update_result = 60;
|
|
BackupResult backup_result = 70;
|
|
RestoreResult restore_result = 71;
|
|
BackupListEntriesResult backup_list_entries_result = 72;
|
|
BackupReadFileResult backup_read_file_result = 73;
|
|
InstanceStatsUpdate instance_stats = 80;
|
|
EmpyrionScenarioInstallResult empyrion_scenario_install_result = 100;
|
|
EmpyrionDiscoveriesResult empyrion_discoveries_result = 101;
|
|
EmpyrionPlayerSummaryResult empyrion_player_summary_result = 102;
|
|
RegionScanResult region_scan_result = 110;
|
|
RegionHealResult region_heal_result = 111;
|
|
InstanceRenderConfigResult instance_render_config_result = 112;
|
|
SeedModsResult seed_mods_result = 113;
|
|
UpdateCheckResult update_check_result = 114;
|
|
}
|
|
}
|
|
|
|
// ---- Handshake ----
|
|
|
|
message AgentHello {
|
|
string agent_id = 1;
|
|
string agent_version = 2;
|
|
string host_os = 3; // "linux", "windows"
|
|
string host_arch = 4; // "amd64", "arm64"
|
|
string hostname = 5;
|
|
repeated string supported_runtimes = 6; // "docker", "host"
|
|
repeated ModuleSummary modules = 7;
|
|
}
|
|
|
|
// ModuleSummary is what an agent advertises about one locally-loaded
|
|
// module at handshake time. The controller aggregates these across all
|
|
// agents so the dashboard can render a "known modules" picker without
|
|
// needing direct filesystem access to each agent's modules/ dir.
|
|
message ModuleSummary {
|
|
string id = 1;
|
|
string name = 2;
|
|
string version = 3;
|
|
repeated string supported_modes = 4;
|
|
repeated ModuleUpdateProviderSummary update_providers = 5;
|
|
bool has_rcon = 6;
|
|
repeated string authors = 7;
|
|
}
|
|
|
|
message ModuleUpdateProviderSummary {
|
|
string id = 1;
|
|
string kind = 2; // "steamcmd" | "github" | "direct"
|
|
// True when the provider sets `requires_steam_login: true` on the
|
|
// manifest (DayZ, Arma). Controller uses this to decide whether to
|
|
// pop the Steam-login modal before forwarding an UpdateRequest.
|
|
bool requires_steam_login = 3;
|
|
}
|
|
|
|
message ControllerHello {
|
|
string controller_version = 1;
|
|
google.protobuf.Timestamp server_time = 2;
|
|
}
|
|
|
|
message Heartbeat {
|
|
google.protobuf.Timestamp at = 1;
|
|
double cpu_percent = 2;
|
|
uint64 mem_used_bytes = 3;
|
|
uint64 mem_total_bytes = 4;
|
|
uint32 instance_count = 5;
|
|
}
|
|
|
|
message Ping { string nonce = 1; }
|
|
message Pong { string nonce = 1; }
|
|
|
|
// ---- Exec / commands ----
|
|
|
|
message ExecRequest {
|
|
string instance_id = 1; // empty = run on Target host
|
|
repeated string argv = 2;
|
|
map<string, string> env = 3;
|
|
string working_dir = 4;
|
|
uint32 timeout_seconds = 5;
|
|
}
|
|
|
|
message CommandResult {
|
|
int32 exit_code = 1;
|
|
bytes stdout = 2;
|
|
bytes stderr = 3;
|
|
Error error = 4;
|
|
}
|
|
|
|
// ---- Instance lifecycle ----
|
|
|
|
message InstanceCreate {
|
|
string instance_id = 1;
|
|
string module_id = 2;
|
|
string version = 3;
|
|
RunMode run_mode = 4;
|
|
ResourceLimits limits = 5;
|
|
map<string, string> config_values = 6; // flattened key/value, module schema-validated
|
|
repeated PortMap ports = 7;
|
|
string data_path = 8;
|
|
// mount_overrides lets the controller swap one of the module's
|
|
// manifest-defined volume mounts at create time. Keyed by container
|
|
// path (matching a volume entry's `container:` field in module.yaml).
|
|
// Value is either a Docker named volume name (no leading slash) or an
|
|
// absolute host path (leading slash = bind mount).
|
|
//
|
|
// Used by the ARK cluster feature to swap an instance's per-instance
|
|
// `panel-<id>-cluster` named volume for a shared `panel-ark-cluster-<id>`
|
|
// volume when the server joins a cluster. Generic enough for future
|
|
// features — any module can benefit from per-instance mount swaps
|
|
// without needing a manifest change.
|
|
map<string, string> mount_overrides = 9;
|
|
}
|
|
|
|
message InstanceStart { string instance_id = 1; }
|
|
message InstanceStop {
|
|
string instance_id = 1;
|
|
uint32 grace_seconds = 2;
|
|
bool force = 3;
|
|
}
|
|
// InstanceDelete tears down the instance record. If purge_volumes is
|
|
// true, every Docker volume labeled panel.instance_id=<instance_id> is
|
|
// removed too; otherwise volumes survive so a subsequent re-create can
|
|
// pick up the existing world/save data.
|
|
message InstanceDelete {
|
|
string instance_id = 1;
|
|
uint32 grace_seconds = 2; // if the container is still running, stop with this grace first
|
|
bool purge_volumes = 3;
|
|
}
|
|
|
|
// ---- State streams ----
|
|
|
|
message InstanceStateUpdate {
|
|
string instance_id = 1;
|
|
InstanceStatus status = 2;
|
|
int32 exit_code = 3;
|
|
google.protobuf.Timestamp at = 4;
|
|
string detail = 5;
|
|
}
|
|
|
|
message AppStateUpdate {
|
|
string instance_id = 1;
|
|
int32 players_online = 2;
|
|
int32 players_max = 3;
|
|
int64 uptime_seconds = 4;
|
|
map<string, string> module_fields = 10; // world_seed, day_number, tps, etc.
|
|
google.protobuf.Timestamp at = 20;
|
|
}
|
|
|
|
message LogLine {
|
|
string instance_id = 1;
|
|
string stream = 2; // "stdout", "stderr", or a named log file
|
|
google.protobuf.Timestamp at = 3;
|
|
string line = 4;
|
|
}
|
|
|
|
// InstanceStatsUpdate is emitted periodically (default every 10s) by the
|
|
// agent's per-instance stats poller. Values come from Docker's stats
|
|
// stream API. Kept sparse — enough for UI gauges + future scheduler
|
|
// expressions like "cpu_percent > 80 sustained 5m -> notify".
|
|
message InstanceStatsUpdate {
|
|
string instance_id = 1;
|
|
double cpu_percent = 2; // 100 = 1 vCPU saturated
|
|
uint64 mem_used_bytes = 3;
|
|
uint64 mem_limit_bytes = 4; // 0 if no explicit limit set (then use container's view)
|
|
uint64 net_rx_bytes = 5; // cumulative since container start
|
|
uint64 net_tx_bytes = 6;
|
|
uint32 pids = 7;
|
|
google.protobuf.Timestamp at = 8;
|
|
// Host core count (docker OnlineCPUs) so UIs can translate the
|
|
// docker-style cpu_percent (100 = 1 core) into percent-of-host.
|
|
uint32 host_cpus = 9;
|
|
}
|
|
|
|
// ---- Ad-hoc RCON ----
|
|
|
|
message RCONCommand {
|
|
string instance_id = 1;
|
|
string command = 2;
|
|
}
|
|
|
|
message RCONResult {
|
|
string output = 1;
|
|
string error = 2; // empty on success; short machine-readable on failure
|
|
}
|
|
|
|
// ---- File management (rooted at each instance's data_path) ----
|
|
|
|
message FsEntry {
|
|
string name = 1; // base name
|
|
string path = 2; // path relative to instance data_path
|
|
bool is_dir = 3;
|
|
int64 size = 4; // bytes (for files)
|
|
google.protobuf.Timestamp mod_time = 5;
|
|
}
|
|
|
|
message FsListRequest { string instance_id = 1; string path = 2; }
|
|
message FsListResult {
|
|
repeated FsEntry entries = 1;
|
|
string path = 2;
|
|
string error = 3;
|
|
}
|
|
|
|
message FsReadRequest { string instance_id = 1; string path = 2; }
|
|
message FsReadResult {
|
|
string path = 1;
|
|
bytes content = 2;
|
|
string error = 3;
|
|
}
|
|
|
|
message FsWriteRequest { string instance_id = 1; string path = 2; bytes content = 3; }
|
|
message FsWriteResult { int64 bytes_written = 1; string error = 2; }
|
|
|
|
message FsDeleteRequest { string instance_id = 1; string path = 2; bool recursive = 3; }
|
|
message FsDeleteResult { string error = 1; }
|
|
|
|
// FsSymlinkRequest creates (or replaces) a symlink inside the instance
|
|
// container. Used by the DayZ mod manager to link @ModName entries to
|
|
// the shared workshop tree at /game/steamapps/workshop/content/...
|
|
message FsSymlinkRequest { string instance_id = 1; string target = 2; string link_path = 3; }
|
|
message FsSymlinkResult { string error = 1; }
|
|
|
|
// FsExtractRequest expands an archive that already lives inside the
|
|
// instance's volume. Format is auto-detected from the archive bytes
|
|
// (zip / tar / tar.gz / tar.bz2). dest_dir is rooted in the instance's
|
|
// browseable root just like every other Fs op; "" means same dir as
|
|
// the archive.
|
|
message FsExtractRequest {
|
|
string instance_id = 1;
|
|
string archive_path = 2;
|
|
string dest_dir = 3;
|
|
bool overwrite = 4;
|
|
}
|
|
message FsExtractResult {
|
|
int64 entries_written = 1;
|
|
int64 bytes_written = 2;
|
|
string error = 3;
|
|
}
|
|
|
|
// FsCompressRequest produces a zip archive at dest_zip_path containing
|
|
// the contents of every path in `sources` (recursive for dirs). All
|
|
// paths must be inside the instance's browseable root.
|
|
message FsCompressRequest {
|
|
string instance_id = 1;
|
|
repeated string sources = 2;
|
|
string dest_zip_path = 3;
|
|
bool overwrite = 4;
|
|
}
|
|
message FsCompressResult {
|
|
int64 entries_written = 1;
|
|
int64 bytes_written = 2;
|
|
string error = 3;
|
|
}
|
|
|
|
// FsRenameRequest is a path-safe `mv` inside the instance's volume —
|
|
// supports rename + move-into-sibling-dir within the same root.
|
|
message FsRenameRequest {
|
|
string instance_id = 1;
|
|
string from_path = 2;
|
|
string to_path = 3;
|
|
}
|
|
message FsRenameResult { string error = 1; }
|
|
|
|
// FsWriteChunkRequest streams a single chunk of a larger upload. Chunks
|
|
// are uploaded in order; each one carries the same upload_id (browser-
|
|
// generated UUID) so the agent can append to the same temp file. On
|
|
// is_final = true the agent finalizes (closes the temp file, ships it
|
|
// to the container via CopyToContainer, then unlinks).
|
|
message FsWriteChunkRequest {
|
|
string instance_id = 1;
|
|
string upload_id = 2;
|
|
string path = 3;
|
|
int64 offset = 4;
|
|
bytes data = 5;
|
|
int64 total_size = 6; // optional; agent uses for sanity checks on final
|
|
bool is_final = 7;
|
|
}
|
|
message FsWriteChunkResult {
|
|
int64 bytes_received = 1;
|
|
int64 total_received = 2; // running total per upload_id
|
|
bool finalized = 3;
|
|
string error = 4;
|
|
}
|
|
|
|
// ArkSaveRestoreRequest swaps a timestamped rolling backup into the
|
|
// active save slot under SavedArks/<subdir>/<subdir>.ark. The previous
|
|
// active save is moved aside (NEVER deleted) by appending a "_replaced"
|
|
// timestamp suffix so even abandoned attempts are recoverable. Path is
|
|
// rooted at ShooterGame/Saved/SavedArks/<saved_arks_subdir>/ in the
|
|
// instance's browseable root.
|
|
//
|
|
// Caller (controller) is responsible for stopping the container before
|
|
// invoking this — the agent only performs file operations. If a backup
|
|
// rolling file (.arkrbf) is selected, it is renamed to .ark before
|
|
// installing as the active save.
|
|
message ArkSaveRestoreRequest {
|
|
string instance_id = 1;
|
|
string saved_arks_subdir = 2; // e.g. "TheIsland_WP"
|
|
string target_filename = 3; // basename within saved_arks_subdir (e.g. "TheIsland_WP_30.04.2026_22.13.00.ark")
|
|
}
|
|
message ArkSaveRestoreResult {
|
|
string aside_filename = 1; // what the previously-active save was renamed to
|
|
string installed_filename = 2; // the filename that's now active (always "<subdir>.ark")
|
|
string error = 3;
|
|
}
|
|
|
|
// DayzModInstallRequest asks the agent to (1) copy the mod's bikeys into
|
|
// /game/keys and (2) create a @ModName symlink at /game/<folder_name>
|
|
// pointing at the shared workshop content at
|
|
// /game/steamapps/workshop/content/221100/<workshop_id>. Agent-side so
|
|
// it can scan the workshop dir and preserve file ownership.
|
|
message DayzModInstallRequest {
|
|
string instance_id = 1;
|
|
string workshop_id = 2;
|
|
string folder_name = 3; // e.g. "@CommunityOnlineTools"
|
|
}
|
|
message DayzModInstallResult {
|
|
string error = 1;
|
|
repeated string bikeys_copied = 2; // filenames of bikeys dropped in /game/keys
|
|
string resolved_mod_path = 3; // /game/steamapps/workshop/content/221100/<id>
|
|
}
|
|
|
|
// DayzModUninstallRequest removes the @ModName symlink and the mod's
|
|
// bikeys from /game/keys (bikeys are matched by file hash — only keys
|
|
// whose content originated from this workshop item are removed).
|
|
message DayzModUninstallRequest {
|
|
string instance_id = 1;
|
|
string folder_name = 2;
|
|
}
|
|
message DayzModUninstallResult {
|
|
string error = 1;
|
|
repeated string bikeys_removed = 2;
|
|
}
|
|
|
|
// ---- Updates ----
|
|
//
|
|
// The agent runs the update asynchronously. The synchronous UpdateResult
|
|
// carries only the acknowledgement + ref-id for later correlation; real
|
|
// progress + final status flow through LogLine (stream="update") and the
|
|
// terminal "update:ok"/"update:error:<msg>" LogLine sentinel at the end.
|
|
|
|
message UpdateRequest {
|
|
string instance_id = 1;
|
|
string provider_id = 2; // empty = use the module's first update_provider
|
|
// Steam login credentials for apps whose SteamCMD refuses +login
|
|
// anonymous (DayZ, Arma, etc. — modules with `requires_steam_login`
|
|
// set on the provider). The controller pulls these from its
|
|
// encrypted steam_credentials store and forwards them via the
|
|
// mTLS-protected control stream. Agent never logs them.
|
|
string steam_username = 3;
|
|
string steam_password = 4;
|
|
}
|
|
|
|
message UpdateResult {
|
|
bool accepted = 1; // true = agent kicked off the update goroutine
|
|
string error = 2; // empty on accept; populated on immediate reject
|
|
string provider_id = 3;
|
|
string provider_kind = 4;
|
|
}
|
|
|
|
// ---- Backup + Restore ----
|
|
//
|
|
// Synchronous over the bidi stream. The sidecar tars the instance's
|
|
// volume into a .tar.gz on the agent's --backup-dir; the terminal
|
|
// BackupResult carries the path + size. Restore takes a backup id
|
|
// (path resolved by the controller via DB) and reverses.
|
|
|
|
message BackupRequest {
|
|
string instance_id = 1;
|
|
string description = 2;
|
|
}
|
|
|
|
message BackupResult {
|
|
string backup_id = 1; // bkp_<hex>
|
|
string path = 2;
|
|
int64 size_bytes = 3;
|
|
string error = 4;
|
|
}
|
|
|
|
message RestoreRequest {
|
|
string instance_id = 1;
|
|
string backup_id = 2;
|
|
string backup_path = 3; // absolute path on the agent
|
|
}
|
|
|
|
message RestoreResult {
|
|
string error = 1;
|
|
int64 bytes_restored = 2;
|
|
}
|
|
|
|
// BackupListEntriesRequest asks the agent to enumerate the contents of a
|
|
// backup tarball without extracting it. Used by the backup-browser UI so
|
|
// operators can drill into a snapshot to confirm what's inside before
|
|
// restoring (or to recover a single file).
|
|
message BackupListEntriesRequest {
|
|
string instance_id = 1;
|
|
string backup_id = 2;
|
|
string backup_path = 3; // absolute path on the agent (resolved controller-side)
|
|
}
|
|
|
|
// BackupTarEntry — one row from `tar -tvzf`. Size is bytes-on-disk, not
|
|
// compressed size. is_dir distinguishes directories so the UI can render
|
|
// a tree without a second pass.
|
|
message BackupTarEntry {
|
|
string path = 1; // path inside the tarball, no leading slash
|
|
int64 size = 2;
|
|
bool is_dir = 3;
|
|
string mode = 4; // permissions string, e.g. "-rw-r--r--"
|
|
google.protobuf.Timestamp mod_time = 5;
|
|
}
|
|
|
|
message BackupListEntriesResult {
|
|
repeated BackupTarEntry entries = 1;
|
|
int64 total_bytes = 2; // sum of file sizes (not compressed size of the tarball)
|
|
string error = 3;
|
|
}
|
|
|
|
// BackupReadFileRequest extracts a single entry's bytes to memory by
|
|
// streaming `tar -xzOf <archive> <path>` into a buffer. Caller-supplied
|
|
// max_bytes caps the read so a malicious / corrupt archive can't OOM the
|
|
// agent — the result includes truncated=true if max_bytes was hit.
|
|
message BackupReadFileRequest {
|
|
string instance_id = 1;
|
|
string backup_id = 2;
|
|
string backup_path = 3;
|
|
string entry_path = 4;
|
|
int64 max_bytes = 5; // 0 → controller-side default (8 MiB)
|
|
}
|
|
|
|
message BackupReadFileResult {
|
|
bytes content = 1;
|
|
int64 size = 2; // actual entry size from tar header (may be > len(content) if truncated)
|
|
bool truncated = 3;
|
|
bool is_binary = 4; // heuristic: NUL byte in first 8 KiB
|
|
string error = 5;
|
|
}
|
|
|
|
message PlayerEvent {
|
|
enum Kind {
|
|
KIND_UNSPECIFIED = 0;
|
|
KIND_JOIN = 1;
|
|
KIND_LEAVE = 2;
|
|
KIND_CHAT = 3;
|
|
KIND_DEATH = 4;
|
|
KIND_CUSTOM = 99;
|
|
}
|
|
string instance_id = 1;
|
|
Kind kind = 2;
|
|
string player_id = 3; // SteamID / UUID where available
|
|
string player_name = 4;
|
|
string detail = 5;
|
|
google.protobuf.Timestamp at = 6;
|
|
}
|
|
|
|
// EmpyrionScenarioInstallRequest is sent controller → agent to install
|
|
// (or refresh) a workshop scenario into a specific Empyrion instance's
|
|
// game volume. The agent runs SteamCMD on its OWN Docker daemon — the
|
|
// previous (single-host) implementation ran on the controller's daemon
|
|
// which has no view of an agent's volumes.
|
|
message EmpyrionScenarioInstallRequest {
|
|
string job_id = 1; // controller-generated; echoed in result + log lines
|
|
string instance_id = 2; // target empyrion instance (must live on this agent)
|
|
string workshop_id = 3; // Steam workshop item ID (e.g. "3041847672" for RE2)
|
|
string scenario_name = 4; // folder name under Content/Scenarios; sanitized by controller
|
|
string app_id = 5; // empyrion app id; defaults to 530870 if empty
|
|
// Steam credentials for the workshop_download_item call. Steam policy
|
|
// (~2023+) requires a logged-in account for most app workshops, even
|
|
// for public/free items — anonymous returns "Access Denied". Forwarded
|
|
// from the controller's encrypted steam_credentials store. Same shape
|
|
// as UpdateRequest's steam_username/steam_password fields.
|
|
string steam_username = 6;
|
|
string steam_password = 7;
|
|
}
|
|
|
|
message EmpyrionScenarioInstallResult {
|
|
string job_id = 1;
|
|
bool ok = 2;
|
|
string error = 3; // empty when ok
|
|
string scenario_name = 4;
|
|
}
|
|
|
|
// EmpyrionDiscoveriesRequest queries the empyrion server's global.db
|
|
// (SQLite) for what a specific player has discovered. The agent mounts
|
|
// the instance's saves volume into a tiny alpine+sqlite sidecar, runs
|
|
// a join query against DiscoveredPOIs / Entities / Playfields /
|
|
// SolarSystems / LoginLogoff, and returns the rows.
|
|
//
|
|
// `mode = "personal"` returns POIs the named player personally
|
|
// discovered. `mode = "faction"` returns POIs anyone in their faction
|
|
// has discovered (the in-game shared-map view). Default: personal.
|
|
message EmpyrionDiscoveriesRequest {
|
|
string instance_id = 1;
|
|
string steam_id = 2; // 17-digit SteamID64 of the player
|
|
string mode = 3; // "personal" | "faction"
|
|
}
|
|
|
|
message DiscoveredPOI {
|
|
string solar_system = 1;
|
|
string playfield = 2;
|
|
string poi_name = 3;
|
|
int32 poi_type = 4; // Entities.etype enum
|
|
int64 game_time = 5; // ticks since save start
|
|
string discoverer_name = 6; // who first discovered (when known)
|
|
string discoverer_steam_id = 7;
|
|
int32 faction_group = 8;
|
|
int32 faction_id = 9;
|
|
}
|
|
|
|
message EmpyrionDiscoveriesResult {
|
|
bool ok = 1;
|
|
string error = 2;
|
|
repeated DiscoveredPOI pois = 3;
|
|
string mode = 4; // echoed back from request
|
|
string steam_id = 5;
|
|
string player_name = 6; // last-known name from LoginLogoff
|
|
}
|
|
|
|
// EmpyrionPlayerSummaryRequest pulls everything we know about a single
|
|
// player from the empyrion savegame DB: stats, login sessions, current
|
|
// inventory snapshot. Used by the public /me page on AMP-Monitor.
|
|
message EmpyrionPlayerSummaryRequest {
|
|
string instance_id = 1;
|
|
string steam_id = 2; // 17-digit SteamID64
|
|
int32 max_login_rows = 3; // limit on session history; default 50
|
|
}
|
|
|
|
message PlayerStatsRow {
|
|
int64 killed_enemies = 1;
|
|
int64 killed_allied = 2;
|
|
int64 killed_animals = 3;
|
|
int64 killed_drones = 4;
|
|
int64 killed_players = 5;
|
|
int64 killed_allied_players = 6;
|
|
int64 died = 7;
|
|
int64 score = 8;
|
|
int64 blocks_placed = 9;
|
|
int64 blocks_digged = 10;
|
|
int64 walked_meters = 11;
|
|
int64 jetpack_meters = 12;
|
|
int64 hv_meters = 13;
|
|
int64 sv_meters = 14;
|
|
int64 cv_meters = 15;
|
|
double playtime = 16; // hours per the schema
|
|
double light_years = 17; // travly
|
|
double astronomical_units = 18; // travau
|
|
}
|
|
|
|
message PlayerLoginRow {
|
|
int64 login_ticks = 1;
|
|
int64 logoff_ticks = 2;
|
|
string player_name = 3;
|
|
int64 build_nr = 4;
|
|
string ip = 5;
|
|
string os = 6;
|
|
string gfx = 7;
|
|
string cpu = 8;
|
|
}
|
|
|
|
message PlayerInventoryItem {
|
|
int32 item_id = 1;
|
|
int32 count = 2;
|
|
}
|
|
|
|
message EmpyrionPlayerSummaryResult {
|
|
bool ok = 1;
|
|
string error = 2;
|
|
string steam_id = 3;
|
|
string player_name = 4;
|
|
int64 entity_id = 5;
|
|
int32 faction_group = 6;
|
|
int32 faction_id = 7;
|
|
|
|
// Aggregate stats. nil when player has no PlayerStatistics row yet
|
|
// (very new player).
|
|
PlayerStatsRow stats = 8;
|
|
|
|
// Recent login sessions, newest first.
|
|
repeated PlayerLoginRow sessions = 9;
|
|
|
|
// Most recent inventory snapshot. May be empty for players without
|
|
// a recorded snapshot.
|
|
int64 inventory_gametime = 10;
|
|
repeated PlayerInventoryItem inventory = 11;
|
|
|
|
// Last-known position.
|
|
string last_playfield = 12;
|
|
double last_x = 13;
|
|
double last_y = 14;
|
|
double last_z = 15;
|
|
int64 credits = 16;
|
|
}
|
|
|
|
// ---- Region Medic (7DTD .7rg region-corruption detect + heal) ----
|
|
|
|
// RegionScanRequest asks the agent to scan an instance's active-world Region
|
|
// directory for corruption evidence (error_backup salvage files + invalid
|
|
// region files).
|
|
message RegionScanRequest {
|
|
string instance_id = 1;
|
|
}
|
|
|
|
// AffectedRegionMsg is one region flagged by a scan.
|
|
message AffectedRegionMsg {
|
|
string region = 1; // canonical id, e.g. "r.-2.0"
|
|
int32 error_backups = 2; // salvage files whose chunks fall in this region
|
|
bool file_present = 3; // r.X.Z.7rg exists on disk
|
|
bool file_corrupt = 4; // region file failed structural validation
|
|
int32 bad_chunks = 5; // count of invalid chunks if validated
|
|
}
|
|
|
|
message RegionScanResult {
|
|
int32 error_backups = 1; // total salvage files found
|
|
repeated AffectedRegionMsg affected = 2;
|
|
string region_dir = 3; // resolved active Region dir (container path)
|
|
Error error = 4;
|
|
}
|
|
|
|
// RegionHealRequest asks the agent to heal one region from the newest clean
|
|
// backup. The agent stops the instance (graceful save first), swaps the region
|
|
// file in place, quarantines the region's error_backups, then restarts (if
|
|
// restart=true).
|
|
message RegionHealRequest {
|
|
string instance_id = 1;
|
|
string region = 2; // "r.-2.0"
|
|
bool restart = 3; // restart the instance after healing
|
|
}
|
|
|
|
message RegionHealResult {
|
|
string region = 1;
|
|
string source_backup = 2; // backup id the clean copy came from
|
|
int64 bytes_written = 3;
|
|
int32 quarantined = 4; // error_backup files moved aside
|
|
string snapshot_path = 5; // where the corrupt original was saved
|
|
bool healed = 6;
|
|
Error error = 7;
|
|
}
|
|
|
|
// ---- Durable config re-render (no container recreate) ----
|
|
|
|
// InstanceRenderConfigRequest asks the agent to re-render the module's
|
|
// declared config_files (templates) into the instance's host DataPath,
|
|
// WITHOUT recreating or restarting the container. For modules whose
|
|
// rendered output is bind-mounted into the container (7dtd's
|
|
// serverconfig.xml.rendered), the entrypoint picks the new content up on
|
|
// the instance's NEXT boot — this is the durable, no-recreate config path.
|
|
//
|
|
// config_values is the FULL desired value map (the controller sends its
|
|
// merged DB row, not a delta), mirroring what InstanceCreate would render.
|
|
message InstanceRenderConfigRequest {
|
|
string instance_id = 1;
|
|
map<string, string> config_values = 2;
|
|
}
|
|
|
|
message InstanceRenderConfigResult {
|
|
repeated string rendered_files = 1; // config_file paths written (manifest-relative)
|
|
string error = 2;
|
|
}
|
|
|
|
// SeedModsRequest asks an agent to clone the Mods/ folder of src_instance_id
|
|
// (the cluster master) onto instance_id (a member), live — no container
|
|
// recreate. Per-server state (RefugeBot homes.json) is excluded from the copy
|
|
// and the member's own homes.json is preserved. The member keeps running on
|
|
// its currently-loaded mods; the new files take effect on its NEXT restart.
|
|
// Both instances must live on the same agent (the sidecar mounts local volumes).
|
|
message SeedModsRequest {
|
|
string instance_id = 1; // destination member to receive the mods
|
|
string src_instance_id = 2; // source cluster master to copy mods from
|
|
}
|
|
|
|
message SeedModsResult {
|
|
string instance_id = 1;
|
|
bool ok = 2;
|
|
string error = 3;
|
|
int32 mod_count = 4; // number of mods present on the member after the copy
|
|
}
|
|
|
|
// ---- Update-available check (WI-14) ----
|
|
//
|
|
// CHECK-ONLY: this RPC never triggers an update or restart. The agent
|
|
// reads the installed buildid from the Steam appmanifest_<appid>.acf in
|
|
// the instance volume and fetches the latest buildid for the instance's
|
|
// branch via a one-shot `steamcmd +app_info_print` sidecar (cached per
|
|
// app_id for 15 minutes agent-side).
|
|
|
|
message UpdateCheckRequest {
|
|
string instance_id = 1;
|
|
// provider_id selects the module update_provider (steamcmd kind) whose
|
|
// app_id/beta define what "installed vs latest" means. Empty = same
|
|
// default the Update RPC uses (first provider in the manifest).
|
|
string provider_id = 2;
|
|
// refresh forces a fresh steamcmd app_info fetch, bypassing the
|
|
// agent-side 15-minute cache.
|
|
bool refresh = 3;
|
|
}
|
|
|
|
message UpdateCheckResult {
|
|
string instance_id = 1;
|
|
string app_id = 2;
|
|
string branch = 3; // Steam branch compared ("public" when no -beta)
|
|
string installed_buildid = 4; // from appmanifest ACF; "" if unreadable
|
|
string latest_buildid = 5; // from app_info branches map; "" if fetch failed
|
|
bool update_available = 6; // installed != latest (both non-empty)
|
|
bool cached = 7; // latest came from the agent-side cache
|
|
string error = 8; // non-empty on failure; other fields best-effort
|
|
}
|