Repository navigation
Map Capture
Translated from Chinese: https://github.com/qiekn/ootss-cmd/blob/main/docs/map-capture.md
freecam -topdown
show camera
set camera 80.25 81 18
freecam disable
-topdown locks the camera to look straight down with north up. Running it again keeps it enabled. Plain freecam is still a toggle; freecam enable switches back to mouse-look free camera. The existing -pan fixed-orientation panning mode is kept; no -top abbreviation is provided.
| Input | Top-down / pan mode | Regular free camera |
|---|---|---|
| W / S | World Y positive / negative (north / south) | Forward / back along the view direction |
| A / D | World X negative / positive (west / east) | Left / right along the view direction |
| Q / E | World Z negative / positive (down / up) | Down / up |
| Shift / Ctrl | Speed up / slow down | Speed up / slow down |
show camera queries the camera's absolute XYZ, orientation, yaw/pitch, and vertical FOV, using a read-only handle. set camera requires freecam to be enabled first. It accepts signed decimals and scientific notation such as 1.5e2; each coordinate must be finite and within ±1,000,000.
Coordinates use the game's native world units, stored as a float32 Vector3. X/Y share the player grid's coordinate system, but the camera position can be fractional; Z is the absolute world height. Only the camera is moved here. When you exit freecam, lose focus on the relevant window, or finish capturing, the view and native follow mode from before entering free camera are restored. If the scene has changed in the meantime, only the native mode is released, and the old view is not written into the new scene.
By default the top-left corner of the game shows the level resource name and the player's XYZ. These can be toggled and reordered independently in config.rc next to the EXE, and changes take effect immediately on save. When multiple characters are controlled, the coordinates reuse the rules of show position: the actual entity with the lexicographically smallest (x, y, z) among the controlled entities is selected, and identical coordinates are tie-broken by entity ID. The controlled set is re-sampled after switching characters. The camera, orientation, Noclip state, and capture progress are not displayed.
hud_enabled = true;
hud_hide_on_focus_loss = false;
hud_text = "Cowabunga?";
hud_show_level_name = true;
hud_text = "";
hud_text = "Cowabunga!";
hud_show_position = false;
hud_show_notifications = true;
The example above shows Cowabunga?, the level name, an empty line, and Cowabunga!, hides the coordinates, and still allows notifications. Each hud_text adds one line; you can write several in a row or interleave them with the level name and coordinates. Even if the text is identical, each one gets its own line. Info lines are arranged in the order they appear in the config, so no separate ordering setting is needed. Move the coordinates setting above the level name setting and change it to true to show the coordinates first. Disabled built-in entries take up no line, empty text keeps one blank line, and notifications are still controlled by their own switch. Content that exceeds the available height of the game window is clipped.
All switches default to true; if no hud_text is written, no custom text is shown. hud_text = ""; adds one blank line, so write several in a row to leave several blank lines; to remove a line, delete or comment out the corresponding declaration. Built-in items you do not write keep their default values and are placed after the items you did write, with the default relative order being level name, then coordinates. If a built-in switch is defined more than once, its last definition determines both its value and its position. Config key names and boolean values are case-insensitive, while custom text keeps its case; booleans may only be true / false, and an invalid value causes the entire config to be rejected, keeping the old settings.
hud_text in quotes supports \n for line breaks. For example, hud_text = "First line\n\nSecond line"; shows three lines, with a blank one in the middle. Both single and double quotes are supported, and leading or trailing line breaks and spaces on either side of the text are preserved. Write \\n to display a literal \n; the newline escape only affects HUD text and does not change how paths and command aliases are parsed. The panel height is calculated from the actual number of lines after expansion.
When coordinates are disabled, only the level resource name is read, and the player entity, type, selection state, and XYZ are no longer queried. When both the level name and coordinates are disabled, no game memory handle is opened, and custom text can still be shown on its own. Disabling notifications hides and discards the current notification immediately; re-enabling them does not replay old ones.
hud enable turns the HUD on, hud disable turns it off, and a bare hud toggles it. This is the master switch for the current session and does not modify the config file. At startup hud_enabled is used; on hot reload, the session switch is overridden only if that config value itself changed, so changing other settings, switching levels, or switching focus will not re-enable a HUD that was manually turned off. The command only changes the master switch and keeps each entry's settings. While the full terminal or the mini input box is open, both HUDs are hidden automatically and restored according to the switch state when you return to the game. The HUD never takes focus and is click-through; it is hidden when the game is minimized. Before taking a screenshot, the helper's HUD is hidden for at least 150 ms; capture pauses while the console / editor covers the game.
hud_hide_on_focus_loss = false; keeps both HUDs visible when you switch to another window, so you can edit config.rc and watch the changes after saving. When set to true, the HUDs are hidden when you switch to any window other than the game and the profile editor; if omitted, true is the default. This switch supports hot reload and only controls behavior on focus loss; the HUDs are still hidden when the game is closed or minimized, when the console is open, and during helper screenshots. The HUD stays topmost on the desktop and may overlap other windows; OBS display capture can include a visible HUD.
Notifications stay horizontally centered. The top of the drawing area is at 30% of the screen height, with the text sitting about one third of the way down it. They briefly show messages such as enabled or disabled, by default for about 1.5 seconds with a fade-out. The font size is uniformly computed as game client height / size, where size is how many lines of the font fit vertically on screen: notifications use HUD_NOTICE_SIZE = 30 and the top-left info uses HUD_INFO_SIZE = 40, adjustable in src/ui/hud.jai. At 1080p these are 36 px and 27 px respectively, with no extra DPI multiplier. Both use the game's KarminaBold.otf, with a fallback to the helper's local data/fonts/Karmina-Bold.otf.
The text keeps FreeType anti-aliasing coverage, using premultiplied alpha and a soft, low-opacity shadow, with no black color-key outline. When the text and size are unchanged, the pixel buffer is reused; the HUD creates no OpenGL window and does not call SwapBuffers, which avoids buffer swaps blocking camera updates. Player coordinates use a separate read-only reader that updates every 200 ms and is not part of the editor's recording or playback flow.
speed queries the current base scale and the combined scale after the native hold-to-change shortcuts. speed 1.25, speed 1.5, speed 3.5, and so on accept any finite value within 0–4; speed 0 pauses, speed 1 restores the normal base speed, and speed reset releases this helper's control and restores the value from before its first modification. A normal exit of the helper also restores its own changes.
When the game is in the foreground and the editor is closed, [ slows down and ] speeds up, stepping through these levels:
0.25 → 0.5 → 1 → 1.25 → 1.5 → 1.75 → 2 → 3 → 4
Each physical key press steps only once, and holding the key does not repeat; for example, after speed 3.5, [ goes to 3 and ] goes to 4. The lower limit is 0.25 and the upper limit is 4; pausing is done with speed 0, and while paused ] returns to 0.25. When the editor is open, the original [ undo and ] single-step are kept; same-key mappings explicitly defined in config.rc take precedence. Typing in an input box, injected keystrokes, and other windows do not trigger these shortcuts.
The game's native temporary slow / normal / fast scales are 0.25 / 1 / 4, usually controlled by Control / release / Space. The helper changes a separate base scale and the two are multiplied: with a base of 1.5, pressing the native fast key gives 6×. The 0–4 range is what the helper accepts for the base scale and has not been proven to be a hard limit of the game engine. This feature writes verified 8-byte floating-point data; it does not modify the EXE or repeatedly overwrite dt every frame. If the helper is force-killed it cannot perform the restore; restart the helper and use speed 1, or restart the game.
In the local User.keymap, Q maps to ToggleLocalAreaMap. The read-only measured position is about (79.968, 68.990, 80), with the orientation looking straight down with north up, a vertical FOV of about 27.4°, and orthoness of 0 on all camera-control entities. This is a native top-down view that can be reused directly; for now it is still treated as a perspective projection, and no claim is made that parallax-free orthographic rendering has been achieved. The native path of orthoness pulls the camera back and narrows the FOV, so it was not used in place of the fixed height of 80.
The default plan requires pressing Q in the big overworld first. The helper checks the original orientation and height, keeps that view's FOV, and then sets the first camera position exactly to (80, 69, 80). The grid is also aligned to the center of this first shot and is captured outward in square rings; all stitching paths are placed by actual row and column, and the actual boundary extends outward to cover the requested range. There is no need to enable freecam manually first.
The console command unfog temporarily hides the exploration fog and fog restores it. Commands are case-insensitive, take no arguments, and support Tab completion. Live testing on the retail build has confirmed that the terrain of unexplored areas is displayed. These commands only change a rendering switch in the current game process; they do not move the player and do not modify the save's exploration progress. When the helper exits normally, it restores any fog it turned off; after a game restart it is restored naturally.
The implementation discovers the reflection metadata by name, verifies the code signature, the nearby parameter upload, and the type, size, and offset of Fog_Voxelization_Params.fog_of_war_enabled, and only temporarily changes the constant 1 assigned each frame to 0. The metadata address, upload distance, and unrelated temporary stack locations can vary between versions; the field and instruction semantics must still match. After a game update, a failed verification causes the modification to be refused. The EXE file is unchanged. If the process holding the switch is force-killed before it can restore it, use fog or restart the game.
Scripts are written in Jai and receive commands through the arguments after -. Relative paths in the scripts below are all resolved from the project root; paths inside the console are resolved from the directory containing the helper EXE. The scripts do not start a second camera controller; instead they queue capture requests to the running helper.
First start the freshly compiled ootss-cmd.exe and the game, exit manual freecam, close the editor, and press Q in the big overworld to switch to the top-down view at height 80. In MSYS2, run:
./scripts/capture_map.jaiIn PowerShell, you can use jai -quiet scripts/capture_map.jai. capture.sh is not needed, and there is no need to pass -import_dir. The parameters are gathered in Capture_Settings at the top of scripts/capture_map.jai:
Capture_Settings :: struct {
directory: string = "map/capture-world";
output: string = "map/map.jpg";
disable_fog: bool = true;
max_side: s64 = 32768;
jpeg_quality: s32 = 92;
plan: Capture_Plan = .{
version = 3,
xmin = -40, ymin = -20,
xmax = 180, ymax = 170,
z = 80,
center_first = true,
center_x = 80,
center_y = 69,
freeze_time = true,
require_map_view = true,
ground = 0,
crop_world_size = 9,
settle_ms = 200,
scene = "overworld",
};
}Every run overwrites the plan, cleans out the old progress and generated files such as frame_000000.png in that capture directory, and captures from scratch. The script waits for this job to finish, then stitches the center tiles into a single north-up JPEG and atomically replaces map/map.jpg; it does not depend on cwebp. crop_world_size = 9 means that only the central 9 × 9 world cells on the reference ground are kept, and adjacent camera centers are spaced exactly 9 cells apart. During capture the tiles are cropped directly and saved as small PNGs, and the surrounding image is never written to disk; stitching does not crop again. In this mode overlap does not determine the crop size, and crop_world_size = 0 restores the old mode of cropping the full screenshot by an overlap percentage.
jpeg_quality ranges from 1 to 100, default 92. max_side only limits the final image; when exceeded, every crop unit is scaled down by the same integer factor, and the source is never upscaled and the camera height is never raised. Set it to 0 to request the original size, which errors if it exceeds the budget; set it to a positive number to allow scaling down as needed. Center tiles are saved as lossless PNG, and only the final JPEG is lossily compressed. Changing the output extension to .png still exports a lossless image, and in that case max_side should be changed to no more than 16384. Other files in the directory are kept. A job number prevents the previous run's completion state from being misused, and running two scripts at the same time is rejected by a directory lock.
The current whole-world preset comes from the 920 Level_Entry entities in overworld.entities in the local retail levels.package, with entrances spanning X -24…164, Y -2…153, Z -25…63; it is not the Demo's catalog of 105 entrances. The XY range above comes from leaving at least 10 units of margin and rounding. The entrance range is not the boundary of all decorative terrain. The camera's absolute height is 80, and the maximum JPEG side length is 32768; the height is not a height above the ground, so high terrain needs a manual check of the framing. 200 ms is the effective lower bound for the wait in the current screenshot flow, and encoding and disk writes take additional time. A center-aligned grid of 9 × 9 cells is 25 × 22, 550 shots in total, with actual bounds of X -41.5…183.5 and Y -25.5…172.5. At 3840×2160 and a Q-view FOV of about 27.4°, each shot is saved at 498×498 pixels and the final JPEG is 12450×10956. The pixel size varies with resolution and FOV, but the world step is always 9. The old map/capture-pilot trial capture directory is kept.
Smaller center tiles reduce perspective offset, but this is not the same as orthographic projection; the 9 cells are computed on the reference plane specified by ground, so seams may still appear for high and low terrain, trees, and buildings. The earlier 40 full screenshots were taken with larger spacing between capture centers and cannot be re-stitched directly into continuous 9-cell tiles; they need to be recaptured under the new plan.
freeze_time = true sets the base time scale to 0 after the camera takes over and stabilizes, before the first shot; on pause, cancel, completion, or normal helper shutdown, it restores the exact scale from before the capture, such as 1.25. Resuming re-saves the current scale and pauses time again. If the restore fails, the recovery state is kept and an error is reported, and the job is not marked complete; cancel can be retried. This only pauses the game's simulation time and the corresponding accumulated clock; rendering and the helper's capture waits use the original clock, and whether all independent animations actually stop still needs manual in-game verification. Set it to false to disable this behavior.
When run without arguments, disable_fog = true temporarily turns off the exploration fog before capture, and restores the pre-capture switch state after completion, cancellation, or failure; if you had already used unfog beforehand, it stays off after capturing. Set it to false to leave the fog alone. The step-by-step plan / start / run modes keep manual control, and you can run unfog and fog yourself.
After submitting, switch back to the unobstructed game and release the keys. When capture is paused, the script shows the reason and keeps waiting; at that point the fog stays off and the time scale is restored. After dealing with the issue, run capture resume in the helper console, then return to the game. On capture cancel, helper exit, or stitching failure, the previous map/map.jpg is kept. Ctrl+C in the terminal requests cancellation of the capture in the helper and restores the fog. A capture still in progress will not be forcibly overwritten by another request.
The first time you use center 9-cell cropping you need to recompile and restart the helper; the new version 3 request is rejected by an old helper, to avoid capturing under the old rules. The serpentine and center-ring plans of versions 1 / 2, with full screenshots, can still be resumed and stitched; the version 3 manifest records the original window size, the exact crop range, and the size of the cropped PNGs, and the two frame formats are never mixed. A JPEG is limited to 32768 pixels per side and 256M pixels in total (the RGBA stitching buffer is up to about 1 GiB, plus per-frame and encoding buffers); PNG is still limited to 16384 per side and 64M pixels in total. Very large maps use the tiled output described below. The actual coverage is still rounded outward to whole crop units.
First adjust the graphics quality in the game, hide any game UI you do not need, and try framing in top-down mode. Then exit freecam, close the editor, and create a trial capture plan for a small area:
jai -quiet scripts/capture_map.jai - plan .build/capture-pilot 76 77 84 85 18 --ground 0 --overlap 0.3 --settle-ms 700 --scene overworld
jai -quiet scripts/capture_map.jai - start .build/capture-pilotThe argument order is directory xmin ymin xmax ymax camera-absolute-Z. --ground is the absolute Z of the reference ground used to compute the screenshot scale; it does not modify the terrain. --crop-world 9 uses the version 3 center 9-cell cropping. If not specified, --overlap defaults to 0.3, with a range of 0.1–0.8; --settle-ms defaults to 500, with a range of 100–10000, and the actual wait is at least 200 ms, reserving time for hiding the HUD. The range and height are only examples and do not represent verified bounds for the whole retail map.
After submitting, return to the game; capture starts only after Enter, modifier keys, and movement keys are released. There is no need to hold any key. The program moves point by point along a serpentine path, waits for the image to settle, and then saves the full lossless PNG.
jai -quiet scripts/capture_map.jai - status .build/capture-pilot
jai -quiet scripts/capture_map.jai - pause
jai -quiet scripts/capture_map.jai - resume
jai -quiet scripts/capture_map.jai - cancelYou can also use the console directly:
capture start "C:/path/to/capture-pilot/plan.json"
capture status
capture pause
capture resume
capture cancel
During capture, physical keyboard input, a focus change, a scene / camera identity change, a size / FOV change, window occlusion, a black frame, or a file write failure all trigger a pause. Completed frames are kept; resume continues from there. After the helper restarts, run start on the same plan to continue, after the existing metadata and frame files are verified. Do not change the graphics quality, the ground reference height, the resolution, or the plan before resuming; use a new directory if you need to change any of them.
The script's receipt for start means the request has been queued. The actual accept / reject result is in the helper console or in status.json in the directory. If no status file was generated, check the console first. Opening the console to look pauses a capture in progress.
Install the libwebp command-line tools so that cwebp is on PATH; for example, mingw-w64-ucrt-x86_64-libwebp on MSYS2 UCRT64. Capturing itself does not depend on cwebp.
jai -quiet scripts/stitch_map.jai - .build/capture-pilot .build/map-pilotThe output directory must not exist, to avoid overwriting the original images or an existing map. The tool rejects jobs that are unfinished or have inconsistent coordinates; the size and decode result are checked as each PNG is read. Only the current tile and a single source image are kept in memory, and no huge full-size image is created.
To re-stitch only a completed capture, use the commands below; no recapture is needed. This mode allows replacing existing images and does not need cwebp; it fully decodes, stitches, and encodes before replacing the output file:
jai -quiet scripts/stitch_map.jai - map/capture-world --jpg map/map.jpg --max-side 32768 --quality 92
jai -quiet scripts/stitch_map.jai - map/capture-world --png map/map.png --max-side 8192If DONE: Capture complete has already been shown but stitching or encoding then fails, just retry with the stitch_map.jai commands above; do not rerun capture_map.jai without arguments, as that would clean out the old frames and recapture everything. Stitching shows progress row by row, followed by the encoding stage. The JPEG is written out through the encoder library's native buffered file output, which avoids byte-by-byte callbacks into Jai's compile-time executor causing memory to balloon; after writing, the file header, dimensions, and end marker are checked before the old image is replaced.
| File | Purpose |
|---|---|
Capture directory plan.json
|
World coordinate range, height, overlap, wait time |
Capture directory frame_000000.png, etc. |
In 9-cell mode, center tiles; in the old overlap mode, full screenshots |
Capture directory manifest.json
|
Resolution, FOV, scale, rows and columns, and the actual XYZ set for each frame |
Capture directory status.json
|
Current phase, completed count, and status message |
Output directory tiles/{z}/{y}/{x}.webp
|
512×512 lossless WebP map tiles |
Output directory pyramid/{z}/{y}/{x}.png
|
Lossless PNG pyramid; 0/0/0.png is the thumbnail of the whole map |
Output directory map.json
|
Actual world range, pixel scale, and maximum native zoom level |
Output directory index.html
|
Leaflet viewer showing world X/Y at the mouse position |
Serve the output directory over HTTP using your existing static server and open index.html. The sample viewer loads Leaflet 1.9.4 from unpkg and needs internet access; you can also swap in a local Leaflet that the project already has. The layer's pixel width and height need not be multiples of 512, the end is padded with transparency, and the actual map extent is recorded in map.json.
Stitching uses the reference-ground scale at a fixed height:
source_pixels_per_unit = image_height / (2 × (camera_z − ground_z) × tan(vertical_fov / 2))
source_crop_size = crop_world_size × source_pixels_per_unit
crop_width = crop_height = floor(source_crop_size)
step_x = step_y = crop_world_size
pixels_per_unit = crop_width / crop_world_size
The edges of the center square can fall on fractional pixels; during capture it is resampled over this exact range into small tiles with integer pixel size, which prevents the world step from drifting due to per-cell rounding. The old overlap mode keeps using integer center cropping and step = crop_pixels / source_pixels_per_unit. The capture order is restored to rows from north to south; X increases to the right and Y decreases downward. The last row / column extends outward to cover a whole crop unit, so the actual range may be slightly larger than planned. Floating-point metadata is saved with round-trippable precision to prevent the grid from drifting when resuming.
Place the generated map directory under the joric site, for example captures/retail/. Its tiles/{z}/{y}/{x}.webp path order matches the existing project, but the Demo's 192-unit range, center point, and transform parameters cannot be reused directly.
Load captures/retail/map.json before creating the Leaflet map and use the CRS below; the existing level markers still use [worldY, worldX]. Prefix the layer path with captures/retail/:
const m = await (await fetch('captures/retail/map.json')).json();
const a = m.pixels_per_unit / 2 ** m.max_zoom;
const crs = L.extend({}, L.CRS.Simple, {
transformation: new L.Transformation(a, -m.xmin * a, -a, m.ymax * a)
});
const bounds = [[m.ymin, m.xmin], [m.ymax, m.xmax]];
const map = L.map('map', { crs, minZoom: 0, maxZoom: m.max_zoom + 2 });
L.tileLayer('captures/retail/' + m.tile_url, {
tileSize: 512, noWrap: true, bounds,
maxNativeZoom: m.max_zoom, maxZoom: m.max_zoom + 2
}).addTo(map);
map.fitBounds(bounds);The generated viewer already includes this transform. Whether the retail level positions match the Demo markers still needs to be verified; the new base map does not automatically correct the old level catalog.
This is a perspective top-down image, not an orthographic projection. The reference ground can be stitched accurately by coordinates; walls, trees, and buildings above / below the reference plane may still produce perspective seams. Increasing the center crop ratio can reduce the seams; a true orthographic mode still requires finding and verifying the game's projection control.
Screenshots use desktop BitBlt on the foreground, unobstructed game client area. Keep windowed / borderless mode and keep the entire client area on screen; capturing in the background or while covered by other windows is not supported. The game's own UI, in-process overlays such as Steam's, dynamic water surfaces, particles, and day-night changes will still appear in the image. The wait time is a fixed delay, not a signal that game resources have finished loading. Do a trial capture of a small area first, then decide on the height, overlap, and wait time.
jai -quiet scripts/check_camera_capture.jai -import_dir ../modules
jai -quiet scripts/check_map_jpeg.jai
jai -quiet scripts/check_world_crop.jai
jai -quiet scripts/stitch_map.jai - .build/map-fixture .build/map-test-outputThe regression checks cover the top-down quaternion, key bindings, command validation / completion, grid ranges, odd-resolution cropping, metadata round-trip precision, serpentine ordering, and PNG pixel stitching, without looking at the images. The generated synthetic test map is 2148×1608 with 29 tiles across four levels. The game memory interface has been verified read-only on the running retail build; full live capture and visual seams still need acceptance through a trial capture.