diff --git a/.gitignore b/.gitignore index 3eaa5d9..25cee6e 100644 --- a/.gitignore +++ b/.gitignore @@ -450,4 +450,5 @@ compile_commands.json # Custom .build +.release .install diff --git a/CMakeLists.txt b/CMakeLists.txt index 80f725d..c48395e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -211,10 +211,14 @@ set(PROJECT_SOURCES "src/JsmData.h" "src/JsmExpression.cpp" "src/JsmExpression.h" + "src/JsmHelpDialog.cpp" + "src/JsmHelpDialog.h" "src/JsmHighlighter.cpp" "src/JsmHighlighter.h" "src/JsmOpcode.cpp" "src/JsmOpcode.h" + "src/JsmPseudoCompiler.cpp" + "src/JsmPseudoCompiler.h" "src/JsmScripts.cpp" "src/JsmScripts.h" "src/Listwidget.cpp" diff --git a/src/JsmHelpDialog.cpp b/src/JsmHelpDialog.cpp new file mode 100644 index 0000000..b7193a2 --- /dev/null +++ b/src/JsmHelpDialog.cpp @@ -0,0 +1,695 @@ +/**************************************************************************** + ** Deling Final Fantasy VIII Field Editor + ** Copyright (C) 2009-2024 Arzel Jérôme + ** + ** This program is free software: you can redistribute it and/or modify + ** it under the terms of the GNU General Public License as published by + ** the Free Software Foundation, either version 3 of the License, or + ** (at your option) any later version. + ** + ** This program is distributed in the hope that it will be useful, + ** but WITHOUT ANY WARRANTY; without even the implied warranty of + ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + ** GNU General Public License for more details. + ** + ** You should have received a copy of the GNU General Public License + ** along with this program. If not, see . + ****************************************************************************/ +#include "JsmHelpDialog.h" + +struct OpcodeHelp { + const char *name; + const char *category; + const char *signature; + const char *description; +}; + +static const OpcodeHelp opcodeHelpData[] = { + // -- Syntax -- + {"if / else / end", "Syntax", "if begin ... else ... end", + "Conditional execution. The else block is optional. Conditions use ==, !=, >, <, >=, <=, and, or."}, + {"while / end", "Syntax", "while begin ... end", + "Loop while condition is true."}, + {"wait while", "Syntax", "wait while ", + "Pause script until condition becomes false. Empty-body while loop."}, + {"wait forever", "Syntax", "wait forever", + "Pause script execution indefinitely."}, + {"forever / end", "Syntax", "forever begin ... end", + "Infinite loop."}, + {"repeat / until", "Syntax", "repeat ... until end", + "Execute body at least once, repeat until condition is true."}, + {"ret", "Syntax", "ret(scriptID)", "Return from the current script."}, + {"Assignment", "Syntax", "variable = expression", + "Assign a value. Compound: +=, -=, *=, /=, %=, &=, |=, ^=, >>=, <<=."}, + {"Variables", "Syntax", "N_ubyte, N_uword, N_ulong, N_sbyte, N_sword, N_slong", + "Memory variables at address N. Type suffix sets read width and signedness."}, + {"Temp Variables", "Syntax", "temp_N", + "Temporary registers 0-7. Many opcodes store results in temp_0."}, + {"Model Reference", "Syntax", "model_N", "Entity/model reference by index."}, + {"Constants", "Syntax", "text_N, map_N, item_N, magic_N, KeyCancel, Squall, etc.", + "Named constants that resolve to numeric values. See the Characters, Keys, and Resources categories for full listings."}, + + // -- Character Constants -- + {"Squall", "Characters", "Squall = 0", "Main protagonist."}, + {"Zell", "Characters", "Zell = 1", "Martial artist party member."}, + {"Irvine", "Characters", "Irvine = 2", "Sharpshooter party member."}, + {"Quistis", "Characters", "Quistis = 3", "Instructor party member."}, + {"Rinoa", "Characters", "Rinoa = 4", "Rinoa Heartilly."}, + {"Selphie", "Characters", "Selphie = 5", "Selphie Tilmitt."}, + {"Seifer", "Characters", "Seifer = 6", "Seifer Almasy."}, + {"Edea", "Characters", "Edea = 7", "Edea Kramer."}, + {"Laguna", "Characters", "Laguna = 8", "Laguna Loire. Used in dream sequences."}, + {"Kiros", "Characters", "Kiros = 9", "Kiros Seagill. Used in dream sequences."}, + {"Ward", "Characters", "Ward = 10", "Ward Zabac. Used in dream sequences."}, + + // -- Key Constants -- + {"KeyL1", "Keys", "KeyL1 = 0x1", "L1 button."}, + {"KeyR1", "Keys", "KeyR1 = 0x2", "R1 button."}, + {"KeyL2", "Keys", "KeyL2 = 0x4", "L2 button."}, + {"KeyR2", "Keys", "KeyR2 = 0x8", "R2 button."}, + {"KeyCancel", "Keys", "KeyCancel = 0x10 (16)", "Cancel / X button."}, + {"KeyMenu", "Keys", "KeyMenu = 0x20 (32)", "Menu / Triangle button."}, + {"KeyChoose", "Keys", "KeyChoose = 0x40 (64)", "Confirm / Circle button."}, + {"KeyCard", "Keys", "KeyCard = 0x80 (128)", "Card / Square button."}, + {"KeySelect", "Keys", "KeySelect = 0x100 (256)", "Select button."}, + {"KeyStart", "Keys", "KeyStart = 0x800 (2048)", "Start button."}, + {"KeyUp", "Keys", "KeyUp = 0x1000 (4096)", "D-pad up."}, + {"KeyRight", "Keys", "KeyRight = 0x2000 (8192)", "D-pad right."}, + {"KeyDown", "Keys", "KeyDown = 0x4000 (16384)", "D-pad down."}, + {"KeyLeft", "Keys", "KeyLeft = 0x8000 (32768)", "D-pad left."}, + + // -- Resource Constants -- + {"text_N", "Resources", "text_N (e.g. text_0, text_5)", "Reference to a text entry in the field's MSD file. N is the text index."}, + {"map_N", "Resources", "map_N (e.g. map_100, map_256)", "Reference to a field map by ID. Used with mapjump and related opcodes."}, + {"item_N", "Resources", "item_N (e.g. item_1, item_32)", "Reference to an item by ID. " + "0=Empty, 1=Potion, 2=Potion+, 3=Hi-Potion, 4=Hi-Potion+, 5=X-Potion, 6=Mega-Potion, " + "7=Phoenix Down, 8=Mega-Phoenix, 9=Elixir, 10=Megalixir, 11=Antidote, 12=Soft, " + "13=Eyedrops, 14=Echo Screen, 15=Holy Water, 16=Remedy, 17=Remedy+, 18=Hero-Trial, " + "19=Hero, 20=Holy War-Trial, 21=Holy War, 22=Shell Stone, 23=Protect Stone, " + "24=Aura Stone, 25=Death Stone, 26=Holy Stone, 27=Flare Stone, 28=Meteor Stone, " + "29=Ultima Stone, 30=Gysahl Greens, 31=Phoenix Pinion, 32=Friendship."}, + {"magic_N", "Resources", "magic_N (e.g. magic_1, magic_20)", "Reference to a magic spell by ID. " + "0=Empty, 1=Fire, 2=Fira, 3=Firaga, 4=Blizzard, 5=Blizzara, 6=Blizzaga, " + "7=Thunder, 8=Thundara, 9=Thundaga, 10=Water, 11=Aero, 12=Bio, 13=Demi, " + "14=Holy, 15=Flare, 16=Meteor, 17=Quake, 18=Tornado, 19=Ultima, 20=Apocalypse, " + "21=Cure, 22=Cura, 23=Curaga, 24=Life, 25=Full-Life, 26=Regan, 27=Esuna, " + "28=Dispel, 29=Protect, 30=Shell, 31=Reflect, 32=Aura, 33=Double, 34=Triple, " + "35=Haste, 36=Slow, 37=Stop, 38=Blind, 39=Confuse, 40=Sleep, 41=Silence, " + "42=Break, 43=Death, 44=Drain, 45=Pain, 46=Berserk, 47=Float, 48=Zombie, " + "49=Meltdown, 50=Scan, 51=Full-Cure, 52=Wall, 53=Rapture, 54=Percent, " + "55=Catastrophe, 56=The End."}, + + // -- Flow Control -- + {"nop", "Flow Control", "nop()", "No operation. Does nothing."}, + {"halt", "Flow Control", "halt()", "Exit current script AND all scripts waiting on it."}, + {"wait", "Flow Control", "wait(frames)", "Pause this script for N frames (30fps)."}, + {"debug", "Flow Control", "debug()", "No function. Development only."}, + + // -- Script Execution -- + {"req", "Script Execution", "req(entityGroupID, methodLabel, priority)", + "Request remote execution. Non-blocking, may fail if busy."}, + {"reqsw", "Script Execution", "reqsw(entityGroupID, methodLabel, priority)", + "Request remote execution, async, guaranteed."}, + {"reqew", "Script Execution", "reqew(entityGroupID, methodLabel, priority)", + "Request remote execution, sync (waits), guaranteed."}, + {"preq", "Script Execution", "preq(partySlot, methodLabel, priority)", + "Request execution on party member entity."}, + {"preqsw", "Script Execution", "preqsw(partySlot, methodLabel, priority)", + "Request party entity execution, async, guaranteed."}, + {"preqew", "Script Execution", "preqew(partySlot, methodLabel, priority)", + "Request party entity execution, sync, guaranteed."}, + + // -- Entity Control -- + {"unuse", "Entity", "unuse(entityID)", "Disable entity scripts, hide model, make passable."}, + {"use", "Entity", "use()", "Re-enable an entity. Opposite of unuse."}, + {"show", "Entity", "show()", "Show this entity's model."}, + {"hide", "Entity", "hide()", "Hide this entity's model."}, + {"throughon", "Entity", "throughon()", "Make entity passable (walk through)."}, + {"throughoff", "Entity", "throughoff()", "Make entity solid (collide)."}, + {"talkon", "Entity", "talkon()", "Enable talk script."}, + {"talkoff", "Entity", "talkoff()", "Disable talk script."}, + {"pushon", "Entity", "pushon()", "Enable push script."}, + {"pushoff", "Entity", "pushoff()", "Disable push script."}, + {"istouch", "Entity", "istouch(actorID)", "Push 1 to temp_0 if actor is in touch range, else 0."}, + {"talkradius", "Entity", "talkradius(radius)", "Set talk trigger distance. Humanoids ~200."}, + {"pushradius", "Entity", "pushradius(radius)", "Set push trigger distance. Active=48, inactive=1."}, + {"actormode", "Entity", "actormode(mode)", "Set transparency/blinking effects on models."}, + {"whoami", "Entity", "whoami()", "Push this character's real ID into temp_0. For Laguna dreams."}, + + // -- Positioning -- + {"set", "Positioning", "set(walkmeshTriID, x, y)", "Place entity at X,Y. Z from walkmesh."}, + {"set3", "Positioning", "set3(walkmeshTriID, x, y, z)", "Place entity at X,Y,Z explicitly."}, + {"idlock", "Positioning", "idlock(walkmeshTriID)", "Lock a walkmesh triangle."}, + {"idunlock", "Positioning", "idunlock(walkmeshTriID)", "Unlock a walkmesh triangle."}, + {"dir", "Positioning", "dir(angle)", "Face angle. 256-degree: 0=down, 64=right, 128=up, 192=left."}, + {"dirp", "Positioning", "dirp(partyMemberID, unknown, speed)", "Face a party member."}, + {"dira", "Positioning", "dira(actorID)", "Face an actor."}, + {"pdira", "Positioning", "pdira(partyMemberID)", "Face a party entity."}, + {"getinfo", "Positioning", "getinfo()", "Push entity X to temp_0, Y to temp_1."}, + {"pgetinfo", "Positioning", "pgetinfo()", "Push party member X to temp_0, Y to temp_1."}, + {"facedira", "Positioning", "facedira(actorID, frames)", "Turn head to face actor over N frames."}, + {"facedirp", "Positioning", "facedirp(partyMemberID, frames)", "Turn head to face party member."}, + {"facedirlimit", "Positioning", "facedirlimit(limit)", "Limit head turn range."}, + {"facediroff", "Positioning", "facediroff(frames)", "Stop head facing over N frames."}, + {"facedirinit", "Positioning", "facedirinit()", "Unlock head from model. Call before FACEDIR."}, + {"facedir", "Positioning", "facedir(angle, frames)", "Set face direction over frames."}, + {"facediri", "Positioning", "facediri(angle)", "Set face direction immediately."}, + {"rfacedir", "Positioning", "rfacedir(angle, frames)", "Resume-script face direction."}, + {"rfacedira", "Positioning", "rfacedira(actorID, frames)", "Resume-script face towards actor."}, + {"rfacedirp", "Positioning", "rfacedirp(partyMemberID, frames)", "Resume-script face towards party member."}, + {"rfacediri", "Positioning", "rfacediri(angle)", "Resume-script face direction immediately."}, + {"rfacediroff", "Positioning", "rfacediroff(frames)", "Resume-script stop face direction."}, + {"facedirsync", "Positioning", "facedirsync()", "Wait for face direction to finish."}, + + // -- Movement -- + {"mspeed", "Movement", "mspeed(speed)", "Set movement speed."}, + {"move", "Movement", "move(walkmeshID, x, y, unknown)", "Walk/run to X,Y."}, + {"movea", "Movement", "movea(actorID, unknown)", "Move towards actor."}, + {"pmovea", "Movement", "pmovea(partyMemberID, unknown)", "Move towards party member."}, + {"cmove", "Movement", "cmove(walkmeshID, x, y, unknown)", "Move without animation or turning."}, + {"fmove", "Movement", "fmove(walkmeshID, x, y, unknown)", "Move without animation, face direction."}, + {"movesync", "Movement", "movesync()", "Wait until MOVE finishes."}, + {"moveflush", "Movement", "moveflush()", "Halt current movement."}, + {"jump", "Movement", "jump(walkmeshID, x, y, speed)", "Jump to X,Y."}, + {"jump3", "Movement", "jump3(walkmeshID, x, y, z, speed)", "Jump to X,Y,Z."}, + {"pjumpa", "Movement", "pjumpa(partyMemberID, unknown)", "Jump to party member."}, + {"ladderup", "Movement", "ladderup(walkmeshID, x, y, z)", "Ladder up movement."}, + {"ladderdown", "Movement", "ladderdown(walkmeshID, x, y, z)", "Ladder down movement."}, + {"ladderup2", "Movement", "ladderup2(...)", "Ladder up variant (9 stack params)."}, + {"ladderdown2", "Movement", "ladderdown2(...)", "Ladder down variant (9 stack params)."}, + {"ladderanime", "Movement", "ladderanime(animID, firstFrame, lastFrame)", "Ladder animation."}, + {"rmove", "Movement", "rmove(walkmeshID, x, y, unknown)", "Resume-script move (no pause)."}, + {"rmovea", "Movement", "rmovea(actorID, unknown)", "Resume-script move to actor."}, + {"rpmovea", "Movement", "rpmovea(partyMemberID, unknown)", "Resume-script move to party member."}, + {"rcmove", "Movement", "rcmove(walkmeshID, x, y, unknown)", "Resume-script cmove."}, + {"rfmove", "Movement", "rfmove(walkmeshID, x, y, unknown)", "Resume-script fmove."}, + {"fmovea", "Movement", "fmovea(actorID, unknown)", "Face-move to actor."}, + {"fmovep", "Movement", "fmovep(partyMemberID, unknown)", "Face-move to party member."}, + {"movecancel", "Movement", "movecancel()", "Cancel current movement."}, + {"pmovecancel", "Movement", "pmovecancel()", "Cancel party member movement."}, + {"maccel", "Movement", "maccel(acceleration)", "Set movement acceleration."}, + {"mlimit", "Movement", "mlimit(limit)", "Set movement speed limit."}, + + // -- Turning -- + {"cturnr", "Turning", "cturnr(angle, speed)", "Turn clockwise. 256-degree system."}, + {"cturnl", "Turning", "cturnl(angle, speed)", "Turn counter-clockwise."}, + {"lturnr", "Turning", "lturnr(angle, speed)", "Linear turn right."}, + {"lturnl", "Turning", "lturnl(angle, speed)", "Linear turn left."}, + {"cturn", "Turning", "cturn()", "Face entity set with PSHAC."}, + {"lturn", "Turning", "lturn()", "Linear turn to face entity."}, + {"pcturn", "Turning", "pcturn(partyMemberID, speed)", "Face a PC. Speed = frame count."}, + {"plturn", "Turning", "plturn(partyMemberID, speed)", "Linear turn to face PC."}, + + // -- Model / Animation -- + {"setmodel", "Animation", "setmodel(modelID)", "Set display model."}, + {"setpc", "Animation", "setpc(characterID)", "Set which PC this entity represents."}, + {"baseanime", "Animation", "baseanime(animID, firstFrame, lastFrame)", "Set idle animation."}, + {"anime", "Animation", "anime(animID)", "Play animation, return to base. Pauses script."}, + {"animekeep", "Animation", "animekeep(animID)", "Play animation, freeze last frame. Pauses."}, + {"canime", "Animation", "canime(animID, firstFrame, lastFrame)", "Play frame range, return to base. Pauses."}, + {"canimekeep", "Animation", "canimekeep(animID, firstFrame, lastFrame)", "Play frame range, freeze. Pauses."}, + {"ranime", "Animation", "ranime(animID)", "Play animation, return to base. No pause."}, + {"ranimekeep", "Animation", "ranimekeep(animID)", "Play animation, freeze. No pause."}, + {"rcanime", "Animation", "rcanime(animID, firstFrame, lastFrame)", "Play frame range, return. No pause."}, + {"rcanimekeep", "Animation", "rcanimekeep(animID, firstFrame, lastFrame)", "Play frame range, freeze. No pause."}, + {"ranimeloop", "Animation", "ranimeloop(animID)", "Loop animation. No pause."}, + {"rcanimeloop", "Animation", "rcanimeloop(animID, firstFrame, lastFrame)", "Loop frame range. No pause."}, + {"animesync", "Animation", "animesync()", "Wait for current animation to finish."}, + {"animestop", "Animation", "animestop()", "Return to base animation immediately."}, + {"animespeed", "Animation", "animespeed(speed)", "Set animation speed. 1 = 2fps."}, + {"footstep", "Animation", "footstep(flag)", "Footstep sound/effect control."}, + + // -- Messages / Dialog -- + {"mesw", "Messages", "mesw(channel, textID)", "Show message, wait for dismiss. Rarely used."}, + {"mes", "Messages", "mes(channel, textID)", "Show message. Size set with winsize."}, + {"messync", "Messages", "messync(channel)", "Wait for message to close."}, + {"mesvar", "Messages", "mesvar(varIndex, value)", "Set message variable {Var0}, {Var1}, etc."}, + {"ask", "Messages", "ask(channel, textID, firstLine, lastLine, defaultLine, cancelLine)", "Show choices. Result in temp_0."}, + {"winsize", "Messages", "winsize(channel, x, y, width, height)", "Set message window size/position."}, + {"winclose", "Messages", "winclose(channel)", "Close message window."}, + {"amesw", "Messages", "amesw(channel, textID, x, y)", "Show positioned message, wait."}, + {"ames", "Messages", "ames(channel, textID, x, y)", "Show positioned message. Stays until winclose."}, + {"aask", "Messages", "aask(channel, textID, first, last, default, unknown, x, y)", "Positioned choices. Result in temp_0."}, + {"setmesspeed", "Messages", "setmesspeed(speed, unknown)", "Set text display speed."}, + {"mesmode", "Messages", "mesmode(mode)", "0=opaque, 1=translucent, 2=no window."}, + {"ramesw", "Messages", "ramesw(channel, textID, x, y)", "Show message and wait, script continues."}, + + // -- Player Control -- + {"ucon", "Player Control", "ucon()", "Enable player control."}, + {"ucoff", "Player Control", "ucoff()", "Disable player control."}, + {"keyscan", "Player Control", "keyscan(keyMask)", "Check keys pressed. Result in temp_0."}, + {"keyon", "Player Control", "keyon(keyMask)", "Re-enable specific keys."}, + {"keyscan2", "Player Control", "keyscan2(keyMask)", "Alternate key scan."}, + {"keyon2", "Player Control", "keyon2(keyMask)", "Alternate key enable."}, + + // -- Lines / Triggers -- + {"setline", "Lines", "setline(x1, y1, z1, x2, y2, z2)", "Set 3D hitbox bounds."}, + {"lineon", "Lines", "lineon()", "Enable line collisions."}, + {"lineoff", "Lines", "lineoff()", "Disable line collisions."}, + + // -- Map Transitions -- + {"mapjump", "Map Transitions", "mapjump(walkmeshID, fieldMapID, x, y, z)", "Jump to another field."}, + {"mapjump3", "Map Transitions", "mapjump3(walkmeshID, fieldMapID, x, y, z, angle)", "Jump to field with angle."}, + {"mapjumpo", "Map Transitions", "mapjumpo(fieldMapID, unknown)", "Jump to field at walkmesh 0. For cutscenes."}, + {"mapjumpon", "Map Transitions", "mapjumpon()", "Enable field exits."}, + {"mapjumpoff", "Map Transitions", "mapjumpoff()", "Disable field exits."}, + {"discjump", "Map Transitions", "discjump(walkmeshID, fieldMapID, x, y, z, angle)", "Map jump + disc switch."}, + {"disc", "Map Transitions", "disc(discNumber)", "Set which disc for discjump."}, + {"worldmapjump", "Map Transitions", "worldmapjump()", "Jump to world map."}, + {"premapjump", "Map Transitions", "premapjump(...)", "Prepare map jump."}, + + // -- Camera / Scrolling -- + {"dscroll", "Camera", "dscroll()", "Scroll the screen."}, + {"lscroll", "Camera", "lscroll(x, y, frameCount)", "Scroll by X,Y pixels over frames."}, + {"cscroll", "Camera", "cscroll(x, y, frameCount)", "Scroll by X,Y pixels over frames."}, + {"dscrolla", "Camera", "dscrolla(actorID)", "Scroll onto actor."}, + {"lscrolla", "Camera", "lscrolla(actorID, frameCount)", "Linear scroll to actor."}, + {"cscrolla", "Camera", "cscrolla(actorID, frameCount)", "Curved scroll to actor."}, + {"scrollsync", "Camera", "scrollsync()", "Wait for scroll to finish."}, + {"dscrollp", "Camera", "dscrollp(partyMemberID)", "Scroll to party member."}, + {"lscrollp", "Camera", "lscrollp(partyMemberID, frameCount)", "Linear scroll to party member."}, + {"cscrollp", "Camera", "cscrollp(partyMemberID, frameCount)", "Curved scroll to party member."}, + {"dscroll2", "Camera", "dscroll2()", "Scroll variant 2."}, + {"lscroll2", "Camera", "lscroll2(x, y, frameCount)", "Linear scroll variant 2."}, + {"cscroll2", "Camera", "cscroll2(x, y, frameCount)", "Curved scroll variant 2."}, + {"dscrolla2", "Camera", "dscrolla2(actorID)", "Scroll to actor variant 2."}, + {"lscrolla2", "Camera", "lscrolla2(actorID, frameCount)", "Linear scroll to actor variant 2."}, + {"cscrolla2", "Camera", "cscrolla2(actorID, frameCount)", "Curved scroll to actor variant 2."}, + {"dscrollp2", "Camera", "dscrollp2(partyMemberID)", "Scroll to party member variant 2."}, + {"lscrollp2", "Camera", "lscrollp2(partyMemberID, frameCount)", "Linear scroll to party variant 2."}, + {"cscrollp2", "Camera", "cscrollp2(partyMemberID, frameCount)", "Curved scroll to party variant 2."}, + {"scrollsync2", "Camera", "scrollsync2()", "Wait for scroll2 to finish."}, + {"scrollmode2", "Camera", "scrollmode2(mode)", "Set scroll mode variant 2."}, + {"dscroll3", "Camera", "dscroll3()", "Scroll variant 3."}, + {"lscroll3", "Camera", "lscroll3(x, y, frameCount)", "Linear scroll variant 3."}, + {"cscroll3", "Camera", "cscroll3(x, y, frameCount)", "Curved scroll variant 3."}, + {"scrollratio2", "Camera", "scrollratio2(ratio)", "Set scroll ratio variant 2."}, + {"setcamera", "Camera", "setcamera()", "Set camera parameters."}, + {"setdcamera", "Camera", "setdcamera()", "Set dynamic camera."}, + {"doffset", "Camera", "doffset(x, y)", "Direct camera offset."}, + {"loffsets", "Camera", "loffsets(x, y, frameCount)", "Linear camera offsets."}, + {"coffsets", "Camera", "coffsets(x, y, frameCount)", "Curved camera offsets."}, + {"loffset", "Camera", "loffset(x, y, frameCount)", "Linear camera offset."}, + {"coffset", "Camera", "coffset(x, y, frameCount)", "Curved camera offset."}, + {"offsetsync", "Camera", "offsetsync()", "Wait for camera offset to finish."}, + + // -- Movies / FMV -- + {"movie", "Movies", "movie()", "Play prepared movie."}, + {"moviesync", "Movies", "moviesync()", "Wait for movie to finish."}, + {"movieready", "Movies", "movieready(movieID)", "Prepare/load a movie."}, + {"moviecut", "Movies", "moviecut()", "Debug only. No function."}, + + // -- Sound Effects -- + {"effectplay2", "Sound", "effectplay2(soundID, pan, volume)", "Play field SE. Pan 0=left 255=right. Vol 0-127."}, + {"effectplay", "Sound", "effectplay(channel)", "Play SE through channel."}, + {"effectload", "Sound", "effectload(soundID)", "Start background looping sound."}, + {"sestop", "Sound", "sestop(channel)", "Stop SE on channel."}, + {"spuready", "Sound", "spuready(frames)", "Set async timer to 0 frames."}, + {"spusync", "Sound", "spusync(frames)", "Wait N frames since spuready."}, + + // -- Music -- + {"musicload", "Music", "musicload(trackID)", "Preload field music."}, + {"musicchange", "Music", "musicchange()", "Stop current, start preloaded music."}, + {"musicstop", "Music", "musicstop()", "Stop music."}, + {"musicvol", "Music", "musicvol(volume)", "Set music volume."}, + {"musicvoltrans", "Music", "musicvoltrans(volume, duration)", "Transition music volume."}, + {"setbattlemusic", "Music", "setbattlemusic(musicID)", "Set battle music."}, + {"allsevol", "Music", "allsevol(volume)", "Set volume for all SE."}, + {"allsevoltrans", "Music", "allsevoltrans(volume, duration)", "Transition all SE volume."}, + {"allsepos", "Music", "allsepos(pan)", "Set pan for all SE."}, + {"allsepostrans", "Music", "allsepostrans(pan, duration)", "Transition all SE pan."}, + {"sevol", "Music", "sevol(channel, volume)", "Set SE channel volume."}, + {"sevoltrans", "Music", "sevoltrans(channel, volume, frames)", "Transition SE channel volume."}, + {"sepos", "Music", "sepos(channel, pan)", "Set SE channel pan. 0=left, 255=right."}, + {"sepostrans", "Music", "sepostrans(channel, pan, frames)", "Transition SE channel pan."}, + + // -- Visual Effects -- + {"fadeblack", "Visual", "fadeblack()", "Fade to black."}, + {"fadein", "Visual", "fadein()", "Fade in from black."}, + {"fadeout", "Visual", "fadeout()", "Fade out to black."}, + {"fadesync", "Visual", "fadesync()", "Wait for fade to complete."}, + {"shadelevel", "Visual", "shadelevel(level)", "Set actor shading."}, + {"fcolsub", "Visual", "fcolsub(r, g, b)", "Color transition on screen."}, + {"setvibrate", "Visual", "setvibrate()", "Vibrate controller."}, + {"clear", "Visual", "clear()", "Reset all variables. New game only."}, + {"shake", "Visual", "shake()", "Screen shake."}, + {"shakeoff", "Visual", "shakeoff()", "Stop screen shake."}, + + // -- Battle -- + {"battle", "Battle", "battle(encounterID)", "Start battle. Flags: +2=no fanfare, +4=timer, +16=music, +32=preemptive, +64=back attack."}, + {"battleresult", "Battle", "battleresult()", "Push last battle result to temp_0."}, + {"battleon", "Battle", "battleon()", "Enable random battles."}, + {"battleoff", "Battle", "battleoff()", "Disable random battles."}, + {"battlemode", "Battle", "battlemode(flags)", "Set random battle flags."}, + {"battlecut", "Battle", "battlecut()", "Debug only."}, + + // -- Party / Characters -- + {"addparty", "Party", "addparty(characterID)", "Add to active party. 0=Squall..10=Ward."}, + {"subparty", "Party", "subparty(characterID)", "Remove from active party."}, + {"setparty", "Party", "setparty(member1, member2, member3)", "Set full party. 255=blank."}, + {"isparty", "Party", "isparty(characterID)", "Push party position to temp_0. -1 if absent."}, + {"addmember", "Party", "addmember(characterID)", "Add to available roster."}, + {"submember", "Party", "submember(characterID)", "Remove from roster and party."}, + {"ismember", "Party", "ismember(characterID)", "Check if character is in roster."}, + {"getparty", "Party", "getparty(slotIndex)", "Push character ID in slot to temp_0."}, + {"join", "Party", "join()", "Start conga line (followers follow leader)."}, + {"split", "Party", "split()", "Disable conga line, send members to spots."}, + {"junction", "Party", "junction(mode)", "0=end dream, 1=Squall junctions carry, 3=whole party."}, + {"setdress", "Party", "setdress(characterID)", "Set character costume."}, + {"getdress", "Party", "getdress(characterID)", "Get character costume."}, + {"resetgf", "Party", "resetgf(characterID)", "Reset GF for character."}, + + // -- Items / Magic / Cards -- + {"additem", "Items", "additem(quantity, itemID)", "Add item to inventory."}, + {"hasitem", "Items", "hasitem(itemID)", "Push 1 to temp_0 if party has item, else 0."}, + {"addmagic", "Items", "addmagic(characterID, magicID)", "Add magic to character."}, + {"setcard", "Items", "setcard(cardID, deckID)", "Move card to deck. Rare cards are unique."}, + {"getcard", "Items", "getcard(cardID)", "Get/check a card. Result in temp_0."}, + {"howmanycard", "Items", "howmanycard(cardID)", "Push the count of a card the player owns to temp_0."}, + {"wherecard", "Items", "wherecard(cardID)", "Push which deck a card is in to temp_0."}, + {"drawpoint", "Items", "drawpoint(drawPointID)", "Initialize draw point."}, + {"setdrawpoint", "Items", "setdrawpoint(isHidden)", "Hide (>=1) or show (0) draw point."}, + {"cardgame", "Items", "cardgame(deckID, knownRules, regionRules, rareChance, u1, u2, u3)", + "Start Triple Triad game."}, + {"addgil", "Items", "addgil(amount)", "Add gil."}, + + // -- Menus / UI -- + {"menunormal", "Menus", "menunormal()", "Open normal menu."}, + {"menuphs", "Menus", "menuphs()", "Open PHS (party switch) menu."}, + {"menushop", "Menus", "menushop(shopID)", "Open shop."}, + {"menusave", "Menus", "menusave()", "Open save menu."}, + {"menuname", "Menus", "menuname(characterOrGfID)", "Name entry screen. Unlocks GFs."}, + {"saveenable", "Menus", "saveenable(enabled)", "Enable/disable saving."}, + {"phsenable", "Menus", "phsenable(enabled)", "Enable/disable PHS submenu."}, + {"phspower", "Menus", "phspower(enabled)", "Enable/disable party switching."}, + {"menuenable", "Menus", "menuenable()", "Enable menu access."}, + {"menudisable", "Menus", "menudisable()", "Disable menu access."}, + + // -- Timers -- + {"settimer", "Timers", "settimer(value)", "Set countdown timer value."}, + {"disptimer", "Timers", "disptimer()", "Display and start countdown timer."}, + {"gettimer", "Timers", "gettimer()", "Get current timer value."}, + {"killtimer", "Timers", "killtimer()", "Stop and hide timer."}, + + // -- Background Animation -- + {"bganime", "Background", "bganime(paramID)", "Animate background object."}, + {"rbganime", "Background", "rbganime(paramID)", "Resume background animation."}, + {"rbganimeloop", "Background", "rbganimeloop(paramID)", "Loop background animation."}, + {"bganimesync", "Background", "bganimesync()", "Wait for background animation."}, + {"bgdraw", "Background", "bgdraw(state)", "Set background draw state."}, + {"bgoff", "Background", "bgoff(paramID)", "Turn off background layer."}, + {"bganimespeed", "Background", "bganimespeed(speed)", "Set background animation speed."}, + {"bgshade", "Background", "bgshade(r, g, b, r2, g2, b2, unknown)", "Set background shading."}, + + // -- Salary / SeeD -- + {"saralyoff", "Salary", "saralyoff()", "Disable salary payments."}, + {"saralyon", "Salary", "saralyon()", "Enable salary payments."}, + {"saralydispoff", "Salary", "saralydispoff()", "Hide salary alerts."}, + {"saralydispon", "Salary", "saralydispon()", "Show salary alerts."}, + {"addseedlevel", "Salary", "addseedlevel(points)", "Add SeeD level points. 100/rank, max 3100."}, + + // -- Game State -- + {"gameover", "Game State", "gameover()", "End game in failure."}, + {"rnd", "Game State", "rnd()", "Push random 0-255 to temp_0."}, + {"setplace", "Game State", "setplace(locationID)", "Set location name for menu/saves."}, + {"runenable", "Game State", "runenable()", "Enable running."}, + {"rundisable", "Game State", "rundisable()", "Disable running."}, + {"hold", "Game State", "hold(characterID, locked)", "Lock/unlock PC in switch menu."}, + {"dying", "Game State", "dying()", "Resurrect dead characters. Used for Laguna party switch."}, + {"setwitch", "Game State", "setwitch()", "Unlock Rinoa's sorceress limit break."}, + {"setodin", "Game State", "setodin()", "Unlock Odin."}, + {"disableangelo", "Game State", "disableangelo(flag)", "Disable Angelo in battle."}, + {"setgeta", "Game State", "setgeta()", "Unknown/unfinished."}, + + // -- Last Dungeon -- + {"lastin", "Last Dungeon", "lastin()", "Disable features for Ultimecia's Castle."}, + {"lastout", "Last Dungeon", "lastout()", "End lastin effects."}, + {"sealedoff", "Last Dungeon", "sealedoff(flags)", + "Enable sealed features. 1=Items, 2=Magic, 4=GF, 8=Draw, 16=Command, 32=Limit, 64=Resurrect, 128=Save."}, + + // -- Tutorial -- + {"showtutorial", "Tutorial", "showtutorial(tutorialID)", "Show a tutorial screen."}, + {"menututo", "Tutorial", "menututo()", "Open tutorial menu."}, + {"menutips", "Tutorial", "menutips()", "Open tips menu."}, + + // -- Misc -- + {"followon", "Misc", "followon()", "Enable camera follow."}, + {"followoff", "Misc", "followoff()", "Disable camera follow."}, + {"polycolor", "Misc", "polycolor(r, g, b)", "Set polygon color."}, + {"polycolorall", "Misc", "polycolorall(r, g, b)", "Set all polygon colors."}, + {"dcoladd", "Misc", "dcoladd(r, g, b)", "Add color to display."}, + {"dcolsub", "Misc", "dcolsub(r, g, b)", "Subtract color from display."}, + {"tcoladd", "Misc", "tcoladd(r, g, b)", "Add color transition."}, + {"tcolsub", "Misc", "tcolsub(r, g, b)", "Subtract color transition."}, + {"fcoladd", "Misc", "fcoladd(r, g, b)", "Add fade color."}, + {"fcolsub", "Misc", "fcolsub(r, g, b)", "Subtract fade color."}, + {"colsync", "Misc", "colsync()", "Wait for color transition."}, + {"inittrace", "Misc", "inittrace()", "Initialize trace."}, + {"copyinfo", "Misc", "copyinfo()", "Copy entity info."}, + {"openeyes", "Misc", "openeyes()", "Open character eyes."}, + {"closeeyes", "Misc", "closeeyes()", "Close character eyes."}, + {"blinkeyes", "Misc", "blinkeyes()", "Blink character eyes."}, + {"swap", "Misc", "swap()", "Swap entities."}, + + // -- Footstep Control -- + {"footstepon", "Footsteps", "footstepon()", "Enable footstep sounds."}, + {"footstepoff", "Footsteps", "footstepoff()", "Disable footstep sounds."}, + {"footstepoffall", "Footsteps", "footstepoffall()", "Disable all footstep sounds."}, + {"footstepcut", "Footsteps", "footstepcut()", "Cut footstep sounds."}, + {"footstepcopy", "Footsteps", "footstepcopy()", "Copy footstep settings from another entity."}, + + // -- Animation State -- + {"pushanime", "Animation", "pushanime()", "Save current animation state to stack."}, + {"popanime", "Animation", "popanime()", "Restore saved animation state from stack."}, + + // -- Particles -- + {"particleon", "Visual", "particleon()", "Enable particle effects."}, + {"particleoff", "Visual", "particleoff()", "Disable particle effects."}, + {"particleset", "Visual", "particleset(params)", "Set particle parameters."}, + + // -- Additional Visual -- + {"ending", "Visual", "ending()", "Play ending sequence."}, + {"shadeform", "Visual", "shadeform()", "Set shade form effect."}, + {"shadeset", "Visual", "shadeset()", "Set shade parameters."}, + {"fadenone", "Visual", "fadenone()", "Cancel fade / no fade."}, + {"mapfadeoff", "Visual", "mapfadeoff()", "Disable map transition fade."}, + {"mapfadeon", "Visual", "mapfadeon()", "Enable map transition fade."}, + {"stopvibrate", "Visual", "stopvibrate()", "Stop controller vibration."}, + + // -- Additional Music/Sound -- + {"crossmusic", "Music", "crossmusic()", "Crossfade between music tracks."}, + {"dualmusic", "Music", "dualmusic()", "Play dual music tracks."}, + {"musicvolfade", "Music", "musicvolfade(volume, duration)", "Fade music volume."}, + {"musicstatus", "Music", "musicstatus()", "Get music playback status."}, + {"musicreplay", "Music", "musicreplay()", "Replay current music."}, + {"musicskip", "Music", "musicskip()", "Skip current music."}, + {"musicvolsync", "Music", "musicvolsync()", "Wait for music volume transition."}, + {"choicemusic", "Music", "choicemusic()", "Set choice/menu music."}, + {"initsound", "Music", "initsound()", "Initialize sound system."}, + {"loadsync", "Music", "loadsync()", "Wait for sound load to complete."}, + + // -- Additional Party -- + {"changeparty", "Party", "changeparty()", "Open party change screen."}, + {"refreshparty", "Party", "refreshparty()", "Refresh party display."}, + {"setparty2", "Party", "setparty2(member1, member2, member3)", "Set party variant 2."}, + + // -- Additional Background -- + {"bganimeflag", "Background", "bganimeflag(flag)", "Set background animation flag."}, + {"bgshadestop", "Background", "bgshadestop()", "Stop background shading."}, + {"rbgshadeloop", "Background", "rbgshadeloop()", "Loop background shading."}, + {"bgshadeoff", "Background", "bgshadeoff()", "Turn off background shade."}, + {"bgclear", "Background", "bgclear()", "Clear background."}, + + // -- Additional Entity -- + {"mesforcus", "Entity", "mesforcus()", "Force message cursor to this entity."}, + {"shadetimer", "Entity", "shadetimer()", "Set shade timer."}, + {"setroottrans", "Entity", "setroottrans()", "Set root transform."}, + {"pcopyinfo", "Entity", "pcopyinfo()", "Copy party member entity info."}, + {"axis", "Entity", "axis()", "Set entity axis."}, + {"axissync", "Entity", "axissync()", "Wait for axis operation."}, + + // -- Lines/Triggers -- + {"doorlineoff", "Lines", "doorlineoff()", "Disable door line trigger."}, + {"doorlineon", "Lines", "doorlineon()", "Enable door line trigger."}, + + // -- UI Bars -- + {"setbar", "Menus", "setbar()", "Set progress/gauge bar."}, + {"dispbar", "Menus", "dispbar()", "Display gauge bar."}, + {"killbar", "Menus", "killbar()", "Remove gauge bar."}, + + // -- Player Input -- + {"key", "Player Control", "key(keyMask)", "Key input check."}, + {"keysighnchange", "Player Control", "keysighnchange()", "Key sign change. Dummied out."}, + + // -- Additional Game State -- + {"sethp", "Game State", "sethp(characterID, hp)", "Set character HP."}, + {"gethp", "Game State", "gethp(characterID)", "Get character HP. Result in temp_0."}, + {"rest", "Game State", "rest()", "Rest/inn — restore party."}, + {"addpastgil", "Game State", "addpastgil(amount)", "Add gil in Laguna's time period."}, + {"broken", "Game State", "broken()", "Broken/unused opcode."}, + + // -- Map Transitions -- + {"premapjump2", "Map Transitions", "premapjump2(fieldMapID)", "Prepare map jump variant 2."}, + + // -- Unknown -- + {"unknown2", "Unknown", "unknown2()", "Unknown opcode."}, + {"unknown3", "Unknown", "unknown3()", "Unknown opcode."}, + {"unknown4", "Unknown", "unknown4()", "Used on Ragnarok hatch screen only."}, + {"unknown6", "Unknown", "unknown6()", "Possibly clockwise turn variant."}, + {"unknown7", "Unknown", "unknown7()", "Possibly counter-clockwise turn variant."}, + {"unknown8", "Unknown", "unknown8()", "Possibly clockwise turn variant 2."}, + {"unknown9", "Unknown", "unknown9()", "Possibly counter-clockwise turn variant 2."}, + {"unknown10", "Unknown", "unknown10()", "Possibly music sample time."}, + {"unknown11", "Unknown", "unknown11()", "Some kind of wait command."}, + {"unknown12", "Unknown", "unknown12()", "Ends unknown11."}, + {"unknown13", "Unknown", "unknown13()", "Possibly sound channel control."}, + {"unknown14", "Unknown", "unknown14()", "Preserve sound channel across field loads."}, + {"unknown15", "Unknown", "unknown15()", "Ladder area entry related."}, + {"unknown16", "Unknown", "unknown16()", "Possibly set draw point ID."}, + + {nullptr, nullptr, nullptr, nullptr} +}; + +JsmHelpDialog::JsmHelpDialog(QWidget *parent) + : QDialog(parent) +{ + setWindowTitle(tr("Script Reference")); + resize(800, 600); + + _searchField = new QLineEdit(this); + _searchField->setPlaceholderText(tr("Search opcodes...")); + + _categoryTree = new QTreeWidget(this); + _categoryTree->setHeaderHidden(true); + _categoryTree->setMaximumWidth(220); + _categoryTree->setMinimumWidth(160); + + _detailView = new QTextBrowser(this); + _detailView->setOpenExternalLinks(true); + + QHBoxLayout *mainLayout = new QHBoxLayout(); + QVBoxLayout *leftLayout = new QVBoxLayout(); + leftLayout->addWidget(_searchField); + leftLayout->addWidget(_categoryTree); + mainLayout->addLayout(leftLayout); + mainLayout->addWidget(_detailView, 1); + + QVBoxLayout *outerLayout = new QVBoxLayout(this); + outerLayout->addLayout(mainLayout); + + QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Close, this); + outerLayout->addWidget(buttonBox); + + connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); + connect(_categoryTree, &QTreeWidget::itemSelectionChanged, this, &JsmHelpDialog::onCategorySelected); + connect(_categoryTree, &QTreeWidget::itemDoubleClicked, this, &JsmHelpDialog::onItemDoubleClicked); + connect(_searchField, &QLineEdit::textChanged, this, &JsmHelpDialog::onSearch); + + buildContent(); +} + +void JsmHelpDialog::buildContent() +{ + QMap categories; + + for (int i = 0; opcodeHelpData[i].name != nullptr; ++i) { + const OpcodeHelp &h = opcodeHelpData[i]; + QString cat = QString::fromLatin1(h.category); + + if (!categories.contains(cat)) { + QTreeWidgetItem *catItem = new QTreeWidgetItem(QStringList() << cat); + catItem->setData(0, Qt::UserRole, -1); + QFont f = catItem->font(0); + f.setBold(true); + catItem->setFont(0, f); + _categoryTree->addTopLevelItem(catItem); + categories[cat] = catItem; + } + + QTreeWidgetItem *item = new QTreeWidgetItem(QStringList() << QString::fromLatin1(h.name)); + item->setData(0, Qt::UserRole, i); + categories[cat]->addChild(item); + } + + _categoryTree->expandAll(); + + _detailView->setHtml( + "

FF8 Field Script Reference

" + "

Select an opcode from the list to see its documentation.

" + "

The code language supports:

" + "
    " + "
  • Control flow: if/else/end, while/end, wait while, forever, repeat/until
  • " + "
  • Assignments: var = expr, var += expr, etc.
  • " + "
  • Function calls: opcodeName(arg1, arg2, ...)
  • " + "
  • Expressions: arithmetic (+, -, *, /, %), comparison (==, !=, >, <), " + "bitwise (&, |, ^, ~, >>, <<), logical (and, or, !)
  • " + "
  • Variables: N_ubyte, N_uword, N_ulong, N_sbyte, N_sword, N_slong, temp_N, model_N
  • " + "
  • Constants: text_N, map_N, item_N, magic_N, character names, key names
  • " + "
" + "

Source: " + "FF8 Modding Wiki

" + ); +} + +void JsmHelpDialog::onCategorySelected() +{ + QList items = _categoryTree->selectedItems(); + if (items.isEmpty()) return; + + int idx = items.first()->data(0, Qt::UserRole).toInt(); + if (idx < 0) { + QTreeWidgetItem *catItem = items.first(); + QString html = QString("

%1

").arg(catItem->text(0).toHtmlEscaped()); + for (int c = 0; c < catItem->childCount(); ++c) { + int childIdx = catItem->child(c)->data(0, Qt::UserRole).toInt(); + if (childIdx >= 0) { + const OpcodeHelp &h = opcodeHelpData[childIdx]; + html += QString("

%1

%2

%3


") + .arg(QString::fromLatin1(h.name).toHtmlEscaped(), + QString::fromLatin1(h.signature).toHtmlEscaped(), + QString::fromLatin1(h.description).toHtmlEscaped()); + } + } + _detailView->setHtml(html); + return; + } + + const OpcodeHelp &h = opcodeHelpData[idx]; + _detailView->setHtml( + QString("

%1

" + "

Category: %2

" + "

Signature

%3
" + "

Description

%4

") + .arg(QString::fromLatin1(h.name).toHtmlEscaped(), + QString::fromLatin1(h.category).toHtmlEscaped(), + QString::fromLatin1(h.signature).toHtmlEscaped(), + QString::fromLatin1(h.description).toHtmlEscaped()) + ); +} + +void JsmHelpDialog::onSearch(const QString &text) +{ + QString lower = text.toLower(); + for (int i = 0; i < _categoryTree->topLevelItemCount(); ++i) { + QTreeWidgetItem *cat = _categoryTree->topLevelItem(i); + bool anyVisible = false; + for (int c = 0; c < cat->childCount(); ++c) { + QTreeWidgetItem *child = cat->child(c); + bool match = lower.isEmpty() || child->text(0).toLower().contains(lower); + child->setHidden(!match); + if (match) anyVisible = true; + } + cat->setHidden(!anyVisible && !lower.isEmpty()); + if (anyVisible) cat->setExpanded(true); + } +} + +void JsmHelpDialog::onItemDoubleClicked(QTreeWidgetItem *item, int column) +{ + Q_UNUSED(column) + int idx = item->data(0, Qt::UserRole).toInt(); + if (idx < 0) return; // Category header, ignore + + QString sig = QString::fromLatin1(opcodeHelpData[idx].signature); + emit insertSignature(sig); +} diff --git a/src/JsmHelpDialog.h b/src/JsmHelpDialog.h new file mode 100644 index 0000000..60cc68e --- /dev/null +++ b/src/JsmHelpDialog.h @@ -0,0 +1,38 @@ +/**************************************************************************** + ** Deling Final Fantasy VIII Field Editor + ** Copyright (C) 2009-2024 Arzel Jérôme + ** + ** This program is free software: you can redistribute it and/or modify + ** it under the terms of the GNU General Public License as published by + ** the Free Software Foundation, either version 3 of the License, or + ** (at your option) any later version. + ** + ** This program is distributed in the hope that it will be useful, + ** but WITHOUT ANY WARRANTY; without even the implied warranty of + ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + ** GNU General Public License for more details. + ** + ** You should have received a copy of the GNU General Public License + ** along with this program. If not, see . + ****************************************************************************/ +#pragma once + +#include + +class JsmHelpDialog : public QDialog +{ + Q_OBJECT +public: + explicit JsmHelpDialog(QWidget *parent = nullptr); +signals: + void insertSignature(const QString &signature); +private: + void buildContent(); + QTreeWidget *_categoryTree; + QTextBrowser *_detailView; + QLineEdit *_searchField; +private slots: + void onCategorySelected(); + void onSearch(const QString &text); + void onItemDoubleClicked(QTreeWidgetItem *item, int column); +}; diff --git a/src/JsmHighlighter.cpp b/src/JsmHighlighter.cpp index 5975134..4e13a1c 100644 --- a/src/JsmHighlighter.cpp +++ b/src/JsmHighlighter.cpp @@ -17,6 +17,14 @@ ****************************************************************************/ #include "JsmHighlighter.h" #include "files/JsmFile.h" +#include + +bool JsmHighlighter::isDarkMode() const +{ + QColor bg = QApplication::palette().color(QPalette::Base); + // Luminance check: dark if background brightness < 128 + return (bg.red() * 299 + bg.green() * 587 + bg.blue() * 114) / 1000 < 128; +} JsmHighlighter::JsmHighlighter(QTextDocument *parent) : QSyntaxHighlighter(parent) @@ -66,22 +74,27 @@ void JsmHighlighter::highlightBlock(const QString &text) void JsmHighlighter::highlightBlockPseudoCode(const QString &text) { + bool dark = isDarkMode(); + // Keywords - applyReg(text, _regKeywords, QColor(0x80, 0x80, 0x00)); // Yellow + applyReg(text, _regKeywords, dark ? QColor(0xD4, 0xD4, 0x6A) : QColor(0x80, 0x80, 0x00)); + + // Numeric literals and constants + applyReg(text, _regNumeric, dark ? QColor(0x6C, 0xBE, 0xF0) : QColor(0x00, 0x00, 0x80)); + applyReg(text, _regConst, dark ? QColor(0x6C, 0xBE, 0xF0) : QColor(0x00, 0x00, 0x80)); - // Types - applyReg(text, _regNumeric, QColor(0x00, 0x00, 0x80)); // blue - applyReg(text, _regConst, QColor(0x00, 0x00, 0x80)); // blue - applyReg(text, _regVar, QColor(0x80, 0x00, 0x00)); // red + // Variables + applyReg(text, _regVar, dark ? QColor(0xF0, 0x90, 0x70) : QColor(0x80, 0x00, 0x00)); - // Methods - applyReg(text, _regExec, QColor(0x80, 0x00, 0x80)); // purple + // Method calls / exec + applyReg(text, _regExec, dark ? QColor(0xC5, 0x80, 0xDA) : QColor(0x80, 0x00, 0x80)); } void JsmHighlighter::highlightBlockOpcodes(const QString &text) { QStringList rows = text.split(QRegularExpression("[\\t ]+"), Qt::SkipEmptyParts); bool ok; + bool dark = isDarkMode(); if (rows.isEmpty()) { return; @@ -92,26 +105,30 @@ void JsmHighlighter::highlightBlockOpcodes(const QString &text) if (opcode != -1) { if (opcode == JsmOpcode::CAL) { - setFormat(text.indexOf(name), name.size(), QColor(0x00,0x66,0xcc)); + setFormat(text.indexOf(name), name.size(), dark ? QColor(0x56,0x9C,0xD6) : QColor(0x00,0x66,0xcc)); } else if (opcode >= JsmOpcode::JMP && opcode <= JsmOpcode::GJMP) { - setFormat(text.indexOf(name), name.size(), QColor(0x66,0xcc,0x00)); + setFormat(text.indexOf(name), name.size(), dark ? QColor(0xB5,0xCE,0xA8) : QColor(0x66,0xcc,0x00)); } else if (opcode == JsmOpcode::LBL) { - setFormat(text.indexOf(name), name.size(), QColor(0xcc,0x00,0x00)); + setFormat(text.indexOf(name), name.size(), dark ? QColor(0xF4,0x70,0x67) : QColor(0xcc,0x00,0x00)); } else if (opcode >= JsmOpcode::RET && opcode <= JsmOpcode::PSHAC) { - setFormat(text.indexOf(name), name.size(), QColor(0x66,0x66,0x66)); + setFormat(text.indexOf(name), name.size(), dark ? QColor(0xA0,0xA0,0xA0) : QColor(0x66,0x66,0x66)); } else if (opcode >= JsmOpcode::REQ && opcode <= JsmOpcode::PREQEW) { - setFormat(text.indexOf(name), name.size(), QColor(0xcc,0x66,0x00)); + setFormat(text.indexOf(name), name.size(), dark ? QColor(0xE0,0xA0,0x50) : QColor(0xcc,0x66,0x00)); } else if (opcode == JsmOpcode::MES || opcode == JsmOpcode::ASK || opcode == JsmOpcode::AMESW || opcode == JsmOpcode::AMES || opcode == JsmOpcode::AASK || opcode == JsmOpcode::RAMESW) { QTextCharFormat textFormat; - textFormat.setBackground(QColor(0xFF,0xFF,0x00)); + if (dark) { + textFormat.setForeground(QColor(0xFF, 0xE0, 0x80)); + } else { + textFormat.setBackground(QColor(0xFF,0xFF,0x00)); + } setFormat(text.indexOf(name), name.size(), textFormat); } } else if (name.startsWith("LABEL", Qt::CaseInsensitive)) { name.mid(5).toInt(&ok); if (ok) { - setFormat(text.indexOf(name), name.size(), QColor(0x66,0xcc,0x00)); + setFormat(text.indexOf(name), name.size(), dark ? QColor(0xB5,0xCE,0xA8) : QColor(0x66,0xcc,0x00)); } return; } @@ -121,37 +138,33 @@ void JsmHighlighter::highlightBlockOpcodes(const QString &text) } const QString ¶m = rows.at(1); + QColor paramColor = dark ? QColor(0xC5,0x80,0xDA) : QColor(0x66,0x00,0xcc); if (opcode == JsmOpcode::CAL && JsmFile::opcodeNameCalc.contains(param.toUpper())) { - setFormat(text.indexOf(param), param.size(), QColor(0x00,0x66,0xcc)); + setFormat(text.indexOf(param), param.size(), dark ? QColor(0x56,0x9C,0xD6) : QColor(0x00,0x66,0xcc)); } else if (opcode >= JsmOpcode::JMP && opcode <= JsmOpcode::GJMP && param.startsWith("LABEL", Qt::CaseInsensitive)) { param.mid(5).toInt(&ok); if (ok) { - setFormat(text.indexOf(param), param.size(), QColor(0x66,0xcc,0x00)); + setFormat(text.indexOf(param), param.size(), dark ? QColor(0xB5,0xCE,0xA8) : QColor(0x66,0xcc,0x00)); } } else if (opcode >= JsmOpcode::PSHI_L && opcode <= JsmOpcode::POPI_L && param.startsWith("TEMP", Qt::CaseInsensitive)) { param.mid(4).toInt(&ok); if (ok) { - setFormat(text.indexOf(param), param.size(), QColor(0x66,0x00,0xcc)); + setFormat(text.indexOf(param), param.size(), paramColor); } } else if (opcode >= JsmOpcode::PSHM_B && opcode <= JsmOpcode::PSHSM_L && param.startsWith("VAR", Qt::CaseInsensitive)) { param.mid(3).toInt(&ok); if (ok) { - setFormat(text.indexOf(param), param.size(), QColor(0x66,0x00,0xcc)); + setFormat(text.indexOf(param), param.size(), paramColor); } } else if (opcode == JsmOpcode::PSHAC && param.startsWith("MODEL", Qt::CaseInsensitive)) { param.mid(5).toInt(&ok); if (ok) { - setFormat(text.indexOf(param), param.size(), QColor(0x66,0x00,0xcc)); + setFormat(text.indexOf(param), param.size(), paramColor); } - } else { -// param.toInt(&ok); -// if (ok) { -// setFormat(text.indexOf(param), param.size(), QColor(0x00,0x66,0xcc)); -// } } } diff --git a/src/JsmHighlighter.h b/src/JsmHighlighter.h index ba90e26..e572e69 100644 --- a/src/JsmHighlighter.h +++ b/src/JsmHighlighter.h @@ -37,6 +37,7 @@ class JsmHighlighter : public QSyntaxHighlighter private: void highlightBlockPseudoCode(const QString &text); void highlightBlockOpcodes(const QString &text); + bool isDarkMode() const; void applyReg(const QString &text, const QRegularExpression ®Exp, const QTextCharFormat &format); void applyReg(const QString &text, const QRegularExpression ®Exp, diff --git a/src/JsmPseudoCompiler.cpp b/src/JsmPseudoCompiler.cpp new file mode 100644 index 0000000..fbdc435 --- /dev/null +++ b/src/JsmPseudoCompiler.cpp @@ -0,0 +1,1126 @@ +/**************************************************************************** + ** Deling Final Fantasy VIII Field Editor + ** Copyright (C) 2009-2024 Arzel Jérôme + ** + ** This program is free software: you can redistribute it and/or modify + ** it under the terms of the GNU General Public License as published by + ** the Free Software Foundation, either version 3 of the License, or + ** (at your option) any later version. + ** + ** This program is distributed in the hope that it will be useful, + ** but WITHOUT ANY WARRANTY; without even the implied warranty of + ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + ** GNU General Public License for more details. + ** + ** You should have received a copy of the GNU General Public License + ** along with this program. If not, see . + ****************************************************************************/ +#include "JsmPseudoCompiler.h" +#include "files/JsmFile.h" + +QMap JsmPseudoCompiler::_opcodeMap; + +JsmPseudoCompiler::JsmPseudoCompiler() +{ + initOpcodeMap(); +} + +void JsmPseudoCompiler::initOpcodeMap() +{ + if (!_opcodeMap.isEmpty()) return; + for (int i = 0; i < JSM_OPCODE_COUNT; ++i) { + _opcodeMap.insert(QString(JsmOpcode::opcodes[i]).toLower(), i); + } +} + +int JsmPseudoCompiler::resolveOpcode(const QString &name) const +{ + return _opcodeMap.value(name.toLower(), -1); +} + +// ============================================================ +// Tokenizer +// ============================================================ + +QList JsmPseudoCompiler::tokenize( + const QString &input, QString &errorStr, int &errorLine) +{ + QList tokens; + int i = 0, line = 1, len = input.length(); + + while (i < len) { + QChar c = input[i]; + + // Skip spaces/tabs + if (c == ' ' || c == '\t') { ++i; continue; } + + // Comments: // to end of line + if (c == '/' && i + 1 < len && input[i + 1] == '/') { + while (i < len && input[i] != '\n') ++i; + continue; + } + + // Newlines + if (c == '\n') { + tokens.append({Token::Newline, "\n", line}); + ++line; ++i; continue; + } + if (c == '\r') { + tokens.append({Token::Newline, "\n", line}); + ++i; + if (i < len && input[i] == '\n') ++i; + ++line; continue; + } + + // Numbers: 0x hex, b binary, or decimal (possibly negative handled as unary) + // But check if number is followed by _ (making it a variable like 474_ubyte) + if (c.isDigit() || (c == '-' && i + 1 < len && input[i + 1].isDigit())) { + int start = i; + if (c == '-') ++i; + if (i + 1 < len && input[i] == '0' && (input[i + 1] == 'x' || input[i + 1] == 'X')) { + i += 2; + while (i < len && (input[i].isDigit() || (input[i] >= 'a' && input[i] <= 'f') + || (input[i] >= 'A' && input[i] <= 'F'))) ++i; + tokens.append({Token::HexNumber, input.mid(start, i - start), line}); + } else { + while (i < len && input[i].isDigit()) ++i; + // Check if followed by _ making it a variable name (e.g. 474_ubyte, 1024_uword) + if (i < len && input[i] == '_') { + while (i < len && (input[i].isLetterOrNumber() || input[i] == '_')) ++i; + tokens.append({Token::Identifier, input.mid(start, i - start), line}); + } else { + tokens.append({Token::Number, input.mid(start, i - start), line}); + } + } + continue; + } + + // Binary: b01010 + if (c == 'b' && i + 1 < len && (input[i + 1] == '0' || input[i + 1] == '1')) { + int start = i; ++i; + while (i < len && (input[i] == '0' || input[i] == '1')) ++i; + // Check it's not an identifier continuation + if (i < len && (input[i].isLetterOrNumber() || input[i] == '_')) { + // It's an identifier starting with 'b' + while (i < len && (input[i].isLetterOrNumber() || input[i] == '_')) ++i; + tokens.append({Token::Identifier, input.mid(start, i - start), line}); + } else { + tokens.append({Token::BinNumber, input.mid(start, i - start), line}); + } + continue; + } + + // Identifiers and keywords + if (c.isLetter() || c == '_') { + int start = i; + while (i < len && (input[i].isLetterOrNumber() || input[i] == '_' || input[i] == '#')) ++i; + QString word = input.mid(start, i - start); + // Check for "and" / "or" keywords + if (word.toLower() == "and") { + tokens.append({Token::OpLogAnd, word, line}); + } else if (word.toLower() == "or") { + tokens.append({Token::OpLogOr, word, line}); + } else { + tokens.append({Token::Identifier, word, line}); + } + continue; + } + + // Two-char operators + if (i + 1 < len) { + QString two = input.mid(i, 2); + if (two == "==") { tokens.append({Token::OpEq, two, line}); i += 2; continue; } + if (two == "!=") { tokens.append({Token::OpNt, two, line}); i += 2; continue; } + if (two == ">=") { tokens.append({Token::OpGe, two, line}); i += 2; continue; } + if (two == "<=") { tokens.append({Token::OpLe, two, line}); i += 2; continue; } + if (two == ">>") { tokens.append({Token::OpRsh, two, line}); i += 2; continue; } + if (two == "<<") { tokens.append({Token::OpLsh, two, line}); i += 2; continue; } + if (two == "+=") { tokens.append({Token::OpAddAssign, two, line}); i += 2; continue; } + if (two == "-=") { tokens.append({Token::OpSubAssign, two, line}); i += 2; continue; } + if (two == "*=") { tokens.append({Token::OpMulAssign, two, line}); i += 2; continue; } + if (two == "/=") { tokens.append({Token::OpDivAssign, two, line}); i += 2; continue; } + if (two == "%=") { tokens.append({Token::OpModAssign, two, line}); i += 2; continue; } + if (two == "&=") { tokens.append({Token::OpAndAssign, two, line}); i += 2; continue; } + if (two == "|=") { tokens.append({Token::OpOrAssign, two, line}); i += 2; continue; } + if (two == "^=") { tokens.append({Token::OpEorAssign, two, line}); i += 2; continue; } + } + // Three-char compound assigns + if (i + 2 < len) { + QString three = input.mid(i, 3); + if (three == ">>=") { tokens.append({Token::OpRshAssign, three, line}); i += 3; continue; } + if (three == "<<=") { tokens.append({Token::OpLshAssign, three, line}); i += 3; continue; } + } + + // Single-char operators + if (c == '(') { tokens.append({Token::LParen, "(", line}); ++i; continue; } + if (c == ')') { tokens.append({Token::RParen, ")", line}); ++i; continue; } + if (c == ',') { tokens.append({Token::Comma, ",", line}); ++i; continue; } + if (c == '.') { tokens.append({Token::Dot, ".", line}); ++i; continue; } + if (c == '=') { tokens.append({Token::OpAssign, "=", line}); ++i; continue; } + if (c == '>') { tokens.append({Token::OpGt, ">", line}); ++i; continue; } + if (c == '<') { tokens.append({Token::OpLs, "<", line}); ++i; continue; } + if (c == '+') { tokens.append({Token::OpAdd, "+", line}); ++i; continue; } + if (c == '-') { tokens.append({Token::OpSub, "-", line}); ++i; continue; } + if (c == '*') { tokens.append({Token::OpMul, "*", line}); ++i; continue; } + if (c == '/') { tokens.append({Token::OpDiv, "/", line}); ++i; continue; } + if (c == '%') { tokens.append({Token::OpMod, "%", line}); ++i; continue; } + if (c == '&') { tokens.append({Token::OpAnd, "&", line}); ++i; continue; } + if (c == '|') { tokens.append({Token::OpOr, "|", line}); ++i; continue; } + if (c == '^') { tokens.append({Token::OpEor, "^", line}); ++i; continue; } + if (c == '~') { tokens.append({Token::OpNot, "~", line}); ++i; continue; } + if (c == '!') { tokens.append({Token::OpLogNot, "!", line}); ++i; continue; } + + errorStr = QObject::tr("Unexpected character: '%1'").arg(c); + errorLine = line; + return QList(); + } + + tokens.append({Token::EndOfInput, "", line}); + return tokens; +} + +// ============================================================ +// Parser helpers +// ============================================================ + +JsmPseudoCompiler::Token JsmPseudoCompiler::peek() const +{ + if (_pos < _tokens.size()) return _tokens[_pos]; + return {Token::EndOfInput, "", _currentLine}; +} + +JsmPseudoCompiler::Token JsmPseudoCompiler::advance() +{ + Token t = peek(); + if (_pos < _tokens.size()) { + _currentLine = t.line; + ++_pos; + } + return t; +} + +bool JsmPseudoCompiler::match(Token::Type type) +{ + if (peek().type == type) { advance(); return true; } + return false; +} + +bool JsmPseudoCompiler::expect(Token::Type type, QString &errorStr) +{ + if (match(type)) return true; + errorStr = QObject::tr("Expected '%1', got '%2'") + .arg(type == Token::Newline ? "newline" : + type == Token::RParen ? ")" : + type == Token::LParen ? "(" : + type == Token::Comma ? "," : "token", + peek().text.isEmpty() ? "end of input" : peek().text); + return false; +} + +void JsmPseudoCompiler::skipNewlines() +{ + while (peek().type == Token::Newline) advance(); +} + +// ============================================================ +// Main compile entry +// ============================================================ + +bool JsmPseudoCompiler::compile(const QString &pseudoCode, JsmData &result, + QString &errorStr, int &errorLine) +{ + errorStr.clear(); + errorLine = 0; + + _tokens = tokenize(pseudoCode, errorStr, errorLine); + if (!errorStr.isEmpty()) return false; + + _pos = 0; + _currentLine = 1; + + return parseStatements(result, errorStr, errorLine, {}); +} + +// ============================================================ +// Statement-level parsing +// ============================================================ + +bool JsmPseudoCompiler::parseStatements(JsmData &result, QString &errorStr, int &errorLine, + const QStringList &allowedTerminators) +{ + skipNewlines(); + while (peek().type != Token::EndOfInput) { + QString kw = peek().text.toLower(); + if (allowedTerminators.contains(kw)) break; + // Check for block-ending keywords that aren't valid in this context + if (kw == "end" || kw == "else" || kw == "until") { + errorStr = QObject::tr("Unexpected '%1' in this block").arg(kw); + errorLine = _currentLine; + return false; + } + + if (!parseStatement(result, errorStr)) { + errorLine = _currentLine; + return false; + } + skipNewlines(); + } + return true; +} + +bool JsmPseudoCompiler::parseStatement(JsmData &result, QString &errorStr) +{ + skipNewlines(); + Token t = peek(); + if (t.type == Token::EndOfInput) return true; + + QString kw = t.text.toLower(); + + // Control flow keywords + if (kw == "if") return parseIf(result, errorStr); + if (kw == "while") return parseWhile(result, errorStr); + if (kw == "wait") { + // 'wait' is both a keyword (wait while, wait forever) and an opcode (wait(frames)) + advance(); // consume 'wait' + if (peek().type == Token::LParen) { + return parseFunctionCall("wait", result, errorStr, true); + } + // Keyword form — put back and delegate to parseWait + --_pos; + return parseWait(result, errorStr); + } + if (kw == "forever") return parseForever(result, errorStr); + if (kw == "repeat") return parseRepeat(result, errorStr); + if (kw == "ret") { + advance(); + // Check if ret has arguments: ret(N) + if (peek().type == Token::LParen) { + return parseFunctionCall("ret", result, errorStr, true); + } + result.append(JsmOpcode(JsmOpcode::RET, 0)); + return true; + } + if (kw == "goto") { + advance(); + // We don't support goto in pseudo-code compilation + errorStr = QObject::tr("'goto' is not supported in pseudo-code mode. Use if/while/repeat instead."); + return false; + } + if (kw == "label") { + advance(); + // Labels are not supported in pseudo-code compilation + errorStr = QObject::tr("'label' is not supported in pseudo-code mode."); + return false; + } + + // Must be an identifier: could be assignment, function call, or method call + if (t.type != Token::Identifier) { + errorStr = QObject::tr("Expected statement, got '%1'").arg(t.text); + return false; + } + + Token ident = advance(); + Token next = peek(); + + // Method call: entity.method(args) + if (next.type == Token::Dot) { + advance(); // consume dot + if (peek().type != Token::Identifier) { + errorStr = QObject::tr("Expected method name after '.'"); + return false; + } + Token methodToken = advance(); + return parseMethodCall(ident.text, methodToken.text, result, errorStr); + } + + // Assignment: var = expr, var += expr, etc. + if (next.type == Token::OpAssign || next.type == Token::OpAddAssign || + next.type == Token::OpSubAssign || next.type == Token::OpMulAssign || + next.type == Token::OpDivAssign || next.type == Token::OpModAssign || + next.type == Token::OpAndAssign || next.type == Token::OpOrAssign || + next.type == Token::OpEorAssign || next.type == Token::OpRshAssign || + next.type == Token::OpLshAssign) { + return parseAssignment(ident.text, result, errorStr); + } + + // Function call: name(args) + if (next.type == Token::LParen) { + return parseFunctionCall(ident.text, result, errorStr, true); + } + + // Bare opcode name with no parens (for zero-arg opcodes) + int opcode = resolveOpcode(ident.text); + if (opcode >= 0) { + result.append(JsmOpcode(opcode)); + return true; + } + + errorStr = QObject::tr("Unknown statement: '%1'").arg(ident.text); + return false; +} + +// ============================================================ +// Variable push/pop helpers +// ============================================================ + +bool JsmPseudoCompiler::emitPushVar(const QString &varName, JsmData &result, QString &errorStr) +{ + QString lower = varName.toLower(); + + // temp_N + if (lower.startsWith("temp_")) { + bool ok; + int idx = lower.mid(5).toInt(&ok); + if (!ok) { errorStr = QObject::tr("Invalid temp index: '%1'").arg(varName); return false; } + result.append(JsmOpcode(JsmOpcode::PSHI_L, idx)); + return true; + } + // model_N + if (lower.startsWith("model_")) { + bool ok; + int idx = lower.mid(6).toInt(&ok); + if (!ok) { errorStr = QObject::tr("Invalid model index: '%1'").arg(varName); return false; } + result.append(JsmOpcode(JsmOpcode::PSHAC, idx)); + return true; + } + // N_ubyte, N_uword, N_ulong, N_sbyte, N_sword, N_slong + // or VARNAME_ubyte etc. + QRegularExpression varRe("^(.+)_(ubyte|uword|ulong|sbyte|sword|slong)$"); + QRegularExpressionMatch m = varRe.match(lower); + if (m.hasMatch()) { + QString varPart = m.captured(1); + QString typePart = m.captured(2); + // Extract the numeric var ID (could be "123" or "123_somename") + bool ok; + int varId = varPart.section('_', 0, 0).toInt(&ok); + if (!ok) varId = varPart.toInt(&ok); + if (!ok) { errorStr = QObject::tr("Cannot resolve variable: '%1'").arg(varName); return false; } + + int opKey; + if (typePart == "ubyte") opKey = JsmOpcode::PSHM_B; + else if (typePart == "uword") opKey = JsmOpcode::PSHM_W; + else if (typePart == "ulong") opKey = JsmOpcode::PSHM_L; + else if (typePart == "sbyte") opKey = JsmOpcode::PSHSM_B; + else if (typePart == "sword") opKey = JsmOpcode::PSHSM_W; + else opKey = JsmOpcode::PSHSM_L; + + result.append(JsmOpcode(opKey, varId)); + return true; + } + + errorStr = QObject::tr("Cannot resolve variable for push: '%1'. " + "Use temp_N, model_N, or N_ubyte/uword/ulong/sbyte/sword/slong format.") + .arg(varName); + return false; +} + +bool JsmPseudoCompiler::emitPopVar(const QString &varName, JsmData &result, QString &errorStr) +{ + QString lower = varName.toLower(); + + if (lower.startsWith("temp_")) { + bool ok; + int idx = lower.mid(5).toInt(&ok); + if (!ok) { errorStr = QObject::tr("Invalid temp index: '%1'").arg(varName); return false; } + result.append(JsmOpcode(JsmOpcode::POPI_L, idx)); + return true; + } + + QRegularExpression varRe("^(.+)_(ubyte|uword|ulong|sbyte|sword|slong)$"); + QRegularExpressionMatch m = varRe.match(lower); + if (m.hasMatch()) { + QString varPart = m.captured(1); + QString typePart = m.captured(2); + bool ok; + int varId = varPart.section('_', 0, 0).toInt(&ok); + if (!ok) varId = varPart.toInt(&ok); + if (!ok) { errorStr = QObject::tr("Cannot resolve variable: '%1'").arg(varName); return false; } + + int opKey; + if (typePart == "ubyte") opKey = JsmOpcode::POPM_B; + else if (typePart == "uword") opKey = JsmOpcode::POPM_W; + else if (typePart == "ulong") opKey = JsmOpcode::POPM_L; + // Signed types still pop to unsigned storage + else if (typePart == "sbyte") opKey = JsmOpcode::POPM_B; + else if (typePart == "sword") opKey = JsmOpcode::POPM_W; + else opKey = JsmOpcode::POPM_L; + + result.append(JsmOpcode(opKey, varId)); + return true; + } + + errorStr = QObject::tr("Cannot resolve variable for assignment: '%1'. " + "Use temp_N or N_ubyte/uword/ulong/sbyte/sword/slong format.") + .arg(varName); + return false; +} + +// ============================================================ +// Assignment: var = expr, var += expr, etc. +// ============================================================ + +bool JsmPseudoCompiler::parseAssignment(const QString &varName, JsmData &result, QString &errorStr) +{ + Token op = advance(); // consume the assignment operator + + if (op.type == Token::OpAssign) { + // var = expr => push expr, pop var + if (!parseExpression(result, errorStr)) return false; + return emitPopVar(varName, result, errorStr); + } + + // Compound assignment: var += expr => push var, push expr, CAL op, pop var + int calOp = -1; + switch (op.type) { + case Token::OpAddAssign: calOp = 0; break; // ADD + case Token::OpSubAssign: calOp = 1; break; // SUB + case Token::OpMulAssign: calOp = 2; break; // MUL + case Token::OpDivAssign: calOp = 3; break; // DIV + case Token::OpModAssign: calOp = 4; break; // MOD + case Token::OpAndAssign: calOp = 12; break; // AND + case Token::OpOrAssign: calOp = 13; break; // OR + case Token::OpEorAssign: calOp = 14; break; // EOR + case Token::OpRshAssign: calOp = 16; break; // RSH + case Token::OpLshAssign: calOp = 17; break; // LSH + default: + errorStr = QObject::tr("Unknown assignment operator"); + return false; + } + + if (!emitPushVar(varName, result, errorStr)) return false; + if (!parseExpression(result, errorStr)) return false; + result.append(JsmOpcode(JsmOpcode::CAL, calOp)); + return emitPopVar(varName, result, errorStr); +} + +// ============================================================ +// Function call: name(arg1, arg2, ...) +// ============================================================ + +bool JsmPseudoCompiler::parseFunctionCall(const QString &name, JsmData &result, + QString &errorStr, bool isStatement) +{ + Q_UNUSED(isStatement) + + if (!expect(Token::LParen, errorStr)) return false; + + // Collect arguments into a temporary buffer + QList argDatas; + if (peek().type != Token::RParen) { + do { + JsmData argData; + if (!parseExpression(argData, errorStr)) return false; + argDatas.append(argData); + } while (match(Token::Comma)); + } + if (!expect(Token::RParen, errorStr)) return false; + + int opcode = resolveOpcode(name); + if (opcode < 0) { + errorStr = QObject::tr("Unknown function: '%1'").arg(name); + return false; + } + + // Opcodes 0x00 through 0x38 (NOP through DISCJUMP) use an inline parameter + // encoded in the opcode word. The first argument becomes the inline param, + // remaining arguments are pushed onto the stack in reverse order. + // Opcodes > 0x38 have no inline param — all args are stack args. + bool hasInlineParam = (opcode <= 0x38); + + if (hasInlineParam) { + if (argDatas.isEmpty()) { + // No args — emit opcode with no param + result.append(JsmOpcode(opcode)); + } else { + // Push stack args (all except first) in forward order + // The decompiler inverts the stack before display, so first pushed = first displayed + for (int i = 1; i < argDatas.size(); ++i) { + result += argDatas[i]; + } + // First arg is the inline param — try to extract a literal + const JsmData &firstArg = argDatas[0]; + if (firstArg.nbOpcode() == 1) { + JsmOpcode firstOp = firstArg.opcode(0); + if (firstOp.key() == JsmOpcode::PSHN_L) { + result.append(JsmOpcode(opcode, firstOp.param())); + } else { + // Not a literal — push it and emit opcode without inline param + result += firstArg; + result.append(JsmOpcode(opcode)); + } + } else { + result += firstArg; + result.append(JsmOpcode(opcode)); + } + } + } else { + // No inline param — all args are stack args, push in forward order + for (int i = 0; i < argDatas.size(); ++i) { + result += argDatas[i]; + } + result.append(JsmOpcode(opcode)); + } + + return true; +} + +// ============================================================ +// Method call: entity.method(priority, ...) => REQ/REQSW/REQEW +// ============================================================ + +bool JsmPseudoCompiler::parseMethodCall(const QString &objectName, const QString &methodName, + JsmData &result, QString &errorStr) +{ + if (!expect(Token::LParen, errorStr)) return false; + + // Parse arguments, but watch for SW/EW exec type identifiers + QList argDatas; + int reqOpcode = JsmOpcode::REQ; + + if (peek().type != Token::RParen) { + do { + // Check if this argument is an exec type modifier (SW or EW) + if (peek().type == Token::Identifier) { + QString upper = peek().text.toUpper(); + if (upper == "SW" || upper == "EW") { + // Check if followed by ) or , — confirms it's a modifier, not a variable + Token execToken = advance(); + if (peek().type == Token::RParen || peek().type == Token::Comma) { + reqOpcode = (upper == "SW") ? JsmOpcode::REQSW : JsmOpcode::REQEW; + continue; + } + // Not followed by ) or , — put it back and parse as expression + --_pos; + } + } + JsmData argData; + if (!parseExpression(argData, errorStr)) return false; + argDatas.append(argData); + } while (match(Token::Comma)); + } + if (!expect(Token::RParen, errorStr)) return false; + + // Resolve entity group ID — must be numeric or name#N format + bool ok; + int groupId = objectName.toInt(&ok); + if (!ok) { + int hashIdx = objectName.indexOf('#'); + if (hashIdx >= 0) { + groupId = objectName.mid(hashIdx + 1).toInt(&ok); + } + if (!ok) { + errorStr = QObject::tr("Cannot resolve entity '%1' to a group ID. " + "Use numeric IDs for entity.method() calls.") + .arg(objectName); + return false; + } + } + + // Emit: push method label, push priority, REQ/REQSW/REQEW(groupID) + if (argDatas.isEmpty()) { + result.append(JsmOpcode(JsmOpcode::PSHN_L, 0)); + result.append(JsmOpcode(JsmOpcode::PSHN_L, 0)); + } else if (argDatas.size() == 1) { + result.append(JsmOpcode(JsmOpcode::PSHN_L, 0)); + result += argDatas[0]; + } else { + result += argDatas[0]; + result += argDatas[1]; + } + + result.append(JsmOpcode(reqOpcode, groupId)); + return true; +} + +// ============================================================ +// Control flow: if/else/end, while/end, wait while, forever, repeat/until +// ============================================================ + +bool JsmPseudoCompiler::parseIf(JsmData &result, QString &errorStr) +{ + advance(); // consume 'if' + + // Parse condition + JsmData condData; + if (!parseExpression(condData, errorStr)) return false; + + // Expect 'begin' + skipNewlines(); + if (peek().text.toLower() != "begin") { + errorStr = QObject::tr("Expected 'begin' after if condition"); + return false; + } + advance(); + + // Parse if-block + JsmData ifBlock; + int dummy; + if (!parseStatements(ifBlock, errorStr, dummy, {"end", "else"})) return false; + + skipNewlines(); + QString kw = peek().text.toLower(); + + if (kw == "else") { + advance(); + skipNewlines(); + + // Check for "else if" + if (peek().text.toLower() == "if") { + JsmData elseBlock; + if (!parseIf(elseBlock, errorStr)) return false; + + // Emit: condition, JPF over if-block+JMP, if-block, JMP over else-block, else-block + result += condData; + int ifSize = ifBlock.nbOpcode() + 1; // +1 for JMP + result.append(JsmOpcode(JsmOpcode::JPF, ifSize + 1)); + result += ifBlock; + result.append(JsmOpcode(JsmOpcode::JMP, elseBlock.nbOpcode() + 1)); + result += elseBlock; + } else { + // Parse else-block + JsmData elseBlock; + if (!parseStatements(elseBlock, errorStr, dummy, {"end"})) return false; + + skipNewlines(); + if (peek().text.toLower() != "end") { + errorStr = QObject::tr("Expected 'end' to close if/else block"); + return false; + } + advance(); + + result += condData; + int ifSize = ifBlock.nbOpcode() + 1; + result.append(JsmOpcode(JsmOpcode::JPF, ifSize + 1)); + result += ifBlock; + result.append(JsmOpcode(JsmOpcode::JMP, elseBlock.nbOpcode() + 1)); + result += elseBlock; + } + } else if (kw == "end") { + advance(); + // Simple if without else + result += condData; + result.append(JsmOpcode(JsmOpcode::JPF, ifBlock.nbOpcode() + 1)); + result += ifBlock; + } else { + errorStr = QObject::tr("Expected 'else' or 'end' after if block, got '%1'").arg(peek().text); + return false; + } + + return true; +} + +bool JsmPseudoCompiler::parseWhile(JsmData &result, QString &errorStr) +{ + advance(); // consume 'while' + + JsmData condData; + if (!parseExpression(condData, errorStr)) return false; + + skipNewlines(); + if (peek().text.toLower() != "begin") { + // "while " without begin = wait while (single-line) + // Emit: condition, JPF +2, JMP back to condition + int condSize = condData.nbOpcode(); + result += condData; + result.append(JsmOpcode(JsmOpcode::JPF, 2)); + result.append(JsmOpcode(JsmOpcode::JMP, -(condSize + 1))); + return true; + } + advance(); // consume 'begin' + + JsmData bodyBlock; + int dummy; + if (!parseStatements(bodyBlock, errorStr, dummy, {"end"})) return false; + + skipNewlines(); + if (peek().text.toLower() != "end") { + errorStr = QObject::tr("Expected 'end' to close while block"); + return false; + } + advance(); + + // Emit: condition, JPF over body+JMP, body, JMP back to condition + int condSize = condData.nbOpcode(); + int bodySize = bodyBlock.nbOpcode(); + result += condData; + result.append(JsmOpcode(JsmOpcode::JPF, bodySize + 2)); // +1 for JMP back + result += bodyBlock; + result.append(JsmOpcode(JsmOpcode::JMP, -(condSize + bodySize + 1))); + + return true; +} + +bool JsmPseudoCompiler::parseWait(JsmData &result, QString &errorStr) +{ + advance(); // consume 'wait' + + skipNewlines(); + QString kw = peek().text.toLower(); + + if (kw == "while") { + advance(); // consume 'while' + JsmData condData; + if (!parseExpression(condData, errorStr)) return false; + + // wait while = empty-body while loop + int condSize = condData.nbOpcode(); + result += condData; + result.append(JsmOpcode(JsmOpcode::JPF, 2)); + result.append(JsmOpcode(JsmOpcode::JMP, -(condSize + 1))); + return true; + } + + if (kw == "forever") { + advance(); // consume 'forever' + // wait forever = while(1) with empty body + result.append(JsmOpcode(JsmOpcode::PSHN_L, 1)); + result.append(JsmOpcode(JsmOpcode::JPF, 2)); + result.append(JsmOpcode(JsmOpcode::JMP, -2)); + return true; + } + + errorStr = QObject::tr("Expected 'while' or 'forever' after 'wait'"); + return false; +} + +bool JsmPseudoCompiler::parseForever(JsmData &result, QString &errorStr) +{ + advance(); // consume 'forever' + + skipNewlines(); + // Check for 'begin' — if present, it's a forever loop with body + if (peek().text.toLower() == "begin") { + advance(); + JsmData bodyBlock; + int dummy; + if (!parseStatements(bodyBlock, errorStr, dummy, {"end"})) return false; + + skipNewlines(); + if (peek().text.toLower() != "end") { + errorStr = QObject::tr("Expected 'end' to close forever block"); + return false; + } + advance(); + + // forever begin ... end = while(1) begin ... end + int bodySize = bodyBlock.nbOpcode(); + result.append(JsmOpcode(JsmOpcode::PSHN_L, 1)); + result.append(JsmOpcode(JsmOpcode::JPF, bodySize + 2)); + result += bodyBlock; + result.append(JsmOpcode(JsmOpcode::JMP, -(bodySize + 2))); + return true; + } + + // Bare "forever" = wait forever + result.append(JsmOpcode(JsmOpcode::PSHN_L, 1)); + result.append(JsmOpcode(JsmOpcode::JPF, 2)); + result.append(JsmOpcode(JsmOpcode::JMP, -2)); + return true; +} + +bool JsmPseudoCompiler::parseRepeat(JsmData &result, QString &errorStr) +{ + advance(); // consume 'repeat' + + JsmData bodyBlock; + int dummy; + if (!parseStatements(bodyBlock, errorStr, dummy, {"until"})) return false; + + skipNewlines(); + if (peek().text.toLower() != "until") { + errorStr = QObject::tr("Expected 'until' to close repeat block"); + return false; + } + advance(); + + JsmData condData; + if (!parseExpression(condData, errorStr)) return false; + + skipNewlines(); + if (peek().text.toLower() == "end") { + advance(); + } + + // repeat ... until end + // Emit: body, condition, JPF back to body start + int bodySize = bodyBlock.nbOpcode(); + int condSize = condData.nbOpcode(); + result += bodyBlock; + result += condData; + result.append(JsmOpcode(JsmOpcode::JPF, -(bodySize + condSize))); + + return true; +} + +// ============================================================ +// Expression parsing (recursive descent with precedence) +// ============================================================ + +bool JsmPseudoCompiler::parseExpression(JsmData &result, QString &errorStr) +{ + return parseLogicalOr(result, errorStr); +} + +bool JsmPseudoCompiler::parseLogicalOr(JsmData &result, QString &errorStr) +{ + if (!parseLogicalAnd(result, errorStr)) return false; + while (peek().type == Token::OpLogOr) { + advance(); + JsmData right; + if (!parseLogicalAnd(right, errorStr)) return false; + // We can't directly emit LogOr as a CAL — it's a virtual op in the decompiler + // Instead: left, right, OR (bitwise) — close enough for most uses + // Actually the decompiler uses LogOr/LogAnd as virtual ops. For compilation, + // we use the pattern: left != 0, right != 0, OR + result += right; + result.append(JsmOpcode(JsmOpcode::CAL, 13)); // OR + return true; + } + return true; +} + +bool JsmPseudoCompiler::parseLogicalAnd(JsmData &result, QString &errorStr) +{ + if (!parseBitwiseOr(result, errorStr)) return false; + while (peek().type == Token::OpLogAnd) { + advance(); + JsmData right; + if (!parseBitwiseOr(right, errorStr)) return false; + result += right; + result.append(JsmOpcode(JsmOpcode::CAL, 12)); // AND + } + return true; +} + +bool JsmPseudoCompiler::parseBitwiseOr(JsmData &result, QString &errorStr) +{ + if (!parseBitwiseEor(result, errorStr)) return false; + while (peek().type == Token::OpOr) { + advance(); + JsmData right; + if (!parseBitwiseEor(right, errorStr)) return false; + result += right; + result.append(JsmOpcode(JsmOpcode::CAL, 13)); // OR + } + return true; +} + +bool JsmPseudoCompiler::parseBitwiseEor(JsmData &result, QString &errorStr) +{ + if (!parseBitwiseAnd(result, errorStr)) return false; + while (peek().type == Token::OpEor) { + advance(); + JsmData right; + if (!parseBitwiseAnd(right, errorStr)) return false; + result += right; + result.append(JsmOpcode(JsmOpcode::CAL, 14)); // EOR + } + return true; +} + +bool JsmPseudoCompiler::parseBitwiseAnd(JsmData &result, QString &errorStr) +{ + if (!parseEquality(result, errorStr)) return false; + while (peek().type == Token::OpAnd) { + advance(); + JsmData right; + if (!parseEquality(right, errorStr)) return false; + result += right; + result.append(JsmOpcode(JsmOpcode::CAL, 12)); // AND + } + return true; +} + +bool JsmPseudoCompiler::parseEquality(JsmData &result, QString &errorStr) +{ + if (!parseRelational(result, errorStr)) return false; + while (peek().type == Token::OpEq || peek().type == Token::OpNt) { + Token op = advance(); + JsmData right; + if (!parseRelational(right, errorStr)) return false; + result += right; + result.append(JsmOpcode(JsmOpcode::CAL, op.type == Token::OpEq ? 6 : 11)); + } + return true; +} + +bool JsmPseudoCompiler::parseRelational(JsmData &result, QString &errorStr) +{ + if (!parseShift(result, errorStr)) return false; + while (peek().type == Token::OpGt || peek().type == Token::OpGe || + peek().type == Token::OpLs || peek().type == Token::OpLe) { + Token op = advance(); + JsmData right; + if (!parseShift(right, errorStr)) return false; + result += right; + int calOp; + switch (op.type) { + case Token::OpGt: calOp = 7; break; + case Token::OpGe: calOp = 8; break; + case Token::OpLs: calOp = 9; break; + default: calOp = 10; break; // LE + } + result.append(JsmOpcode(JsmOpcode::CAL, calOp)); + } + return true; +} + +bool JsmPseudoCompiler::parseShift(JsmData &result, QString &errorStr) +{ + if (!parseAdditive(result, errorStr)) return false; + while (peek().type == Token::OpRsh || peek().type == Token::OpLsh) { + Token op = advance(); + JsmData right; + if (!parseAdditive(right, errorStr)) return false; + result += right; + result.append(JsmOpcode(JsmOpcode::CAL, op.type == Token::OpRsh ? 16 : 17)); + } + return true; +} + +bool JsmPseudoCompiler::parseAdditive(JsmData &result, QString &errorStr) +{ + if (!parseMultiplicative(result, errorStr)) return false; + while (peek().type == Token::OpAdd || peek().type == Token::OpSub) { + Token op = advance(); + JsmData right; + if (!parseMultiplicative(right, errorStr)) return false; + result += right; + result.append(JsmOpcode(JsmOpcode::CAL, op.type == Token::OpAdd ? 0 : 1)); + } + return true; +} + +bool JsmPseudoCompiler::parseMultiplicative(JsmData &result, QString &errorStr) +{ + if (!parseUnary(result, errorStr)) return false; + while (peek().type == Token::OpMul || peek().type == Token::OpDiv || peek().type == Token::OpMod) { + Token op = advance(); + JsmData right; + if (!parseUnary(right, errorStr)) return false; + result += right; + int calOp; + switch (op.type) { + case Token::OpMul: calOp = 2; break; + case Token::OpDiv: calOp = 3; break; + default: calOp = 4; break; // MOD + } + result.append(JsmOpcode(JsmOpcode::CAL, calOp)); + } + return true; +} + +bool JsmPseudoCompiler::parseUnary(JsmData &result, QString &errorStr) +{ + if (peek().type == Token::OpSub) { + advance(); + if (!parseUnary(result, errorStr)) return false; + result.append(JsmOpcode(JsmOpcode::CAL, 5)); // MIN (negate) + return true; + } + if (peek().type == Token::OpNot) { + advance(); + if (!parseUnary(result, errorStr)) return false; + result.append(JsmOpcode(JsmOpcode::CAL, 15)); // NOT (bitwise) + return true; + } + if (peek().type == Token::OpLogNot) { + advance(); + if (!parseUnary(result, errorStr)) return false; + // Logical not: compare with 0 using EQ + result.append(JsmOpcode(JsmOpcode::PSHN_L, 0)); + result.append(JsmOpcode(JsmOpcode::CAL, 6)); // EQ + return true; + } + return parsePrimary(result, errorStr); +} + +bool JsmPseudoCompiler::parsePrimary(JsmData &result, QString &errorStr) +{ + Token t = peek(); + + // Parenthesized expression + if (t.type == Token::LParen) { + advance(); + if (!parseExpression(result, errorStr)) return false; + return expect(Token::RParen, errorStr); + } + + // Numeric literals + if (t.type == Token::Number) { + advance(); + result.append(JsmOpcode(JsmOpcode::PSHN_L, t.text.toInt())); + return true; + } + if (t.type == Token::HexNumber) { + advance(); + bool ok; + int val = t.text.toInt(&ok, 16); + if (!ok) val = (int)t.text.toUInt(&ok, 16); + result.append(JsmOpcode(JsmOpcode::PSHN_L, val)); + return true; + } + if (t.type == Token::BinNumber) { + advance(); + bool ok; + int val = t.text.mid(1).toInt(&ok, 2); // skip 'b' prefix + result.append(JsmOpcode(JsmOpcode::PSHN_L, val)); + return true; + } + + // Identifier: variable, function call, or named constant + if (t.type == Token::Identifier) { + Token ident = advance(); + QString lower = ident.text.toLower(); + + // Named constants from pseudo-code: text_N, map_N, item_N, magic_N + // These are just numeric values in the opcode stream + QRegularExpression constRe("^(text|map|item|magic)_(\\d+)$"); + QRegularExpressionMatch cm = constRe.match(lower); + if (cm.hasMatch()) { + int val = cm.captured(2).toInt(); + result.append(JsmOpcode(JsmOpcode::PSHN_L, val)); + return true; + } + + // Key constants + if (lower == "keyl1") { result.append(JsmOpcode(JsmOpcode::PSHN_L, 0x1)); return true; } + if (lower == "keyr1") { result.append(JsmOpcode(JsmOpcode::PSHN_L, 0x2)); return true; } + if (lower == "keyl2") { result.append(JsmOpcode(JsmOpcode::PSHN_L, 0x4)); return true; } + if (lower == "keyr2") { result.append(JsmOpcode(JsmOpcode::PSHN_L, 0x8)); return true; } + if (lower == "keycancel") { result.append(JsmOpcode(JsmOpcode::PSHN_L, 0x10)); return true; } + if (lower == "keymenu") { result.append(JsmOpcode(JsmOpcode::PSHN_L, 0x20)); return true; } + if (lower == "keychoose") { result.append(JsmOpcode(JsmOpcode::PSHN_L, 0x40)); return true; } + if (lower == "keycard") { result.append(JsmOpcode(JsmOpcode::PSHN_L, 0x80)); return true; } + if (lower == "keyselect") { result.append(JsmOpcode(JsmOpcode::PSHN_L, 0x100)); return true; } + if (lower == "keystart") { result.append(JsmOpcode(JsmOpcode::PSHN_L, 0x800)); return true; } + if (lower == "keyup") { result.append(JsmOpcode(JsmOpcode::PSHN_L, 0x1000)); return true; } + if (lower == "keyright") { result.append(JsmOpcode(JsmOpcode::PSHN_L, 0x2000)); return true; } + if (lower == "keydown") { result.append(JsmOpcode(JsmOpcode::PSHN_L, 0x4000)); return true; } + if (lower == "keyleft") { result.append(JsmOpcode(JsmOpcode::PSHN_L, 0x8000)); return true; } + + // Character name constants + if (lower == "squall") { result.append(JsmOpcode(JsmOpcode::PSHN_L, 0)); return true; } + if (lower == "zell") { result.append(JsmOpcode(JsmOpcode::PSHN_L, 1)); return true; } + if (lower == "irvine") { result.append(JsmOpcode(JsmOpcode::PSHN_L, 2)); return true; } + if (lower == "quistis") { result.append(JsmOpcode(JsmOpcode::PSHN_L, 3)); return true; } + if (lower == "rinoa") { result.append(JsmOpcode(JsmOpcode::PSHN_L, 4)); return true; } + if (lower == "selphie") { result.append(JsmOpcode(JsmOpcode::PSHN_L, 5)); return true; } + if (lower == "seifer") { result.append(JsmOpcode(JsmOpcode::PSHN_L, 6)); return true; } + if (lower == "edea") { result.append(JsmOpcode(JsmOpcode::PSHN_L, 7)); return true; } + if (lower == "laguna") { result.append(JsmOpcode(JsmOpcode::PSHN_L, 8)); return true; } + if (lower == "kiros") { result.append(JsmOpcode(JsmOpcode::PSHN_L, 9)); return true; } + if (lower == "ward") { result.append(JsmOpcode(JsmOpcode::PSHN_L, 10)); return true; } + + // Function call + if (peek().type == Token::LParen) { + return parseFunctionCall(ident.text, result, errorStr, false); + } + + // Variable reference + return emitPushVar(ident.text, result, errorStr); + } + + errorStr = QObject::tr("Unexpected token: '%1'").arg(t.text.isEmpty() ? "end of input" : t.text); + return false; +} diff --git a/src/JsmPseudoCompiler.h b/src/JsmPseudoCompiler.h new file mode 100644 index 0000000..4c74274 --- /dev/null +++ b/src/JsmPseudoCompiler.h @@ -0,0 +1,99 @@ +/**************************************************************************** + ** Deling Final Fantasy VIII Field Editor + ** Copyright (C) 2009-2024 Arzel Jérôme + ** + ** This program is free software: you can redistribute it and/or modify + ** it under the terms of the GNU General Public License as published by + ** the Free Software Foundation, either version 3 of the License, or + ** (at your option) any later version. + ** + ** This program is distributed in the hope that it will be useful, + ** but WITHOUT ANY WARRANTY; without even the implied warranty of + ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + ** GNU General Public License for more details. + ** + ** You should have received a copy of the GNU General Public License + ** along with this program. If not, see . + ****************************************************************************/ +#pragma once + +#include +#include "JsmData.h" +#include "JsmOpcode.h" + +class JsmPseudoCompiler +{ +public: + JsmPseudoCompiler(); + + bool compile(const QString &pseudoCode, JsmData &result, QString &errorStr, int &errorLine); + +private: + struct Token { + enum Type { + Identifier, Number, HexNumber, BinNumber, + LParen, RParen, Comma, Dot, + OpAssign, OpAddAssign, OpSubAssign, OpMulAssign, OpDivAssign, + OpModAssign, OpAndAssign, OpOrAssign, OpEorAssign, OpRshAssign, OpLshAssign, + OpEq, OpNt, OpGt, OpGe, OpLs, OpLe, + OpAdd, OpSub, OpMul, OpDiv, OpMod, + OpAnd, OpOr, OpEor, OpNot, OpRsh, OpLsh, + OpLogAnd, OpLogOr, OpLogNot, + Newline, EndOfInput + }; + Type type; + QString text; + int line; + }; + + // Lexer + QList tokenize(const QString &input, QString &errorStr, int &errorLine); + + // Parser state + QList _tokens; + int _pos; + int _currentLine; + + Token peek() const; + Token advance(); + bool match(Token::Type type); + bool expect(Token::Type type, QString &errorStr); + void skipNewlines(); + + // Parser — statement level + bool parseStatements(JsmData &result, QString &errorStr, int &errorLine, + const QStringList &allowedTerminators = QStringList({"end", "else", "until"})); + bool parseStatement(JsmData &result, QString &errorStr); + bool parseIf(JsmData &result, QString &errorStr); + bool parseWhile(JsmData &result, QString &errorStr); + bool parseWait(JsmData &result, QString &errorStr); + bool parseForever(JsmData &result, QString &errorStr); + bool parseRepeat(JsmData &result, QString &errorStr); + + // Parser — expression level (for conditions and arguments) + bool parseExpression(JsmData &result, QString &errorStr); + bool parseLogicalOr(JsmData &result, QString &errorStr); + bool parseLogicalAnd(JsmData &result, QString &errorStr); + bool parseBitwiseOr(JsmData &result, QString &errorStr); + bool parseBitwiseEor(JsmData &result, QString &errorStr); + bool parseBitwiseAnd(JsmData &result, QString &errorStr); + bool parseEquality(JsmData &result, QString &errorStr); + bool parseRelational(JsmData &result, QString &errorStr); + bool parseShift(JsmData &result, QString &errorStr); + bool parseAdditive(JsmData &result, QString &errorStr); + bool parseMultiplicative(JsmData &result, QString &errorStr); + bool parseUnary(JsmData &result, QString &errorStr); + bool parsePrimary(JsmData &result, QString &errorStr); + + // Helpers + bool parseAssignment(const QString &varName, JsmData &result, QString &errorStr); + bool parseFunctionCall(const QString &name, JsmData &result, QString &errorStr, bool isStatement); + bool parseMethodCall(const QString &objectName, const QString &methodName, + JsmData &result, QString &errorStr); + bool emitPushVar(const QString &varName, JsmData &result, QString &errorStr); + bool emitPopVar(const QString &varName, JsmData &result, QString &errorStr); + int resolveOpcode(const QString &name) const; + + static QMap _opcodeMap; + static void initOpcodeMap(); +}; diff --git a/src/files/JsmFile.cpp b/src/files/JsmFile.cpp index 43369ed..2d9f2a0 100644 --- a/src/files/JsmFile.cpp +++ b/src/files/JsmFile.cpp @@ -952,6 +952,11 @@ const JsmScripts &JsmFile::getScripts() const return scripts; } +JsmScripts &JsmFile::getScripts() +{ + return scripts; +} + bool JsmFile::search(SearchType type, quint64 value, int &groupID, int &methodID, int &opcodeID) const { int groupListSize = scripts.nbGroup(), nbOpcode, methodCount; diff --git a/src/files/JsmFile.h b/src/files/JsmFile.h index 3bb4cfa..e0bda2e 100644 --- a/src/files/JsmFile.h +++ b/src/files/JsmFile.h @@ -82,6 +82,7 @@ class JsmFile : public File int fromString(int groupID, int methodID, const QString &text, QString &errorStr); const JsmScripts &getScripts() const; + JsmScripts &getScripts(); bool search(SearchType type, quint64 value, int &groupID, int &methodID, int &opcodeID) const; bool search(SearchType type, const QList &values, int &groupID, int &methodID, int &opcodeID) const; diff --git a/src/main.cpp b/src/main.cpp index d163a21..928d620 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -35,6 +35,19 @@ int main(int argc, char *argv[]) { + // Check for --scale argument before QApplication is created + for (int i = 1; i < argc; ++i) { + if (qstrcmp(argv[i], "--scale") == 0 && i + 1 < argc) { + qputenv("QT_SCALE_FACTOR", argv[i + 1]); + // Remove --scale and its value from argv + for (int j = i; j + 2 < argc; ++j) { + argv[j] = argv[j + 2]; + } + argc -= 2; + break; + } + } + #ifdef DELING_CONSOLE QCoreApplication app(argc, argv); QCoreApplication::setApplicationName(DELING_NAME); diff --git a/src/widgets/JsmWidget.cpp b/src/widgets/JsmWidget.cpp index 72fae6a..3e4bd23 100644 --- a/src/widgets/JsmWidget.cpp +++ b/src/widgets/JsmWidget.cpp @@ -19,6 +19,8 @@ #include "3d/WalkmeshGLWidget.h" #include "Config.h" #include "Data.h" +#include "JsmHelpDialog.h" +#include "JsmPseudoCompiler.h" JsmWidget::JsmWidget(QWidget *parent) : PageWidget(parent), mainModels(nullptr), fieldArchive(nullptr), @@ -71,7 +73,7 @@ void JsmWidget::build() tabBar = new QTabBar(this); tabBar->setDrawBase(false); - tabBar->addTab(tr("Pseudo-code")); + tabBar->addTab(tr("Code")); tabBar->addTab(tr("Instructions")); PlainTextEdit *te = new PlainTextEdit(this); @@ -79,6 +81,8 @@ void JsmWidget::build() QFont font2 = textEdit->document()->defaultFont(); font2.setStyleHint(QFont::TypeWriter); font2.setFamily("Courier"); + int savedFontSize = Config::value("scriptFontSize", 10).toInt(); + font2.setPointSize(savedFontSize); textEdit->document()->setDefaultFont(font2); highlighter = new JsmHighlighter(textEdit->document()); // continue highlight when window is inactive @@ -106,13 +110,47 @@ void JsmWidget::build() toolBar->addWidget(errorWidget); toolBar->setEnabled(false); + // Pseudo-code toolbar with compile and help buttons + pseudoToolBar = new QToolBar(this); + QAction *pseudoCompileAction = pseudoToolBar->addAction(tr("Compile"), this, SLOT(compilePseudo())); + pseudoCompileAction->setToolTip(tr("Compile pseudo-code to opcodes (Ctrl+Shift+B)")); + pseudoCompileAction->setStatusTip(tr("Compile pseudo-code to opcodes (Ctrl+Shift+B)")); + pseudoCompileAction->setShortcutContext(Qt::ApplicationShortcut); + pseudoCompileAction->setShortcut(QKeySequence("Ctrl+Shift+B")); + QAction *helpAction = pseudoToolBar->addAction(tr("Help"), this, SLOT(showHelp())); + helpAction->setToolTip(tr("Script reference (F1)")); + helpAction->setStatusTip(tr("Script reference (F1)")); + helpAction->setShortcutContext(Qt::ApplicationShortcut); + helpAction->setShortcut(QKeySequence("F1")); + pseudoToolBar->setEnabled(false); + + // Font size control — shared across both tabs + QLabel *fontSizeLabel = new QLabel(tr("Font:"), this); + fontSizeSpinner = new QSpinBox(this); + fontSizeSpinner->setRange(6, 36); + fontSizeSpinner->setValue(savedFontSize); + fontSizeSpinner->setSuffix(tr("pt")); + fontSizeSpinner->setToolTip(tr("Code editor font size")); + connect(fontSizeSpinner, QOverload::of(&QSpinBox::valueChanged), this, [this](int size) { + QFont f = textEdit->document()->defaultFont(); + f.setPointSize(size); + textEdit->document()->setDefaultFont(f); + Config::setValue("scriptFontSize", size); + }); + + QHBoxLayout *tabBarLayout = new QHBoxLayout(); + tabBarLayout->addWidget(tabBar, 1); + tabBarLayout->addWidget(fontSizeLabel); + tabBarLayout->addWidget(fontSizeSpinner); + QGridLayout *mainLayout = new QGridLayout(this); mainLayout->addWidget(warningWidget, 0, 0, 1, 4); mainLayout->addLayout(list1Layout, 1, 0, 3, 1); mainLayout->addWidget(list2, 1, 1, 3, 1); - mainLayout->addWidget(tabBar, 1, 2, 1, 2); + mainLayout->addLayout(tabBarLayout, 1, 2, 1, 2); mainLayout->addWidget(te, 2, 2); mainLayout->addWidget(textEdit, 2, 3); + mainLayout->addWidget(pseudoToolBar, 3, 2, 1, 2); mainLayout->addWidget(toolBar, 3, 2, 1, 2); mainLayout->setContentsMargins(QMargins()); @@ -120,7 +158,7 @@ void JsmWidget::build() connect(list1, SIGNAL(itemSelectionChanged()), SLOT(fillList2())); connect(list2, SIGNAL(itemSelectionChanged()), SLOT(fillTextEdit())); - connect(tabBar, SIGNAL(currentChanged(int)), SLOT(fillTextEdit())); + connect(tabBar, SIGNAL(currentChanged(int)), SLOT(onTabChanged(int))); connect(textEdit, SIGNAL(lineHovered(QString,QPoint)), SLOT(showPreview(QString,QPoint))); PageWidget::build(); @@ -145,10 +183,75 @@ void JsmWidget::compile() pal.setColor(QPalette::Inactive, QPalette::ButtonText, Qt::darkGreen); errorLabel->setPalette(pal); errorLabel->setText(tr("Successfully compiled")); + textEdit->document()->setModified(false); emit modified(); } } +void JsmWidget::compilePseudo() +{ + groupID = currentItem(list1); + methodID = currentItem(list2); + if (groupID == -1 || methodID == -1) return; + + JsmPseudoCompiler compiler; + JsmData compiled; + QString errorStr; + int errorLine = 0; + + if (compiler.compile(textEdit->toPlainText(), compiled, errorStr, errorLine)) { + // Replace the script data and invalidate caches + data()->getJsmFile()->getScripts().replaceScript(groupID, methodID, compiled); + // Mark the JsmFile as modified so the save system knows to write it + data()->getJsmFile()->setModified(true); + // Clear cached decompiled scripts so both views regenerate + data()->getJsmFile()->setDecompiledScript(groupID, methodID, QString(), false); + data()->getJsmFile()->setDecompiledScript(groupID, methodID, QString(), true); + + QMessageBox::information(this, tr("Compile"), + tr("Successfully compiled (%1 opcodes)").arg(compiled.nbOpcode())); + textEdit->document()->setModified(false); + emit modified(); + } else { + QString msg; + if (errorLine > 0) { + msg = tr("Line %1: %2").arg(errorLine).arg(errorStr); + + // Move cursor to the error line + QTextCursor cursor = textEdit->textCursor(); + QTextBlock block = textEdit->document()->findBlockByLineNumber(errorLine - 1); + if (block.isValid()) { + cursor.setPosition(block.position()); + cursor.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor); + textEdit->setTextCursor(cursor); + textEdit->ensureCursorVisible(); + } + } else { + msg = errorStr; + } + + QMessageBox::critical(this, tr("Compile Error"), msg); + } +} + +void JsmWidget::showHelp() +{ + if (!_helpDialog) { + _helpDialog = new JsmHelpDialog(this); + _helpDialog->setAttribute(Qt::WA_DeleteOnClose); + connect(_helpDialog, &QDialog::destroyed, this, [this]() { _helpDialog = nullptr; }); + connect(_helpDialog, &JsmHelpDialog::insertSignature, this, [this](const QString &sig) { + if (tabBar->currentIndex() <= 0 && !textEdit->isReadOnly()) { + textEdit->insertPlainText(sig); + textEdit->setFocus(); + } + }); + } + _helpDialog->show(); + _helpDialog->raise(); + _helpDialog->activateWindow(); +} + void JsmWidget::clear() { if (!isFilled()) return; @@ -177,10 +280,12 @@ void JsmWidget::saveSession() data()->getJsmFile()->setCurrentOpcodeScroll(this->groupID, this->methodID, textEdit->verticalScrollBar()->value(), textEdit->textCursor()); if (textEdit->document()->isModified()) { + // Save to the correct cache slot based on which tab is active + bool isPseudo = (tabBar->currentIndex() <= 0); data()->getJsmFile()->setDecompiledScript(this->groupID, this->methodID, textEdit->toPlainText(), - false); + isPseudo); } } @@ -282,6 +387,34 @@ void JsmWidget::fillList2() if (item) list2->setCurrentItem(item); } +void JsmWidget::onTabChanged(int index) +{ + Q_UNUSED(index) + + // Check if the editor has uncompiled changes + if (textEdit->document()->isModified() && groupID != -1 && methodID != -1) { + QMessageBox::StandardButton reply = QMessageBox::warning( + this, tr("Unsaved Changes"), + tr("You have uncompiled changes. Switching tabs will discard them.\n\n" + "Do you want to switch anyway?"), + QMessageBox::Yes | QMessageBox::No, + QMessageBox::No); + + if (reply == QMessageBox::No) { + // Revert the tab switch + tabBar->blockSignals(true); + tabBar->setCurrentIndex(index == 0 ? 1 : 0); + tabBar->blockSignals(false); + return; + } + + // User chose to discard — clear the modified flag so saveSession doesn't cache stale text + textEdit->document()->setModified(false); + } + + fillTextEdit(); +} + void JsmWidget::fillTextEdit() { // qDebug() << QString("JsmWidget::fillTextEdit(%1, %2)").arg(currentItem(list1)).arg(currentItem(list2)); @@ -294,17 +427,22 @@ void JsmWidget::fillTextEdit() if (groupID==-1 || methodID==-1) { textEdit->clear(); toolBar->setEnabled(false); + pseudoToolBar->setEnabled(false); return; } list2->scrollToItem(list2->currentItem()); if (tabBar->currentIndex() <= 0) { - toolBar->setEnabled(false); - textEdit->setReadOnly(true); + toolBar->setVisible(false); + pseudoToolBar->setVisible(true); + pseudoToolBar->setEnabled(!isReadOnly()); + textEdit->setReadOnly(isReadOnly()); highlighter->setPseudoCode(true); textEdit->setPlainText(data()->getJsmFile()->toString(groupID, methodID, true, data())); } else { + pseudoToolBar->setVisible(false); + toolBar->setVisible(true); toolBar->setEnabled(!isReadOnly()); textEdit->setReadOnly(isReadOnly()); highlighter->setPseudoCode(false); diff --git a/src/widgets/JsmWidget.h b/src/widgets/JsmWidget.h index 92646be..f132450 100644 --- a/src/widgets/JsmWidget.h +++ b/src/widgets/JsmWidget.h @@ -25,6 +25,9 @@ #include "PlainTextEdit.h" #include "CharaModel.h" #include "FieldArchive.h" +#include "JsmPseudoCompiler.h" + +class JsmHelpDialog; class JsmWidget : public PageWidget { @@ -54,15 +57,21 @@ class JsmWidget : public PageWidget PlainTextEditPriv *textEdit; JsmHighlighter *highlighter; QToolBar *toolBar; + QToolBar *pseudoToolBar; + QSpinBox *fontSizeSpinner; QLabel *errorLabel; QLabel *warningWidget; QRegularExpression _regConst, _regSetLine, _regColor, _regPlace; + JsmHelpDialog *_helpDialog = nullptr; static int currentItem(QTreeWidget *); // void gotoScriptLabel(int groupID, int labelID); int groupID, methodID; private slots: void compile(); + void compilePseudo(); + void showHelp(); + void onTabChanged(int index); void fillList2(); void fillTextEdit(); void showPreview(const QString &line, QPoint cursorPos); diff --git a/translations/Deling_es.ts b/translations/Deling_es.ts index 1d09c85..608c214 100644 --- a/translations/Deling_es.ts +++ b/translations/Deling_es.ts @@ -2138,6 +2138,21 @@ Dollet Harbor Tinkatzea + + JsmHelpDialog + + Script Reference + + + + Search opcodes... + + + + Close + + + JsmWidget @@ -2170,7 +2185,7 @@ Dollet Harbor Pseudo-code - Pseudo-code + Pseudo-code Compile @@ -2180,6 +2195,18 @@ Dollet Harbor Compile (Ctrl+B) Bildumaratu (Ctrl+B) + + Compile pseudo-code to opcodes (Ctrl+Shift+B) + + + + Help + + + + Script reference (F1) + + Line %1 -> %2 Lerro %1 -> %2 @@ -2188,10 +2215,36 @@ Dollet Harbor Successfully compiled Bildumaratua arrakastaz + + Successfully compiled (%1 opcodes) + + Scripts Gidoiak + + Line %1: %2 + + + + Compile Error + + + + Unsaved Changes + + + + You have uncompiled changes. Switching tabs will discard them. + +Do you want to switch anyway? + + + + Code + + MainWindow @@ -2980,6 +3033,98 @@ Error message: Unable to read the worldmap (readTextures). Unable to read the worldmap (readTextures). + + Unexpected character: '%1' + + + + Expected '%1', got '%2' + + + + 'goto' is not supported in pseudo-code mode. Use if/while/repeat instead. + + + + 'label' is not supported in pseudo-code mode. + + + + Expected statement, got '%1' + + + + Expected method name after '.' + + + + Unknown statement: '%1' + + + + Invalid temp index: '%1' + + + + Invalid model index: '%1' + + + + Cannot resolve variable: '%1' + + + + Cannot resolve variable for push: '%1'. Use temp_N, model_N, or N_ubyte/uword/ulong/sbyte/sword/slong format. + + + + Cannot resolve variable for assignment: '%1'. Use temp_N or N_ubyte/uword/ulong/sbyte/sword/slong format. + + + + Unknown assignment operator + + + + Unknown function: '%1' + + + + Cannot resolve entity '%1' to a group ID. Use numeric IDs for entity.method() calls. + + + + Expected 'begin' after if condition + + + + Expected 'end' to close if/else block + + + + Expected 'else' or 'end' after if block, got '%1' + + + + Expected 'end' to close while block + + + + Expected 'while' or 'forever' after 'wait' + + + + Expected 'end' to close forever block + + + + Expected 'until' to close repeat block + + + + Unexpected token: '%1' + + Search diff --git a/translations/Deling_fr.ts b/translations/Deling_fr.ts index 2075b22..296a3c7 100644 --- a/translations/Deling_fr.ts +++ b/translations/Deling_fr.ts @@ -2149,6 +2149,21 @@ Dollet Harbor Compression + + JsmHelpDialog + + Script Reference + + + + Search opcodes... + + + + Close + + + JsmWidget @@ -2181,7 +2196,7 @@ Dollet Harbor Pseudo-code - Pseudo-code + Pseudo-code Compile @@ -2191,6 +2206,18 @@ Dollet Harbor Compile (Ctrl+B) Compiler (Ctrl+B) + + Compile pseudo-code to opcodes (Ctrl+Shift+B) + + + + Help + + + + Script reference (F1) + + Line %1 -> %2 Ligne %1 -> %2 @@ -2199,10 +2226,36 @@ Dollet Harbor Successfully compiled Compilé avec succès + + Successfully compiled (%1 opcodes) + + Scripts Scripts + + Line %1: %2 + + + + Compile Error + + + + Unsaved Changes + + + + You have uncompiled changes. Switching tabs will discard them. + +Do you want to switch anyway? + + + + Code + + MainWindow @@ -3004,6 +3057,98 @@ Message d'erreur : Unable to read the worldmap (readTextures). Impossible de lire la mappemonde (readTextures). + + Unexpected character: '%1' + + + + Expected '%1', got '%2' + + + + 'goto' is not supported in pseudo-code mode. Use if/while/repeat instead. + + + + 'label' is not supported in pseudo-code mode. + + + + Expected statement, got '%1' + + + + Expected method name after '.' + + + + Unknown statement: '%1' + + + + Invalid temp index: '%1' + + + + Invalid model index: '%1' + + + + Cannot resolve variable: '%1' + + + + Cannot resolve variable for push: '%1'. Use temp_N, model_N, or N_ubyte/uword/ulong/sbyte/sword/slong format. + + + + Cannot resolve variable for assignment: '%1'. Use temp_N or N_ubyte/uword/ulong/sbyte/sword/slong format. + + + + Unknown assignment operator + + + + Unknown function: '%1' + + + + Cannot resolve entity '%1' to a group ID. Use numeric IDs for entity.method() calls. + + + + Expected 'begin' after if condition + + + + Expected 'end' to close if/else block + + + + Expected 'else' or 'end' after if block, got '%1' + + + + Expected 'end' to close while block + + + + Expected 'while' or 'forever' after 'wait' + + + + Expected 'end' to close forever block + + + + Expected 'until' to close repeat block + + + + Unexpected token: '%1' + + Search diff --git a/translations/Deling_ja.ts b/translations/Deling_ja.ts index 237ce1c..e88a96c 100644 --- a/translations/Deling_ja.ts +++ b/translations/Deling_ja.ts @@ -2180,6 +2180,21 @@ Dollet Harbor 圧縮 + + JsmHelpDialog + + Script Reference + + + + Search opcodes... + + + + Close + + + JsmWidget @@ -2216,7 +2231,7 @@ Dollet Harbor Pseudo-code - Pseudo-code + Pseudo-code Compile @@ -2228,6 +2243,18 @@ Dollet Harbor Compile (Ctrl+B) コンパイル (Ctrl+B) + + Compile pseudo-code to opcodes (Ctrl+Shift+B) + + + + Help + + + + Script reference (F1) + + Line %1 -> %2 Line %1 -> %2 @@ -2238,10 +2265,36 @@ Dollet Harbor Successfully compiled コンパイルを完了しました + + Successfully compiled (%1 opcodes) + + Scripts スクリプト + + Line %1: %2 + + + + Compile Error + + + + Unsaved Changes + + + + You have uncompiled changes. Switching tabs will discard them. + +Do you want to switch anyway? + + + + Code + + MainWindow @@ -3148,6 +3201,98 @@ Error message: Unable to read the worldmap (readTextures). Unable to read the worldmap (readTextures). + + Unexpected character: '%1' + + + + Expected '%1', got '%2' + + + + 'goto' is not supported in pseudo-code mode. Use if/while/repeat instead. + + + + 'label' is not supported in pseudo-code mode. + + + + Expected statement, got '%1' + + + + Expected method name after '.' + + + + Unknown statement: '%1' + + + + Invalid temp index: '%1' + + + + Invalid model index: '%1' + + + + Cannot resolve variable: '%1' + + + + Cannot resolve variable for push: '%1'. Use temp_N, model_N, or N_ubyte/uword/ulong/sbyte/sword/slong format. + + + + Cannot resolve variable for assignment: '%1'. Use temp_N or N_ubyte/uword/ulong/sbyte/sword/slong format. + + + + Unknown assignment operator + + + + Unknown function: '%1' + + + + Cannot resolve entity '%1' to a group ID. Use numeric IDs for entity.method() calls. + + + + Expected 'begin' after if condition + + + + Expected 'end' to close if/else block + + + + Expected 'else' or 'end' after if block, got '%1' + + + + Expected 'end' to close while block + + + + Expected 'while' or 'forever' after 'wait' + + + + Expected 'end' to close forever block + + + + Expected 'until' to close repeat block + + + + Unexpected token: '%1' + + Search diff --git a/translations/Deling_ru.ts b/translations/Deling_ru.ts index f6e8f19..d2369bf 100644 --- a/translations/Deling_ru.ts +++ b/translations/Deling_ru.ts @@ -2143,6 +2143,21 @@ Dollet Harbor Сжатие + + JsmHelpDialog + + Script Reference + + + + Search opcodes... + + + + Close + + + JsmWidget @@ -2176,7 +2191,7 @@ Dollet Harbor Pseudo-code - Псевдокод + Псевдокод Compile @@ -2186,6 +2201,18 @@ Dollet Harbor Compile (Ctrl+B) Компилировать (Ctrl+B) + + Compile pseudo-code to opcodes (Ctrl+Shift+B) + + + + Help + + + + Script reference (F1) + + Line %1 -> %2 Строка %1 -> %2 @@ -2194,10 +2221,36 @@ Dollet Harbor Successfully compiled Успешно скомпилирован + + Successfully compiled (%1 opcodes) + + Scripts Скрипты + + Line %1: %2 + + + + Compile Error + + + + Unsaved Changes + + + + You have uncompiled changes. Switching tabs will discard them. + +Do you want to switch anyway? + + + + Code + + MainWindow @@ -2999,6 +3052,98 @@ Error message: Unable to read the worldmap (readTextures). Невозможно прочитать карту мира (readTextures). + + Unexpected character: '%1' + + + + Expected '%1', got '%2' + + + + 'goto' is not supported in pseudo-code mode. Use if/while/repeat instead. + + + + 'label' is not supported in pseudo-code mode. + + + + Expected statement, got '%1' + + + + Expected method name after '.' + + + + Unknown statement: '%1' + + + + Invalid temp index: '%1' + + + + Invalid model index: '%1' + + + + Cannot resolve variable: '%1' + + + + Cannot resolve variable for push: '%1'. Use temp_N, model_N, or N_ubyte/uword/ulong/sbyte/sword/slong format. + + + + Cannot resolve variable for assignment: '%1'. Use temp_N or N_ubyte/uword/ulong/sbyte/sword/slong format. + + + + Unknown assignment operator + + + + Unknown function: '%1' + + + + Cannot resolve entity '%1' to a group ID. Use numeric IDs for entity.method() calls. + + + + Expected 'begin' after if condition + + + + Expected 'end' to close if/else block + + + + Expected 'else' or 'end' after if block, got '%1' + + + + Expected 'end' to close while block + + + + Expected 'while' or 'forever' after 'wait' + + + + Expected 'end' to close forever block + + + + Expected 'until' to close repeat block + + + + Unexpected token: '%1' + + Search