Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions .luacheckrc
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ stds.creative_mod = {
"gui_menu_cheats",
"gui_menu_magicwand",
"gui_menu_modding",
"gui_menu_surface",
"item_providers_util",
"item_source",
"item_void",
Expand All @@ -135,6 +136,7 @@ stds.creative_mod = {
"remote_interface",
"rights",
"static_item_container_type",
"surface_creation",
"super_boiler",
"super_cooler",
"transport_belt_item_distance",
Expand Down
129 changes: 129 additions & 0 deletions GUI-DESIGN-SYSTEM.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
# GUI Design System

Conventions for building menus in this mod so new tabs/menus look and behave like the
existing ones. Follow this when adding a submenu, a sub-sub-menu, or a backend action behind
a menu. File:line references point at the canonical examples to copy from.

## Menu hierarchy

Three nesting levels, all hosted in `mod_gui.get_frame_flow(player)` → `main_menu_container`:

```
Main menu ("Creative Mode") button column of submenu buttons
└─ Submenu ("Cheats", "Surface") button column of section buttons
└─ Sub-sub-menu content frame opened beside the column
```

- **Main menu**: one vertical `frame` (caption "Creative Mode") of `main_menu_button`s, one per
submenu. Built in `gui-menu.lua` `create_or_hide_main_menu_for_player` from the
`submenus_gui_data` registry (`gui-menu.lua:47`).
- **Submenu**: its own horizontal `flow` container (e.g. `surface_menus_container`) holding a
captioned vertical `frame` of `main_menu_button`s — the **button column**. Each button opens a
sub-sub-menu *beside* the column inside the same container.
- **Sub-sub-menu**: a captioned vertical `frame` added next to the button column, holding the
actual controls. Toggled open/closed; opening one closes the siblings.

Canonical examples: the **Cheats** submenu (`gui-menu-cheats.lua`) and the **Surface** submenu
(`gui-menu-surface.lua`), which deliberately mirror each other.

## Adding a new submenu

1. **New module** `scripts/gui-menu-<name>.lua` defining `gui_menu_<name>` with:
- `get_container_name()` → a unique container name from `defines.lua`.
- `create_or_destroy_menu_for_player(player)` — toggles the submenu container; when building,
adds a captioned `frame` (the button column) + one `main_menu_button` per section.
- `on_gui_click(element, element_name, player, button, alt, control, shift)` returning whether
the event was consumed.
- `on_gui_selection_state_changed(...)` if it has drop-downs.
2. **Register** in `submenus_gui_data` (`gui-menu.lua:47`): `button_name`, `button_caption`,
`get_player_can_access_function` (e.g. `function(player) return player.admin end` for
admin-only), `get_submenu_container_name_function`, `open_submenu_for_player_function`,
`update_accessibility_for_player_function` (or `nil`).
3. **Wire the click fan-out**: add `gui_menu_<name>.on_gui_click` to the `on_gui_click` dispatch in
`gui-menu.lua`, and `on_gui_selection_state_changed` to that fan-out if needed.
4. **`control.lua`**: `require("scripts.gui-menu-<name>")` **before** `require("scripts.gui-menu")`
— `gui-menu.lua` references submenu globals at load time when building `submenus_gui_data`.
5. **`defines.lua`**: add element-name constants. **`locale/en/base.cfg`**: captions/messages.
6. **`.luacheckrc`**: register the new module global.

## Section pattern within a submenu (the three-column nav)

Drive the button column from a `section_data` table + an explicit `section_order` array (because
`pairs()` is unordered). See `gui-menu-surface.lua` `section_data` / `section_order`. Each section:

- `button_name`, `button_caption`, plus either:
- `build_content = function(frame) ... end` — fills a content frame the helper creates for you, **or**
- `create_or_destroy = function(player, destroy_only) ... end` — delegate open/close to another
module (used to host the Surface Cheats menu, whose content is built by the cheats machinery).
- Optional `get_player_can_access_function` to gate the button's visibility.
- Optional gating flag (e.g. `space_age_only`, checked via `script.feature_flags["space_travel"]`).

`create_or_destroy_section_for_player(player, data, destroy_only)` toggles the section's content
frame inside the submenu container; the click handler closes the other sections first, then toggles
the clicked one — mirroring `create_or_destroy_cheats_menu_for_player` (`gui-menu-cheats.lua:2560`)
and its fan-out (`gui-menu-cheats.lua:3761`).

## Shared style vocabulary

Reuse these styles (defined in `prototypes/style.lua`, named in `defines.lua` under `gui_styles`)
so new menus match the rest of the UI:

| Element | Style |
|---|---|
| Submenu / section column button | `main_menu_button` |
| Selection list scroll-pane | `cheat_scroll_pane` |
| Selection list frame | `cheat_target_selection_container_frame` |
| Selectable list button (active / inactive) | `cheat_target_selected_button` / `cheat_target_unselected_button` |
| Form row table | `cheat_table` |
| Form label | `cheat_name_label` |
| Text input | `cheat_numeric_textfield` |
| Spacer between input and button | `cheat_textfield_and_button_separate_flow` |
| Apply / action button (short caption) | `cheat_apply_button` |
| Action button (longer caption, auto-sized) | `small_default_bold_button` |

A captioned `frame` renders its `caption` as a title bar — that is how submenus/sub-sub-menus get
their heading; no separate title-bar helper exists.

**Gotcha:** `cheat_apply_button` has a *fixed* width sized for short captions (e.g. "Apply") and
clips longer ones like "Create" → "Cre...". A runtime `minimal_width` cannot grow past a fixed
width, so for a longer caption use its parent style `small_default_bold_button` instead — same bold
apply-button look, but auto-sizes to the caption.

## Event wiring

Engine events are forwarded down a fixed chain, mirroring existing handlers:

`scripts/events.lua` (local forwarder, registered in `event_handlers_look_up`) → `gui.on_<event>`
→ `gui_menu.on_<event>` → the relevant `gui_menu_<name>.on_<event>`.

The generic dispatcher in `control.lua` loops over all `defines.events`, so a new handler needs
only the forwarder + `event_handlers_look_up` entry, not a new explicit `script.on_event`
registration. `on_surface_created` (Phase 1 of the surface tool) is the worked example. Note the
engine payload carries `surface_index`, not `surface` — resolve via `game.surfaces[event.surface_index]`.

**Live list refresh:** to make a newly created/removed target appear without reopening the menu,
route through `add_or_remove_target_in_cheats_menu_for_all_players(surface, <menu_gui_data>, true)`;
each target descriptor's `verify_target_for_insert_function` enforces per-player access for free.

## Backend actions behind a menu

- **Put logic in a backend module** (e.g. `scripts/surface-creation.lua`), not the GUI. Return a
structured result — `true, <value>` on success or `false, <localised-error>` — so both the GUI
and the remote interface can report without re-validating.
- **Take a `force`, not a `player`,** where possible, so the action is callable headless. The GUI
passes `player.force`; the remote wrapper resolves a force from an optional `player_index` and
falls back to `game.forces["player"]` (which always exists, even headless). See `resolve_force`
in `scripts/remote-interface.lua`.
- **Feature-gate Space Age paths** on `script.feature_flags["space_travel"]` in *both* the backend
(refuse with an error key) and the UI (don't build the control).
- **Expose every action through the remote interface** (`scripts/remote-interface.lua`
`remote_functions`) returning a boolean/structured result, so `verify.py behavior` can assert it
via `remote.call` in the headless sandbox (which has Space Age enabled).

## Verification

Every user-facing change runs `uv run verify.py static | load | behavior` (see `DEBUG.md`), and
adds a `changelog.txt` entry. GUI layout/behavior that the headless suite can't exercise is
checked manually via `uv run verify.py debug --gui`. Only one Factorio instance can hold the
`.debug` sandbox lock at a time — close a running `debug --gui` session before running
`load`/`behavior`.
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ This mod provides a suite of tools and entities that allow for creative building

To use Creative Mode, simply enable the mod in the Factorio mod manager. Once enabled, you'll have access to the creative tools and entities in the game.

## Development

* **GUI conventions:** see [`GUI-DESIGN-SYSTEM.md`](GUI-DESIGN-SYSTEM.md) for the menu hierarchy, styling vocabulary, event wiring, and backend/remote patterns to follow when adding menus.
* **Debugging & verification:** see [`DEBUG.md`](DEBUG.md).

## Credits

This mod is based on the original Creative Mode mod by Y.Petremann, with patches by Pac0master, and also incorporates elements from the Test Mode mod by rk84. It is a fork of the Creative Mode (Fix for 0.16) by [Chrisgbk](https://mods.factorio.com/user/chrisgbk).
Expand Down
11 changes: 11 additions & 0 deletions changelog.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,15 @@
---------------------------------------------------------------------------------------------------
Version: 2.3.0
Date: ????
Features:
- Add an admin-only Surface tab. Admins can create a new blank surface by typing a name; the surface immediately appears in the Surface Cheats target list for every connected player allowed to see other surfaces.
- Add space-platform creation to the Surface tab (Space Age only). Admins can type a name and pick the planet to orbit; the platform comes up with a hub already placed and its surface immediately appears in the Surface Cheats target list.
- Add planet-surface creation to the Surface tab (Space Age only). Admins can pick a planet and create its surface; if the surface already exists the action is a harmless no-op, and any newly created surface immediately appears in the Surface Cheats target list.
Changes:
- Move the Surface Cheats menu from the Cheats tab into the Surface tab, grouping it with the surface-creation tools.
Bugfixes:
- Fix Teleport doing nothing useful while in remote view (map view): it now exits remote view first and teleports your actual character/player instead of just moving the remote-view camera.
---------------------------------------------------------------------------------------------------
Version: 2.2.0
Date: 2026-06-27
Features:
Expand Down
2 changes: 2 additions & 0 deletions control.lua
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ require("scripts.gui-menu-admin")
require("scripts.gui-menu-cheats")
require("scripts.gui-menu-magicwand")
require("scripts.gui-menu-modding")
require("scripts.surface-creation")
require("scripts.gui-menu-surface")
require("scripts.gui-menu") -- It has to be loaded after the submenus.
require("scripts.item-providers-util")
require("scripts.item-source")
Expand Down
31 changes: 31 additions & 0 deletions defines.lua
Original file line number Diff line number Diff line change
Expand Up @@ -1280,6 +1280,37 @@ creative_mode_defines.names.gui = {
.. "item-void-can-remove-from-player-checkbox",
item_void_can_remove_from_ground_checkbox = creative_mode_defines.name_prefix
.. "item-void-can-remove-from-ground-checkbox",
-- Surface creation submenu.
main_menu_open_surface_button = creative_mode_defines.name_prefix .. "main-menu-open-surface-button",
surface_menus_container = creative_mode_defines.name_prefix .. "surface-menus-container",
-- The button-column frame, exactly like the Cheats submenu's button frame: a captioned ("Surface")
-- vertical frame holding one button per section. Clicking a button opens that section's content
-- frame beside the column (the surface_*_frame content frames below).
surface_menu_frame = creative_mode_defines.name_prefix .. "surface-menu-frame",
-- Section column buttons (styled like the Cheats column buttons).
surface_nav_blank_button = creative_mode_defines.name_prefix .. "surface-nav-blank-button",
surface_nav_platform_button = creative_mode_defines.name_prefix .. "surface-nav-platform-button",
surface_nav_planet_button = creative_mode_defines.name_prefix .. "surface-nav-planet-button",
-- Blank-surface section content frame (the sub-sub-menu opened by the Blank surface button).
surface_blank_frame = creative_mode_defines.name_prefix .. "surface-blank-frame",
surface_blank_container = creative_mode_defines.name_prefix .. "surface-blank-container",
surface_blank_name_label = creative_mode_defines.name_prefix .. "surface-blank-name-label",
surface_blank_name_textfield = creative_mode_defines.name_prefix .. "surface-blank-name-textfield",
surface_blank_create_button = creative_mode_defines.name_prefix .. "surface-blank-create-button",
surface_platform_frame = creative_mode_defines.name_prefix .. "surface-platform-frame",
surface_platform_container = creative_mode_defines.name_prefix .. "surface-platform-container",
surface_platform_name_label = creative_mode_defines.name_prefix .. "surface-platform-name-label",
surface_platform_name_textfield = creative_mode_defines.name_prefix .. "surface-platform-name-textfield",
surface_platform_planet_label = creative_mode_defines.name_prefix .. "surface-platform-planet-label",
surface_platform_planet_drop_down = creative_mode_defines.name_prefix .. "surface-platform-planet-drop-down",
surface_platform_create_button = creative_mode_defines.name_prefix .. "surface-platform-create-button",
-- Planet-surface section content frame (the sub-sub-menu opened by the Planet surface button).
-- Its planet drop-down is independent from the platform-orbit picker above.
surface_planet_frame = creative_mode_defines.name_prefix .. "surface-planet-frame",
surface_planet_container = creative_mode_defines.name_prefix .. "surface-planet-container",
surface_planet_planet_label = creative_mode_defines.name_prefix .. "surface-planet-planet-label",
surface_planet_planet_drop_down = creative_mode_defines.name_prefix .. "surface-planet-planet-drop-down",
surface_planet_create_button = creative_mode_defines.name_prefix .. "surface-planet-create-button",
-- Legacy GUI. Used for finding and destroying the legacy "more cheats" popup.
cheats_table = creative_mode_defines.name_prefix .. "cheats-table",
}
Expand Down
26 changes: 26 additions & 0 deletions locale/en/base.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,18 @@ creative-mode_already-enabled=Creative Mode has already been enabled!
creative-mode_not-yet-enabled=Creative Mode has not been enabled!
creative-mode_enabled-all-cheats=__1__ has enabled all personal cheats to all existing players, team cheats to all existing teams and freeze daytime at midday on all existing surfaces.
creative-mode_available_commands=Available commands:
creative-mode_surface-creation-blank-success=Created blank surface "__1__".
creative-mode_surface-creation-blank-empty-name=Cannot create surface: please enter a name.
creative-mode_surface-creation-blank-duplicate-name=Cannot create surface: a surface named "__1__" already exists.
creative-mode_surface-creation-blank-failed=Failed to create surface "__1__".
creative-mode_surface-creation-platform-success=Created space platform on surface "__1__".
creative-mode_surface-creation-platform-failed=Failed to create space platform.
creative-mode_surface-creation-platform-invalid-planet=Cannot create space platform: please pick a planet to orbit.
creative-mode_surface-creation-planet-success=Created planet surface "__1__".
creative-mode_surface-creation-planet-already-existed=Planet surface "__1__" already existed.
creative-mode_surface-creation-planet-failed=Failed to create planet surface.
creative-mode_surface-creation-planet-invalid-planet=Cannot create planet surface: please pick a planet.
creative-mode_surface-creation-not-available-without-space-age=This feature is only available with the Space Age expansion.

creative-mode_failed-to-apply-to-single-player-admin=Failed to apply to __1__. Reason: __2__
creative-mode_failed-to-apply-to-multiple-players-admin=Failed to apply to __1__ players. Reason: __2__
Expand Down Expand Up @@ -387,6 +399,20 @@ creative-mode_build-options=Build Options
creative-mode_magic-wand=Magic Wand
creative-mode_modding=Modding
creative-mode_admin=Admin
creative-mode_surface=Surface
creative-mode_surface-creation-nav-blank=Blank surface
creative-mode_surface-creation-nav-platform=Space platform
creative-mode_surface-creation-nav-planet=Planet surface
creative-mode_surface-creation-blank-title=Create Blank Surface
creative-mode_surface-creation-blank-name-label=Name
creative-mode_surface-creation-blank-create-button=Create
creative-mode_surface-creation-platform-title=Create Space Platform
creative-mode_surface-creation-platform-name-label=Name
creative-mode_surface-creation-platform-planet-label=Orbit
creative-mode_surface-creation-platform-create-button=Create
creative-mode_surface-creation-planet-title=Create Planet Surface
creative-mode_surface-creation-planet-planet-label=Planet
creative-mode_surface-creation-planet-create-button=Create
creative-mode_list-select-all=Select All [?]
creative-mode_list-select-all-tooltip=Hold control and click to select additional targets without deselecting the selected ones.\nHold shift and click to select multiple targets.

Expand Down
7 changes: 7 additions & 0 deletions scripts/cheats.lua
Original file line number Diff line number Diff line change
Expand Up @@ -1728,6 +1728,13 @@ cheats.teleport_cheats_data = {
if not (target_surface and target_surface.valid and source_player) then
return nil
end
-- In remote view (map view) the active controller is the remote camera, so
-- source_player.surface/position and source_player.teleport() act on the camera, not the
-- physical body (since 2.1.7 teleport no longer implicitly exits remote view). Exit remote
-- view first so the rest of this function operates on, and moves, the real player.
if source_player.controller_type == defines.controllers.remote then
source_player.exit_remote_view()
end
local dest = compute_safe_position(source_player, target_surface)
if source_player.surface == target_surface then
-- Same-surface selection: reposition to a fresh safe spot on the current surface.
Expand Down
6 changes: 6 additions & 0 deletions scripts/events.lua
Original file line number Diff line number Diff line change
Expand Up @@ -575,6 +575,11 @@ local function on_player_changed_surface(event)
gui.on_player_changed_surface(event)
end

-- Callback of the on_surface_created event, which is invoked after a new surface is created.
local function on_surface_created(event)
gui.on_surface_created(event)
end

-- Callback of the on_player_cursor_stack_changed event, which is invoked after a player picks up or puts down an item stack.
local function on_player_cursor_stack_changed(event)
gui.on_player_cursor_stack_changed(event)
Expand Down Expand Up @@ -690,6 +695,7 @@ local event_handlers_look_up = {
[defines.events.on_player_joined_game] = on_player_joined_game,
[defines.events.on_player_left_game] = on_player_left_game,
[defines.events.on_player_changed_surface] = on_player_changed_surface,
[defines.events.on_surface_created] = on_surface_created,
[defines.events.on_player_selected_area] = on_player_selected_area,
[defines.events.on_player_alt_selected_area] = on_player_alt_selected_area,
[defines.events.on_player_cursor_stack_changed] = on_player_cursor_stack_changed,
Expand Down
Loading
Loading