Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
27e7f00
Ship per-cell terrain height as a resource for admin actions
GeekOfWires Aug 18, 2026
281a183
Relay admin-driven session actions through PlayerControl/AvatarActor
GeekOfWires Aug 18, 2026
8bcaa81
Add the admin API's world-mutation and query commands
GeekOfWires Aug 18, 2026
370d7ac
Add the admin command to place a vehicle, deployed AMS, or Router
GeekOfWires Aug 18, 2026
c0db3fa
Correct CmdPlaceVehicle's doc comment: the telepad is not yet built
GeekOfWires Aug 18, 2026
7263660
Replace the raw-TCP PSAdmin protocol with an Akka HTTP admin API
GeekOfWires Aug 18, 2026
e807100
Report capture/LLU events to the admin API's interstellar log
GeekOfWires Aug 18, 2026
8bf2a99
Record the facility a portal designation changed, not its local id
GeekOfWires Aug 20, 2026
7a8a4a1
Serve the web portal's data, so it needs no database of its own
GeekOfWires Aug 20, 2026
29ce45e
Call this the PSF-Server HTTP API in comments and log output
GeekOfWires Aug 20, 2026
c5cb879
Call it the PSF-HTTP API
GeekOfWires Oct 3, 2026
b3c3821
Ignore .env and the local compose overlay
GeekOfWires Oct 3, 2026
fae9d67
Make first-login initialisation repeatable
GeekOfWires Oct 3, 2026
705ce75
Pace continent broadcasts and index the resend history
GeekOfWires Oct 3, 2026
49fadb8
Express portal queries as Quill quotations
GeekOfWires Oct 3, 2026
888203b
Authorise PSF-HTTP API callers from their own session
GeekOfWires Oct 3, 2026
30ac263
Add Packet Review: capture chosen opcodes on demand
GeekOfWires Oct 3, 2026
0e2d4f9
Let game masters set a character's experience
GeekOfWires Oct 3, 2026
788461a
Rank weapons and vehicles by kills
GeekOfWires Oct 3, 2026
dd37ea9
Let game masters list and manage squads
GeekOfWires Oct 3, 2026
f30a633
Run scheduled actions from the login server
GeekOfWires Oct 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,13 @@ logs/
pscrypto-lib/
.bsp/
project/project/

# Compose reads `.env` from this directory automatically, for variable substitution in the compose
# files. That makes it the natural home for host-specific values and secrets, so it must never be
# committed.
.env

# Local-only compose overlay: host-specific addresses, published ports and dev secrets. Auto-merged
# by `docker compose` without being referenced anywhere, which is exactly how it gets committed by
# accident.
docker-compose.override.yml
1 change: 1 addition & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ COPY --from=builder /PSF-LoginServer/server/target/pack/ /usr/local

EXPOSE 51000/udp
EXPOSE 51001/udp
# The PSF-HTTP API.
EXPOSE 51002/tcp

CMD ["psforever-server"]
76 changes: 76 additions & 0 deletions server/src/main/resources/db/migration/V019__PortalSchedule.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
-- Scheduled actions the portal performs against the world server.
--
-- WHY THIS LIVES HERE
-- The portal has no database of its own -- it reads and writes everything through the PSF-HTTP API,
-- deliberately, so the login server owns its data outright. A schedule still has to be durable
-- (it must survive a portal restart) and it has to be shared (two portal processes must agree on
-- whether a run already happened), so it belongs in this database even though nothing in the world
-- server reads it. Hence the `portal` prefix: these rows are the portal's, stored here because this
-- is where durable shared state lives.
--
-- TRIGGERS
-- A schedule may carry a time trigger, a condition trigger, or both. With both, either one firing
-- runs the action -- they are alternatives, not a conjunction, because "at 04:00, and also whenever
-- the server empties out" is the useful reading and "only at 04:00 if the server is also empty" is
-- not what anyone asks for.
--
-- time at_time 'HH:MM' plus every_days (1 = daily, 7 = weekly)
-- condition players_below / players_above a threshold
--
-- CONDITIONS FIRE ON A CROSSING, NOT A STATE
-- "Below 10 players" is true continuously all night, so firing on the state would re-apply the
-- layout every time the evaluator ticked. `armed` is what makes it an edge: it is cleared when the
-- condition fires and set again only once the population has gone back the other way, so one
-- crossing produces exactly one run. It is stored rather than kept in memory because a portal
-- restart in the middle of a quiet night must not re-arm and fire a second time.

CREATE TABLE IF NOT EXISTS "portalschedule" (
"id" SERIAL PRIMARY KEY,
"name" TEXT NOT NULL,
-- What to do. Only 'continent_layout' exists today; the column is text so adding another action
-- is a row rather than a migration.
"action" TEXT NOT NULL,
-- The action's parameters as JSON. For 'continent_layout' this is the `control` map that
-- /zones/apply-layout takes: zone number -> { building local id -> faction id }.
"payload" TEXT NOT NULL,
"enabled" BOOLEAN NOT NULL DEFAULT TRUE,

-- Time trigger. Both NULL when the schedule is condition-only.
-- 'HH:MM' in the server's own local time, kept as text because that is what it is -- a wall-clock
-- time of day, not an instant, and storing it as a timestamp would invite a date to come with it.
"at_time" TEXT,
"every_days" INTEGER,

-- Condition trigger. NULL when the schedule is time-only.
"condition" TEXT,
"threshold" INTEGER,
-- Whether the condition is ready to fire; see the note above on crossings.
"armed" BOOLEAN NOT NULL DEFAULT TRUE,

-- Bookkeeping. `last_run` is also the claim: a process wins the right to run a schedule by
-- advancing it, so two portals cannot both act on the same due schedule.
"last_run" TIMESTAMP,
"last_result" TEXT,
"created_by" TEXT NOT NULL,
"created" TIMESTAMP NOT NULL DEFAULT NOW(),

CONSTRAINT "portalschedule_action_known"
CHECK ("action" IN ('continent_layout')),
CONSTRAINT "portalschedule_condition_known"
CHECK ("condition" IS NULL OR "condition" IN ('players_below', 'players_above')),
-- A condition without a threshold, or a threshold without a condition, is a half-written trigger
-- that would either never fire or fire on nothing.
CONSTRAINT "portalschedule_condition_complete"
CHECK (("condition" IS NULL) = ("threshold" IS NULL)),
-- Same for the time trigger.
CONSTRAINT "portalschedule_time_complete"
CHECK (("at_time" IS NULL) = ("every_days" IS NULL)),
CONSTRAINT "portalschedule_every_days_positive"
CHECK ("every_days" IS NULL OR "every_days" >= 1),
-- A schedule with neither trigger can never run, so it is not a schedule.
CONSTRAINT "portalschedule_has_a_trigger"
CHECK ("at_time" IS NOT NULL OR "condition" IS NOT NULL)
);

-- The evaluator reads the enabled rows every tick and nothing else ever scans this table.
CREATE INDEX IF NOT EXISTS "portalschedule_enabled_idx" ON "portalschedule" ("enabled");
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
-- Let the portal decide which scheduled actions exist.
--
-- V019 pinned `action` to a single value with a CHECK, on the assumption that constraining it here
-- was free. It is not: the actions are implemented in the portal, in a registry it can extend, and
-- a constraint in this database means every new action needs a migration in a repository that has
-- nothing to do with it -- and a deploy ordering, since the portal would be writing a value the
-- database still refused.
--
-- The action is validated where it is implemented instead: the portal refuses an unknown action at
-- creation, with a message naming the ones it does know. What this database keeps enforcing is the
-- part that is genuinely structural and outlives any particular action -- that a schedule has at
-- least one trigger, and that each trigger it has is complete.

ALTER TABLE "portalschedule" DROP CONSTRAINT IF EXISTS "portalschedule_action_known";
23 changes: 12 additions & 11 deletions server/src/main/scala/net/psforever/server/Server.scala
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import ch.qos.logback.classic.joran.JoranConfigurator
import io.sentry.{Sentry, SentryOptions}
import net.psforever.actors.net.{LoginActor, MiddlewareActor, SocketSetup, SocketSetupInfo, SocketPane}
import net.psforever.actors.session.SessionActor
import net.psforever.login.psadmin.PsAdminActor
import net.psforever.actors.api.{AdminHttpService, ScheduledActionsActor}
import net.psforever.login._
import net.psforever.objects.Default
import net.psforever.objects.zones._
Expand Down Expand Up @@ -148,16 +148,17 @@ object Server {
name = SocketPane.SocketPaneKey.id
)

val adminListener = system.actorOf(
classic.Props(
new TcpListener(
classOf[PsAdminActor],
"psadmin-client-",
InetAddress.getByName(Config.app.admin.bind),
Config.app.admin.port
)
),
"psadmin-tcp-endpoint"
val adminEndpoint = system.actorOf(
classic.Props(new AdminHttpService(Config.app.admin.bind, Config.app.admin.port)),
"admin-http-endpoint"
)

// Scheduled actions. Evaluated here rather than in the portal because a schedule that fires at
// four in the morning has to be evaluated by a process that is running at four in the morning,
// and this is the process that is up whenever the game is.
val scheduledActions = system.actorOf(
classic.Props(new ScheduledActionsActor()),
"scheduled-actions"
)

logger.info(
Expand Down
10 changes: 6 additions & 4 deletions src/main/resources/application.conf
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# The socket bind address for all net.psforever.services except admin. 127.0.0.1 is the
# default for local testing, for public servers use 0.0.0.0 instead.
# The socket bind address for the game sockets (login and world). 127.0.0.1 is the default for
# local testing, for public servers use 0.0.0.0 instead. The PSF-HTTP API binds separately,
# under `admin` below.
bind = 127.0.0.1

# The public host name or IP address. Used to forward clients from the login
Expand Down Expand Up @@ -30,9 +31,10 @@ world {
server-type = development
}

# Admin API configuration
# PSF-HTTP API configuration. The `admin` key name is kept for compatibility: it is what
# CONFIG_FORCE_admin_bind and existing deployments set.
admin {
# TCP listening port
# HTTP listening port
port = 51002

# The socket bind address
Expand Down
1 change: 1 addition & 0 deletions src/main/resources/terrain/map01.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions src/main/resources/terrain/map02.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions src/main/resources/terrain/map03.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions src/main/resources/terrain/map04.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions src/main/resources/terrain/map05.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions src/main/resources/terrain/map06.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions src/main/resources/terrain/map07.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions src/main/resources/terrain/map08.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions src/main/resources/terrain/map09.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions src/main/resources/terrain/map10.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions src/main/resources/terrain/map11.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions src/main/resources/terrain/map12.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions src/main/resources/terrain/map13.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions src/main/resources/terrain/map14.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions src/main/resources/terrain/map15.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions src/main/resources/terrain/map16.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions src/main/resources/terrain/map96.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions src/main/resources/terrain/map97.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions src/main/resources/terrain/map98.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions src/main/resources/terrain/map99.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions src/main/resources/terrain/ugd01.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions src/main/resources/terrain/ugd02.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions src/main/resources/terrain/ugd03.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions src/main/resources/terrain/ugd04.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions src/main/resources/terrain/ugd05.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions src/main/resources/terrain/ugd06.json

Large diffs are not rendered by default.

Loading
Loading