Skip to content
Closed
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
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ This codebase is inspired by **Quake 2** (id Software). The developer is deeply
| WC3 JASS native coverage, callback contracts, state ownership | [games/warcraft-3/jass-native-coverage.md](games/warcraft-3/jass-native-coverage.md) |
| WC3 AI next-upgrade cost queries and `ShiftTownSpot` construction placement state | [docs/games/warcraft-3/ai-upgrade-costs-and-town-spot.md](docs/games/warcraft-3/ai-upgrade-costs-and-town-spot.md) |
| WC3 event-trigger queueing, synchronous `TriggerExecute`, coroutine context | [docs/games/warcraft-3/trigger-events.md](docs/games/warcraft-3/trigger-events.md) |
| WC3 JASS choice dialogs, modal input, event context, remaining retail gaps | [docs/games/warcraft-3/jass-dialogs.md](docs/games/warcraft-3/jass-dialogs.md) |
| WC3 timer-dialog mission countdowns, stock FDF HUD, local visibility, save/load identity | [docs/games/warcraft-3/timer-dialogs.md](docs/games/warcraft-3/timer-dialogs.md) |
| WC3 leaderboards, counted-objective HUDs, per-player assignment, save/load identity | [docs/games/warcraft-3/leaderboards.md](docs/games/warcraft-3/leaderboards.md) |
| WC3 multiboard/texttag JASS registries, local display bits, presentation gaps | [docs/games/warcraft-3/multiboard-and-texttag.md](docs/games/warcraft-3/multiboard-and-texttag.md) |
Expand Down
4 changes: 4 additions & 0 deletions client/cl_parse.c
Original file line number Diff line number Diff line change
Expand Up @@ -1043,6 +1043,10 @@ static void CL_ParseWindow(sizeBuf_t *msg) {
cstring_t text;

def.id = MSG_ReadLong(msg);
if (op == UI_WINDOW_CLOSE) {
CL_WindowClose(def.id);
return;
}
if (op != UI_WINDOW_OPEN) {
fprintf(stderr, "CL_ParseWindow: bad operation %u\n", (unsigned)op);
msg->readcount = msg->cursize;
Expand Down
3 changes: 2 additions & 1 deletion client/cl_scrn.c
Original file line number Diff line number Diff line change
Expand Up @@ -502,7 +502,8 @@ void SCR_LayoutDrawBackdrop2(uiFrame_t const *frame, rect_t const *screen, uiBac
.insets.bottom = bd->BackgroundInsets[2],
.insets.left = bd->BackgroundInsets[3],
.flags = (bd->TileBackground ? DRAW_TILE : 0)
| (bd->Mirrored ? DRAW_MIRRORED : 0)));
| (bd->Mirrored ? DRAW_MIRRORED : 0)
| (bd->BlendAll ? DRAW_BLEND_ALL : 0)));
}

void SCR_LayoutDrawBackdrop(uiFrame_t const *frame, rect_t const *screen) {
Expand Down
1 change: 1 addition & 0 deletions client/tr_public.h
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ enum {
DRAW_TILE = 1 << 2,
DRAW_MIRRORED = 1 << 3,
DRAW_EDGE_2X2 = 1 << 4, /* edge texture uses WoW 2×2 quadrant UV layout */
DRAW_BLEND_ALL = 1 << 5, /* backdrop background uses texture alpha */
};

/* Text drawing parameters */
Expand Down
1 change: 1 addition & 0 deletions common/shared.h
Original file line number Diff line number Diff line change
Expand Up @@ -508,6 +508,7 @@ typedef enum {

typedef enum {
UI_WINDOW_OPEN,
UI_WINDOW_CLOSE, /* generic server-authored close: opcode and window ID only */
} uiWindowOp_t;

#define UI_WINDOW_MOVABLE (1u << 0) // flag bit; permits client-local pointer dragging; used by server-authored windows
Expand Down
79 changes: 79 additions & 0 deletions docs/games/warcraft-3/jass-dialogs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# JASS choice dialogs

The Warcraft game module owns `DialogCreate`, `DialogDestroy`, `DialogClear`,
`DialogSetMessage`, `DialogAddButton`, `DialogAddQuitButton`, `DialogDisplay`,
`TriggerRegisterDialogEvent`, `TriggerRegisterDialogButtonEvent`,
`GetClickedButton`, and `GetClickedDialog`.

## Ownership and input

`level.dialogs` and `level.dialog_buttons` contain bounded, map-lifetime,
one-based handle IDs. Destroyed and cleared handles are not recycled within a
map: a late click cannot be mistaken for a newly created button. The server
retains the message, labels, hotkeys, visibility, and ownership; the universal
client receives an ordinary server-authored `svc_window` and returns
`jassdialog <dialog-id> <button-id>`. The game validates both IDs, parentage,
client identity, and player-specific visibility, then consumes that visibility
before emitting an event. Duplicated network commands are ignored.

`UI_WINDOW_CLOSE` is a generic server-to-client `svc_window` operation carrying
only the window ID. It is not a Warcraft-specific UI protocol. It lets
`DialogDisplay(false)`, `DialogClear`, and `DialogDestroy` release modal focus
without synthesizing an answer. The client's `close_window_command` handles a
selected button's local closure without changing simulation state.

The generic interactive window uses `UI_WINDOW_MODAL | UI_WINDOW_UNIQUE |
UI_WINDOW_NO_PAUSE | UI_WINDOW_NO_ESCAPE`; it captures gameplay input without
inventing a simulation pause. Text is resolved through `G_LevelString` and the
existing FDF UI layer.

## Events and save/load

The event registration records a dialog ID and optional button ID. A click
queues the matching `EVENT_DIALOG_CLICK` and/or `EVENT_DIALOG_BUTTON_CLICK`
registration through the normal `GAMEEVENT` pipeline. `JASSCONTEXT` stores
both IDs and the clicking player's `playerState`, including across coroutine
suspension. The response survives game saves via the level event serializer,
JASS snapshots, and stable dialog/button handle indexes. The game save version
is **66**, and the JASS snapshot version is **7**; earlier layouts are rejected.

A choice is one-shot per `DialogDisplay` call. Re-display is allowed and does not
recreate the JASS handles. A new dialog shown to the same player replaces the
previous active choice and clears its authoritative visible bit.

## Deliberate remaining compatibility work

* `DialogAddQuitButton` creates an ordinary selectable button carrying the
quit and score-screen metadata. The retail-specific quit/end-game transition
and score-screen policy are not implemented here.
* Button hotkeys are stored, but the generic `svc_window` keyboard shortcut
routing is not implemented. Mouse input is supported.
* UI emits at most 12 buttons in a window; omitted choices are reported to
stderr. Unusually large custom-map dialogs need scrolling/pagination and
retail scaling research.
* ScriptDialog and ScriptDialogButton use generated bindings to stock FDF.
The retail FDF has no variable-height choice rows, so row anchors and dialog
height are generated from the runtime button count. Final visual alignment,
actual clickable rendering, and exact retail dialog layout require in-game
verification at 640×480 and widescreen resolutions.
* Save restores dialog identities and visibility. The load and client-begin
paths republish the active choice, though manual save/reconnect testing is
still required.
* Nested simultaneous dialogs, priority and per-button hotkey collision rules
need further retail verification.

The NightElfX06 fork requires both the global dialog event and specific-button
event pathways. It must not use a map-specific event or client UI hook.

### Regression tests

`wc3_dialog.*` covers creation, distinct handles, clear/destroy lifecycle,
JASS native calls, a choice event, correct `GetClickedButton` /
`GetClickedDialog` / `GetTriggerPlayer` context, registration selection, and
duplicate click rejection. It also verifies player-number identity when a
client slot differs from its Warcraft player number, verifies clear hides the
matching client's window, and checks that repeated serialization reclaims
temporary FDF frames. The disabled-label
serialization path and save-version rejection have focused regressions.
`make test` passed with 46,715 assertions after the audit fixes. Retail visual
alignment and campaign integration still require user-side game verification.
9 changes: 9 additions & 0 deletions docs/games/warcraft-3/trigger-events.md
Original file line number Diff line number Diff line change
Expand Up @@ -203,3 +203,12 @@ make test-wc3-engine WC3_PATTERN='wc3_api.*'
make test-wc3-engine WC3_PATTERN='wc3_spell.*'
make test-wc3-engine WC3_PATTERN='wc3_save.round_trip_jass_timers'
```

## JASS interactive dialog choices

`DialogDisplay` creates a player-scoped modal `svc_window` without pausing the
simulation. A validated click publishes events targeted at the registered
dialog and button, with the authoritative player ID and one-based clicked
handle IDs. This response context is copied to a coroutine, so
`GetClickedButton` / `GetClickedDialog` continue to work after a trigger wait.
See [JASS choice dialogs](jass-dialogs.md) for lifecycle and limitations.
27 changes: 27 additions & 0 deletions games/warcraft-3/common/stb_fdf.h
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,7 @@ void UI_ClearTextures(void);
/* -------------------------------------------------------------------------- */
frameDef_t *UI_Spawn(FRAMETYPE type, frameDef_t *parent);
frameDef_t *UI_CloneFrameTree(frameDef_t const *source, frameDef_t *parent);
void UI_FreeFrameTree(frameDef_t *root);
uint32_t UI_FindFrameNumber(cstring_t name);
void UI_SetText(frameDef_t *frame, cstring_t format, ...);
void UI_SetTextPointer(frameDef_t *frame, cstring_t text);
Expand Down Expand Up @@ -1141,6 +1142,31 @@ frameDef_t *UI_Spawn(FRAMETYPE type, frameDef_t *parent) {
return NULL;
}

/* Release one pooled frame and the dynamic strings, lists, and camera token it owns. */
static void UI_ReleaseFrame(frameDef_t *frame) {
if (frame->camera_event_instance_id && ui_release_camera_event_instance)
ui_release_camera_event_instance(frame->camera_event_instance_id);
UI_FreeFrameDynamicText(frame);
UI_FreeFrameMenuItems(frame);
memset(frame, 0, sizeof(*frame));
}

/* Reclaim a serialized temporary tree so repeated server-authored windows do not exhaust the shared FDF frame pool. */
void UI_FreeFrameTree(frameDef_t *root) {
bool release[MAX_UI_CLASSES] = { false };
uintptr_t address = (uintptr_t)root, base = (uintptr_t)frames;

if (!root || address < base || address >= base + sizeof(frames) ||
(address - base) % sizeof(frames[0])) return;
FOR_LOOP(i, MAX_UI_CLASSES) {
frameDef_t *frame = frames + i;
for (frameDef_t const *parent = frame; parent; parent = parent->Parent) {
if (parent == root) { release[i] = true; break; }
}
}
FOR_LOOP(i, MAX_UI_CLASSES) if (release[i]) UI_ReleaseFrame(frames + i);
}

typedef struct {
cstring_t name;
uint32_t fofs;
Expand Down Expand Up @@ -1878,6 +1904,7 @@ frameDef_t *UI_CloneFrameTree(frameDef_t const *source, frameDef_t *parent) {
FOR_LOOP(i, count) {
copies[i] = UI_Spawn(sources[i]->Type, parent);
if (!copies[i]) {
while (i) UI_ReleaseFrame(copies[--i]);
return NULL;
}
uintptr_t camera_event_instance_id = copies[i]->camera_event_instance_id;
Expand Down
41 changes: 26 additions & 15 deletions games/warcraft-3/game/api/api_misc.h
Original file line number Diff line number Diff line change
Expand Up @@ -572,10 +572,10 @@ uint32_t GetTriggeringTrackable(jass_t *j) {
return jass_pushnullhandle(j, "trackable");
}
uint32_t GetClickedButton(jass_t *j) {
return jass_pushnullhandle(j, "button");
return jass_pushlighthandle(j, G_JassDialogButtonById(jass_getcontext(j)->dialog_button_id), "button");
}
uint32_t GetClickedDialog(jass_t *j) {
return jass_pushnullhandle(j, "dialog");
return jass_pushlighthandle(j, G_JassDialogById(jass_getcontext(j)->dialog_id), "dialog");
}
uint32_t GetLevelingUnit(jass_t *j) {
return jass_pushlighthandle(j, jass_getcontext(j)->unit, "unit");
Expand Down Expand Up @@ -1046,35 +1046,46 @@ uint32_t SetDefaultDifficulty(jass_t *j) {
return 0;
}
uint32_t DialogCreate(jass_t *j) {
return jass_pushnullhandle(j, "dialog");
return jass_pushlighthandle(j, G_JassDialogCreate(), "dialog");
}
uint32_t DialogDestroy(jass_t *j) {
//handle_t whichDialog = jass_checkhandle(j, 1, "dialog");
G_JassDialogDestroy(G_JassDialog(jass_checkhandle(j, 1, "dialog")));
return 0;
}
uint32_t DialogSetAsync(jass_t *j) {
//handle_t whichDialog = jass_checkhandle(j, 1, "dialog");
/* Compatibility extension. Standard dialogs remain authoritative. */
return 0;
}
uint32_t DialogClear(jass_t *j) {
//handle_t whichDialog = jass_checkhandle(j, 1, "dialog");
G_JassDialogClear(G_JassDialog(jass_checkhandle(j, 1, "dialog")));
return 0;
}
uint32_t DialogSetMessage(jass_t *j) {
//handle_t whichDialog = jass_checkhandle(j, 1, "dialog");
//cstring_t messageText = jass_checkstring(j, 2);
jassDialog_t *dialog = G_JassDialog(jass_checkhandle(j, 1, "dialog"));
cstring_t message = jass_checkstring(j, 2);
if (dialog) snprintf(dialog->message, sizeof(dialog->message), "%s", message ? G_LevelString(message) : "");
return 0;
}
uint32_t DialogAddButton(jass_t *j) {
//handle_t whichDialog = jass_checkhandle(j, 1, "dialog");
//cstring_t buttonText = jass_checkstring(j, 2);
//int32_t hotkey = jass_checkinteger(j, 3);
return jass_pushnullhandle(j, "button");
jassDialog_t *dialog = G_JassDialog(jass_checkhandle(j, 1, "dialog"));
cstring_t text = jass_checkstring(j, 2);
int32_t hotkey = jass_checkinteger(j, 3);
jassDialogButtonOptions_t options = { .hotkey = hotkey };
return jass_pushlighthandle(j, G_JassDialogAddButton(dialog, text, &options), "button");
}
uint32_t DialogAddQuitButton(jass_t *j) {
jassDialog_t *dialog = G_JassDialog(jass_checkhandle(j, 1, "dialog"));
bool score = jass_checkboolean(j, 2);
cstring_t text = jass_checkstring(j, 3);
int32_t hotkey = jass_checkinteger(j, 4);
jassDialogButtonOptions_t options = { .hotkey = hotkey, .quit = true, .score_screen = score };
return jass_pushlighthandle(j, G_JassDialogAddButton(dialog, text, &options), "button");
}
uint32_t DialogDisplay(jass_t *j) {
//player_t *whichPlayer = jass_checkhandle(j, 1, "player");
//handle_t whichDialog = jass_checkhandle(j, 2, "dialog");
//bool flag = jass_checkboolean(j, 3);
player_t *player = jass_checkhandle(j, 1, "player");
jassDialog_t *dialog = G_JassDialog(jass_checkhandle(j, 2, "dialog"));
bool visible = jass_checkboolean(j, 3);
G_JassDialogDisplay(player, dialog, visible);
return 0;
}
uint32_t InitGameCache(jass_t *j) {
Expand Down
1 change: 1 addition & 0 deletions games/warcraft-3/game/api/api_module.c
Original file line number Diff line number Diff line change
Expand Up @@ -741,6 +741,7 @@ jassModule_t jass_funcs[] = {
{ "DialogClear", DialogClear },
{ "DialogSetMessage", DialogSetMessage },
{ "DialogAddButton", DialogAddButton },
{ "DialogAddQuitButton", DialogAddQuitButton },
{ "DialogDisplay", DialogDisplay },
{ "InitGameCache", InitGameCache },
{ "SaveGameCache", SaveGameCache },
Expand Down
23 changes: 17 additions & 6 deletions games/warcraft-3/game/api/api_trigger.h
Original file line number Diff line number Diff line change
Expand Up @@ -238,14 +238,25 @@ uint32_t TriggerRegisterGameStateEvent(jass_t *j) {
return jass_pushlighthandle(j, evt, "event");
}
uint32_t TriggerRegisterDialogEvent(jass_t *j) {
//trigger_t *whichTrigger = jass_checkhandle(j, 1, "trigger");
//handle_t whichDialog = jass_checkhandle(j, 2, "dialog");
return jass_pushnullhandle(j, "event");
trigger_t *trigger = jass_checkhandle(j, 1, "trigger");
jassDialog_t *dialog = G_JassDialog(jass_checkhandle(j, 2, "dialog"));
event_t *event;
if (!trigger || !dialog || !(event = G_MakeEvent(EVENT_DIALOG_CLICK)))
return jass_pushnullhandle(j, "event");
event->trigger = trigger;
event->dialog_id = dialog->id;
return jass_pushlighthandle(j, event, "event");
}
uint32_t TriggerRegisterDialogButtonEvent(jass_t *j) {
//trigger_t *whichTrigger = jass_checkhandle(j, 1, "trigger");
//handle_t whichButton = jass_checkhandle(j, 2, "button");
return jass_pushnullhandle(j, "event");
trigger_t *trigger = jass_checkhandle(j, 1, "trigger");
jassDialogButton_t *button = G_JassDialogButton(jass_checkhandle(j, 2, "button"));
event_t *event;
if (!trigger || !button || !(event = G_MakeEvent(EVENT_DIALOG_BUTTON_CLICK)))
return jass_pushnullhandle(j, "event");
event->trigger = trigger;
event->dialog_id = button->dialog_id;
event->button_id = button->id;
return jass_pushlighthandle(j, event, "event");
}
uint32_t TriggerRegisterGameEvent(jass_t *j) {
//trigger_t *whichTrigger = jass_checkhandle(j, 1, "trigger");
Expand Down
13 changes: 13 additions & 0 deletions games/warcraft-3/game/g_commands.c
Original file line number Diff line number Diff line change
Expand Up @@ -2898,6 +2898,18 @@ static void CMD_UICanvas(edict_t *ent, uint32_t argc, cstring_t argv[]) {
ent->client->canvas = (UICANVASCLASS)value;
}

/* The client sends opaque map-lifetime IDs; the game validates ownership. */
static void CMD_JassDialogChoice(edict_t *ent, uint32_t argc, cstring_t argv[]) {
char *end_dialog, *end_button;
unsigned long dialog, button;
if (!ent || !ent->client || argc != 3) return;
dialog = strtoul(argv[1], &end_dialog, 10);
button = strtoul(argv[2], &end_button, 10);
if (!argv[1][0] || *end_dialog || !argv[2][0] || *end_button ||
dialog > MAX_JASS_DIALOGS || button > MAX_JASS_DIALOG_BUTTONS) return;
G_JassDialogClick(ent, (uint32_t)dialog, (uint32_t)button);
}

clientCommand_t clientCommands[] = {
{ "give", CMD_Give },
{ "god", CMD_God },
Expand Down Expand Up @@ -2948,6 +2960,7 @@ clientCommand_t clientCommands[] = {
{ "gameresult_restart", CMD_GameResultRestart },
{ "gameresult_load", CMD_GameResultLoad },
{ "gameresult_quit", CMD_GameResultQuit },
{ "jassdialog", CMD_JassDialogChoice },
{ "debugspawn", CMD_DebugSpawn },
{ "enemiesclear", CMD_EnemiesClear },
{ "eclear", CMD_EnemiesClear },
Expand Down
Loading
Loading