docs(reference): import Dune: Awakening server-manager references
Phase 2 references for the host-agent Dune adapter, moved out of volatile /tmp
into docs/reference-repos/ (per Commander). Three upstream projects, .git +
node_modules + compiled binaries stripped (16MB source). Nested AI-instruction
files (.claude/, CLAUDE.md) removed so they don't pollute Corrosion sessions.
- icehunter/ dune-admin (Go+React) — 4 control planes; SETUP_DOCKER.md is the
closest analog to our agent's Dune docker control plane (compose
lifecycle, docker logs, RabbitMQ-via-exec, dune Postgres schema)
- adainrivers/ Rust/Tauri desktop — SSH+k8s BattleGroup control, maintenance
daemon, in-game admin console (Rust idiom reference)
- the4rchangel/ Node web UI replacing battlegroup.bat — matches the Commander's
Hyper-V self-host path + game-config schema
See docs/reference-repos/README.md for the full index + how we use each.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
3
docs/reference-repos/the4rchangel/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
tools/**/bin/
|
||||
tools/**/obj/
|
||||
tools/Cue4ParsePatents/*.txt
|
||||
71
docs/reference-repos/the4rchangel/CHANGELOG.md
Normal file
@@ -0,0 +1,71 @@
|
||||
# Changelog
|
||||
|
||||
## 1.0.7 — 2026-06-02
|
||||
|
||||
### SSH — fix false "SSH exited with code 1" on battlegroup commands
|
||||
|
||||
- **Root cause** — Interactive SSH (`-tt`) was spawned with stdin closed. OpenSSH exits code 1 with no output in that case, so status/start/stop looked broken even when the VM was fine.
|
||||
- **`lib/ssh.js`** — Pipe stdin when a pseudo-TTY is requested; treat PTY sessions with stdout as success when stderr is only "Connection closed".
|
||||
|
||||
## 1.0.6 — 2026-06-02
|
||||
|
||||
### SSH key path — fix battlegroup failure after reboot (WSL)
|
||||
|
||||
- **Root cause** — When the manager runs from WSL, `LOCALAPPDATA` is unset. SSH was given a relative path (`AppData/Local/DuneAwakeningServer/sshKey`) instead of the real key at `C:\Users\<you>\AppData\Local\DuneAwakeningServer\sshKey`.
|
||||
- **`lib/paths.js`** — Resolves Windows `LOCALAPPDATA` via PowerShell when env vars are missing; mirrors the key into `~/.dune-awakening-server-manager/sshKey` with `0600` permissions for WSL OpenSSH.
|
||||
- **Status / battlegroup** — `/api/status` reports `ssh.keyPresent`; battlegroup routes fail fast with a clear message; dashboard shows an SSH key warning banner.
|
||||
|
||||
## 1.0.5 — 2026-06-02
|
||||
|
||||
### Start VM — fix silent failure on low host RAM
|
||||
|
||||
- **Root cause** — Dashboard **Start VM** called Hyper-V with the VM's configured startup RAM (often 30 GB). When the Windows host couldn't allocate that much, Hyper-V returned `OutOfMemory` / `0x8007000E`. The error only appeared in the collapsed console, so it looked like nothing happened.
|
||||
- **`lib/vm.js`** — Shared `startVm()` helper with automatic memory step-down (40→32→30→24→20→18→16→14→12 GB) when the host is low on RAM, plus clearer error messages.
|
||||
- **`POST /api/vm/start`** — Uses `startVm()`; accepts optional `{ memoryGB }` for manual retry; returns HTTP 507 with `startFailed: true` on OOM.
|
||||
- **Dashboard UI** — Shows an alert on failure, auto-expands the console, and displays a **Retry Start** panel with a memory selector when start fails.
|
||||
- **Status** — VM card shows configured startup memory when the VM is off.
|
||||
- **Setup import** — Also uses auto step-down on first start.
|
||||
|
||||
## 1.0.4 — 2026-06-02
|
||||
|
||||
### Tech tree — Unlock All Recipes fix
|
||||
|
||||
- **Root cause** — The old endpoint only flipped `UnlockedState` on recipes already present in the character save (~128 entries). The in-game tech tree has **356** nodes (`DA_GRP_*` groups + `DA_REC_*` recipes) that were never added to the save, so they stayed locked even after “Unlock All.”
|
||||
- **`public/data/tech-recipe-catalog.json`** — Full tech node list extracted from game pak files via `tools/Cue4ParsePatents` (regenerate with `dotnet run` in that folder).
|
||||
- **`POST /api/characters/:id/tech/unlock-all`** — Merges every catalog node into `m_TechKnowledgeData` as `Purchased`, preserves existing `RCP_*` / `BLD_*` save entries, and sets `m_TechKnowledgePoints` to 99999.
|
||||
- **UI** — Tech Tree badge shows `purchased / in save / in game`; unlock result reports how many nodes were added. Reminder to stop battlegroup and relog after changes.
|
||||
|
||||
## 1.0.3 — 2026-06-03
|
||||
|
||||
### Incomplete bootstrap repair
|
||||
|
||||
- **`needsBootstrap` in `/api/status`** — Detects the “No resources found in funcom-seabass-… namespace” state (VM imported but bootstrap never finished).
|
||||
- **`POST /api/setup/repair`** — Deletes empty seabass namespace(s) when no battlegroup CR exists, then re-runs bootstrap automatically.
|
||||
- **Dashboard repair panel** — Yellow banner with token/world/region form when incomplete setup is detected.
|
||||
|
||||
## 1.0.2 — 2026-06-03
|
||||
|
||||
### Setup — delete and start fresh
|
||||
|
||||
- **Setup tab → Delete & Start Fresh** — When pre-flight detects an existing `dune-awakening` VM, a reset panel appears on step 1.
|
||||
- **`POST /api/setup/reset`** — Stops the battlegroup (if reachable), removes the Hyper-V VM, deletes `DuneAwakeningServer` folders on all drives, removes SSH keys, and clears cached manager state.
|
||||
- Requires typing **DELETE** to confirm. Re-runs pre-flight automatically after reset so you can walk through the wizard again.
|
||||
|
||||
## 1.0.1 — 2026-06-03
|
||||
|
||||
### Server finder / WAN visibility fixes
|
||||
|
||||
- **Reliable visibility IP writes** — `settings.conf` is now written via base64 over SSH instead of fragile `printf` escaping. Line 4 is what the gateway reads at startup as `GameRmqAddress` when registering with Funcom.
|
||||
- **Stop then start required** — UI and console now clearly state that visibility changes require a full battlegroup **stop → start** (not just restart-in-place). The gateway only publishes the join address on startup.
|
||||
- **WAN port-forward guide** — When Public (WAN) or custom public IP is selected in **Game Config → Server Visibility**, a port-forward checklist appears:
|
||||
- **31982 TCP** — queue/matchmaking (commonly missed; required for server finder)
|
||||
- **Director NodePort TCP** — from your Dashboard (e.g. 31402)
|
||||
- **7777–7810 UDP** — game traffic
|
||||
All forwards must target the **VM IP**, not the Windows host.
|
||||
- **Setup wizard** — Same port-forward notice when choosing a public/custom player IP during initial setup.
|
||||
- **Experimental tab reminder** — Self-hosted worlds appear under **Servers → Experimental** in the game client, not Official or Private.
|
||||
- **API** — `GET /api/server-visibility` now returns `directorPort`, `isWan`, and structured `portForward` info. `POST` returns restart guidance.
|
||||
|
||||
### Other
|
||||
|
||||
- Improved **Server Display Name** field hint in Game Config (empty sietch names can hide servers in the browser).
|
||||
308
docs/reference-repos/the4rchangel/README.md
Normal file
@@ -0,0 +1,308 @@
|
||||
# Dune: Awakening — Server Manager
|
||||
|
||||
> [!CAUTION]
|
||||
> **This is an experimental assistant dashboard.** I am not associated with Funcom or the Dune: Awakening team and cannot guarantee this will work perfectly for you. Always take backups. This works for me and I thought the community might find it helpful. I ran this myself including the server setup and played on it no problem. I hope you experience the same.
|
||||
|
||||
A local web-based UI for managing **Dune: Awakening Self-Hosted Servers**. Replaces the clunky `battlegroup.bat` terminal menu with a clean, modern dashboard that handles everything from first-time setup to daily operations and game configuration.
|
||||
|
||||

|
||||

|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
### Table of Contents
|
||||
|
||||
- [Features](#features)
|
||||
- [Prerequisites](#prerequisites)
|
||||
- [Quick Start](#quick-start)
|
||||
- [Setup Wizard](#setup-wizard)
|
||||
- [Tabs](#tabs)
|
||||
- [Dashboard](#dashboard)
|
||||
- [Battlegroup](#battlegroup)
|
||||
- [Monitoring](#monitoring)
|
||||
- [Database](#database)
|
||||
- [Characters](#characters)
|
||||
- [Game Config](#game-config)
|
||||
- [Settings](#settings)
|
||||
- [Experimental](#experimental)
|
||||
- [How It Works](#how-it-works)
|
||||
- [Project Structure](#project-structure)
|
||||
- [Configuration](#configuration)
|
||||
- [Troubleshooting](#troubleshooting)
|
||||
- [Ports](#ports)
|
||||
- [License](#license)
|
||||
|
||||
---
|
||||
|
||||
## Features
|
||||
|
||||
- **Setup Wizard** — Guided 6-step first-time installation (VM import, network, SSH, bootstrap — no bat file needed)
|
||||
- **Dashboard** — VM status, memory, uptime, IP, and battlegroup health at a glance
|
||||
- **Battlegroup Controls** — Start, stop, restart, and update with one click
|
||||
- **Character Editor** — Edit stats (health, tech points, hydration, spice, Eyes of Ibad) and manage inventory with a searchable 1,000-item catalog
|
||||
- **Game Config** — Edit PvP, sandstorms, sandworm behavior, mining rates, decay, building limits, and more through a visual editor
|
||||
- **Monitoring** — Direct links to the File Browser and Director web interfaces
|
||||
- **Database** — Take and import backups
|
||||
- **Log Export** — Download battlegroup and operator logs
|
||||
- **Settings** — Change VM password, rotate SSH keys, enable swap memory
|
||||
- **Live Console** — Real-time command output streamed to the browser via WebSocket
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before using this tool, you need the following installed and ready:
|
||||
|
||||
1. **Windows 10/11 Pro** with hardware virtualization enabled in BIOS
|
||||
2. **Hyper-V** enabled (Settings → Apps → Optional Features → More Windows Features → Hyper-V)
|
||||
3. **Dune: Awakening Self-Hosted Server** downloaded via Steam (search "Dune Awakening Self-Hosted Server" in your Steam library under Tools)
|
||||
4. **Node.js 18+** — [download here](https://nodejs.org) (LTS recommended)
|
||||
5. **OpenSSH Client** — included with Windows 10/11 by default (verify: run `ssh` in a command prompt)
|
||||
6. **A server token** from [account.duneawakening.com](https://account.duneawakening.com/)
|
||||
|
||||
The app assumes the default Steam install path:
|
||||
|
||||
```
|
||||
C:\Program Files (x86)\Steam\steamapps\common\Dune Awakening Self-Hosted Server\
|
||||
```
|
||||
|
||||
If yours differs, edit `DEFAULT_SERVER_PATH` at the top of `server.js`.
|
||||
|
||||
## Quick Start
|
||||
|
||||
1. **Clone** this repo anywhere on your server machine:
|
||||
|
||||
```
|
||||
git clone https://github.com/YOUR_USER/dune-server-manager.git
|
||||
cd dune-server-manager
|
||||
```
|
||||
|
||||
2. **Double-click `start_as_admin.bat`** in Windows Explorer.
|
||||
|
||||
It will:
|
||||
- Request **Administrator** privileges (UAC prompt) — required for Hyper-V
|
||||
- Install npm dependencies automatically on first run
|
||||
- Open `http://localhost:3000` in your default browser
|
||||
|
||||
3. If this is a **fresh install**, click the **Setup** tab and follow the wizard. If you've already run the official `battlegroup.bat` initial-setup before, go straight to the **Dashboard**.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> **The app must run as Administrator.** Hyper-V commands require elevated privileges. If you see permission errors, the Node process is not running as admin.
|
||||
>
|
||||
> - **Use `start_as_admin.bat`** — it auto-elevates via UAC.
|
||||
> - Or open an **Administrator Command Prompt / PowerShell**, `cd` into the project folder, and run `npm start`.
|
||||
> - **Do not run from WSL** — WSL cannot elevate to Windows admin for Hyper-V operations.
|
||||
|
||||
## Setup Wizard
|
||||
|
||||
The **Setup** tab provides a guided walkthrough that replaces the entire `battlegroup.bat → initial-setup` flow:
|
||||
|
||||

|
||||
|
||||
| Step | What happens |
|
||||
|------|-------------|
|
||||
| **1. Pre-flight** | Verifies Hyper-V is enabled, server files (`.vmcx`) are present, and a drive has 100GB+ free |
|
||||
| **2. Configuration** | Enter your server token, choose install drive, VM memory (10–40 GB), network mode, and NIC |
|
||||
| **3. Installing** | Imports the VM, creates the network switch, resizes the virtual disk, sets memory, and starts the VM — progress streams live |
|
||||
| **4. Security** | Generates and installs an SSH key, then sets a new password for the `dune` user |
|
||||
| **5. Networking** | DHCP vs static IP, auto-detects your public IP, lets you pick the player-facing IP |
|
||||
| **6. Finalize** | Enter your world name and region, upload bootstrap files, run first-time battlegroup setup, optional swap memory |
|
||||
|
||||
After setup, flip to the **Dashboard** to start your battlegroup.
|
||||
|
||||
## Tabs
|
||||
|
||||
### Dashboard
|
||||
|
||||
Live overview of your VM and battlegroup with one-click controls.
|
||||
|
||||

|
||||
|
||||
### Battlegroup
|
||||
|
||||
Start, stop, restart, check for updates, and enable swap memory.
|
||||
|
||||

|
||||
|
||||
### Monitoring
|
||||
|
||||
Quick access to the VM's built-in File Browser and Director interfaces, plus log exports.
|
||||
|
||||

|
||||
|
||||
The **File Browser** lets you browse config files, logs, database dumps, and UserSettings directly:
|
||||
|
||||

|
||||
|
||||
The **Director** shows live battlegroup stats, player counts, character transfer settings, and per-server details:
|
||||
|
||||

|
||||
|
||||
### Database
|
||||
|
||||
Back up and restore the battlegroup database. Stop the battlegroup first for best results.
|
||||
|
||||

|
||||
|
||||
### Characters
|
||||
|
||||
Edit player stats and inventory directly in the game database. **Stop the battlegroup and have the player logged out before editing.** Includes a searchable catalog of **1,000 items** (wiki scrape + game-file IDs for vehicle modules, patents, and more).
|
||||
|
||||

|
||||
|
||||
Search for any item by display name **or template ID** (e.g. `TreadwheelChassis_5`, `Patent`), filter by category, and add it to a character's inventory:
|
||||
|
||||

|
||||
|
||||
> [!WARNING]
|
||||
> Editing characters directly modifies the game database. This may corrupt save data and cause total character loss. Always take a database backup first.
|
||||
|
||||
| Section | What you can edit |
|
||||
|---------|------------------|
|
||||
| **Stats** | Max Health, Tech Knowledge Points, Hydration, Heat Exhaustion, Spice, Addiction Level, Tolerance, Eyes of Ibad |
|
||||
| **Inventory** | View all items, remove items, add items by name or template ID from a 1,000-item catalog with category filter |
|
||||
| **Stack limits** | Equipment (weapons/armor/tools) enforced at 1, resources at 100, consumables at 20 — warns before exceeding |
|
||||
| **Tech Tree** | Unlock or lock all fabrication recipes and blueprints — **Unlock All** merges all **356** game tech nodes into your save (not just ones already discovered) |
|
||||
| **Specializations** | Set level and XP for Combat, Crafting, Exploration, Gathering, Sabotage — unlock all 205 keystones (perks) per tree |
|
||||
| **Economy** | Set Solari and House Scrip balances |
|
||||
| **Faction Reputation** | Set reputation with Atreides, Harkonnen, and Smuggler factions |
|
||||
| **Cosmetics & Skins** | Searchable catalog of **621** cosmetics (dye packs, MTX skins, armor/weapon/vehicle swatches) — click Add/Remove per row, or **Unlock All Cosmetics & Swatches** in one shot |
|
||||
|
||||
Tech tree, specializations, economy, and faction reputation:
|
||||
|
||||

|
||||
|
||||
Cosmetics and skins — searchable list with Add/Remove toggle and bulk unlock:
|
||||
|
||||

|
||||
|
||||
> **Patents vs skins:** Building/furniture vendor packs are **Patent** items in inventory (search `furniture` or `CHOAM`). Weapon, armor, and vehicle looks are **cosmetics** — use the Cosmetics section above, not the item catalog. **Unlock All Recipes** only covers the fabrication tech tree.
|
||||
|
||||
### Game Config
|
||||
|
||||
Visual editor for all gameplay settings. **Stop the battlegroup before editing** — changes apply on next start.
|
||||
|
||||

|
||||
|
||||
Available settings:
|
||||
|
||||
| Category | Settings |
|
||||
|----------|----------|
|
||||
| **PvP & Security** | Force PvP on all partitions, security zones |
|
||||
| **Environment** | Coriolis storms, sandstorms, sandstorm treasure |
|
||||
| **Sandworm** | Enable/disable, danger zones, vehicle collision, invulnerability timers |
|
||||
| **Economy & Resources** | Mining multipliers, PvP resource multiplier, item decay rate, vehicle durability |
|
||||
| **Building** | Max landclaims, blueprint extensions, base backup extensions, restriction limits |
|
||||
| **Server** | Display name, login password, game port, IGW port |
|
||||
|
||||
### Settings
|
||||
|
||||
Change the VM password and rotate SSH keys.
|
||||
|
||||

|
||||
|
||||
### Experimental
|
||||
|
||||
> [!WARNING]
|
||||
> Features on this tab are untested and may break your battlegroup. Always take a database backup before making changes.
|
||||
|
||||
**Multi-Sietch** allows you to add additional Hagga Basin instances (sietches) to your battlegroup. All sietches share the same Overmap, Deep Desert, Arrakeen, Harkonnen Village, and instanced content (dungeons, story missions). Players from different sietches will see each other in shared areas but not in their respective Hagga Basin maps.
|
||||
|
||||
| Sietches | Estimated RAM |
|
||||
|----------|--------------|
|
||||
| 1 (default) | ~18 GB |
|
||||
| 2 | ~30 GB |
|
||||
| 3 | ~42 GB |
|
||||
|
||||
Each sietch requires approximately **12 GB RAM**. The Overmap and infrastructure (database, RabbitMQ, Kubernetes) use an additional ~6 GB on top of that.
|
||||
|
||||
**Port forwarding:** Each additional sietch adds a game server pod using host networking. When in doubt, forward **UDP 7777–7900** to your VM to cover any additional game server ports.
|
||||
|
||||
After adding or removing a sietch, **restart the battlegroup** for changes to take effect.
|
||||
|
||||
## How It Works
|
||||
|
||||
The app is a lightweight Node.js server that:
|
||||
|
||||
1. Calls **PowerShell** for Hyper-V operations (start/stop VM, query status, import, configure)
|
||||
2. Uses **SSH** to communicate with the VM for battlegroup commands (same key and mechanism as the official scripts)
|
||||
3. Reads and writes **INI config files** on the VM for game settings
|
||||
4. Queries the **PostgreSQL** database via `kubectl exec` for character editing
|
||||
5. Serves a static web UI that talks to the REST API and receives real-time output over WebSocket
|
||||
|
||||
No data leaves your machine. Everything runs locally on `localhost:3000`.
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
├── server.js # Express + WebSocket server, all API routes
|
||||
├── lib/
|
||||
│ ├── powershell.js # PowerShell execution wrapper
|
||||
│ └── ssh.js # SSH execution wrapper (with timeout + TTY support)
|
||||
├── public/
|
||||
│ ├── index.html # UI (dashboard, setup wizard, game config, character editor, all tabs)
|
||||
│ ├── css/style.css # Dune-themed dark UI
|
||||
│ ├── js/app.js # Frontend logic (tabs, wizard, config editor, character editor, API calls)
|
||||
│ └── data/
|
||||
│ ├── item-catalog.json # 1,000 items (wiki + CUE4Parse game-file IDs)
|
||||
│ ├── cosmetic-catalog.json # 621 cosmetics/skins/swatches (from Systems.pak)
|
||||
│ └── stat-reference.json # Character stat keys and inventory type mapping
|
||||
├── scripts/
|
||||
│ └── build-cosmetic-catalog.py # Regenerate cosmetic-catalog.json from game paks
|
||||
├── tools/
|
||||
│ └── Cue4ParsePatents/ # CUE4Parse scanner for item/patent/module IDs in game files
|
||||
├── start_as_admin.bat # One-click Windows launcher (auto-elevates to admin)
|
||||
├── docs/screenshots/ # README screenshots
|
||||
└── package.json
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
| Setting | Default | Where to change |
|
||||
|---------|---------|----------------|
|
||||
| Server install path | `C:\Program Files (x86)\Steam\steamapps\common\Dune Awakening Self-Hosted Server\` | `DEFAULT_SERVER_PATH` in `server.js` |
|
||||
| SSH key path | `%LOCALAPPDATA%\DuneAwakeningServer\sshKey` | `getKeyPath()` in `lib/ssh.js` |
|
||||
| VM name | `dune-awakening` | `VM_NAME` in `server.js` |
|
||||
| Web UI port | `3000` | `PORT` in `server.js` or `PORT` env variable |
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Problem | Fix |
|
||||
|---------|-----|
|
||||
| _"You do not have the required permission"_ | Run the app as **Administrator** (use `start_as_admin.bat` or an admin terminal) |
|
||||
| _"Cannot find module 'express'"_ | Dependencies weren't installed. Run `npm install` in the project folder, then try again |
|
||||
| _"running scripts is disabled on this system"_ | PowerShell execution policy is blocking npm. Open PowerShell as Admin and run: `Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned` |
|
||||
| _"EADDRINUSE: address already in use :::3000"_ | Another process is on port 3000. Kill it or set a different port: `set PORT=3001 && npm start` |
|
||||
| _"Node.js is required but not found"_ | Install Node.js 18+ from [nodejs.org](https://nodejs.org) and restart your terminal |
|
||||
| Battlegroup says "Starting" then nothing | Check the console output — it may be a timeout or SSH issue. Make sure the VM is fully booted and SSH is reachable. The battlegroup can take several minutes to start on first boot |
|
||||
| Battlegroup shows as inactive despite VM running | The battlegroup may not have been started yet — click Start and wait. If it was recently started, the game servers take a few minutes to reach "Running" state |
|
||||
| SSH connection failures | Make sure the VM is running and you've completed initial setup (the SSH key is generated in step 4) |
|
||||
| _"No .vmcx file found"_ | Verify the Dune Awakening Self-Hosted Server is installed in Steam and the path is correct |
|
||||
| VM fails to start with memory error | Your system doesn't have enough free RAM. Lower the memory allocation and enable swap memory |
|
||||
| _"No battlegroup found"_ on start | The initial setup didn't complete. Re-run the setup wizard or use `battlegroup.bat → initial-setup` |
|
||||
| Database stuck on "Modifying" after first start | The app auto-detects and fixes this (placeholder image tags). If it persists, restart the battlegroup — the app will patch the Kubernetes CRD with the correct image versions on the next start |
|
||||
| Server not in finder / 0 ping | Set visibility to your **public IP** in Game Config, forward **TCP 31982**, **Director NodePort**, and **UDP 7777–7810** to the **VM IP**, then **stop and start** the battlegroup. Look under **Servers → Experimental** in-game (not Official/Private). |
|
||||
| Server visible in browser but players can't connect | Make sure **Server Visibility** is set to your public IP (not a private/LAN IP), the battlegroup was **stopped and started** after changing it, and the required ports are forwarded on your router to the VM (see Game Config port-forward panel) |
|
||||
| Visibility IP reverts to LAN after applying | Update the app — this was a bug in older versions where `settings.conf` wasn't being read correctly |
|
||||
|
||||
## Ports
|
||||
|
||||
If players outside your LAN need to connect, forward these on your router **to your VM's IP address** (not your Windows host IP):
|
||||
|
||||
| Port | Protocol | Purpose |
|
||||
|------|----------|---------|
|
||||
| 7777–7800 | UDP | Game server traffic |
|
||||
| Director NodePort | TCP | Client matchmaking (check `kubectl get svc -A` for the actual port — it's randomized by Kubernetes, typically 30000–32767) |
|
||||
|
||||
> [!TIP]
|
||||
> To find the Director NodePort, look at the Dashboard — it's shown in the Quick Links section. You can also SSH into the VM and run:
|
||||
> ```
|
||||
> sudo kubectl get svc -A | grep bgd-svc
|
||||
> ```
|
||||
> The number after `11717:` (e.g. `11717:31642`) is the NodePort to forward.
|
||||
|
||||
The web UI itself (`localhost:3000`) does **not** need to be exposed — it's for local management only.
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 55 KiB |
|
After Width: | Height: | Size: 66 KiB |
|
After Width: | Height: | Size: 84 KiB |
|
After Width: | Height: | Size: 63 KiB |
BIN
docs/reference-repos/the4rchangel/docs/screenshots/dashboard.png
Normal file
|
After Width: | Height: | Size: 57 KiB |
BIN
docs/reference-repos/the4rchangel/docs/screenshots/database.png
Normal file
|
After Width: | Height: | Size: 26 KiB |
BIN
docs/reference-repos/the4rchangel/docs/screenshots/director.png
Normal file
|
After Width: | Height: | Size: 102 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 63 KiB |
|
After Width: | Height: | Size: 32 KiB |
BIN
docs/reference-repos/the4rchangel/docs/screenshots/settings.png
Normal file
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 36 KiB |
114
docs/reference-repos/the4rchangel/lib/paths.js
Normal file
@@ -0,0 +1,114 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
const { execSync } = require('child_process');
|
||||
|
||||
let cachedLocalAppData = null;
|
||||
|
||||
function isWsl() {
|
||||
if (process.env.WSL_DISTRO_NAME) return true;
|
||||
try {
|
||||
return /microsoft/i.test(fs.readFileSync('/proc/version', 'utf8'));
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function getWindowsLocalAppData() {
|
||||
if (cachedLocalAppData) return cachedLocalAppData;
|
||||
if (process.env.LOCALAPPDATA) {
|
||||
cachedLocalAppData = process.env.LOCALAPPDATA;
|
||||
return cachedLocalAppData;
|
||||
}
|
||||
if (process.env.USERPROFILE) {
|
||||
cachedLocalAppData = path.join(process.env.USERPROFILE, 'AppData', 'Local');
|
||||
return cachedLocalAppData;
|
||||
}
|
||||
try {
|
||||
const out = execSync(
|
||||
'powershell.exe -NoProfile -NonInteractive -Command "[Environment]::GetFolderPath(\'LocalApplicationData\')"',
|
||||
{ encoding: 'utf8', windowsHide: true, timeout: 15000 }
|
||||
).trim();
|
||||
if (out) {
|
||||
cachedLocalAppData = out;
|
||||
return cachedLocalAppData;
|
||||
}
|
||||
} catch { /* fall through */ }
|
||||
throw new Error(
|
||||
'Could not resolve Windows LOCALAPPDATA. Start the manager with start_as_admin.bat on Windows.'
|
||||
);
|
||||
}
|
||||
|
||||
function windowsToWslPath(winPath) {
|
||||
const normalized = winPath.replace(/\\/g, '/');
|
||||
const match = normalized.match(/^([A-Za-z]):\/(.*)$/);
|
||||
if (!match) return winPath;
|
||||
return `/mnt/${match[1].toLowerCase()}/${match[2]}`;
|
||||
}
|
||||
|
||||
function getDuneKeyDir() {
|
||||
return path.join(getWindowsLocalAppData(), 'DuneAwakeningServer');
|
||||
}
|
||||
|
||||
function getKeyPath() {
|
||||
return path.join(getDuneKeyDir(), 'sshKey');
|
||||
}
|
||||
|
||||
function getWslKeyMirrorPath() {
|
||||
return path.join(os.homedir(), '.dune-awakening-server-manager', 'sshKey');
|
||||
}
|
||||
|
||||
function sshKeyExists() {
|
||||
try {
|
||||
const keyPath = isWsl() ? windowsToWslPath(getKeyPath()) : getKeyPath();
|
||||
return fs.existsSync(keyPath);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function getSshIdentityPath() {
|
||||
const keyPath = getKeyPath();
|
||||
if (!isWsl()) return keyPath;
|
||||
|
||||
const source = windowsToWslPath(keyPath);
|
||||
if (!fs.existsSync(source)) {
|
||||
throw new Error(
|
||||
`SSH key not found at ${keyPath}. Use Settings → Rotate SSH Key, or re-run Setup → Security.`
|
||||
);
|
||||
}
|
||||
|
||||
const localKey = getWslKeyMirrorPath();
|
||||
fs.mkdirSync(path.dirname(localKey), { recursive: true });
|
||||
|
||||
const srcStat = fs.statSync(source);
|
||||
let needsCopy = !fs.existsSync(localKey);
|
||||
if (!needsCopy) {
|
||||
const dstStat = fs.statSync(localKey);
|
||||
needsCopy = srcStat.mtimeMs > dstStat.mtimeMs || srcStat.size !== dstStat.size;
|
||||
}
|
||||
if (needsCopy) {
|
||||
fs.copyFileSync(source, localKey);
|
||||
}
|
||||
fs.chmodSync(localKey, 0o600);
|
||||
return localKey;
|
||||
}
|
||||
|
||||
function removeWslKeyMirror() {
|
||||
try {
|
||||
const mirror = getWslKeyMirrorPath();
|
||||
if (fs.existsSync(mirror)) fs.unlinkSync(mirror);
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
isWsl,
|
||||
getWindowsLocalAppData,
|
||||
getDuneKeyDir,
|
||||
getKeyPath,
|
||||
getSshIdentityPath,
|
||||
getWslKeyMirrorPath,
|
||||
sshKeyExists,
|
||||
removeWslKeyMirror,
|
||||
windowsToWslPath,
|
||||
};
|
||||
52
docs/reference-repos/the4rchangel/lib/powershell.js
Normal file
@@ -0,0 +1,52 @@
|
||||
const { spawn } = require('child_process');
|
||||
|
||||
const PS_ARGS = ['-NoProfile', '-NoLogo', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-Command'];
|
||||
|
||||
function run(command, onData) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const ps = spawn('powershell.exe', [...PS_ARGS, command], {
|
||||
windowsHide: true,
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
});
|
||||
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
|
||||
ps.stdout.on('data', (chunk) => {
|
||||
const text = chunk.toString();
|
||||
stdout += text;
|
||||
if (onData) onData(text);
|
||||
});
|
||||
|
||||
ps.stderr.on('data', (chunk) => {
|
||||
const text = chunk.toString();
|
||||
stderr += text;
|
||||
if (onData) onData(text);
|
||||
});
|
||||
|
||||
ps.on('error', (err) => reject(err));
|
||||
ps.on('close', (code) => {
|
||||
if (code === 0) {
|
||||
resolve(stdout.trim());
|
||||
} else {
|
||||
const err = new Error(stderr.trim() || `PowerShell exited with code ${code}`);
|
||||
err.code = code;
|
||||
err.stdout = stdout;
|
||||
err.stderr = stderr;
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function runJson(command) {
|
||||
return run(command).then((out) => {
|
||||
try {
|
||||
return JSON.parse(out);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { run, runJson };
|
||||
98
docs/reference-repos/the4rchangel/lib/ssh.js
Normal file
@@ -0,0 +1,98 @@
|
||||
const { spawn } = require('child_process');
|
||||
const paths = require('./paths');
|
||||
|
||||
/**
|
||||
* @param {string} ip
|
||||
* @param {string} command
|
||||
* @param {function} [onData] — streaming callback
|
||||
* @param {object} [opts]
|
||||
* @param {number} [opts.timeout] — kill after N ms (default 300000 = 5 min)
|
||||
* @param {boolean} [opts.tty] — force pseudo-terminal (-tt)
|
||||
* @param {string} [opts.stdin] — data to write to stdin then close
|
||||
*/
|
||||
function run(ip, command, onData, opts = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let keyPath;
|
||||
try {
|
||||
keyPath = paths.getSshIdentityPath();
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
const timeout = opts.timeout || 300000;
|
||||
|
||||
const args = [
|
||||
'-o', 'StrictHostKeyChecking=no',
|
||||
'-o', 'LogLevel=QUIET',
|
||||
'-o', 'ConnectTimeout=10',
|
||||
'-o', 'ServerAliveInterval=15',
|
||||
'-o', 'ServerAliveCountMax=4',
|
||||
];
|
||||
|
||||
if (opts.tty) args.push('-tt');
|
||||
|
||||
args.push('-i', keyPath, `dune@${ip}`, command);
|
||||
|
||||
const needsStdinPipe = opts.tty || opts.stdin != null;
|
||||
|
||||
const proc = spawn('ssh', args, {
|
||||
windowsHide: true,
|
||||
stdio: [needsStdinPipe ? 'pipe' : 'ignore', 'pipe', 'pipe'],
|
||||
});
|
||||
|
||||
if (needsStdinPipe) {
|
||||
if (opts.stdin != null) proc.stdin.write(opts.stdin);
|
||||
proc.stdin.end();
|
||||
}
|
||||
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
let killed = false;
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
killed = true;
|
||||
proc.kill('SIGKILL');
|
||||
}, timeout);
|
||||
|
||||
proc.stdout.on('data', (chunk) => {
|
||||
const text = chunk.toString();
|
||||
stdout += text;
|
||||
if (onData) onData(text);
|
||||
});
|
||||
|
||||
proc.stderr.on('data', (chunk) => {
|
||||
const text = chunk.toString();
|
||||
stderr += text;
|
||||
if (onData) onData(text);
|
||||
});
|
||||
|
||||
proc.on('error', (err) => { clearTimeout(timer); reject(err); });
|
||||
proc.on('close', (code) => {
|
||||
clearTimeout(timer);
|
||||
if (killed) {
|
||||
const err = new Error(`SSH command timed out after ${timeout / 1000}s`);
|
||||
err.stdout = stdout;
|
||||
err.stderr = stderr;
|
||||
return reject(err);
|
||||
}
|
||||
if (code === 0) {
|
||||
resolve(stdout.trim());
|
||||
} else {
|
||||
const stderrTrim = stderr.trim();
|
||||
const stdoutTrim = stdout.trim();
|
||||
const ptyClosed = /Connection to .+ closed\.?$/m.test(stderrTrim);
|
||||
if (stdoutTrim && (!stderrTrim || ptyClosed)) {
|
||||
resolve(stdoutTrim);
|
||||
return;
|
||||
}
|
||||
const err = new Error(stderrTrim || stdoutTrim || `SSH exited with code ${code}`);
|
||||
err.code = code;
|
||||
err.stdout = stdout;
|
||||
err.stderr = stderr;
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { run, getKeyPath: paths.getKeyPath, sshKeyExists: paths.sshKeyExists };
|
||||
126
docs/reference-repos/the4rchangel/lib/vm.js
Normal file
@@ -0,0 +1,126 @@
|
||||
const ps = require('./powershell');
|
||||
|
||||
const VM_NAME = 'dune-awakening';
|
||||
const MIN_MEMORY_GB = 12;
|
||||
const STEP_DOWN_GB = [40, 32, 30, 24, 20, 18, 16, 14, 12];
|
||||
|
||||
function isOutOfMemoryError(err) {
|
||||
const msg = `${err && err.message || ''}\n${err && err.stderr || ''}`;
|
||||
return /OutOfMemory|Not enough memory|0x8007000E/i.test(msg);
|
||||
}
|
||||
|
||||
function shortVmError(message) {
|
||||
if (!message) return 'Unknown error';
|
||||
if (/Not enough memory|OutOfMemory|0x8007000E/i.test(message)) {
|
||||
const match = message.match(/ram size (\d+) megabytes/i);
|
||||
const gb = match ? Math.round(parseInt(match[1], 10) / 1024) : null;
|
||||
return gb
|
||||
? `Not enough free RAM on this PC to start the VM at ${gb} GB. Choose a lower memory setting and try again.`
|
||||
: 'Not enough free RAM on this PC to start the VM. Choose a lower memory setting and try again.';
|
||||
}
|
||||
const line = message.split(/\r?\n/).find((l) => l.trim() && !l.startsWith('At line:'));
|
||||
return (line || message).trim().slice(0, 240);
|
||||
}
|
||||
|
||||
function buildStepDownList(configuredGB) {
|
||||
const cap = Math.max(MIN_MEMORY_GB, configuredGB || 30);
|
||||
return STEP_DOWN_GB.filter((gb) => gb >= MIN_MEMORY_GB && gb <= cap)
|
||||
.filter((gb, i, arr) => arr.indexOf(gb) === i)
|
||||
.sort((a, b) => b - a);
|
||||
}
|
||||
|
||||
async function getVmStartupMemoryGB() {
|
||||
const raw = await ps.run(`(Get-VM -Name '${VM_NAME}').MemoryStartup`);
|
||||
const bytes = parseInt(String(raw).trim(), 10);
|
||||
if (!Number.isFinite(bytes) || bytes <= 0) return null;
|
||||
return Math.round(bytes / 1073741824);
|
||||
}
|
||||
|
||||
async function setVmMemoryGB(memoryGB, log) {
|
||||
const memBytes = memoryGB * 1073741824;
|
||||
if (log) log(`Setting VM memory to ${memoryGB} GB...\n`);
|
||||
await ps.run(`Set-VMMemory -VMName '${VM_NAME}' -StartupBytes ${memBytes}`, log);
|
||||
}
|
||||
|
||||
async function waitForVmIp(log, maxAttempts = 60) {
|
||||
for (let i = 0; i < maxAttempts; i++) {
|
||||
await new Promise((r) => setTimeout(r, 2000));
|
||||
try {
|
||||
const raw = await ps.run(
|
||||
`(Get-VMNetworkAdapter -VMName '${VM_NAME}').IPAddresses | ` +
|
||||
`Where-Object { $_ -match '^\\d+\\.\\d+\\.\\d+\\.\\d+$' } | ` +
|
||||
`Select-Object -First 1`
|
||||
);
|
||||
const ip = String(raw || '').trim();
|
||||
if (/^\d+\.\d+\.\d+\.\d+$/.test(ip)) return ip;
|
||||
} catch { /* keep waiting */ }
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async function startVm({ memoryGB, log, autoStepDown = true } = {}) {
|
||||
let configuredGB = memoryGB;
|
||||
if (!configuredGB) {
|
||||
try {
|
||||
configuredGB = await getVmStartupMemoryGB();
|
||||
} catch {
|
||||
configuredGB = 30;
|
||||
}
|
||||
}
|
||||
|
||||
const tryList = memoryGB
|
||||
? [memoryGB]
|
||||
: autoStepDown
|
||||
? buildStepDownList(configuredGB)
|
||||
: [configuredGB];
|
||||
|
||||
let lastErr = null;
|
||||
|
||||
for (let i = 0; i < tryList.length; i++) {
|
||||
const gb = tryList[i];
|
||||
const needsSet = gb !== configuredGB || Boolean(memoryGB) || i > 0;
|
||||
if (needsSet) {
|
||||
await setVmMemoryGB(gb, log);
|
||||
}
|
||||
|
||||
try {
|
||||
if (log) log('Starting VM...\n');
|
||||
await ps.run(`Start-VM -Name '${VM_NAME}' -ErrorAction Stop`, log);
|
||||
if (log) log('Waiting for IP address...\n');
|
||||
const ip = await waitForVmIp(log);
|
||||
if (ip) {
|
||||
if (log) log(`VM ready at ${ip}\n`);
|
||||
return { success: true, ip, memoryGB: gb };
|
||||
}
|
||||
if (log) log('VM started but could not detect IP within 2 minutes.\n');
|
||||
return { success: true, ip: null, memoryGB: gb };
|
||||
} catch (err) {
|
||||
lastErr = err;
|
||||
if (!isOutOfMemoryError(err) || memoryGB) throw err;
|
||||
if (log) {
|
||||
log(`\nNot enough host RAM for ${gb} GB (${shortVmError(err.message)})\n`);
|
||||
if (i < tryList.length - 1) {
|
||||
log(`Trying ${tryList[i + 1]} GB instead...\n`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const err = new Error(shortVmError(lastErr && lastErr.message));
|
||||
err.code = 'OUT_OF_MEMORY';
|
||||
err.cause = lastErr;
|
||||
err.attemptedGB = tryList;
|
||||
throw err;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
VM_NAME,
|
||||
MIN_MEMORY_GB,
|
||||
STEP_DOWN_GB,
|
||||
isOutOfMemoryError,
|
||||
shortVmError,
|
||||
getVmStartupMemoryGB,
|
||||
setVmMemoryGB,
|
||||
waitForVmIp,
|
||||
startVm,
|
||||
};
|
||||
850
docs/reference-repos/the4rchangel/package-lock.json
generated
Normal file
@@ -0,0 +1,850 @@
|
||||
{
|
||||
"name": "dune-server-manager",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "dune-server-manager",
|
||||
"version": "1.0.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"express": "^4.21.0",
|
||||
"ws": "^8.18.0"
|
||||
}
|
||||
},
|
||||
"node_modules/accepts": {
|
||||
"version": "1.3.8",
|
||||
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
|
||||
"integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mime-types": "~2.1.34",
|
||||
"negotiator": "0.6.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/array-flatten": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
|
||||
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/body-parser": {
|
||||
"version": "1.20.5",
|
||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.5.tgz",
|
||||
"integrity": "sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bytes": "~3.1.2",
|
||||
"content-type": "~1.0.5",
|
||||
"debug": "2.6.9",
|
||||
"depd": "2.0.0",
|
||||
"destroy": "~1.2.0",
|
||||
"http-errors": "~2.0.1",
|
||||
"iconv-lite": "~0.4.24",
|
||||
"on-finished": "~2.4.1",
|
||||
"qs": "~6.15.1",
|
||||
"raw-body": "~2.5.3",
|
||||
"type-is": "~1.6.18",
|
||||
"unpipe": "~1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8",
|
||||
"npm": "1.2.8000 || >= 1.4.16"
|
||||
}
|
||||
},
|
||||
"node_modules/bytes": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
|
||||
"integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/call-bind-apply-helpers": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
||||
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"function-bind": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/call-bound": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
|
||||
"integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.2",
|
||||
"get-intrinsic": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/content-disposition": {
|
||||
"version": "0.5.4",
|
||||
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
|
||||
"integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safe-buffer": "5.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/content-type": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
|
||||
"integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/cookie": {
|
||||
"version": "0.7.2",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
|
||||
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/cookie-signature": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz",
|
||||
"integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "2.6.9",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
||||
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/depd": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
|
||||
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/destroy": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
|
||||
"integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8",
|
||||
"npm": "1.2.8000 || >= 1.4.16"
|
||||
}
|
||||
},
|
||||
"node_modules/dunder-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.1",
|
||||
"es-errors": "^1.3.0",
|
||||
"gopd": "^1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/ee-first": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
||||
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/encodeurl": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
|
||||
"integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/es-define-property": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-errors": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
||||
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-object-atoms": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
|
||||
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/escape-html": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
|
||||
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/etag": {
|
||||
"version": "1.8.1",
|
||||
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
|
||||
"integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/express": {
|
||||
"version": "4.22.2",
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz",
|
||||
"integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"accepts": "~1.3.8",
|
||||
"array-flatten": "1.1.1",
|
||||
"body-parser": "~1.20.5",
|
||||
"content-disposition": "~0.5.4",
|
||||
"content-type": "~1.0.4",
|
||||
"cookie": "~0.7.1",
|
||||
"cookie-signature": "~1.0.6",
|
||||
"debug": "2.6.9",
|
||||
"depd": "2.0.0",
|
||||
"encodeurl": "~2.0.0",
|
||||
"escape-html": "~1.0.3",
|
||||
"etag": "~1.8.1",
|
||||
"finalhandler": "~1.3.1",
|
||||
"fresh": "~0.5.2",
|
||||
"http-errors": "~2.0.0",
|
||||
"merge-descriptors": "1.0.3",
|
||||
"methods": "~1.1.2",
|
||||
"on-finished": "~2.4.1",
|
||||
"parseurl": "~1.3.3",
|
||||
"path-to-regexp": "~0.1.12",
|
||||
"proxy-addr": "~2.0.7",
|
||||
"qs": "~6.15.1",
|
||||
"range-parser": "~1.2.1",
|
||||
"safe-buffer": "5.2.1",
|
||||
"send": "~0.19.0",
|
||||
"serve-static": "~1.16.2",
|
||||
"setprototypeof": "1.2.0",
|
||||
"statuses": "~2.0.1",
|
||||
"type-is": "~1.6.18",
|
||||
"utils-merge": "1.0.1",
|
||||
"vary": "~1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.10.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/finalhandler": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz",
|
||||
"integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "2.6.9",
|
||||
"encodeurl": "~2.0.0",
|
||||
"escape-html": "~1.0.3",
|
||||
"on-finished": "~2.4.1",
|
||||
"parseurl": "~1.3.3",
|
||||
"statuses": "~2.0.2",
|
||||
"unpipe": "~1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/forwarded": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
||||
"integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/fresh": {
|
||||
"version": "0.5.2",
|
||||
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
|
||||
"integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/function-bind": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/get-intrinsic": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.2",
|
||||
"es-define-property": "^1.0.1",
|
||||
"es-errors": "^1.3.0",
|
||||
"es-object-atoms": "^1.1.1",
|
||||
"function-bind": "^1.1.2",
|
||||
"get-proto": "^1.0.1",
|
||||
"gopd": "^1.2.0",
|
||||
"has-symbols": "^1.1.0",
|
||||
"hasown": "^2.0.2",
|
||||
"math-intrinsics": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/get-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"dunder-proto": "^1.0.1",
|
||||
"es-object-atoms": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/gopd": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/has-symbols": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
||||
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/hasown": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz",
|
||||
"integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"function-bind": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/http-errors": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
|
||||
"integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"depd": "~2.0.0",
|
||||
"inherits": "~2.0.4",
|
||||
"setprototypeof": "~1.2.0",
|
||||
"statuses": "~2.0.2",
|
||||
"toidentifier": "~1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/iconv-lite": {
|
||||
"version": "0.4.24",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
|
||||
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safer-buffer": ">= 2.1.2 < 3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/inherits": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/ipaddr.js": {
|
||||
"version": "1.9.1",
|
||||
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
||||
"integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/math-intrinsics": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/media-typer": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
|
||||
"integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/merge-descriptors": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
|
||||
"integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/methods": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
|
||||
"integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/mime": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
|
||||
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"mime": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-db": {
|
||||
"version": "1.52.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-types": {
|
||||
"version": "2.1.35",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mime-db": "1.52.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/negotiator": {
|
||||
"version": "0.6.3",
|
||||
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
|
||||
"integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/object-inspect": {
|
||||
"version": "1.13.4",
|
||||
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
|
||||
"integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/on-finished": {
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
|
||||
"integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ee-first": "1.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/parseurl": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
|
||||
"integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/path-to-regexp": {
|
||||
"version": "0.1.13",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz",
|
||||
"integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/proxy-addr": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
||||
"integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"forwarded": "0.2.0",
|
||||
"ipaddr.js": "1.9.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/qs": {
|
||||
"version": "6.15.2",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz",
|
||||
"integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"side-channel": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/range-parser": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
|
||||
"integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/raw-body": {
|
||||
"version": "2.5.3",
|
||||
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz",
|
||||
"integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bytes": "~3.1.2",
|
||||
"http-errors": "~2.0.1",
|
||||
"iconv-lite": "~0.4.24",
|
||||
"unpipe": "~1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/safe-buffer": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/safer-buffer": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/send": {
|
||||
"version": "0.19.2",
|
||||
"resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz",
|
||||
"integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "2.6.9",
|
||||
"depd": "2.0.0",
|
||||
"destroy": "1.2.0",
|
||||
"encodeurl": "~2.0.0",
|
||||
"escape-html": "~1.0.3",
|
||||
"etag": "~1.8.1",
|
||||
"fresh": "~0.5.2",
|
||||
"http-errors": "~2.0.1",
|
||||
"mime": "1.6.0",
|
||||
"ms": "2.1.3",
|
||||
"on-finished": "~2.4.1",
|
||||
"range-parser": "~1.2.1",
|
||||
"statuses": "~2.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/send/node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/serve-static": {
|
||||
"version": "1.16.3",
|
||||
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz",
|
||||
"integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"encodeurl": "~2.0.0",
|
||||
"escape-html": "~1.0.3",
|
||||
"parseurl": "~1.3.3",
|
||||
"send": "~0.19.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/setprototypeof": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
|
||||
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/side-channel": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
|
||||
"integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"object-inspect": "^1.13.3",
|
||||
"side-channel-list": "^1.0.0",
|
||||
"side-channel-map": "^1.0.1",
|
||||
"side-channel-weakmap": "^1.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/side-channel-list": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz",
|
||||
"integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"object-inspect": "^1.13.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/side-channel-map": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
|
||||
"integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bound": "^1.0.2",
|
||||
"es-errors": "^1.3.0",
|
||||
"get-intrinsic": "^1.2.5",
|
||||
"object-inspect": "^1.13.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/side-channel-weakmap": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
|
||||
"integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bound": "^1.0.2",
|
||||
"es-errors": "^1.3.0",
|
||||
"get-intrinsic": "^1.2.5",
|
||||
"object-inspect": "^1.13.3",
|
||||
"side-channel-map": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/statuses": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
|
||||
"integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/toidentifier": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
|
||||
"integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/type-is": {
|
||||
"version": "1.6.18",
|
||||
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
|
||||
"integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"media-typer": "0.3.0",
|
||||
"mime-types": "~2.1.24"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/unpipe": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
|
||||
"integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/utils-merge": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
|
||||
"integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vary": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
||||
"integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.20.1",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.20.1.tgz",
|
||||
"integrity": "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": ">=5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
"optional": true
|
||||
},
|
||||
"utf-8-validate": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
15
docs/reference-repos/the4rchangel/package.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "dune-server-manager",
|
||||
"version": "1.0.7",
|
||||
"description": "Web-based management UI for Dune: Awakening Self-Hosted Servers",
|
||||
"main": "server.js",
|
||||
"scripts": {
|
||||
"start": "node server.js"
|
||||
},
|
||||
"keywords": ["dune", "awakening", "server", "manager", "battlegroup"],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"express": "^4.21.0",
|
||||
"ws": "^8.18.0"
|
||||
}
|
||||
}
|
||||
668
docs/reference-repos/the4rchangel/public/css/style.css
Normal file
@@ -0,0 +1,668 @@
|
||||
/* ========================================================================
|
||||
Dune: Awakening Server Manager — Theme
|
||||
======================================================================== */
|
||||
|
||||
:root {
|
||||
--bg: #08080c;
|
||||
--surface: #111118;
|
||||
--surface2: #1a1a24;
|
||||
--border: #2a2a38;
|
||||
--gold: #c4973a;
|
||||
--gold-dim: #8b6914;
|
||||
--gold-glow: rgba(196, 151, 58, .15);
|
||||
--green: #2d8a4e;
|
||||
--green-glow: rgba(45, 138, 78, .2);
|
||||
--red: #b83c3c;
|
||||
--red-glow: rgba(184, 60, 60, .2);
|
||||
--yellow: #b89c3c;
|
||||
--blue: #3c6fb8;
|
||||
--text: #ddd6c8;
|
||||
--text-dim: #7a7468;
|
||||
--mono: 'JetBrains Mono', monospace;
|
||||
--sans: 'Inter', system-ui, sans-serif;
|
||||
--radius: 8px;
|
||||
}
|
||||
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
|
||||
html { font-size: 14px; }
|
||||
|
||||
body {
|
||||
font-family: var(--sans);
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
/* ------ Topbar ------ */
|
||||
.topbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: .75rem 1.5rem;
|
||||
background: var(--surface);
|
||||
border-bottom: 1px solid var(--border);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.topbar-left { display: flex; align-items: baseline; gap: .75rem; }
|
||||
.logo {
|
||||
font-size: 1.15rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: .12em;
|
||||
color: var(--gold);
|
||||
}
|
||||
.logo-dim { color: var(--text-dim); }
|
||||
.logo-sub {
|
||||
font-size: .7rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: .2em;
|
||||
color: var(--text-dim);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.topbar-right { display: flex; gap: .75rem; }
|
||||
|
||||
/* Status chips */
|
||||
.status-chip {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: .4rem;
|
||||
padding: .3rem .7rem;
|
||||
background: var(--surface2);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 999px;
|
||||
font-size: .75rem;
|
||||
}
|
||||
.status-chip .dot {
|
||||
width: 8px; height: 8px;
|
||||
border-radius: 50%;
|
||||
background: var(--text-dim);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.status-chip.running .dot { background: #3ddc84; box-shadow: 0 0 6px rgba(61,220,132,.5); }
|
||||
.status-chip.stopped .dot { background: var(--red); }
|
||||
.status-chip.unknown .dot { background: var(--text-dim); }
|
||||
.chip-label { color: var(--text-dim); }
|
||||
.chip-value { color: var(--text); font-weight: 500; }
|
||||
|
||||
/* ------ Tabs ------ */
|
||||
.tabs {
|
||||
display: flex;
|
||||
gap: 0;
|
||||
padding: 0 1.5rem;
|
||||
background: var(--surface);
|
||||
border-bottom: 1px solid var(--border);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.tab {
|
||||
padding: .65rem 1.25rem;
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-dim);
|
||||
font: inherit;
|
||||
font-size: .8rem;
|
||||
font-weight: 500;
|
||||
letter-spacing: .04em;
|
||||
cursor: pointer;
|
||||
border-bottom: 2px solid transparent;
|
||||
transition: color .15s, border-color .15s;
|
||||
}
|
||||
.tab:hover { color: var(--text); }
|
||||
.tab.active { color: var(--gold); border-bottom-color: var(--gold); }
|
||||
|
||||
/* ------ Content ------ */
|
||||
.content {
|
||||
flex: 1;
|
||||
padding: 1.5rem;
|
||||
overflow-y: auto;
|
||||
padding-bottom: 60px;
|
||||
}
|
||||
.panel { display: none; }
|
||||
.panel.active { display: block; animation: fadeIn .2s ease; }
|
||||
|
||||
@keyframes fadeIn { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; } }
|
||||
|
||||
/* ------ Cards ------ */
|
||||
.card-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(380px, 1fr)); gap: 1rem; margin-bottom: 1rem; }
|
||||
.card {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
overflow: hidden;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.card-row > .card { margin-bottom: 0; }
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: .75rem 1rem;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.card-header h2 { font-size: .85rem; font-weight: 600; letter-spacing: .04em; }
|
||||
.card-body { padding: 1rem; }
|
||||
.card-actions {
|
||||
display: flex;
|
||||
gap: .5rem;
|
||||
padding: .75rem 1rem;
|
||||
border-top: 1px solid var(--border);
|
||||
background: rgba(0,0,0,.15);
|
||||
}
|
||||
.card-desc {
|
||||
color: var(--text-dim);
|
||||
font-size: .82rem;
|
||||
margin-bottom: 1rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.card-desc code {
|
||||
background: var(--surface2);
|
||||
padding: .1em .35em;
|
||||
border-radius: 3px;
|
||||
font-family: var(--mono);
|
||||
font-size: .8em;
|
||||
}
|
||||
|
||||
/* Badges */
|
||||
.badge {
|
||||
font-size: .65rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: .05em;
|
||||
text-transform: uppercase;
|
||||
padding: .2rem .55rem;
|
||||
border-radius: 999px;
|
||||
background: var(--surface2);
|
||||
color: var(--text-dim);
|
||||
}
|
||||
.badge.running { background: var(--green-glow); color: #3ddc84; }
|
||||
.badge.stopped { background: var(--red-glow); color: #ef5350; }
|
||||
|
||||
/* Stat grid */
|
||||
.stat-grid { display: grid; grid-template-columns: 1fr 1fr; gap: .6rem; }
|
||||
.stat { display: flex; flex-direction: column; gap: .15rem; }
|
||||
.stat-label { font-size: .7rem; color: var(--text-dim); text-transform: uppercase; letter-spacing: .06em; }
|
||||
.stat-value { font-size: .9rem; font-weight: 500; font-family: var(--mono); }
|
||||
|
||||
/* Battlegroup status output */
|
||||
.bg-status-output {
|
||||
font-family: var(--mono);
|
||||
font-size: .75rem;
|
||||
color: var(--text-dim);
|
||||
white-space: pre-wrap;
|
||||
max-height: 180px;
|
||||
overflow-y: auto;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* Quick links */
|
||||
.link-row { display: flex; gap: .75rem; flex-wrap: wrap; }
|
||||
.link-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: .4rem;
|
||||
padding: .5rem 1rem;
|
||||
background: var(--surface2);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
color: var(--gold);
|
||||
text-decoration: none;
|
||||
font-size: .8rem;
|
||||
font-weight: 500;
|
||||
transition: background .15s, border-color .15s;
|
||||
}
|
||||
.link-btn:hover { background: var(--gold-glow); border-color: var(--gold-dim); }
|
||||
.link-btn.disabled { opacity: .4; pointer-events: none; }
|
||||
|
||||
/* ------ Buttons ------ */
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: .4rem;
|
||||
padding: .45rem .9rem;
|
||||
border: 1px solid transparent;
|
||||
border-radius: var(--radius);
|
||||
font: inherit;
|
||||
font-size: .78rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: background .15s, box-shadow .15s, opacity .15s;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.btn:disabled { opacity: .35; cursor: not-allowed; }
|
||||
.btn-lg { padding: .55rem 1.1rem; font-size: .82rem; }
|
||||
|
||||
.btn-green { background: var(--green); color: #fff; }
|
||||
.btn-green:not(:disabled):hover { box-shadow: 0 0 12px var(--green-glow); }
|
||||
.btn-red { background: var(--red); color: #fff; }
|
||||
.btn-red:not(:disabled):hover { box-shadow: 0 0 12px var(--red-glow); }
|
||||
.btn-yellow { background: var(--yellow); color: #111; }
|
||||
.btn-yellow:not(:disabled):hover { opacity: .9; }
|
||||
.btn-blue { background: var(--blue); color: #fff; }
|
||||
.btn-blue:not(:disabled):hover { opacity: .9; }
|
||||
|
||||
.btn-group { display: flex; flex-wrap: wrap; gap: .5rem; }
|
||||
|
||||
/* ------ Forms ------ */
|
||||
.settings-form { display: flex; flex-direction: column; gap: .75rem; max-width: 320px; }
|
||||
.field { display: flex; flex-direction: column; gap: .25rem; }
|
||||
.field label { font-size: .72rem; color: var(--text-dim); text-transform: uppercase; letter-spacing: .05em; }
|
||||
.field input {
|
||||
padding: .5rem .65rem;
|
||||
background: var(--bg);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
color: var(--text);
|
||||
font: inherit;
|
||||
font-size: .85rem;
|
||||
}
|
||||
.field input:focus { outline: none; border-color: var(--gold-dim); box-shadow: 0 0 0 2px var(--gold-glow); }
|
||||
|
||||
/* ------ Console ------ */
|
||||
.console-wrapper {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 100;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-height: 50vh;
|
||||
transition: max-height .25s ease;
|
||||
}
|
||||
.console-wrapper.collapsed { max-height: 36px; }
|
||||
.console-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: .4rem;
|
||||
padding: .5rem 1rem;
|
||||
background: var(--surface2);
|
||||
border: none;
|
||||
border-top: 1px solid var(--border);
|
||||
color: var(--text-dim);
|
||||
font: inherit;
|
||||
font-size: .72rem;
|
||||
font-weight: 500;
|
||||
letter-spacing: .04em;
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.console-toggle:hover { color: var(--text); }
|
||||
.console-badge {
|
||||
font-size: .6rem;
|
||||
padding: .1rem .35rem;
|
||||
border-radius: 999px;
|
||||
background: var(--gold);
|
||||
color: #111;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.console-body {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
background: #050508;
|
||||
padding: .75rem 1rem;
|
||||
min-height: 0;
|
||||
}
|
||||
.collapsed .console-body { display: none; }
|
||||
#console-output {
|
||||
font-family: var(--mono);
|
||||
font-size: .72rem;
|
||||
line-height: 1.55;
|
||||
color: #9a9a8c;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
/* ------ Overlay ------ */
|
||||
.overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 200;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(8, 8, 12, .8);
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
.overlay[hidden] { display: none; }
|
||||
.spinner {
|
||||
width: 36px; height: 36px;
|
||||
border: 3px solid var(--border);
|
||||
border-top-color: var(--gold);
|
||||
border-radius: 50%;
|
||||
animation: spin .7s linear infinite;
|
||||
}
|
||||
@keyframes spin { to { transform: rotate(360deg); } }
|
||||
.overlay-text { margin-top: .75rem; color: var(--text-dim); font-size: .85rem; }
|
||||
|
||||
/* ------ Wizard ------ */
|
||||
.wizard-card { max-width: 720px; }
|
||||
.wizard-progress {
|
||||
height: 3px;
|
||||
background: var(--surface2);
|
||||
}
|
||||
.wizard-progress-fill {
|
||||
height: 100%;
|
||||
background: var(--gold);
|
||||
transition: width .3s ease;
|
||||
}
|
||||
.wizard-step { display: none; }
|
||||
.wizard-step.active { display: block; animation: fadeIn .2s ease; }
|
||||
.wizard-step h3 {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: .35rem;
|
||||
color: var(--gold);
|
||||
}
|
||||
.wizard-nav {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
/* Preflight items */
|
||||
.preflight-results { display: flex; flex-direction: column; gap: .5rem; margin-top: .75rem; }
|
||||
.preflight-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: .5rem;
|
||||
padding: .5rem .75rem;
|
||||
background: var(--surface2);
|
||||
border-radius: var(--radius);
|
||||
font-size: .82rem;
|
||||
}
|
||||
.preflight-item.pass { border-left: 3px solid #3ddc84; }
|
||||
.preflight-item.fail { border-left: 3px solid var(--red); }
|
||||
.pf-icon { font-size: 1rem; width: 1.2em; text-align: center; }
|
||||
|
||||
/* Setup forms */
|
||||
.setup-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: .75rem;
|
||||
margin-bottom: .75rem;
|
||||
}
|
||||
@media (max-width: 600px) { .setup-row { grid-template-columns: 1fr; } }
|
||||
|
||||
.select-input {
|
||||
width: 100%;
|
||||
padding: .5rem .65rem;
|
||||
background: var(--bg);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
color: var(--text);
|
||||
font: inherit;
|
||||
font-size: .85rem;
|
||||
appearance: none;
|
||||
}
|
||||
.select-input:focus { outline: none; border-color: var(--gold-dim); box-shadow: 0 0 0 2px var(--gold-glow); }
|
||||
select.select-input {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%237a7468' stroke-width='2'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
|
||||
background-repeat: no-repeat;
|
||||
background-position: right .65rem center;
|
||||
padding-right: 2rem;
|
||||
}
|
||||
|
||||
/* Setup progress */
|
||||
.setup-progress-area { margin-top: .75rem; }
|
||||
.setup-log {
|
||||
font-family: var(--mono);
|
||||
font-size: .72rem;
|
||||
color: #9a9a8c;
|
||||
background: #050508;
|
||||
padding: .75rem;
|
||||
border-radius: var(--radius);
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* Radio / checkbox labels */
|
||||
.ip-options { display: flex; flex-direction: column; gap: .4rem; margin-top: .35rem; }
|
||||
.radio-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: .4rem;
|
||||
font-size: .82rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
.radio-label input { accent-color: var(--gold); }
|
||||
|
||||
/* Done banner */
|
||||
.done-banner {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
padding: 2rem 1rem;
|
||||
gap: .5rem;
|
||||
}
|
||||
.done-banner h3 { color: #3ddc84; font-size: 1.2rem; }
|
||||
|
||||
.field-hint { font-size: .68rem; color: var(--text-dim); margin-top: .15rem; }
|
||||
.field-hint-inline { font-size: .68rem; color: var(--text-dim); font-weight: 400; }
|
||||
|
||||
/* Config editor */
|
||||
.config-header-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: .5rem;
|
||||
}
|
||||
.section-title { font-size: 1rem; font-weight: 600; }
|
||||
.config-actions { display: flex; gap: .5rem; }
|
||||
.config-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
|
||||
gap: .75rem;
|
||||
}
|
||||
.config-item { display: flex; flex-direction: column; gap: .25rem; }
|
||||
.config-item label {
|
||||
font-size: .72rem;
|
||||
color: var(--text-dim);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .05em;
|
||||
}
|
||||
.config-item.wide { grid-column: 1 / -1; max-width: 480px; }
|
||||
.input-with-unit { display: flex; align-items: center; gap: .4rem; }
|
||||
.input-with-unit .unit { font-size: .75rem; color: var(--text-dim); }
|
||||
.config-item .select-input { width: 100%; }
|
||||
.cfg-dirty { border-color: var(--gold) !important; }
|
||||
.config-warning {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: .5rem;
|
||||
padding: .6rem 1rem;
|
||||
margin-bottom: 1rem;
|
||||
background: rgba(184, 156, 60, .1);
|
||||
border: 1px solid var(--yellow);
|
||||
border-radius: var(--radius);
|
||||
color: var(--yellow);
|
||||
font-size: .82rem;
|
||||
}
|
||||
.config-warning.ok {
|
||||
border-color: var(--green);
|
||||
background: rgba(45, 138, 78, .1);
|
||||
color: #3ddc84;
|
||||
}
|
||||
|
||||
.port-forward-notice {
|
||||
margin-top: .75rem;
|
||||
padding: .75rem 1rem;
|
||||
background: var(--surface2);
|
||||
border: 1px solid var(--gold);
|
||||
border-radius: var(--radius);
|
||||
font-size: .82rem;
|
||||
line-height: 1.55;
|
||||
}
|
||||
.port-forward-notice strong { color: var(--gold); }
|
||||
.port-forward-notice table {
|
||||
width: 100%;
|
||||
margin: .5rem 0;
|
||||
border-collapse: collapse;
|
||||
font-size: .8rem;
|
||||
}
|
||||
.port-forward-notice th,
|
||||
.port-forward-notice td {
|
||||
text-align: left;
|
||||
padding: .35rem .5rem;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.port-forward-notice th { color: var(--text-dim); font-weight: 600; }
|
||||
.port-forward-notice .pf-target {
|
||||
margin-top: .5rem;
|
||||
color: var(--yellow);
|
||||
}
|
||||
|
||||
/* Retry panel */
|
||||
.retry-panel {
|
||||
margin-top: 1rem;
|
||||
padding: .75rem;
|
||||
background: var(--surface2);
|
||||
border: 1px solid var(--yellow);
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
.retry-msg { color: var(--yellow); margin-bottom: .75rem; }
|
||||
|
||||
.setup-reset-panel {
|
||||
margin-top: 1.25rem;
|
||||
padding: 1rem;
|
||||
background: rgba(180, 60, 60, .08);
|
||||
border: 1px solid var(--red);
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
.setup-reset-title {
|
||||
font-weight: 600;
|
||||
color: var(--red);
|
||||
margin-bottom: .35rem;
|
||||
}
|
||||
.setup-reset-list {
|
||||
margin: .5rem 0 .75rem 1.2rem;
|
||||
padding: 0;
|
||||
font-size: .82rem;
|
||||
line-height: 1.6;
|
||||
color: var(--text-dim);
|
||||
}
|
||||
.setup-reset-list code {
|
||||
color: var(--text);
|
||||
font-size: .8rem;
|
||||
}
|
||||
|
||||
#repair-bootstrap-panel {
|
||||
margin-bottom: 1rem;
|
||||
border: 1px solid var(--yellow);
|
||||
background: rgba(200, 160, 40, .08);
|
||||
}
|
||||
#repair-bootstrap-panel .card-header h2 {
|
||||
color: var(--yellow);
|
||||
}
|
||||
|
||||
/* ------ Character Editor ------ */
|
||||
.char-warning {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: .6rem;
|
||||
padding: .75rem 1rem;
|
||||
margin-bottom: 1rem;
|
||||
background: rgba(184, 60, 60, .12);
|
||||
border: 1px solid var(--red);
|
||||
border-radius: var(--radius);
|
||||
color: #ef5350;
|
||||
font-size: .82rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.char-warning svg { flex-shrink: 0; margin-top: 2px; }
|
||||
.char-select-row { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; }
|
||||
.btn-sm { padding: .3rem .7rem; font-size: .72rem; }
|
||||
|
||||
.inv-table-wrap { overflow-x: auto; }
|
||||
.inv-table { width: 100%; border-collapse: collapse; font-size: .8rem; }
|
||||
.inv-table th {
|
||||
text-align: left;
|
||||
padding: .55rem .75rem;
|
||||
font-size: .68rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .06em;
|
||||
color: var(--text-dim);
|
||||
background: var(--surface2);
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.inv-table td {
|
||||
padding: .45rem .75rem;
|
||||
border-bottom: 1px solid var(--border);
|
||||
vertical-align: middle;
|
||||
}
|
||||
.inv-table tbody tr:hover { background: rgba(196, 151, 58, .04); }
|
||||
.inv-table .item-name { color: var(--gold); font-weight: 500; }
|
||||
.inv-table .item-tid { color: var(--text-dim); font-family: var(--mono); font-size: .72rem; }
|
||||
.inv-table .btn-remove {
|
||||
background: transparent;
|
||||
border: 1px solid var(--red);
|
||||
color: var(--red);
|
||||
padding: .2rem .5rem;
|
||||
border-radius: 4px;
|
||||
font-size: .68rem;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
}
|
||||
.inv-table .btn-remove:hover { background: var(--red); color: #fff; }
|
||||
.inv-table .btn-add {
|
||||
background: var(--green);
|
||||
border: none;
|
||||
color: #fff;
|
||||
padding: .25rem .6rem;
|
||||
border-radius: 4px;
|
||||
font-size: .72rem;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
}
|
||||
.inv-table .btn-add:hover { opacity: .85; }
|
||||
.inv-table input[type="number"] {
|
||||
width: 60px;
|
||||
padding: .2rem .4rem;
|
||||
background: var(--bg);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
color: var(--text);
|
||||
font: inherit;
|
||||
font-size: .8rem;
|
||||
text-align: center;
|
||||
}
|
||||
.inv-table select {
|
||||
padding: .2rem .35rem;
|
||||
background: var(--bg);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
color: var(--text);
|
||||
font: inherit;
|
||||
font-size: .72rem;
|
||||
}
|
||||
.item-search-row { display: flex; gap: .5rem; margin-bottom: .75rem; }
|
||||
.item-results-wrap { max-height: 350px; overflow-y: auto; border: 1px solid var(--border); border-radius: var(--radius); }
|
||||
|
||||
|
||||
/* ------ Radio Group (Server Visibility) ------ */
|
||||
.radio-group { display: flex; flex-direction: column; gap: .5rem; }
|
||||
.radio-option { display: flex; align-items: center; gap: .6rem; padding: .5rem .75rem; border: 1px solid var(--border); border-radius: var(--radius); cursor: pointer; transition: border-color .15s, background .15s; }
|
||||
.radio-option:hover { border-color: var(--gold); }
|
||||
.radio-option input[type="radio"] { accent-color: var(--gold); }
|
||||
.radio-option.selected { border-color: var(--gold); background: rgba(212,175,55,.08); }
|
||||
.radio-option .radio-label { font-size: .85rem; }
|
||||
.radio-option .radio-hint { font-size: .72rem; color: var(--text-dim); margin-left: auto; }
|
||||
|
||||
/* ------ Scrollbars ------ */
|
||||
::-webkit-scrollbar { width: 6px; height: 6px; }
|
||||
::-webkit-scrollbar-track { background: transparent; }
|
||||
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
|
||||
::-webkit-scrollbar-thumb:hover { background: var(--text-dim); }
|
||||
|
||||
/* ------ Responsive ------ */
|
||||
@media (max-width: 700px) {
|
||||
.card-row { grid-template-columns: 1fr; }
|
||||
.topbar { flex-direction: column; gap: .5rem; align-items: flex-start; }
|
||||
.topbar-right { width: 100%; }
|
||||
.stat-grid { grid-template-columns: 1fr; }
|
||||
}
|
||||
3795
docs/reference-repos/the4rchangel/public/data/cosmetic-catalog.json
Normal file
4010
docs/reference-repos/the4rchangel/public/data/item-catalog.json
Normal file
@@ -0,0 +1,178 @@
|
||||
{
|
||||
"_meta": {
|
||||
"source": "database inspection",
|
||||
"date": "2026-05-20"
|
||||
},
|
||||
"actor_properties": {
|
||||
"DamageableActorComponent": {
|
||||
"m_TotalMaxHealth": {
|
||||
"type": "float",
|
||||
"description": "Maximum health pool",
|
||||
"default": 100.0
|
||||
},
|
||||
"m_CurrentMaxHealth": {
|
||||
"type": "float",
|
||||
"description": "Current max health (should match TotalMax)",
|
||||
"default": 100.0
|
||||
}
|
||||
},
|
||||
"TechKnowledgePlayerComponent": {
|
||||
"m_TechKnowledgePoints": {
|
||||
"type": "int",
|
||||
"description": "Unspent tech knowledge points",
|
||||
"default": 0
|
||||
},
|
||||
"m_NextTechTreeUpgradeIndex": {
|
||||
"type": "int",
|
||||
"description": "Number of tech tree upgrades purchased",
|
||||
"default": 0
|
||||
}
|
||||
},
|
||||
"BP_DunePlayerCharacter_C": {
|
||||
"m_EyesOfIbadValue": {
|
||||
"type": "float",
|
||||
"description": "Blue-in-blue eye intensity (0.0 to 1.0)",
|
||||
"default": 0.0
|
||||
},
|
||||
"m_bShowHelmet": {
|
||||
"type": "bool",
|
||||
"description": "Show/hide helmet",
|
||||
"default": true
|
||||
},
|
||||
"m_bCharacterHasBeenFinalized": {
|
||||
"type": "bool",
|
||||
"description": "Character creation complete",
|
||||
"default": true
|
||||
}
|
||||
},
|
||||
"WeaponActorComponent": {
|
||||
"m_FavoriteWeaponItemDatabaseId": {
|
||||
"type": "string",
|
||||
"description": "Favorite weapon reference",
|
||||
"default": "!!itm#0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"gas_attributes": {
|
||||
"DuneHydrationAttributeSet": {
|
||||
"CurrentHydration": {
|
||||
"type": "float_pair",
|
||||
"description": "Water level",
|
||||
"default": 100.0,
|
||||
"fields": [
|
||||
"BaseValue",
|
||||
"CurrentValue"
|
||||
]
|
||||
},
|
||||
"HeatExhaustion": {
|
||||
"type": "float_pair",
|
||||
"description": "Heat exhaustion level",
|
||||
"default": 0.0,
|
||||
"fields": [
|
||||
"BaseValue",
|
||||
"CurrentValue"
|
||||
]
|
||||
},
|
||||
"DehydrationPenalty": {
|
||||
"type": "float_pair",
|
||||
"description": "Dehydration debuff",
|
||||
"default": 0.0,
|
||||
"fields": [
|
||||
"BaseValue",
|
||||
"CurrentValue"
|
||||
]
|
||||
},
|
||||
"ClothingCapturedWater": {
|
||||
"type": "float_pair",
|
||||
"description": "Water collected by stillsuit",
|
||||
"default": 0.0,
|
||||
"fields": [
|
||||
"BaseValue",
|
||||
"CurrentValue"
|
||||
]
|
||||
}
|
||||
},
|
||||
"DuneSpiceAddictionAttributeSet": {
|
||||
"CurrentSpice": {
|
||||
"type": "float_pair",
|
||||
"description": "Spice amount",
|
||||
"default": 0.0,
|
||||
"fields": [
|
||||
"BaseValue",
|
||||
"CurrentValue"
|
||||
]
|
||||
},
|
||||
"SpiceExposure": {
|
||||
"type": "float_pair",
|
||||
"description": "Spice exposure level",
|
||||
"default": 0.0,
|
||||
"fields": [
|
||||
"BaseValue",
|
||||
"CurrentValue"
|
||||
]
|
||||
},
|
||||
"SpiceTolerance": {
|
||||
"type": "float_pair",
|
||||
"description": "Spice tolerance",
|
||||
"default": 0.0,
|
||||
"fields": [
|
||||
"BaseValue",
|
||||
"CurrentValue"
|
||||
]
|
||||
},
|
||||
"SpiceAddictionLevel": {
|
||||
"type": "float_pair",
|
||||
"description": "Addiction level",
|
||||
"default": 0.0,
|
||||
"fields": [
|
||||
"BaseValue",
|
||||
"CurrentValue"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"inventory_types": {
|
||||
"0": "Main Inventory (backpack)",
|
||||
"1": "Recipes/Knowledge",
|
||||
"12": "Emote Wheel",
|
||||
"14": "Social/Cosmetic Outfits",
|
||||
"15": "Hotbar (equipment slots)",
|
||||
"20": "Quick-use Consumables",
|
||||
"25": "Unknown (25)",
|
||||
"27": "Equipped Garments",
|
||||
"29": "Unknown (29)",
|
||||
"30": "Storage/Overflow",
|
||||
"31": "Unknown (31)",
|
||||
"32": "Unknown (32)",
|
||||
"33": "Unknown (33)"
|
||||
},
|
||||
"item_stats_format": {
|
||||
"stackable": {
|
||||
"FItemStackAndDurabilityStats": [
|
||||
[],
|
||||
{
|
||||
"DecayedMaxDurability": 0.0
|
||||
}
|
||||
]
|
||||
},
|
||||
"equipment": {
|
||||
"FCustomizationStats": [
|
||||
[],
|
||||
{}
|
||||
],
|
||||
"FItemStackAndDurabilityStats": [
|
||||
[],
|
||||
{}
|
||||
]
|
||||
},
|
||||
"simple": {
|
||||
"FItemStackAndDurabilityStats": [
|
||||
[],
|
||||
{}
|
||||
]
|
||||
}
|
||||
},
|
||||
"currency_table": "player_virtual_currency_balances",
|
||||
"faction_table": "player_faction_reputation",
|
||||
"specialization_table": "specialization_tracks"
|
||||
}
|
||||
999
docs/reference-repos/the4rchangel/public/index.html
Normal file
@@ -0,0 +1,999 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Dune: Awakening — Server Manager</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="/css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- Header -->
|
||||
<header class="topbar">
|
||||
<div class="topbar-left">
|
||||
<h1 class="logo">DUNE<span class="logo-dim">:</span> AWAKENING</h1>
|
||||
<span class="logo-sub">SERVER MANAGER</span>
|
||||
</div>
|
||||
<div class="topbar-right">
|
||||
<div class="status-chip" id="vm-chip">
|
||||
<span class="dot"></span>
|
||||
<span class="chip-label">VM</span>
|
||||
<span class="chip-value" id="vm-chip-state">—</span>
|
||||
</div>
|
||||
<div class="status-chip" id="bg-chip">
|
||||
<span class="dot"></span>
|
||||
<span class="chip-label">Battlegroup</span>
|
||||
<span class="chip-value" id="bg-chip-state">—</span>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Tabs -->
|
||||
<nav class="tabs">
|
||||
<button class="tab active" data-tab="dashboard">Dashboard</button>
|
||||
<button class="tab" data-tab="setup">Setup</button>
|
||||
<button class="tab" data-tab="battlegroup">Battlegroup</button>
|
||||
<button class="tab" data-tab="monitoring">Monitoring</button>
|
||||
<button class="tab" data-tab="database">Database</button>
|
||||
<button class="tab" data-tab="characters">Characters</button>
|
||||
<button class="tab" data-tab="gameconfig">Game Config</button>
|
||||
<button class="tab" data-tab="settings">Settings</button>
|
||||
<button class="tab" data-tab="experimental">Experimental</button>
|
||||
</nav>
|
||||
|
||||
<main class="content">
|
||||
|
||||
<!-- ============ Dashboard ============ -->
|
||||
<section class="panel active" id="tab-dashboard">
|
||||
<div class="card-row">
|
||||
<div class="card status-card" id="card-vm">
|
||||
<div class="card-header">
|
||||
<h2>Virtual Machine</h2>
|
||||
<span class="badge" id="vm-badge">Unknown</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="stat-grid">
|
||||
<div class="stat"><span class="stat-label">State</span><span class="stat-value" id="vm-state">—</span></div>
|
||||
<div class="stat"><span class="stat-label">IP Address</span><span class="stat-value" id="vm-ip">—</span></div>
|
||||
<div class="stat"><span class="stat-label">Memory</span><span class="stat-value" id="vm-memory">—</span></div>
|
||||
<div class="stat"><span class="stat-label">Uptime</span><span class="stat-value" id="vm-uptime">—</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<button class="btn btn-green" id="btn-vm-start" disabled>Start VM</button>
|
||||
<button class="btn btn-red" id="btn-vm-stop" disabled>Stop VM</button>
|
||||
</div>
|
||||
<div id="vm-start-retry" class="retry-panel" hidden>
|
||||
<p class="card-desc retry-msg">VM failed to start — usually not enough free RAM on this PC. Lower memory and retry.</p>
|
||||
<div class="setup-row" style="align-items:end">
|
||||
<div class="field">
|
||||
<label for="dashboard-retry-memory">Startup memory</label>
|
||||
<select id="dashboard-retry-memory" class="select-input">
|
||||
<option value="12">12 GB</option>
|
||||
<option value="14">14 GB</option>
|
||||
<option value="16">16 GB</option>
|
||||
<option value="18">18 GB</option>
|
||||
<option value="20" selected>20 GB</option>
|
||||
<option value="24">24 GB</option>
|
||||
<option value="30">30 GB</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<button class="btn btn-green btn-lg" id="btn-dashboard-retry-start" style="margin-bottom:1px">Retry Start</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card status-card" id="card-bg">
|
||||
<div class="card-header">
|
||||
<h2>Battlegroup</h2>
|
||||
<span class="badge" id="bg-badge">Unknown</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<pre class="bg-status-output" id="bg-status-text">Waiting for status...</pre>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<button class="btn btn-green" id="btn-bg-start" disabled>Start</button>
|
||||
<button class="btn btn-yellow" id="btn-bg-restart" disabled>Restart</button>
|
||||
<button class="btn btn-red" id="btn-bg-stop" disabled>Stop</button>
|
||||
<button class="btn btn-blue" id="btn-bg-update" disabled>Update</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" id="ssh-key-warning" hidden>
|
||||
<div class="card-header">
|
||||
<h2>SSH Key Problem</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="card-desc retry-msg">The manager cannot authenticate to the VM. Battlegroup commands will fail until this is fixed.</p>
|
||||
<p class="card-desc" id="ssh-key-hint"></p>
|
||||
<p class="card-desc">If you start the manager from WSL, prefer <strong>start_as_admin.bat</strong> on Windows after a reboot. You can also go to <strong>Settings → Rotate SSH Key</strong> while the VM is running.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" id="repair-bootstrap-panel" hidden>
|
||||
<div class="card-header">
|
||||
<h2>Battlegroup setup incomplete</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="card-desc">The VM exists but the battlegroup was never fully installed (empty namespace). Enter your server token and run repair — this deletes the empty namespace and re-runs bootstrap.</p>
|
||||
<div class="setup-row">
|
||||
<div class="field">
|
||||
<label for="repair-world-name">World Name</label>
|
||||
<input type="text" id="repair-world-name" class="select-input" maxlength="50" placeholder="Dunewatchers" value="Dunewatchers">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="repair-region">Region</label>
|
||||
<select id="repair-region" class="select-input">
|
||||
<option value="1">Asia</option>
|
||||
<option value="2">Europe</option>
|
||||
<option value="3" selected>North America</option>
|
||||
<option value="4">Oceania</option>
|
||||
<option value="5">South America</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field" style="margin-top:.75rem">
|
||||
<label for="repair-token">Server Token</label>
|
||||
<input type="password" id="repair-token" class="select-input" placeholder="Paste token from account.duneawakening.com">
|
||||
</div>
|
||||
<div class="field" style="margin-top:.75rem">
|
||||
<label class="radio-label">
|
||||
<input type="checkbox" id="repair-swap">
|
||||
Enable experimental swap memory
|
||||
</label>
|
||||
</div>
|
||||
<div class="setup-progress-area" style="margin-top:1rem">
|
||||
<div class="spinner" id="repair-spinner" hidden></div>
|
||||
<pre class="setup-log" id="repair-log"></pre>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<button type="button" class="btn btn-blue btn-lg" id="btn-repair-bootstrap">Repair & Complete Setup</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card quick-links-card" id="quick-links">
|
||||
<div class="card-header"><h2>Quick Links</h2></div>
|
||||
<div class="card-body link-row">
|
||||
<a class="link-btn" id="link-filebrowser" href="#" target="_blank">
|
||||
<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"/></svg>
|
||||
File Browser
|
||||
</a>
|
||||
<a class="link-btn" id="link-director" href="#" target="_blank">
|
||||
<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/></svg>
|
||||
Director
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ============ Setup Wizard ============ -->
|
||||
<section class="panel" id="tab-setup">
|
||||
<div class="card wizard-card">
|
||||
<div class="card-header">
|
||||
<h2>Initial Server Setup</h2>
|
||||
<span class="badge" id="setup-step-badge">Step 1 of 6</span>
|
||||
</div>
|
||||
|
||||
<!-- Progress bar -->
|
||||
<div class="wizard-progress">
|
||||
<div class="wizard-progress-fill" id="wizard-progress-fill" style="width: 16.6%"></div>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
|
||||
<!-- Step 1: Pre-flight -->
|
||||
<div class="wizard-step active" id="wiz-step-1">
|
||||
<h3>Pre-flight Checks</h3>
|
||||
<p class="card-desc">Verifying your system is ready for the Dune: Awakening server.</p>
|
||||
<div class="preflight-results" id="preflight-results">
|
||||
<div class="preflight-item" id="pf-hyperv">
|
||||
<span class="pf-icon">☐</span>
|
||||
<span>Hyper-V enabled and running</span>
|
||||
</div>
|
||||
<div class="preflight-item" id="pf-vmcx">
|
||||
<span class="pf-icon">☐</span>
|
||||
<span>Server files found (vmcx)</span>
|
||||
</div>
|
||||
<div class="preflight-item" id="pf-drives">
|
||||
<span class="pf-icon">☐</span>
|
||||
<span>Drive with 100GB+ free space</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="btn-group" style="margin-top: 1rem">
|
||||
<button class="btn btn-blue btn-lg" id="btn-preflight">Run Checks</button>
|
||||
</div>
|
||||
|
||||
<div id="setup-reset-panel" class="setup-reset-panel" hidden>
|
||||
<div class="setup-reset-title">Existing installation detected</div>
|
||||
<p class="card-desc" id="setup-reset-desc">A Dune server VM is already on this machine. Delete everything and run setup again from scratch.</p>
|
||||
<ul class="setup-reset-list">
|
||||
<li>Hyper-V VM <code>dune-awakening</code></li>
|
||||
<li><code>DuneAwakeningServer</code> folders on all drives</li>
|
||||
<li>SSH keys and saved VM credentials</li>
|
||||
<li>All in-VM battlegroup data (world, characters, configs)</li>
|
||||
</ul>
|
||||
<p class="field-hint" style="color:var(--yellow)">This cannot be undone. Export a database backup first if you need your world data.</p>
|
||||
<button type="button" class="btn btn-red btn-lg" id="btn-setup-reset">Delete & Start Fresh</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 2: Configuration -->
|
||||
<div class="wizard-step" id="wiz-step-2">
|
||||
<h3>VM Configuration</h3>
|
||||
<p class="card-desc">Choose where to install and how much resources to allocate.</p>
|
||||
<form id="form-setup-config" class="settings-form" style="max-width:100%">
|
||||
<div class="setup-row">
|
||||
<div class="field" style="grid-column: 1 / -1">
|
||||
<label for="setup-token">Server Token</label>
|
||||
<input type="text" id="setup-token" class="select-input" placeholder="Paste your token here" required>
|
||||
<span class="field-hint">Generate one at <a href="https://account.duneawakening.com/" target="_blank" style="color:var(--gold)">account.duneawakening.com</a></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="setup-row">
|
||||
<div class="field">
|
||||
<label for="setup-drive">Install Drive</label>
|
||||
<select id="setup-drive" class="select-input"></select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="setup-memory">Memory</label>
|
||||
<select id="setup-memory" class="select-input">
|
||||
<option value="10">10 GB (requires swap)</option>
|
||||
<option value="20" selected>20 GB (Hagga Basin)</option>
|
||||
<option value="30">30 GB (+ Story/Social)</option>
|
||||
<option value="40">40 GB (+ Deep Desert)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="setup-row">
|
||||
<div class="field">
|
||||
<label for="setup-network">Network Mode</label>
|
||||
<select id="setup-network" class="select-input">
|
||||
<option value="external">External Switch (recommended — bridge mode)</option>
|
||||
<option value="default">Default Switch (local only)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field" id="nic-field" style="display:none">
|
||||
<label for="setup-nic">Network Adapter</label>
|
||||
<select id="setup-nic" class="select-input"></select>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Step 3: Import (progress) -->
|
||||
<div class="wizard-step" id="wiz-step-3">
|
||||
<h3>Installing</h3>
|
||||
<p class="card-desc">Importing VM, configuring network and memory, starting the virtual machine. This can take several minutes.</p>
|
||||
<div class="setup-progress-area">
|
||||
<div class="spinner" id="setup-spinner" hidden></div>
|
||||
<pre class="setup-log" id="setup-log"></pre>
|
||||
</div>
|
||||
<div id="retry-start" class="retry-panel" hidden>
|
||||
<p class="card-desc retry-msg">VM imported but failed to start — most likely not enough RAM. Reduce the memory and retry.</p>
|
||||
<div class="setup-row" style="align-items:end">
|
||||
<div class="field">
|
||||
<label for="retry-memory">Memory</label>
|
||||
<select id="retry-memory" class="select-input">
|
||||
<option value="10">10 GB (requires swap)</option>
|
||||
<option value="20" selected>20 GB</option>
|
||||
<option value="30">30 GB</option>
|
||||
<option value="40">40 GB</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<button class="btn btn-green btn-lg" id="btn-retry-start" style="margin-bottom:1px">Retry Start</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 4: SSH Key + Password (combined) -->
|
||||
<div class="wizard-step" id="wiz-step-4">
|
||||
<h3>Security</h3>
|
||||
<p class="card-desc">This will generate an SSH key, install it on the VM, and change the default password — all in one step.</p>
|
||||
<form id="form-setup-pw" class="settings-form" style="max-width:100%">
|
||||
<div class="setup-row">
|
||||
<div class="field">
|
||||
<label for="setup-curpw">Current VM Password</label>
|
||||
<input type="password" id="setup-curpw" value="dune" autocomplete="off">
|
||||
<span class="field-hint">Default is "dune" — change only if you've set it before</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="setup-row">
|
||||
<div class="field">
|
||||
<label for="setup-pw">New Password</label>
|
||||
<input type="password" id="setup-pw" required autocomplete="new-password">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="setup-pw2">Confirm New Password</label>
|
||||
<input type="password" id="setup-pw2" required autocomplete="new-password">
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Step 5: Networking -->
|
||||
<div class="wizard-step" id="wiz-step-5">
|
||||
<h3>Player Connectivity</h3>
|
||||
<p class="card-desc">Choose how the VM gets its IP and which IP players will connect to.</p>
|
||||
<div class="setup-row">
|
||||
<div class="field">
|
||||
<label>VM IP Assignment</label>
|
||||
<select id="setup-ip-mode" class="select-input">
|
||||
<option value="dhcp">DHCP (automatic — recommended)</option>
|
||||
<option value="static">Static IP</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div id="static-fields" class="setup-row" style="display:none">
|
||||
<div class="field">
|
||||
<label for="setup-static-ip">Static IP</label>
|
||||
<input type="text" id="setup-static-ip" placeholder="192.168.1.100" class="select-input">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="setup-static-gw">Gateway</label>
|
||||
<input type="text" id="setup-static-gw" placeholder="192.168.1.1" class="select-input">
|
||||
</div>
|
||||
</div>
|
||||
<div class="field" style="margin-top: .75rem">
|
||||
<label>Player-facing IP</label>
|
||||
<div class="ip-options" id="ip-options">
|
||||
<label class="radio-label"><input type="radio" name="playerIpChoice" value="public" checked> <span id="opt-public">Public IP (detecting...)</span></label>
|
||||
<label class="radio-label"><input type="radio" name="playerIpChoice" value="private"> <span id="opt-private">Private IP</span></label>
|
||||
<label class="radio-label"><input type="radio" name="playerIpChoice" value="manual"> Custom</label>
|
||||
</div>
|
||||
<input type="text" id="setup-player-ip-manual" placeholder="Enter IP" class="select-input" style="display:none; margin-top: .5rem">
|
||||
</div>
|
||||
<div id="setup-port-forward" class="port-forward-notice" style="display:none;margin-top:1rem" hidden></div>
|
||||
</div>
|
||||
|
||||
<!-- Step 6: Bootstrap -->
|
||||
<div class="wizard-step" id="wiz-step-6">
|
||||
<h3>Finalize</h3>
|
||||
<p class="card-desc">Configure your world, then upload bootstrap files and run the first-time battlegroup setup. This is the final step.</p>
|
||||
<form id="form-world-config" class="settings-form" style="max-width:100%; margin-bottom: 1rem">
|
||||
<div class="setup-row">
|
||||
<div class="field">
|
||||
<label for="setup-world-name">World Name <span style="color:var(--text-dim)">(max 50 chars)</span></label>
|
||||
<input type="text" id="setup-world-name" class="select-input" maxlength="50" placeholder="My Arrakis Server" required>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="setup-region">Region</label>
|
||||
<select id="setup-region" class="select-input">
|
||||
<option value="1">Asia</option>
|
||||
<option value="2">Europe</option>
|
||||
<option value="3" selected>North America</option>
|
||||
<option value="4">Oceania</option>
|
||||
<option value="5">South America</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<div class="field" style="margin-bottom: 1rem">
|
||||
<label class="radio-label">
|
||||
<input type="checkbox" id="setup-swap">
|
||||
Enable experimental swap memory (recommended if memory < 20GB)
|
||||
</label>
|
||||
</div>
|
||||
<div class="setup-progress-area">
|
||||
<div class="spinner" id="bootstrap-spinner" hidden></div>
|
||||
<pre class="setup-log" id="bootstrap-log"></pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 7: Done -->
|
||||
<div class="wizard-step" id="wiz-step-7">
|
||||
<div class="done-banner">
|
||||
<svg viewBox="0 0 24 24" width="48" height="48" fill="none" stroke="var(--gold)" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M8 12l2.5 2.5L16 9"/></svg>
|
||||
<h3>Setup Complete</h3>
|
||||
<p class="card-desc">Your Dune: Awakening server is ready. Head to the Dashboard to start your battlegroup.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Wizard navigation -->
|
||||
<div class="card-actions wizard-nav">
|
||||
<button class="btn btn-blue" id="wiz-back" hidden>Back</button>
|
||||
<div style="flex:1"></div>
|
||||
<button class="btn btn-green btn-lg" id="wiz-next">Next</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ============ Battlegroup ============ -->
|
||||
<section class="panel" id="tab-battlegroup">
|
||||
<div class="card">
|
||||
<div class="card-header"><h2>Battlegroup Controls</h2></div>
|
||||
<div class="card-body">
|
||||
<p class="card-desc">Manage the lifecycle of your battlegroup. Start, stop, or restart game servers, and check for updates.</p>
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-green btn-lg" data-action="bg/start">Start Battlegroup</button>
|
||||
<button class="btn btn-red btn-lg" data-action="bg/stop">Stop Battlegroup</button>
|
||||
<button class="btn btn-yellow btn-lg" data-action="bg/restart">Restart Battlegroup</button>
|
||||
<button class="btn btn-blue btn-lg" data-action="bg/update">Check for Updates</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header"><h2>Swap Memory</h2></div>
|
||||
<div class="card-body">
|
||||
<p class="card-desc">Enable experimental swap memory to significantly reduce RAM requirements per game server. Recommended if VM has less than 20GB allocated.</p>
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-yellow btn-lg" data-action="bg/enable-experimental-swap">Enable Swap Memory</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ============ Monitoring ============ -->
|
||||
<section class="panel" id="tab-monitoring">
|
||||
<div class="card-row">
|
||||
<div class="card">
|
||||
<div class="card-header"><h2>Web Interfaces</h2></div>
|
||||
<div class="card-body">
|
||||
<p class="card-desc">Access the built-in web tools running inside the VM.</p>
|
||||
<div class="btn-group">
|
||||
<a class="btn btn-blue btn-lg" id="mon-filebrowser" href="#" target="_blank">Open File Browser</a>
|
||||
<a class="btn btn-blue btn-lg" id="mon-director" href="#" target="_blank">Open Director</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header"><h2>Log Export</h2></div>
|
||||
<div class="card-body">
|
||||
<p class="card-desc">Download logs from all battlegroup components or operator pods.</p>
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-blue btn-lg" data-action="logs/export">Export Battlegroup Logs</button>
|
||||
<button class="btn btn-blue btn-lg" data-action="logs/operators">Export Operator Logs</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ============ Database ============ -->
|
||||
<section class="panel" id="tab-database">
|
||||
<div class="card">
|
||||
<div class="card-header"><h2>Database Management</h2></div>
|
||||
<div class="card-body">
|
||||
<p class="card-desc">Back up or restore the battlegroup database. For best results, stop the battlegroup before performing these operations.</p>
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-green btn-lg" data-action="bg/backup">
|
||||
<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
|
||||
Take Backup
|
||||
</button>
|
||||
<button class="btn btn-yellow btn-lg" data-action="bg/import">
|
||||
<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></svg>
|
||||
Import Backup
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ============ Character Editor ============ -->
|
||||
<section class="panel" id="tab-characters">
|
||||
<div class="char-warning">
|
||||
<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2"><path d="M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>
|
||||
<span><strong>Editing characters directly modifies the game database.</strong> This may corrupt save data and cause total character loss. Always take a database backup before making changes. Only edit while the battlegroup is stopped and the player is logged out.</span>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header"><h2>Select Character</h2></div>
|
||||
<div class="card-body">
|
||||
<div class="char-select-row">
|
||||
<select id="char-select" class="select-input" style="max-width:280px"><option value="">— Choose a character —</option></select>
|
||||
<button class="btn btn-blue" id="btn-char-load">Load Character</button>
|
||||
<button class="btn btn-blue" id="btn-char-refresh" title="Refresh character list">Refresh List</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="char-editor" style="display:none">
|
||||
<div class="card">
|
||||
<div class="card-header"><h2>Stats</h2><button class="btn btn-green btn-sm" id="btn-stats-save">Save Stats</button></div>
|
||||
<div class="card-body">
|
||||
<div class="config-grid">
|
||||
<div class="config-item">
|
||||
<label>Max Health</label>
|
||||
<input type="number" class="select-input char-stat" id="cs-max-health" data-field="properties" data-path="DamageableActorComponent.m_TotalMaxHealth" step="1" min="1">
|
||||
</div>
|
||||
<div class="config-item">
|
||||
<label>Tech Knowledge Points</label>
|
||||
<input type="number" class="select-input char-stat" id="cs-tech-pts" data-field="properties" data-path="TechKnowledgePlayerComponent.m_TechKnowledgePoints" step="1" min="0">
|
||||
</div>
|
||||
<div class="config-item">
|
||||
<label>Hydration</label>
|
||||
<input type="number" class="select-input char-stat" id="cs-hydration" data-field="gas_attributes" data-path="DuneHydrationAttributeSet.CurrentHydration" step="0.1" min="0">
|
||||
</div>
|
||||
<div class="config-item">
|
||||
<label>Heat Exhaustion</label>
|
||||
<input type="number" class="select-input char-stat" id="cs-heat" data-field="gas_attributes" data-path="DuneHydrationAttributeSet.HeatExhaustion" step="0.1" min="0">
|
||||
</div>
|
||||
<div class="config-item">
|
||||
<label>Current Spice</label>
|
||||
<input type="number" class="select-input char-stat" id="cs-spice" data-field="gas_attributes" data-path="DuneSpiceAddictionAttributeSet.CurrentSpice" step="1" min="0">
|
||||
</div>
|
||||
<div class="config-item">
|
||||
<label>Spice Addiction Level</label>
|
||||
<input type="number" class="select-input char-stat" id="cs-addiction" data-field="gas_attributes" data-path="DuneSpiceAddictionAttributeSet.SpiceAddictionLevel" step="0.1" min="0">
|
||||
</div>
|
||||
<div class="config-item">
|
||||
<label>Spice Tolerance</label>
|
||||
<input type="number" class="select-input char-stat" id="cs-tolerance" data-field="gas_attributes" data-path="DuneSpiceAddictionAttributeSet.SpiceTolerance" step="0.1" min="0">
|
||||
</div>
|
||||
<div class="config-item">
|
||||
<label>Eyes of Ibad <span class="field-hint-inline">0.0 – 1.0</span></label>
|
||||
<input type="number" class="select-input char-stat" id="cs-eyes" data-field="properties" data-path="BP_DunePlayerCharacter_C.m_EyesOfIbadValue" step="0.05" min="0" max="1">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h2>Inventory</h2>
|
||||
<span class="badge" id="inv-count">0 items</span>
|
||||
</div>
|
||||
<div class="card-body" style="padding:0">
|
||||
<div class="inv-table-wrap">
|
||||
<table class="inv-table" id="inv-table">
|
||||
<thead><tr><th>Item</th><th>ID</th><th>Qty</th><th>Location</th><th></th></tr></thead>
|
||||
<tbody id="inv-tbody"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header"><h2>Add Item</h2></div>
|
||||
<div class="card-body">
|
||||
<div class="item-search-row">
|
||||
<input type="text" class="select-input" id="item-search" placeholder="Search items by name..." style="flex:1">
|
||||
<select class="select-input" id="item-cat-filter" style="max-width:200px">
|
||||
<option value="">All Categories</option>
|
||||
<option value="Resources">Resources</option>
|
||||
<option value="Consumables">Consumables</option>
|
||||
<option value="Ammo">Ammo</option>
|
||||
<option value="Weapons - Melee">Weapons - Melee</option>
|
||||
<option value="Weapons - Ranged">Weapons - Ranged</option>
|
||||
<option value="Garments">Garments (all)</option>
|
||||
<option value="Tools">Tools</option>
|
||||
<option value="Vehicle Modules">Vehicle Modules</option>
|
||||
<option value="Building">Building</option>
|
||||
<option value="Contract Items">Contract Items</option>
|
||||
<option value="Fuel">Fuel</option>
|
||||
<option value="Misc">Misc</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="item-results-wrap" id="item-results" style="display:none">
|
||||
<table class="inv-table">
|
||||
<thead><tr><th>Item</th><th>Category</th><th>Qty</th><th>To</th><th></th></tr></thead>
|
||||
<tbody id="item-results-body"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<p class="card-desc" id="item-results-hint" style="margin-top:.5rem">Type at least 2 characters to search by name or template ID across 1000 items.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header"><h2>Tech Tree</h2><span class="badge" id="tech-count">—</span></div>
|
||||
<div class="card-body">
|
||||
<p class="card-desc">Unlock or lock all recipes and blueprints in the tech knowledge tree. Unlock All merges every game recipe node into your save (not just ones already discovered). Stop the battlegroup and relog after changes.</p>
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-green" id="btn-tech-unlock-all">Unlock All Recipes</button>
|
||||
<button class="btn btn-red" id="btn-tech-lock-all">Lock All Recipes</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header"><h2>Specializations</h2></div>
|
||||
<div class="card-body">
|
||||
<p class="card-desc">Set XP and level for each specialization track, and unlock all keystones (perks) per tree.</p>
|
||||
<div id="spec-tracks-grid" class="config-grid" style="margin-bottom:1rem"></div>
|
||||
<h3 style="font-size:.8rem;color:var(--text-dim);margin-bottom:.5rem;text-transform:uppercase;letter-spacing:.05em">Unlock Keystones (Perks)</h3>
|
||||
<div class="btn-group" id="spec-keystone-btns">
|
||||
<button class="btn btn-green btn-sm" data-prefix="Combat_">All Combat</button>
|
||||
<button class="btn btn-green btn-sm" data-prefix="Crafting_">All Crafting</button>
|
||||
<button class="btn btn-green btn-sm" data-prefix="Exploration_">All Exploration</button>
|
||||
<button class="btn btn-green btn-sm" data-prefix="Gathering_">All Gathering</button>
|
||||
<button class="btn btn-green btn-sm" data-prefix="Sabotage_">All Sabotage</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header"><h2>Economy</h2></div>
|
||||
<div class="card-body">
|
||||
<div class="config-grid">
|
||||
<div class="config-item">
|
||||
<label>Solari (Currency 0)</label>
|
||||
<div class="input-with-unit">
|
||||
<input type="number" class="select-input" id="econ-currency-0" min="0" step="1000" value="0">
|
||||
<button class="btn btn-green btn-sm" data-currency="0">Set</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="config-item">
|
||||
<label>House Scrip (Currency 1)</label>
|
||||
<div class="input-with-unit">
|
||||
<input type="number" class="select-input" id="econ-currency-1" min="0" step="100" value="0">
|
||||
<button class="btn btn-green btn-sm" data-currency="1">Set</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header"><h2>Faction Reputation</h2></div>
|
||||
<div class="card-body">
|
||||
<div class="config-grid" id="faction-rep-grid"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header"><h2>Cosmetics & Skins</h2><span class="badge" id="cosmetic-count">—</span></div>
|
||||
<div class="card-body">
|
||||
<p class="card-desc">Search and unlock cosmetics by name. <strong>Vehicle Paints</strong> and <strong>Vehicle Variants</strong> are what show in the garage paint menu — not Swatch Tokens (those are inventory items; use Add Item instead).</p>
|
||||
<div class="btn-group" style="margin-bottom:.75rem">
|
||||
<button class="btn btn-green btn-sm" id="btn-cosmetic-unlock-all">Unlock All Cosmetics & Swatches</button>
|
||||
</div>
|
||||
<div class="item-search-row" style="margin-bottom:.5rem">
|
||||
<input type="text" class="select-input" id="cosmetic-search" placeholder="Search cosmetics by name or ID..." style="flex:1">
|
||||
<select class="select-input" id="cosmetic-cat-filter" style="max-width:200px">
|
||||
<option value="">All Categories</option>
|
||||
<option value="Vehicle Paints">Vehicle Paints</option>
|
||||
<option value="Vehicle Variants">Vehicle Variants</option>
|
||||
<option value="Vehicle Skins">Vehicle Skins (MTX)</option>
|
||||
<option value="Weapon Skins">Weapon Skins</option>
|
||||
<option value="Weapon Paints">Weapon Paints</option>
|
||||
<option value="Armor Skins">Armor Skins</option>
|
||||
<option value="Armor Paints">Armor Paints</option>
|
||||
<option value="Dye Packs">Dye Packs</option>
|
||||
<option value="Swatch Tokens (Inventory)">Swatch Tokens (Inventory)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="item-results-wrap" id="cosmetic-results" style="display:none;margin-bottom:.75rem">
|
||||
<table class="inv-table">
|
||||
<thead><tr><th>Cosmetic</th><th>Category</th><th></th></tr></thead>
|
||||
<tbody id="cosmetic-results-body"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<p class="card-desc" id="cosmetic-results-hint" style="margin-bottom:.75rem">Type at least 2 characters to search across 621 cosmetics, or pick a category filter.</p>
|
||||
<details>
|
||||
<summary style="cursor:pointer;color:var(--text-dim);font-size:.8rem">Advanced: add by exact ID</summary>
|
||||
<div class="item-search-row" style="margin-top:.5rem">
|
||||
<input type="text" class="select-input" id="cosmetic-add-input" placeholder="Enter cosmetic ID to add..." style="flex:1">
|
||||
<button class="btn btn-green btn-sm" id="btn-cosmetic-add">Add</button>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ============ Game Config ============ -->
|
||||
<section class="panel" id="tab-gameconfig">
|
||||
<div class="config-header-row">
|
||||
<h2 class="section-title">Game Configuration</h2>
|
||||
<div class="config-actions">
|
||||
<button class="btn btn-blue" id="btn-config-reload">Reload</button>
|
||||
<button class="btn btn-green" id="btn-config-save">Save & Apply</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="config-warning" id="config-warning">
|
||||
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2"><path d="M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>
|
||||
<span><strong>Stop the battlegroup before editing.</strong> Changes require the battlegroup to be offline and will apply on next start.</span>
|
||||
</div>
|
||||
|
||||
<div id="config-loading" class="card"><div class="card-body" style="text-align:center;color:var(--text-dim)">Loading configuration...</div></div>
|
||||
|
||||
<div id="config-panels" style="display:none">
|
||||
|
||||
<!-- Server Visibility -->
|
||||
<div class="card">
|
||||
<div class="card-header"><h2>Server Visibility</h2></div>
|
||||
<div class="card-body">
|
||||
<p class="card-desc">Controls how players find your server. <strong>LAN</strong> = only players on your network. <strong>Public (WAN)</strong> = internet players (requires router port forwarding to your <em>VM</em> IP).</p>
|
||||
<p class="card-desc" style="margin-top:.35rem;font-size:.8rem">Self-hosted worlds appear in-game under <strong>Servers → Experimental</strong> (not Official or Private). Match the region you chose during setup.</p>
|
||||
<div id="visibility-loading" style="color:var(--text-dim)">Detecting IPs...</div>
|
||||
<div id="visibility-controls" style="display:none">
|
||||
<div class="radio-group" id="visibility-radios"></div>
|
||||
<div class="config-item" id="visibility-custom-row" style="display:none;margin-top:.5rem">
|
||||
<label>Custom IP</label>
|
||||
<input type="text" class="select-input" id="visibility-custom-ip" placeholder="e.g. 203.0.113.42">
|
||||
</div>
|
||||
<div id="visibility-port-forward" class="port-forward-notice" style="display:none" hidden></div>
|
||||
<div style="margin-top:.75rem">
|
||||
<button type="button" class="btn btn-green" id="btn-visibility-save">Apply</button>
|
||||
<span class="visibility-current" id="visibility-current" style="margin-left:1rem;font-size:.8rem;color:var(--text-dim)"></span>
|
||||
</div>
|
||||
<p class="field-hint" style="margin-top:.5rem">After changing visibility, <strong>stop the battlegroup completely</strong>, then start it again. The gateway reads this IP only at startup.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- PvP & Security -->
|
||||
<div class="card">
|
||||
<div class="card-header"><h2>PvP & Security</h2></div>
|
||||
<div class="card-body">
|
||||
<div class="config-grid">
|
||||
<div class="config-item">
|
||||
<label>Force PvP on All Partitions</label>
|
||||
<select class="select-input cfg" data-file="game" data-key="m_bShouldForceEnablePvpOnAllPartitions">
|
||||
<option value="False">Off</option><option value="True">On</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="config-item">
|
||||
<label>Security Zones Enabled</label>
|
||||
<select class="select-input cfg" data-file="game" data-key="m_bAreSecurityZonesEnabled">
|
||||
<option value="True">On</option><option value="False">Off (PvP everywhere)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Environment -->
|
||||
<div class="card">
|
||||
<div class="card-header"><h2>Environment</h2></div>
|
||||
<div class="card-body">
|
||||
<div class="config-grid">
|
||||
<div class="config-item">
|
||||
<label>Coriolis Storm</label>
|
||||
<select class="select-input cfg" data-file="game" data-key="m_bCoriolisAutoSpawnEnabled">
|
||||
<option value="True">On</option><option value="False">Off</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="config-item">
|
||||
<label>Sandstorm</label>
|
||||
<select class="select-input cfg" data-file="engine" data-key="Sandstorm.Enabled">
|
||||
<option value="1">On</option><option value="0">Off</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="config-item">
|
||||
<label>Sandstorm Treasure Spawns</label>
|
||||
<select class="select-input cfg" data-file="engine" data-key="Sandstorm.Treasure.Enabled">
|
||||
<option value="1">On</option><option value="0">Off</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sandworm -->
|
||||
<div class="card">
|
||||
<div class="card-header"><h2>Sandworm</h2></div>
|
||||
<div class="card-body">
|
||||
<div class="config-grid">
|
||||
<div class="config-item">
|
||||
<label>Sandworm Enabled</label>
|
||||
<select class="select-input cfg" data-file="engine" data-key="sandworm.dune.Enabled">
|
||||
<option value="1">On</option><option value="0">Off</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="config-item">
|
||||
<label>Danger Zones Enabled</label>
|
||||
<select class="select-input cfg" data-file="engine" data-key="Sandworm.SandwormDangerZonesEnabled">
|
||||
<option value="true">On</option><option value="false">Off</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="config-item">
|
||||
<label>Sandworm Pushes Vehicles</label>
|
||||
<select class="select-input cfg" data-file="engine" data-key="Vehicle.SandwormCollisionInteraction">
|
||||
<option value="false">Off</option><option value="true">On</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="config-item">
|
||||
<label>Invulnerability on Vehicle Exit</label>
|
||||
<div class="input-with-unit">
|
||||
<input type="number" class="select-input cfg" data-file="engine" data-key="Vehicle.SandwormInvulnerabilitySecondsOnExit" step="1" min="0">
|
||||
<span class="unit">sec</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="config-item">
|
||||
<label>Invulnerability on Server Restart</label>
|
||||
<div class="input-with-unit">
|
||||
<input type="number" class="select-input cfg" data-file="engine" data-key="Vehicle.SandwormInvulnerabilitySecondsOnServerRestart" step="1" min="0">
|
||||
<span class="unit">sec</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Economy & Resources -->
|
||||
<div class="card">
|
||||
<div class="card-header"><h2>Economy & Resources</h2></div>
|
||||
<div class="card-body">
|
||||
<div class="config-grid">
|
||||
<div class="config-item">
|
||||
<label>Global Mining Multiplier</label>
|
||||
<input type="number" class="select-input cfg" data-file="engine" data-key="Dune.GlobalMiningOutputMultiplier" step="0.1" min="0">
|
||||
</div>
|
||||
<div class="config-item">
|
||||
<label>Vehicle Mining Multiplier</label>
|
||||
<input type="number" class="select-input cfg" data-file="engine" data-key="Dune.GlobalVehicleMiningOutputMultiplier" step="0.1" min="0">
|
||||
</div>
|
||||
<div class="config-item">
|
||||
<label>PvP Resource Multiplier</label>
|
||||
<input type="number" class="select-input cfg" data-file="engine" data-key="SecurityZones.PvpResourceMultiplier" step="0.1" min="0">
|
||||
</div>
|
||||
<div class="config-item">
|
||||
<label>Item Decay Rate <span class="field-hint-inline">0=off, 1-10</span></label>
|
||||
<input type="number" class="select-input cfg" data-file="game" data-key="UpdateRateInSeconds" step="0.1" min="0" max="10">
|
||||
</div>
|
||||
<div class="config-item">
|
||||
<label>Vehicle Durability Damage <span class="field-hint-inline">0=off, 1-10</span></label>
|
||||
<input type="number" class="select-input cfg" data-file="engine" data-key="dw.VehicleDurabilityDamageMultiplier" step="0.1" min="0" max="10">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Building -->
|
||||
<div class="card">
|
||||
<div class="card-header"><h2>Building</h2></div>
|
||||
<div class="card-body">
|
||||
<div class="config-grid">
|
||||
<div class="config-item">
|
||||
<label>Max Landclaim Segments</label>
|
||||
<input type="number" class="select-input cfg" data-file="game" data-key="m_MaxNumLandclaimSegments" step="1" min="1">
|
||||
</div>
|
||||
<div class="config-item">
|
||||
<label>Blueprint Max Extensions</label>
|
||||
<input type="number" class="select-input cfg" data-file="game" data-key="m_BuildingBlueprintMaxExtensions" step="1" min="0">
|
||||
</div>
|
||||
<div class="config-item">
|
||||
<label>Base Backup Max Extensions</label>
|
||||
<input type="number" class="select-input cfg" data-file="game" data-key="m_BaseBackupMaxExtensions" step="1" min="0">
|
||||
</div>
|
||||
<div class="config-item">
|
||||
<label>Building Restriction Limits</label>
|
||||
<select class="select-input cfg" data-file="game" data-key="m_bBuildingRestrictionLimitsEnabled">
|
||||
<option value="True">On</option><option value="False">Off</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Server -->
|
||||
<div class="card">
|
||||
<div class="card-header"><h2>Server</h2></div>
|
||||
<div class="card-body">
|
||||
<div class="config-grid">
|
||||
<div class="config-item wide">
|
||||
<label>Server Display Name <span class="field-hint-inline">shown to players</span></label>
|
||||
<input type="text" class="select-input cfg" data-file="engine" data-key="Bgd.ServerDisplayName" placeholder="e.g. Sietch Dunewatchers">
|
||||
<span class="field-hint">Shown in the in-game server browser. Leave empty only if you know your world display name from setup.</span>
|
||||
</div>
|
||||
<div class="config-item wide">
|
||||
<label>Server Login Password <span class="field-hint-inline">blank = no password</span></label>
|
||||
<input type="text" class="select-input cfg" data-file="engine" data-key="Bgd.ServerLoginPassword" placeholder="No password">
|
||||
</div>
|
||||
<div class="config-item">
|
||||
<label>Game Port (starting)</label>
|
||||
<input type="number" class="select-input cfg" data-file="engine" data-key="Port" step="1" min="1024" max="65535">
|
||||
</div>
|
||||
<div class="config-item">
|
||||
<label>IGW Port (starting)</label>
|
||||
<input type="number" class="select-input cfg" data-file="engine" data-key="IGWPort" step="1" min="1024" max="65535">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ============ Settings ============ -->
|
||||
<section class="panel" id="tab-settings">
|
||||
<div class="card-row">
|
||||
<div class="card">
|
||||
<div class="card-header"><h2>Change VM Password</h2></div>
|
||||
<div class="card-body">
|
||||
<p class="card-desc">Change the <code>dune</code> user password on the VM.</p>
|
||||
<form id="form-password" class="settings-form">
|
||||
<div class="field">
|
||||
<label for="pw-new">New Password</label>
|
||||
<input type="password" id="pw-new" required autocomplete="new-password">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="pw-confirm">Confirm Password</label>
|
||||
<input type="password" id="pw-confirm" required autocomplete="new-password">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-green">Change Password</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header"><h2>SSH Key</h2></div>
|
||||
<div class="card-body">
|
||||
<p class="card-desc">Generate a new SSH key pair and install it on the VM, replacing the current one.</p>
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-yellow btn-lg" id="btn-rotate-key">Rotate SSH Key</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ============ Experimental ============ -->
|
||||
<section class="panel" id="tab-experimental">
|
||||
<div class="card-row">
|
||||
<div class="card" style="border-left:3px solid var(--warn)">
|
||||
<div class="card-header"><h2>Experimental Features</h2></div>
|
||||
<div class="card-body">
|
||||
<p class="card-desc" style="color:var(--warn)"><strong>Warning:</strong> Features on this tab are untested and may break your battlegroup. Always take a database backup before making changes. Use at your own risk.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-row">
|
||||
<div class="card">
|
||||
<div class="card-header"><h2>Multi-Sietch</h2></div>
|
||||
<div class="card-body">
|
||||
<p class="card-desc">Add additional sietches (Hagga Basin instances) to your battlegroup. All sietches share the same Overmap, Deep Desert, Arrakeen, Harkonnen Village, and instanced content (dungeons, story missions). Players from different sietches will see each other in shared areas but not in their respective Hagga Basin maps.</p>
|
||||
|
||||
<div class="info-box" style="background:var(--card-bg);border:1px solid var(--border);border-radius:8px;padding:1rem;margin:1rem 0;font-size:.85rem;line-height:1.6">
|
||||
<strong>RAM Requirements</strong>
|
||||
<ul style="margin:.5rem 0 0 1.2rem;padding:0">
|
||||
<li>Each sietch requires approximately <strong>12 GB RAM</strong></li>
|
||||
<li>Overmap + infrastructure (DB, RabbitMQ, K8s) uses ~6 GB</li>
|
||||
<li>1 sietch: ~18 GB minimum (current default)</li>
|
||||
<li>2 sietches: ~30 GB minimum</li>
|
||||
<li>3 sietches: ~42 GB minimum</li>
|
||||
</ul>
|
||||
<br>
|
||||
<strong>Port Forwarding</strong>
|
||||
<p style="margin:.25rem 0 0 0">Each additional sietch adds one game server pod using <strong>host networking</strong>. Game ports auto-assign starting at UDP 7777. For N sietches you need ports <strong>7777–7778+N</strong> (UDP) forwarded, plus the corresponding beacon ports. When in doubt, forward the full range <strong>7777–7900 UDP</strong>.</p>
|
||||
</div>
|
||||
|
||||
<div id="sietch-status" style="margin:1rem 0;color:var(--text-dim)">Loading sietch info...</div>
|
||||
|
||||
<div id="sietch-controls" style="display:none">
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-green btn-lg" id="btn-add-sietch">Add Sietch</button>
|
||||
<button class="btn btn-red btn-lg" id="btn-remove-sietch">Remove Sietch</button>
|
||||
</div>
|
||||
<p class="card-desc" style="margin-top:.75rem">After adding or removing a sietch, <strong>restart the battlegroup</strong> for changes to take effect.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<!-- Console -->
|
||||
<div class="console-wrapper" id="console-wrapper">
|
||||
<button class="console-toggle" id="console-toggle">
|
||||
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2"><polyline points="4 17 10 11 4 5"/><line x1="12" y1="19" x2="20" y2="19"/></svg>
|
||||
Console Output
|
||||
<span class="console-badge" id="console-badge" hidden>new</span>
|
||||
</button>
|
||||
<div class="console-body" id="console-body">
|
||||
<pre id="console-output"></pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Loading overlay -->
|
||||
<div class="overlay" id="overlay" hidden>
|
||||
<div class="spinner"></div>
|
||||
<p class="overlay-text" id="overlay-text">Working...</p>
|
||||
</div>
|
||||
|
||||
<script src="/js/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
1880
docs/reference-repos/the4rchangel/public/js/app.js
Normal file
@@ -0,0 +1,375 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build public/data/cosmetic-catalog.json from game pak strings + known unlock IDs."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
OUT = ROOT / "public" / "data" / "cosmetic-catalog.json"
|
||||
DEFAULT_PAK = Path(
|
||||
"/mnt/c/Program Files (x86)/Steam/steamapps/common/DuneAwakening/DuneSandbox/Content/Paks/Systems.pak"
|
||||
)
|
||||
|
||||
# Hand-curated labels for IDs that auto-label poorly (from live DB / prior testing).
|
||||
MANUAL: dict[str, tuple[str, str]] = {
|
||||
"AllDyepackChoam": ("CHOAM Dye Pack", "Dye Packs"),
|
||||
"AllDyepackMaula": ("Maula Dye Pack", "Dye Packs"),
|
||||
"AllDyePackBonusUniversal01": ("Universal Bonus Dye Pack", "Dye Packs"),
|
||||
"RedDesertGlobal": ("Red Desert Global Dye", "Dye Packs"),
|
||||
"SmugglerGlobal": ("Smuggler Global Dye", "Dye Packs"),
|
||||
"Watershippers Global": ("Watershippers Global Dye", "Dye Packs"),
|
||||
"FVehDyePackUltimate01": ("Ultimate Vehicle Dye Pack", "Dye Packs"),
|
||||
"SandbikeDyePackDeluxe01": ("Deluxe Sandbike Dye Pack", "Dye Packs"),
|
||||
"SunsetDyeGlobal": ("Sunset Global Dye", "Dye Packs"),
|
||||
"Beta_Sword": ("Beta Sword Skin", "Weapon Skins"),
|
||||
"MTX_Frameblade_Knife": ("Frameblade Knife Skin", "Weapon Skins"),
|
||||
"MTX_Smuggler_Kindjal": ("Smuggler Kindjal Skin", "Weapon Skins"),
|
||||
"MTX_Smug_Rifle": ("Smuggler Rifle Skin", "Weapon Skins"),
|
||||
"MTX_Taligari_Rifle": ("Taligari Rifle Skin", "Weapon Skins"),
|
||||
"MTX_Taligari_SMG": ("Taligari SMG Skin", "Weapon Skins"),
|
||||
"MTX_GunnerSniper_Rifle": ("Gunner Sniper Rifle Skin", "Weapon Skins"),
|
||||
"MTX_Gunner_Battlerifle": ("Gunner Battle Rifle Skin", "Weapon Skins"),
|
||||
"MTX_WaterS_Drillshot": ("Water Shipper Drillshot Skin", "Weapon Skins"),
|
||||
"MTX_WaterS_Rapier": ("Water Shipper Rapier Skin", "Weapon Skins"),
|
||||
"MTX_WaterS_Light_Orni": ("Water Shipper Light Ornithopter", "Vehicle Skins"),
|
||||
"MTX_Buggy_Nomad": ("Nomad Buggy Skin", "Vehicle Skins"),
|
||||
"MTX_WaterFat_Ornithopter_01": ("Water Fat Ornithopter Skin", "Vehicle Skins"),
|
||||
}
|
||||
|
||||
FACTIONS = {
|
||||
"Atre": "Atreides",
|
||||
"Atreides": "Atreides",
|
||||
"Hark": "Harkonnen",
|
||||
"Harkonnen": "Harkonnen",
|
||||
"Choam": "CHOAM",
|
||||
"CHOAM": "CHOAM",
|
||||
"Frem": "Fremen",
|
||||
"Fremen": "Fremen",
|
||||
"Smug": "Smuggler",
|
||||
"Smuggler": "Smuggler",
|
||||
"Ecaz": "Ecaz",
|
||||
"Morit": "Moritani",
|
||||
"Moritani": "Moritani",
|
||||
"Agrosaz": "Agrosaz",
|
||||
"Alexin": "Alexin",
|
||||
"Dyvetz": "Dyvetz",
|
||||
"Hagal": "Hagal",
|
||||
"Hurata": "Hurata",
|
||||
"Imota": "Imota",
|
||||
"Kenola": "Kenola",
|
||||
"Kirab": "Kirab",
|
||||
"Lindaren": "Lindaren",
|
||||
"MaasK": "Maas K",
|
||||
"Maros": "Maros",
|
||||
"Maula": "Maula",
|
||||
"Mikarrol": "Mikarrol",
|
||||
"Mutelli": "Mutelli",
|
||||
"Novebruns": "Novebruns",
|
||||
"Ordos": "Ordos",
|
||||
"PolarGuards": "Polar Guards",
|
||||
"RedD": "Red Duke",
|
||||
"Richese": "Richese",
|
||||
"SandF": "Sand Fisher",
|
||||
"Scav": "Scavenger",
|
||||
"Slav": "Slav",
|
||||
"SmugTech": "Smuggler Tech",
|
||||
"Sor": "Sor",
|
||||
"Spinette": "Spinette",
|
||||
"Talgari": "Taligari",
|
||||
"Taligari": "Taligari",
|
||||
"Thorvald": "Thorvald",
|
||||
"Tseida": "Tseida",
|
||||
"Varota": "Varota",
|
||||
"Vernius": "Vernius",
|
||||
"Wallach": "Wallach",
|
||||
"WaterFat": "Water Fat",
|
||||
"WaterS": "Water Shipper",
|
||||
"Wayku": "Wayku",
|
||||
"Wydras": "Wydras",
|
||||
"Graben": "Graben",
|
||||
"Sard": "Sardaukar",
|
||||
"Sardaukar": "Sardaukar",
|
||||
"Gunner": "Gunner",
|
||||
"Frameblade": "Frameblade",
|
||||
"Nomad": "Nomad",
|
||||
"Ultimate": "Ultimate",
|
||||
"Universal": "Universal",
|
||||
"Bonus": "Bonus",
|
||||
"Deluxe": "Deluxe",
|
||||
"Frem": "Fremen",
|
||||
}
|
||||
|
||||
PIECES = {
|
||||
"Top": "Chest",
|
||||
"Bottom": "Pants",
|
||||
"Bottoms": "Pants",
|
||||
"Boots": "Boots",
|
||||
"Gloves": "Gloves",
|
||||
"Helmet": "Helmet",
|
||||
"Headwear": "Helmet",
|
||||
"Footwear": "Boots",
|
||||
}
|
||||
|
||||
ARMOR_WORDS = (
|
||||
"Armor", "Stillsuit", "Scout", "Assault", "Heavy", "Light", "Formal",
|
||||
"Trenchcoat", "MovieSuit", "Caladan", "Garment", "Cloth",
|
||||
)
|
||||
WEAPON_WORDS = (
|
||||
"Rifle", "SMG", "Knife", "Kindjal", "Drillshot", "Rapier", "Sword",
|
||||
"Pistol", "Shotgun", "Battlerifle", "Sniper", "Fireballer", "Minotaur",
|
||||
"Frameblade", "Orni", "Ornithopter",
|
||||
)
|
||||
VEHICLE_PAINT_WORDS = (
|
||||
"Buggy", "Sandbike", "Ornithopter", "Sandcrawler", "Transport", "1MGC", "Orni",
|
||||
)
|
||||
TYPO = re.compile(
|
||||
r"(Ornitopther|TransportOrnithop$|HeavyArmorlmet|HeavyArmor_Bot$|LightArmor_Bot$|"
|
||||
r"Atreula_|hkula_|_Bot$|Hkula_|Smug_Ornithopter_Transport$)",
|
||||
re.I,
|
||||
)
|
||||
VEHICLE_WORDS = (
|
||||
"Buggy", "Sandbike", "Ground", "Flying", "Orni", "Ornithopter",
|
||||
"CargoContainer", "Vehicle", "Sandcrawler", "Transport",
|
||||
)
|
||||
|
||||
NOISE = re.compile(
|
||||
r"(_DESC$|_NAME$|_Data$|_MeshData$|_Icon$|_Texture$|_Material$|"
|
||||
r"GUI|Widget|Blueprint|Placeable|Preview|Banner|Store|Bundle|Popup|"
|
||||
r"Emote|Quest|NPC|Audio|Anim|Sequence|Shader|Physics|Skeleton|"
|
||||
r"Variant_Data|Placeholder|DebugRGB|Patent|Placable|Localization|"
|
||||
r"StringTable|Loc_|Tooltip|Thumbnail|Loading|Cinematic|VO_|FX_|VFX)",
|
||||
re.I,
|
||||
)
|
||||
|
||||
|
||||
def grep_ids(pattern: str, pak: Path) -> set[str]:
|
||||
if not pak.is_file():
|
||||
return set()
|
||||
proc = subprocess.run(
|
||||
["grep", "-a", "-oE", pattern, str(pak)],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
return {line.strip() for line in proc.stdout.splitlines() if line.strip()}
|
||||
|
||||
|
||||
def is_noise(cid: str) -> bool:
|
||||
if len(cid) < 4 or cid.endswith("_"):
|
||||
return True
|
||||
if cid.startswith("DA_"):
|
||||
return True
|
||||
if TYPO.search(cid):
|
||||
return True
|
||||
if NOISE.search(cid):
|
||||
return True
|
||||
if cid.count("_") < 1 and cid not in MANUAL:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def unlock_mode(cid: str) -> str:
|
||||
"""Swatch_* are inventory consumables — not customization-library unlock IDs."""
|
||||
if cid.startswith("Swatch_"):
|
||||
return "inventory"
|
||||
return "customization"
|
||||
|
||||
|
||||
def categorize(cid: str) -> str:
|
||||
if cid.startswith("DyePack_") or "Dyepack" in cid or "DyePack" in cid or cid.endswith("Global"):
|
||||
return "Dye Packs"
|
||||
if cid.startswith("VehicleVariant_"):
|
||||
return "Vehicle Variants"
|
||||
if cid.startswith("MaterialVariant_"):
|
||||
if any(w in cid for w in VEHICLE_PAINT_WORDS) and not any(w in cid for w in ARMOR_WORDS):
|
||||
return "Vehicle Paints"
|
||||
if any(w in cid for w in WEAPON_WORDS):
|
||||
return "Weapon Paints"
|
||||
if any(w in cid for w in ARMOR_WORDS):
|
||||
return "Armor Paints"
|
||||
return "Material Variants"
|
||||
if cid.startswith("Swatch_Vehicle_"):
|
||||
return "Swatch Tokens (Inventory)"
|
||||
if cid.startswith("Swatch_Wpn_"):
|
||||
return "Swatch Tokens (Inventory)"
|
||||
if cid.startswith("Swatch_Cloth_"):
|
||||
return "Swatch Tokens (Inventory)"
|
||||
if cid.startswith("Swatch_"):
|
||||
return "Swatch Tokens (Inventory)"
|
||||
upper = cid.upper()
|
||||
if any(w.upper() in upper for w in VEHICLE_WORDS) and "Armor" not in cid:
|
||||
if cid.endswith("_MeshVariant"):
|
||||
return "Vehicle Skins"
|
||||
return "Vehicle Skins"
|
||||
if any(w in cid for w in WEAPON_WORDS) and not cid.endswith("_MeshVariant"):
|
||||
return "Weapon Skins"
|
||||
if cid.endswith("_MeshVariant") or any(w in cid for w in ARMOR_WORDS):
|
||||
return "Armor Skins"
|
||||
if cid.startswith("WaterS_") or cid.startswith("Beta_"):
|
||||
if any(w in cid for w in WEAPON_WORDS):
|
||||
return "Weapon Skins"
|
||||
return "Armor Skins"
|
||||
if cid.startswith("MTX_"):
|
||||
return "Premium (MTX)"
|
||||
return "Other"
|
||||
|
||||
|
||||
def expand_token(token: str) -> str:
|
||||
if token in FACTIONS:
|
||||
return FACTIONS[token]
|
||||
if token in PIECES:
|
||||
return PIECES[token]
|
||||
if token.isupper() and len(token) <= 4:
|
||||
return token
|
||||
# CamelCase / acronym boundaries
|
||||
spaced = re.sub(r"([a-z])([A-Z])", r"\1 \2", token)
|
||||
spaced = spaced.replace("_", " ")
|
||||
return spaced.strip()
|
||||
|
||||
|
||||
def auto_label(cid: str) -> str:
|
||||
if cid in MANUAL:
|
||||
return MANUAL[cid][0]
|
||||
|
||||
body = cid
|
||||
if body.startswith("MTX_"):
|
||||
body = body[4:]
|
||||
if body.startswith("DyePack_"):
|
||||
faction = body[len("DyePack_") :]
|
||||
return f"{expand_token(faction)} Dye Pack"
|
||||
if body.startswith("Swatch_Vehicle_"):
|
||||
rest = body[len("Swatch_Vehicle_") :].replace("_", " ")
|
||||
return f"Vehicle Swatch Token — {rest}"
|
||||
if body.startswith("Swatch_Wpn_"):
|
||||
rest = body[len("Swatch_Wpn_") :].replace("_", " ")
|
||||
return f"Weapon Swatch Token — {rest}"
|
||||
if body.startswith("Swatch_Cloth_"):
|
||||
rest = body[len("Swatch_Cloth_") :].replace("_", " ")
|
||||
return f"Armor Swatch Token — {rest}"
|
||||
if body.startswith("MaterialVariant_"):
|
||||
body = body[len("MaterialVariant_") :]
|
||||
label = " ".join(expand_token(t) for t in body.split("_") if t)
|
||||
return f"{label} Paint"
|
||||
if body.startswith("VehicleVariant_"):
|
||||
body = body[len("VehicleVariant_") :]
|
||||
label = " ".join(expand_token(t) for t in body.split("_") if t)
|
||||
return f"{label} Vehicle Variant"
|
||||
if body.endswith("_MeshVariant"):
|
||||
body = body[: -len("_MeshVariant")]
|
||||
|
||||
tokens = [t for t in body.split("_") if t]
|
||||
words = [expand_token(t) for t in tokens]
|
||||
label = " ".join(words)
|
||||
|
||||
if categorize(cid) == "Weapon Skins" and "Skin" not in label:
|
||||
label += " Skin"
|
||||
if categorize(cid) == "Vehicle Skins" and "Skin" not in label and "Ornithopter" not in label:
|
||||
label += " Skin"
|
||||
return label
|
||||
|
||||
|
||||
def should_include(cid: str) -> bool:
|
||||
if is_noise(cid):
|
||||
return False
|
||||
if cid in MANUAL:
|
||||
return True
|
||||
if cid.startswith("MaterialVariant_") or cid.startswith("VehicleVariant_"):
|
||||
return True
|
||||
if cid.startswith("Swatch_"):
|
||||
return True
|
||||
if cid.startswith("DyePack_") or cid.endswith("Global") or "Dyepack" in cid or "DyePack" in cid:
|
||||
return True
|
||||
if cid.endswith("_MeshVariant"):
|
||||
return True
|
||||
if cid.startswith("MTX_"):
|
||||
if cid.endswith("_MeshVariant"):
|
||||
return True
|
||||
if any(w in cid for w in WEAPON_WORDS + VEHICLE_WORDS):
|
||||
return True
|
||||
if any(w in cid for w in ARMOR_WORDS) and any(p in cid for p in PIECES):
|
||||
return True
|
||||
return False
|
||||
if cid.startswith("WaterS_") or cid.startswith("Beta_"):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def prefer_mesh_variant(ids: set[str]) -> set[str]:
|
||||
"""Drop base armor IDs when a MeshVariant sibling exists."""
|
||||
out = set(ids)
|
||||
for cid in list(ids):
|
||||
if cid.endswith("_MeshVariant"):
|
||||
base = cid[: -len("_MeshVariant")]
|
||||
if base in out and categorize(base) == "Armor Skins":
|
||||
out.discard(base)
|
||||
elif any(p in cid for p in PIECES) and f"{cid}_MeshVariant" in out:
|
||||
out.discard(cid)
|
||||
return out
|
||||
|
||||
|
||||
def collect_ids(pak: Path) -> set[str]:
|
||||
ids: set[str] = set(MANUAL.keys())
|
||||
ids |= grep_ids(r"[A-Za-z0-9_]+_MeshVariant", pak)
|
||||
ids |= grep_ids(r"MTX_[A-Za-z][A-Za-z0-9_]{4,80}", pak)
|
||||
ids |= grep_ids(r"MaterialVariant_[A-Za-z0-9_]+", pak)
|
||||
ids |= grep_ids(r"VehicleVariant_[A-Za-z0-9_]+", pak)
|
||||
ids |= grep_ids(r"Swatch_[A-Za-z0-9_]+", pak)
|
||||
ids |= grep_ids(
|
||||
r"(AllDyepack[A-Za-z0-9_]+|AllDyePack[A-Za-z0-9_]+|"
|
||||
r"RedDesertGlobal|SmugglerGlobal|SunsetDyeGlobal|"
|
||||
r"FVehDyePack[A-Za-z0-9_]+|SandbikeDyePack[A-Za-z0-9_]+|DyePack_[A-Za-z0-9_]+)",
|
||||
pak,
|
||||
)
|
||||
ids |= grep_ids(r"(WaterS_[A-Za-z0-9_]+|Beta_Sword)", pak)
|
||||
ids = {i for i in ids if should_include(i)}
|
||||
return prefer_mesh_variant(ids)
|
||||
|
||||
|
||||
def build_catalog(pak: Path) -> dict:
|
||||
ids = collect_ids(pak)
|
||||
cosmetics: dict[str, dict[str, str]] = {}
|
||||
for cid in sorted(ids, key=str.lower):
|
||||
if cid in MANUAL:
|
||||
name, category = MANUAL[cid]
|
||||
else:
|
||||
name = auto_label(cid)
|
||||
category = categorize(cid)
|
||||
cosmetics[cid] = {
|
||||
"name": name,
|
||||
"category": category,
|
||||
"unlock": unlock_mode(cid),
|
||||
}
|
||||
|
||||
unlockable = sum(1 for c in cosmetics.values() if c["unlock"] == "customization")
|
||||
return {
|
||||
"_meta": {
|
||||
"total": len(cosmetics),
|
||||
"unlockable": unlockable,
|
||||
"source": "Dune Awakening Systems.pak string extraction + curated unlock IDs",
|
||||
"pak": str(pak) if pak.is_file() else "not found — used curated subset only",
|
||||
},
|
||||
"cosmetics": cosmetics,
|
||||
}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
pak = Path(sys.argv[1]) if len(sys.argv) > 1 else DEFAULT_PAK
|
||||
data = build_catalog(pak)
|
||||
OUT.parent.mkdir(parents=True, exist_ok=True)
|
||||
OUT.write_text(json.dumps(data, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
|
||||
print(f"Wrote {OUT} ({data['_meta']['total']} cosmetics)")
|
||||
cats: dict[str, int] = {}
|
||||
for info in data["cosmetics"].values():
|
||||
cats[info["category"]] = cats.get(info["category"], 0) + 1
|
||||
for cat, n in sorted(cats.items(), key=lambda x: (-x[1], x[0])):
|
||||
print(f" {cat}: {n}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
1930
docs/reference-repos/the4rchangel/server.js
Normal file
70
docs/reference-repos/the4rchangel/start_as_admin.bat
Normal file
@@ -0,0 +1,70 @@
|
||||
@echo off
|
||||
title Dune: Awakening Server Manager
|
||||
|
||||
:: Check for admin privileges (required for Hyper-V)
|
||||
net session >nul 2>&1
|
||||
if %errorLevel% neq 0 (
|
||||
echo Requesting administrator privileges...
|
||||
powershell -Command "Start-Process cmd -ArgumentList '/c cd /d \"%~dp0\" && \"%~f0\"' -Verb RunAs"
|
||||
exit /b
|
||||
)
|
||||
|
||||
cd /d "%~dp0"
|
||||
|
||||
where node >nul 2>&1
|
||||
if %errorLevel% neq 0 (
|
||||
echo.
|
||||
echo ERROR: Node.js is required but not found.
|
||||
echo Download it from https://nodejs.org
|
||||
echo.
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
if not exist node_modules (
|
||||
echo Installing dependencies...
|
||||
call npm install
|
||||
if %errorLevel% neq 0 (
|
||||
echo.
|
||||
echo npm install failed. If you see a script execution policy error,
|
||||
echo open PowerShell as Administrator and run:
|
||||
echo.
|
||||
echo Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned
|
||||
echo.
|
||||
echo Then try again.
|
||||
echo.
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
echo.
|
||||
)
|
||||
|
||||
:: Verify express is installed (catches partial installs)
|
||||
if not exist "node_modules\express" (
|
||||
echo Dependencies missing or incomplete. Reinstalling...
|
||||
call npm install
|
||||
if %errorLevel% neq 0 (
|
||||
echo.
|
||||
echo Failed to install dependencies. Check your internet connection
|
||||
echo and try running "npm install" manually.
|
||||
echo.
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
)
|
||||
|
||||
echo.
|
||||
echo ==========================================
|
||||
echo Dune: Awakening Server Manager
|
||||
echo http://localhost:3000
|
||||
echo ==========================================
|
||||
echo.
|
||||
|
||||
start http://localhost:3000
|
||||
node server.js
|
||||
if %errorLevel% neq 0 (
|
||||
echo.
|
||||
echo Server exited with an error. Check the output above.
|
||||
echo.
|
||||
)
|
||||
pause
|
||||
@@ -0,0 +1,14 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="/home/archangel/.cache/CUE4Parse/CUE4Parse/CUE4Parse.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,283 @@
|
||||
using CUE4Parse.FileProvider;
|
||||
using CUE4Parse.UE4.Assets.Exports.Engine;
|
||||
using CUE4Parse.UE4.Versions;
|
||||
using CUE4Parse.Utils;
|
||||
using System.Text.Json;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
const string defaultPaksDir =
|
||||
"/mnt/c/Program Files (x86)/Steam/steamapps/common/DuneAwakening/DuneSandbox/Content/Paks";
|
||||
|
||||
var paksDir = args.Length > 0 ? args[0] : defaultPaksDir;
|
||||
var outDir = args.Length > 1 ? args[1] : Directory.GetCurrentDirectory();
|
||||
|
||||
if (!Directory.Exists(paksDir))
|
||||
{
|
||||
Console.Error.WriteLine($"Paks directory not found: {paksDir}");
|
||||
return 1;
|
||||
}
|
||||
|
||||
Console.WriteLine($"Scanning: {paksDir}");
|
||||
|
||||
var pathComparer = StringComparer.OrdinalIgnoreCase;
|
||||
var provider = new DefaultFileProvider(
|
||||
paksDir,
|
||||
SearchOption.TopDirectoryOnly,
|
||||
versions: new VersionContainer(EGame.GAME_DuneAwakening),
|
||||
pathComparer: pathComparer);
|
||||
|
||||
provider.Initialize();
|
||||
var mounted = provider.Mount();
|
||||
Console.WriteLine($"Mounted {mounted} archives; {provider.Files.Count:N0} files");
|
||||
|
||||
var allPaths = provider.Files.Keys.ToList();
|
||||
|
||||
var catalogPath = Path.GetFullPath(Path.Combine(outDir, "..", "..", "public", "data", "item-catalog.json"));
|
||||
var catalogIds = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
if (File.Exists(catalogPath))
|
||||
{
|
||||
var json = File.ReadAllText(catalogPath);
|
||||
foreach (Match m in Regex.Matches(json, "\"([^\"]+)\"\\s*:\\s*\\{"))
|
||||
catalogIds.Add(m.Groups[1].Value);
|
||||
Console.WriteLine($"Loaded {catalogIds.Count} catalog IDs");
|
||||
}
|
||||
|
||||
// Inventory template IDs for vehicle modules: SandbikeChassis_5, TreadwheelEngine_4, etc.
|
||||
var modulePattern = new Regex(
|
||||
@"^(?<prefix>Sandbike|Buggy|Treadwheel|OrnithopterTransport|OrnithopterLight|OrnithopterMedium|AssaultOrnithopter|CarrierOrnithopter|Sandcrawler|Groundcar|OrnithopterHeavy)(?<part>(?:Unique_[A-Za-z0-9_]+|[A-Za-z_]+))_(?<tier>\d+)$",
|
||||
RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
|
||||
var moduleIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
foreach (var path in allPaths)
|
||||
{
|
||||
var baseName = Path.GetFileNameWithoutExtension(path);
|
||||
if (string.IsNullOrEmpty(baseName)) continue;
|
||||
|
||||
if (modulePattern.IsMatch(baseName))
|
||||
{
|
||||
moduleIds.TryAdd(baseName, path);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (baseName.StartsWith("DA_REC_", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var embedded = baseName["DA_REC_".Length..];
|
||||
if (modulePattern.IsMatch(embedded))
|
||||
moduleIds.TryAdd(embedded, path);
|
||||
}
|
||||
}
|
||||
|
||||
// Also collect item-ish paths under /Items/ for manual review
|
||||
var itemPaths = allPaths
|
||||
.Where(p => p.Contains("/Items/", StringComparison.OrdinalIgnoreCase) &&
|
||||
(p.Contains("Treadwheel", StringComparison.OrdinalIgnoreCase) ||
|
||||
p.Contains("OrnithopterTransport", StringComparison.OrdinalIgnoreCase) ||
|
||||
p.Contains("CargoContainer", StringComparison.OrdinalIgnoreCase) ||
|
||||
p.Contains("Sandcrawler", StringComparison.OrdinalIgnoreCase)))
|
||||
.OrderBy(p => p, StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
|
||||
Directory.CreateDirectory(outDir);
|
||||
var moduleOut = Path.Combine(outDir, "vehicle-module-ids.txt");
|
||||
File.WriteAllLines(moduleOut, moduleIds.Keys.OrderBy(x => x, StringComparer.OrdinalIgnoreCase));
|
||||
var moduleMissing = moduleIds.Keys.Where(id => !catalogIds.Contains(id)).OrderBy(x => x).ToList();
|
||||
var moduleMissingOut = Path.Combine(outDir, "missing-vehicle-module-ids.txt");
|
||||
File.WriteAllLines(moduleMissingOut, moduleMissing);
|
||||
File.WriteAllLines(Path.Combine(outDir, "item-path-hints.txt"), itemPaths);
|
||||
|
||||
Console.WriteLine();
|
||||
Console.WriteLine($"=== Vehicle module template IDs: {moduleIds.Count} ({moduleMissing.Count} missing from catalog) ===");
|
||||
foreach (var prefix in new[] { "Treadwheel", "OrnithopterTransport", "CarrierOrnithopter", "Sandcrawler", "Groundcar" })
|
||||
{
|
||||
var ids = moduleIds.Keys.Where(k => k.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)).OrderBy(x => x).ToList();
|
||||
if (ids.Count == 0) continue;
|
||||
Console.WriteLine();
|
||||
Console.WriteLine($"-- {prefix} ({ids.Count}) --");
|
||||
foreach (var id in ids)
|
||||
{
|
||||
var flag = catalogIds.Contains(id) ? "OK" : "MISSING";
|
||||
Console.WriteLine($" [{flag}] {id}");
|
||||
}
|
||||
}
|
||||
|
||||
Console.WriteLine();
|
||||
Console.WriteLine($"Wrote {moduleOut}");
|
||||
Console.WriteLine($"Wrote {moduleMissingOut}");
|
||||
Console.WriteLine($"Wrote {Path.Combine(outDir, "item-path-hints.txt")} ({itemPaths.Count} paths)");
|
||||
|
||||
// Broader item-source discovery for future catalog patches
|
||||
var sourceHints = new[] { "/Items/", "ItemTemplate", "ItemDefinition", "ItemData", "DataTable", "DT_", "InventoryItem", "CargoContainer", "StorageContainer", "PlacableSet", "Patent" };
|
||||
foreach (var hint in sourceHints)
|
||||
{
|
||||
var count = allPaths.Count(p => p.Contains(hint, StringComparison.OrdinalIgnoreCase));
|
||||
if (count > 0) Console.WriteLine($" paths[{hint}]: {count}");
|
||||
}
|
||||
|
||||
var recPattern = new Regex(@"^DA_REC_(.+)$", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
var recIds = new SortedSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var path in allPaths)
|
||||
{
|
||||
var baseName = Path.GetFileNameWithoutExtension(path);
|
||||
if (baseName == null) continue;
|
||||
var m = recPattern.Match(baseName);
|
||||
if (m.Success) recIds.Add(m.Groups[1].Value);
|
||||
}
|
||||
var recOut = Path.Combine(outDir, "da-rec-template-ids.txt");
|
||||
File.WriteAllLines(recOut, recIds);
|
||||
Console.WriteLine($"DA_REC-derived template IDs: {recIds.Count} -> {recOut}");
|
||||
|
||||
var registryPaths = allPaths
|
||||
.Where(p => Regex.IsMatch(p, @"(ItemTemplates|ItemDefinitions|ItemRegistry|MasterItem|AllItems|ItemCatalog|DT_.*Item|ItemTable)", RegexOptions.IgnoreCase))
|
||||
.OrderBy(p => p, StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
var registryOut = Path.Combine(outDir, "item-registry-paths.txt");
|
||||
File.WriteAllLines(registryOut, registryPaths);
|
||||
Console.WriteLine($"Item registry candidate paths: {registryPaths.Count} -> {registryOut}");
|
||||
|
||||
var cargoPaths = allPaths
|
||||
.Where(p => p.Contains("Cargo", StringComparison.OrdinalIgnoreCase) &&
|
||||
(p.Contains("Ornithopter", StringComparison.OrdinalIgnoreCase) ||
|
||||
p.Contains("Transport", StringComparison.OrdinalIgnoreCase) ||
|
||||
p.Contains("Container", StringComparison.OrdinalIgnoreCase) ||
|
||||
p.Contains("Storage", StringComparison.OrdinalIgnoreCase)))
|
||||
.OrderBy(p => p, StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
var cargoOut = Path.Combine(outDir, "cargo-container-paths.txt");
|
||||
File.WriteAllLines(cargoOut, cargoPaths);
|
||||
Console.WriteLine($"Cargo/container ornithopter paths: {cargoPaths.Count} -> {cargoOut}");
|
||||
foreach (var p in cargoPaths.Where(p => p.EndsWith(".uasset", StringComparison.OrdinalIgnoreCase)).Take(15))
|
||||
Console.WriteLine(" " + p);
|
||||
|
||||
var baseItemTables = new[]
|
||||
{
|
||||
"DuneSandbox/Content/Dune/Systems/Items/BaseItems/DT_BaseItems_Vehicles",
|
||||
"DuneSandbox/Content/Dune/Systems/Items/BaseItems/DT_BaseItems_BuildingSets",
|
||||
"DuneSandbox/Content/Dune/Systems/Items/BaseItems/DT_BaseItems_Placeables",
|
||||
"DuneSandbox/Content/Dune/Systems/Items/BaseItems/DT_BaseItems_Resources",
|
||||
"DuneSandbox/Content/Dune/Systems/Items/CDT_BaseItems",
|
||||
"DuneSandbox/Content/Dune/GUI/Widgets/Menus/Gameplay/AdminPanel/Items/DT_Admin_QuickItems_Presets",
|
||||
};
|
||||
|
||||
Console.WriteLine();
|
||||
Console.WriteLine("=== DT_BaseItems row keys ===");
|
||||
foreach (var tablePath in baseItemTables)
|
||||
{
|
||||
var tableName = tablePath.SubstringAfterLast('/');
|
||||
try
|
||||
{
|
||||
var table = provider.SafeLoadPackageObject<UDataTable>(tablePath, tableName);
|
||||
if (table?.RowMap == null)
|
||||
{
|
||||
Console.WriteLine($" {tableName}: failed to load");
|
||||
continue;
|
||||
}
|
||||
|
||||
var keys = table.RowMap.Keys.Select(k => k.Text).OrderBy(x => x, StringComparer.OrdinalIgnoreCase).ToList();
|
||||
var tableOut = Path.Combine(outDir, $"{tableName}-rows.txt");
|
||||
File.WriteAllLines(tableOut, keys);
|
||||
Console.WriteLine($" {tableName}: {keys.Count} rows -> {tableOut}");
|
||||
|
||||
var cargo = keys.Where(k => k.Contains("Cargo", StringComparison.OrdinalIgnoreCase) ||
|
||||
k.Contains("Container", StringComparison.OrdinalIgnoreCase) ||
|
||||
k.Contains("Treadwheel", StringComparison.OrdinalIgnoreCase) ||
|
||||
k.Contains("Patent", StringComparison.OrdinalIgnoreCase)).ToList();
|
||||
foreach (var k in cargo.Take(20)) Console.WriteLine($" {k}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($" {tableName}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: extract printable template-id-like strings from key table uexp blobs
|
||||
Console.WriteLine();
|
||||
Console.WriteLine("=== UEXP string fallback (template ID candidates) ===");
|
||||
var idFromBytes = new Regex(@"\b[A-Z][A-Za-z0-9_]{2,80}\b", RegexOptions.Compiled);
|
||||
foreach (var tablePath in baseItemTables)
|
||||
{
|
||||
var uexpPath = tablePath + ".uexp";
|
||||
if (!provider.Files.TryGetValue(uexpPath, out var file)) continue;
|
||||
try
|
||||
{
|
||||
var bytes = file.Read();
|
||||
var text = System.Text.Encoding.ASCII.GetString(bytes);
|
||||
var ids = idFromBytes.Matches(text)
|
||||
.Select(m => m.Value)
|
||||
.Where(v => v.Contains('_') && !v.StartsWith("DA_", StringComparison.Ordinal) &&
|
||||
!v.EndsWith("Placeable", StringComparison.OrdinalIgnoreCase) &&
|
||||
char.IsUpper(v[0]))
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.OrderBy(x => x, StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
var tableName = tablePath.SubstringAfterLast('/');
|
||||
var outPath = Path.Combine(outDir, $"{tableName}-uexp-ids.txt");
|
||||
File.WriteAllLines(outPath, ids);
|
||||
Console.WriteLine($" {tableName}: {ids.Count} id-like strings -> {outPath}");
|
||||
foreach (var id in ids.Where(i => i.Contains("Cargo", StringComparison.OrdinalIgnoreCase) ||
|
||||
i.Contains("Container", StringComparison.OrdinalIgnoreCase) ||
|
||||
i.Contains("Treadwheel", StringComparison.OrdinalIgnoreCase) ||
|
||||
i.Contains("Patent", StringComparison.OrdinalIgnoreCase)).Take(12))
|
||||
Console.WriteLine($" {id}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($" {tablePath}: uexp read failed: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
static string TechCategory(string id)
|
||||
{
|
||||
if (id.StartsWith("DA_GRP_", StringComparison.OrdinalIgnoreCase)) return "Group";
|
||||
if (id.StartsWith("DA_REC_", StringComparison.OrdinalIgnoreCase)) return "Recipe";
|
||||
if (id.StartsWith("RCP_", StringComparison.OrdinalIgnoreCase)) return "Recipe";
|
||||
if (id.StartsWith("BLD_", StringComparison.OrdinalIgnoreCase)) return "Building";
|
||||
return "Other";
|
||||
}
|
||||
|
||||
static bool IsTechTreeNodeId(string name) =>
|
||||
name.StartsWith("DA_GRP_", StringComparison.OrdinalIgnoreCase) ||
|
||||
name.StartsWith("DA_REC_", StringComparison.OrdinalIgnoreCase) ||
|
||||
name.StartsWith("RCP_", StringComparison.OrdinalIgnoreCase) ||
|
||||
name.StartsWith("BLD_", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
var techNodeIds = allPaths
|
||||
.Where(p => p.Contains("/TechKnowledge/", StringComparison.OrdinalIgnoreCase) &&
|
||||
p.EndsWith(".uasset", StringComparison.OrdinalIgnoreCase))
|
||||
.Select(p => Path.GetFileNameWithoutExtension(p)!)
|
||||
.Where(IsTechTreeNodeId)
|
||||
.Concat(allPaths
|
||||
.Where(p => p.EndsWith(".uasset", StringComparison.OrdinalIgnoreCase))
|
||||
.Select(p => Path.GetFileNameWithoutExtension(p)!)
|
||||
.Where(n => n.StartsWith("RCP_", StringComparison.OrdinalIgnoreCase) ||
|
||||
n.StartsWith("BLD_", StringComparison.OrdinalIgnoreCase)))
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.OrderBy(x => x, StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
|
||||
var techOut = Path.Combine(outDir, "tech-tree-node-ids.txt");
|
||||
File.WriteAllLines(techOut, techNodeIds);
|
||||
|
||||
var techCatalogPath = Path.GetFullPath(Path.Combine(outDir, "..", "..", "public", "data", "tech-recipe-catalog.json"));
|
||||
var recipes = new Dictionary<string, object>();
|
||||
foreach (var id in techNodeIds)
|
||||
recipes[id] = new { category = TechCategory(id) };
|
||||
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(techCatalogPath)!);
|
||||
File.WriteAllText(
|
||||
techCatalogPath,
|
||||
JsonSerializer.Serialize(
|
||||
new { total = techNodeIds.Count, recipes },
|
||||
new JsonSerializerOptions { WriteIndented = true }));
|
||||
|
||||
Console.WriteLine();
|
||||
Console.WriteLine($"=== Tech tree node IDs: {techNodeIds.Count} ===");
|
||||
foreach (var prefix in new[] { "DA_GRP_", "DA_REC_", "RCP_", "BLD_" })
|
||||
{
|
||||
var n = techNodeIds.Count(id => id.StartsWith(prefix, StringComparison.OrdinalIgnoreCase));
|
||||
if (n > 0) Console.WriteLine($" {prefix}: {n}");
|
||||
}
|
||||
Console.WriteLine($"Wrote {techOut}");
|
||||
Console.WriteLine($"Wrote {techCatalogPath}");
|
||||
|
||||
return 0;
|
||||
@@ -0,0 +1,30 @@
|
||||
# Cue4ParsePatents
|
||||
|
||||
Scans Dune Awakening pak files via [CUE4Parse](https://github.com/FabianFG/CUE4Parse) (`GAME_DuneAwakening`) to discover inventory template IDs missing from the wiki-scraped catalog.
|
||||
|
||||
## Requirements
|
||||
|
||||
- .NET 8 SDK (`~/.dotnet` on WSL; set `DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1` if `libicu` is missing)
|
||||
- CUE4Parse source cloned to `~/.cache/CUE4Parse` (NuGet 1.2.2 lacks Dune support)
|
||||
- Game install: `DuneAwakening/DuneSandbox/Content/Paks`
|
||||
|
||||
## Run
|
||||
|
||||
```bash
|
||||
export DOTNET_ROOT=$HOME/.dotnet PATH="$DOTNET_ROOT:$PATH" DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1
|
||||
cd tools/Cue4ParsePatents
|
||||
dotnet run -c Release
|
||||
```
|
||||
|
||||
Outputs `vehicle-module-ids.txt`, `missing-vehicle-module-ids.txt`, `da-rec-template-ids.txt`, `item-registry-paths.txt`, etc.
|
||||
|
||||
## Central item ID sources (in game files)
|
||||
|
||||
| Path | Purpose |
|
||||
|------|---------|
|
||||
| `Content/Dune/Systems/Items/BaseItems/DT_BaseItems_*.uasset` | Master item tables (Vehicles, BuildingSets, Placeables, …) |
|
||||
| `Content/Dune/Systems/Items/CDT_BaseItems.uasset` | Composite table referencing all base item tables |
|
||||
| `Content/Dune/Systems/TechKnowledge/.../DA_REC_*.uasset` | Tech recipes; row name after `DA_REC_` is often the template ID |
|
||||
| `Content/Dune/GUI/.../DT_Admin_QuickItems_Presets.uasset` | Admin quick-spawn presets |
|
||||
|
||||
Full row export from `DT_BaseItems_*` requires Oodle decompression + UE5 `.usmap` mappings in CUE4Parse (not yet wired up here). Pak string grep on `Systems.pak` works as a fallback for many IDs.
|
||||