Skip to content

Smoke stack integration#248

Open
itpick wants to merge 20 commits into
timiimit:masterfrom
ut4-hub:smoke-stack-integration
Open

Smoke stack integration#248
itpick wants to merge 20 commits into
timiimit:masterfrom
ut4-hub:smoke-stack-integration

Conversation

@itpick

@itpick itpick commented Jun 16, 2026

Copy link
Copy Markdown

No description provided.

itpick added 20 commits June 15, 2026 12:35
Adds a complete password-reset flow on the master server:

API
  POST /account/api/forgot-password (email) → anti-enumeration response,
    issues a token, sends an email.
  POST /account/api/reset-password  (token, newPassword) → consumes the
    token, updates the password, invalidates existing sessions.

Backend
  - Models/Database/PasswordResetToken.cs: single-use hex token, TTL index.
  - Services/Scoped/PasswordResetService.cs: issue, look-up, atomic consume.
  - Services/Scoped/EmailService.cs: MailKit SMTP transport with optional
    console logging for dev.
  - Models/Settings/MailSettings.cs: SMTP config, no-op if Host/Port empty.

Web
  - pages/ForgotPassword.vue: email input, anti-enumeration response.
  - pages/ResetPassword.vue: token from query, SHA-512-hashes new password
    client-side (matching Login/Register pattern), submits.
  - Routes added; "Forgot password?" link on Login.

Security notes
  - Anti-enumeration: forgot-password ALWAYS returns the same response,
    independent of whether the email matches an account. EmailService and
    PasswordResetService don't throw to callers either; the response shape
    is stable.
  - Tokens are SHA-512 hex (32 random bytes), single-use, 1h TTL via mongo
    TTL index + explicit IsValid() check on consume.
  - Reset invalidates active sessions, forcing re-login with the new
    password (same pattern as ChangePassword).

Dev wiring
  - Smoke docker-compose adds mailpit (port 8025 UI); api SMTP env points
    at mailpit:1025 with FromAddress=noreply@ut4-hub.local.
The previous chore commit accidentally added the debug logging instead of
removing it. This commit deletes the DEBUG-prefixed lines from the password
grant path.
The CloudFiles admin page only supported uploading a whole file via a
file-picker. For the ⓘ-icon-driven MCP files (announcement, news,
storage, playlists, rulesets) admins almost always want to tweak a
single string or array entry, not reupload a fresh JSON file. That
made any minor announcement change a multi-step download/edit/upload
ceremony.

Add an inline editor:
- AdminService.getCloudFileText(filename) pulls the current bytes as
  text via the existing admin/mcp_files/{filename} endpoint.
- EditCloudFile.vue gets a two-tab UX: 'Edit contents' (default) and
  'Upload file' (existing behaviour). The edit tab preloads the
  current contents into a monospace textarea, surfaces inline JSON
  validation errors, and offers a 'Pretty-print JSON' button.
- Submit uses a Blob with the original filename so the existing
  upsertCloudFile pipeline is unchanged.

Non-JSON files still save fine: the validator only warns; the server
never required JSON.

Verified: vue-tsc --noEmit clean; vite production build clean.
Add POST /ut/api/matchmaking/quickplay which returns one running
game-server suitable for an immediate Quick-Play join. Selection is
server-side so the master server can steer Quick-Play players onto a
small curated pool of always-on, bot-filled servers — humans
displace bots on the same instance instead of spawning thin empty
matches.

Selection rules:
- non-stale (LastUpdated within StaleAfter)
- optional UT_RULETAG_s match (RulesetTag from request body, matches
  UniqueTag values in UnrealTournmentMCPGameRulesets.json)
- optional MAPNAME_s match (PreferredMap)
- optional BuildUniqueID match
- at least one open public slot
- fewest open slots wins (concentrate onto fullest with capacity)

To participate in the Quick-Play pool, a dedicated server advertises
UT_RULETAG_s in its game-server attributes when it registers via
POST /ut/api/matchmaking/session. The recommended deployment is 3
always-on bot-filled servers per ruleset (Duel / iDM / CTF), per the
original Epic Quick-Play design.

Files:
- UT4MasterServer.Models/DTO/Request/QuickPlayRequest.cs (new)
- UT4MasterServer.Services/Scoped/MatchmakingService.cs:
  FindQuickPlayServerAsync()
- UT4MasterServer/Controllers/UT/MatchmakingController.cs:
  POST /ut/api/matchmaking/quickplay

Verified: dotnet build UT4MasterServer.csproj clean (0 errors).
UT4 client deserializes /ut/api/game/v2/wait_times/estimate as an
object; returning a bare array caused JSON parse errors and brought
down the matchmaking search. Wrap the wait-times list in
{ waitTimes: [...] }.
Earlier commit 20abe1e wrapped the response in { waitTimes: [...] }
on the hypothesis that UT4 wanted an object. That was wrong.

Per Epic's released source (UTMcpUtils.cpp `GetEstimatedWaitTimes`),
the client iterates the response with JsonValue->AsArray() — the
canonical wire format is a bare top-level JSON array of FWaitTimeInfo
objects, each shaped { ratingType, numSamples, averageWaitTimeSecs }.
An empty array is acceptable (the loop simply yields zero entries).

Reverts to the original `service.GetWaitTimes()` return and adds a
comment citing the source so future maintainers don't repeat the
envelope guesswork.
Iterating against the UT4 pre-alpha XAN-3525360 client revealed several
spots where the matchmaker's HTTP path mis-matched what the OnlineSubsystemMcp
parser actually accepts. Each of these in isolation is a small bug; together
they let the master server reach the point where UT4's matchmaker submits
a search, receives candidates, but stops at the client-side validity check
(reservation step is still blocked separately).

* MatchmakingService.ListAsync: normalize criterion keys to UT_ prefix so
  unprefixed client criteria (PLAYLISTID_i, REGION_s, NEEDS_i) line up with
  how game servers actually advertise attributes. Also skip Quick-Play sort
  hints + criteria that don't map onto our model (NEEDS_i, NEEDSSORT_i,
  REGION_s, UT_SERVERTRUSTLEVEL_i) so a curated QuickPlay server isn't
  excluded just because we don't enforce Epic's geo/trust dimensions.

* MatchmakingController.CreateGameServer: when the heartbeat source IP is in
  the docker bridge range (172.16-31.x.x), collapse to 127.0.0.1. Local-dev
  master runs in a container and sees game-server heartbeats arrive via the
  bridge gateway IP; UT4 clients can't reach that, so it must be rewritten
  before being stored. Also instrument matchMakingRequest entry with an
  info-level log so future matchmaker debugging is one grep away.

* WaitTimesController.QuickplayWaitEstimate: emit the bare "application/json"
  Content-Type (no charset suffix). UTMcpUtils.cpp compares Content-Type via
  exact string match and surfaces "Error: 1" on a 200 response if the suffix
  doesn't match — looks like success on the wire but the parser silently
  drops the result.

* WaitTimeEstimateResponse: revert PascalCase guess back to camelCase. UE4's
  FJsonObject::GetStringField reads the field names verbatim and UT4 source
  shows the keys are camelCase (ratingType / averageWaitTimeSecs / numSamples).

* GameServer.ToJson: when responding to a client, backfill UT_NEEDS_i (from
  free public-slot count), UT_TEAMELO_i, and UT_TEAMELO2_i defaults. UT4's
  client-side matchmaking gather pulls these directly and drops the candidate
  if absent, so the master has to provide them even when the game server
  itself doesn't track ELO.
* GameServer.ToJson: parse BuildUniqueID string into int32 when emitting to
  clients. UE4's FOnlineSessionMcp::ReadSessionSettings calls
  TryGetNumberField(buildUniqueId) which accepts numeric strings via
  FJsonValueString::TryGetNumber, but emitting a real JSON number matches
  what Epic's own FOnlineSessionMcp::WriteSessionSettings produces and
  survives any future Engine tightening.

* MatchmakingController: keep a structured log line on each
  /matchmaking/session/matchMakingRequest with the requesting account,
  build id, and criteria count. Reasoning: previously we only had the
  anonymous-access warning, so the common authenticated request path was
  invisible in logs and matchmaker debugging required code edits. The
  new log entry is one line per request at Information level, which is
  cheap and pays off the next time the QuickPlay tile path needs work.
…roadmap

GameServer.cs: auto-inject UT_NEEDS_i, UT_TEAMELO_i, UT_TEAMELO2_i, plus
the QuickPlay-required attrs (UT_RULETAG_s, UT_PLAYLISTID_i, PLAYLISTID_i,
UT_SERVERTRUSTLEVEL_i, UT_GAMEINSTANCE_i, UT_MATCHSTATE_s, UT_SERVERNAME_s)
whenever UT_RANKED_i=1, so dedicated Blitz servers stay matchmaker-visible
across mongo PUTs.

MatchmakingService.cs: auto-promote — when a criterion is UT_PLAYLISTID_i,
also match any UT_RANKED_i=1 server via a BsonDocument $or so the in-game
Quick Play tile lands on a ranked Blitz instance even if the playlist id
hasn't been written by the server yet.

Announcement JSON / news HTML: replace expired Epic placeholders with a
local Dallas Pick roadmap (shipped + coming-soon items). MinHeight=0 hides
the dead-code WebBrowserPanel body box (SUTWebBrowserPanel::Construct in
UT4 CL 3525360 never invokes ConstructPanel for inline panels, so the
inner SWebBrowser is never instantiated and the body just spins).
STextBlock doesn't honor faux-centering via padding spaces, so revert
to plain left-aligned roadmap text.
AUTGameSessionRanked drops back to UTEmptyServerGameMode after each
match. The reservation-bypass binary patch in ut4-install lets the
empty-mode server still accept QuickPlay joins, but the client lands
on the void ut-entry map instead of a real FlagRun match. The watchdog
tails the server log and relaunches the process whenever it detects the
empty-mode transition.

Configurable via env vars (INSTALL_DIR, PORT, QUERY_PORT, MAP, GAMEMODE,
BOT_FILL, MIN_PLAYERS, etc.).
Covers the smoke docker stack (api/web/xmpp/mongo/mailpit), the routes
UT4 clients hit, the password-grant login flow (no auth tokens needed),
the auto-inject + auto-promote matchmaking logic, the blitz-watchdog
companion, and notes on the dead-code announcement panel body box.

Cross-linked to ut4-install/docs/DEPLOYMENT.md which covers the client
side.
…entPlayers, clear-all

- FriendRequest: add Created (DateTime, defaults to UtcNow on insert) so
  GetFriends returns a real timestamp instead of fabricating one per call.
- FriendsController:
  - rename 'favourite' -> 'favorite' (UE4 expects US spelling)
  - emit friend.Created.ToStringISO() in responses
  - new DELETE /friends/api/public/friends/{id} (clear all relationships)
  - new GET /friends/api/public/list/{namespace}/{id}/recentPlayers
    (stub returning {recentplayers:[]})
- Announcement roadmap: note in-game friends UI is compile-time-stripped
  on the 2017 Linux client (UTLocalPlayer.cpp:6254 — early return on
  PLATFORM_LINUX). Backend is ready; UI needs client rebuild or a menu
  binary-patch. Server-side endpoints are verified working end-to-end
  via the nginx hijack at friends-public-service-prod06.ol.epicgames.com.
UE4 FOnlineFriendsMcp::QueryBlockedPlayers expects an object envelope
with a blockedUsers array. Returning a bare [] makes the client log:
  MCP: QueryBlockedPlayers request failed. Invalid response payload=[]
SUTPlayerInfoDialog::OnReadUserFileComplete (in the shipping client
binary, differs from the open-source mirror) reads
/ut/api/cloudstorage/user/{id}/oldplayercard as an async MCP fetch.
On bWasSuccessful=false the gate at vaddr 0x15a3684 skips
UpdatePlayerCustomization, which is what clears the loading text +
populates the dialog tabs. Returning 404 left the dialog hung on
"Requesting Player Information..." forever.

Mirror the existing stats.json stub pattern: when oldplayercard is
missing, return {} so the success path runs. UpdatePlayerCustomization
null-checks all fetched data so empty is safe.

Also adds:
- docs/2026-06-16-overnight-progress.md: progress summary for the user
- scripts/smoke-test-endpoints.sh: idempotent end-to-end backend check
Smoke tests confirm:
- Mono works on NixOS via nix-shell
- GitDependencies.exe runs
- uproject + Engine source bundled in the JimmieKJ mirror
- cdn.unrealengine.com/dependencies returns 403 Forbidden
- Linux toolchain URL returns 403
- Akamai mirror returns 404

Without the engine binary deps (~5-10GB of ThirdParty .so/.dll and
prebuilt UBT helpers), the build can't proceed. The CDN being dead
is the hard blocker.

Recommended path: accept the binary-patch debt. AUTPartyBeaconHost
patch (already in ut4-server-base.nix) and the new SUTPlayerInfoDialog
NOP patch we just applied to the live client cover the main gaps.
Friends list would need LD_PRELOAD + vtable rewrite (sketched in
agent report), or wait for someone to mirror the 4.15 deps.
@timiimit

Copy link
Copy Markdown
Owner

There's so much wrong with this. That I don't know if i should even properly review this...

Random notes as I skim across the PR:

  • Email verification has already been implemented in Account improvements #213 I just haven't had the time to give it a test, because I would need to setup a test server. It should be a separate PR if for some reason you wish to have a different take on its implementation.
  • I saw that Friends popup on Linux is supposedly not feasible, which is not true, because I did it with UT4UU without source recompile...
  • "UT4UU plugin authors (Letgam3rs)" what???
  • "The UT4 binary has Epic prod hostnames hardcoded" false, there are configs in ini which can change them
  • QuickPlay is not to be part of UT4MS project. QuickPlay servers can be hosted by anyone. UT4MS should only enable the buttons on main menu once QuickPlay is up in some way.

If this really does what it's supposed to then great, but it introduces so much noise that I am not willing to include it in a current state. Every change should be a separate PR to make changes human readable.
I really don't want to be rude, but honestly it looks like a joke PR. Just like all vibecoding.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants