Warcraft Studio Code v5.0
Warcraft Studio Code v5.0
Warcraft Studio Code v5.0 支持在 Warcraft 3 中按 Ctrl + F1 打开编辑器编写并执行 Lua 脚本。调试需将脚本转译为 debug 形式,会带来约 10x-20x 性能下降,导致 ALICE 等计算密集型脚本可能无法调试。
Using the Editor
Parsing the Map Script
Initialization
In-Game Code Editing
Wrapping GUI Triggers
Enabling Quick Restart
Using the Editor
General
Features
Hotkeys
Open the editor by pressing Ctrl + F1 (customizable). You can start typing code and execute it by pressing the Execute button at the bottom.
You can paste any code from outside the game into the editor by focusing the text field and pressing Ctrl + V. If the code contains a beginFile statement, the editor will automatically open a tab with the name of the script and override all existing code in that tab.
All code that is written in or imported into the editor is saved to backup files in the CustomMapData folder. The filenames are based on the name of the current tab. You can then reload them on a later session with the Load Backup File button at the bottom.
You can also use this feature to export your code if you've done substantial edits to it in-game. You will find two files in the CustomMapData folder. One is FileNameLoadFile and the other is FileNameExport. LoadFile is the file that WSCode requires to load it back into the editor. Export is the file that can be converted into a regular script file by removing all the Preload code. Simply replace all occurences of call Preload( " and " ) as well as the header and tail of the Preload code.
Layout & Interface
The editor has the inherent downside that it fights for screen space with the rest of the game. If it is too big, the world and the default UI are obfuscated. If the editor is too small, working with it is much less convenient.
You can adjust the editor layout in the config script. There are two recommended options:
- Set MAX_LINES to 39. Do not hide console UI when the editor is shown. To do this, comment out the ToggleUIIfNecessary() lines in the ON_ENABLE and ON_DISABLE callback functions.
- Set MAX_LINES to 50. There are more lines visible at a time, but the editor is now too large and displays over the console UI, so we hide the UI when the editor is shown with ToggleUIIfNecessary() in the ON_ENABLE and ON_DISABLE callback functions.
Breakpoints
Debugging with WSCode is mostly done via breakpoints. You can halt the execution of a script, look at the values of each local variable, then continue executing. There is a lot going on under the hood to make these breakpoints work and not everything is without side effects:
- On a global level, all Warcraft 3 Lua entry point functions (TriggerAddAction, Condition, TimerStart, ForGroup, ForForce, EnumItemsInRect, and EnumDestructablesInRect) are altered such that the created thread is wrapped into a coroutine that can be halted. This, unfortunately, breaks all GUI waits. There is a hacky fix to this problem in the config option DO_NOT_BREAK_GUI_WAITS, but this fix is not fully tested. If the coroutine wraps break your map, WRAP_ALL_LUA_ENTRY_POINTS needs to be disabled.
- The code being debugged needs to be transpiled into debug form. WSCode parses the code and adds additional debug hooks in between the different statements in the code. Transpiling a script into this form incurs approximately a 10x-20x performance decrease. Some computation-intensive scripts (such as ALICE) might not be able to be debugged this way because of this. The debug hooks should not alter what the code is doing.
Breakpoints are a powerful tool, but also fragile. Many things can interfere with the code you're trying to debug as it is halted. For example, halting a periodic callback function may prove difficult as the timer keeps running and invoke the function again while it is halted. This might lead to unforeseen consequences. The best solution here is to write your code using ALICE, as the entire cycle can be automatically halted on a breakpoint.
Breakpoints
To the right, next to the line numbers are hidden buttons that you can click (left-click or right-click) to insert breakpoints or inject prints. When your script encounters a breakpoint, the code execution will halt and you can view all currently visible variables in the variable viewer. When you right-click, you can enter an expression that will be evaluated and the result printed every time the code reaches that line. For breakpoints or injected prints to work, your code must have been transpiled into debug form.
Once the code execution has been halted, you can resume it by pressing F3 or Ctrl + F3. F3 will execute the next line; Ctrl + F3 will advance to the next breakpoint.
All currently visible variables and their values are displayed in the variable viewer. You can view the full value of variables and the content of tables by clicking on them.
Do not put breakpoints before all event response functions such as GetTriggerUnit have been called as those functions will return nil otherwise.
Context Menu
You can right-click anywhere in your code to open the context menu. This menu gives you various options such as Executing a specific line of code.
Tab Context Menu
You can right-click on a tab to open the tab context menu. This menu gives you options mostly to alter the behavior of breakpoints for the selected tab.
The Break Condition button warrants further explanation. It is a powerful tool that allows you to only halt the code at a breakpoint in a tab when certain conditions are met. This could make it much easier to debug a problem in which a bug occurs only randomly. To set a break condition, open a new tab and switch to it. Then enter the break condition code and right-click on the tab for which the condition should be set and select Break Condition. To remove a break condition, have the same tab opened while you click Break Condition.
The break condition needs to be the body of a function that returns a boolean. Examples:
Lua:
return i and i >= 50 --check if i exists first or the function will crash if i is not visible in a lineLua:
local hasData = UNIT_DATA[key] ~= nil
local dataIsValid = UNIT_DATA[key] > 0
return hasData and not dataIsValidBreak conditions can be combined with Break Everywhere to immediately halt anywhere in the code when the break condition is met. Note that this will incur heavy performance costs.
Drag Window
You can left-click on the "Warcraft Studio Code" text at the top to drag the editor around.
| Ctrl + F1 | Show or hide the editor. |
| F3 | If the code execution is currently halted, go to the next line. |
| Ctrl + F3 | If the code execution is currently halted, resume until the next breakpoint. |
| Ctrl + C | Copy selected text. Does not add the text to the os clipboard; it can only be pasted inside WSCode. |
| Ctrl + X | Cut selected text. Does not add the text to the os clipboard; it can only be pasted inside WSCode. |
| Ctrl + V | Paste the text that is either in the os clipboard or in the internal WSCode clipboard. If there is text in both, it will prompt you to choose which clipboard should be copied. |
| Ctrl + A | Select all. |
| Ctrl + Z | Undo. |
| Ctrl + Y | Redo. |
| Tab | Indent the selected code. |
| Shift + Tab | Deindent the selected code. |
| F12 | Go to the definition of the selected variable. |
| Ctrl + D | Expand or collapse the editor. |
| Ctrl + Q | Execute the selected code or the current line of the cursor if no code is selected. |
| Ctrl + E | Open the Smart Evaluate dialog. |
| Alt + Up/Down | Swap the current line with the previous/next line. |
| Ctrl + Shift + R | Comment out the selected code. |
| Ctrl + Shift + T | Uncomment the selected code. |
The Parse Function
Writing code in the editor or importing it and executing it directly is great way to prototype and debug code snippets or to test native behavior, but to debug your existing map script we need to enable WSCode to load it on map initialization. This is done via the WSCode.Parse function. The entire map script is converted to a giant string, which is then passed into the Parse function. WSCode will execute that script, but also analyze it and store it so that it can be pulled up in the editor later. By adding special parser instructions, the attributes of individual script files can be changed. For example, you can instruct WSCode to transpile a file into its debug form, which will enable breakpoints to be used in that script.
The second major advantage of using the Parse function is that it allows you to use WSCode's Quick Restart feature. With Quick Restart enabled, you can make changes to your map script and simply restart the map in-game for those changes to apply.
Using Parse also enables function previews for functions defined in your map script and protects you from main menu yeets due to script errors.
To wrap your map script, create a new script directly below your essential libraries. Into that script, write:
Lua:
WSCode.Parse([===[Then, at the very end of your map script, close the long string and function call by creating another script with the content:
Lua:
]===])Now, your entire map script is wrapped and gets parsed before being executed.
Warning: Do not wrap any of DebugUtils, WarcraftStudioCode, WSCodeFunctionPreviews, TotalInitialization, and FileIO. These should be on top of the map script as explained in Installation.
To enable the Parse function to correctly separate your map script into individual files, you need to begin each script file with a Debug.beginFile call:
Lua:
if Debug then Debug.beginFile "ScriptName" endor alternatively with
Lua:
---@beginFile ScriptNameWSCode will execute your map script in protected mode in chunks separated by these delimiting tokens. If the initialization of one script file fails, the rest will still execute and once the map initialization completes, the editor will pop up and show you the encountered error.
You can load any script that was parsed with the Pull Script button at the bottom of the editor.
Parser Instructions
You can use various parser instructions to direct how the parser treats an individual script file. These are added as comments and preceded by an @-sign. They can be added anywhere in your code, but the natural place to put them is in the script header below the beginFile statement.
| @debug | Transpiles the current script file into debug form and adds it to the editor upon map launch, allowing you to do further edits in game, look at its local variables, and halt its execution with breakpoints. If you're writing a new spell or system, simply add this instruction and remove it once you're moving on to the next script. |
| @nodebug | Prevents the automatic transpilation of a file into debug mode when it is added as a tab in the editor. Use this for scripts that are just too slow in debug mode. |
| @transpile | Transpiles the current script file into debug form, but does not add it to the debugger. This instruction could be useful for investigating a rare bug that cannot be consistently reproduced. |
| @store | Skips executing the current script file at map initialization and adds it to the editor. Useful for testing code snippets. |
| @require <requirement1, requirement2, ...> | Declares required libraries for this file. The scripts will be rearranged so that all requirements are above the requiring script and executed before it. A ? after the requirement name declares an optional requirement. |
| @map <mapName> | Only executes this script file if the declared map name matches the one set in the global MapName variable. If a ! is in front of the map name, it will invert the condition and only execute the script if the names do not match. |
| @version <versionNumber> | If multiple versions of a script file with this file's name are encountered, only the one with the higher version number is executed. |
| @development | Marks this script file as development-only, which causes it to be ignored when FINALIZE_MAP_SCRIPT is used. |
| @loadfile | If this script file also exists as a backup file, the backup file will take priority over the file existing in the regular map script and replace it. @loadfile needs to be present in the main script file, not the backup file. |
| @execute | This instruction only affects backup files. It will cause the script to be executed on map initialization as it is added to the editor. |
Initialization
WSCode can help you debug your map initialization process by identifying the causes of script errors or map crashes. It also offers its own initialization methods, similar to Total Initialization, but with a few advantages when it comes to debugging.
We cannot set breakpoints via the editor during map initialization, so instead, we use a WSCode.BreakHere() call to halt the map script. Once the map initialization completes, the editor will initialize and pull up the halted script file. The script must be compiled in debug mode by adding ---@debug token.
The behavior of your map script now depends on the initialization method used:
- WSCode Initialization: The entire map script will be halted. Blizzard functions will be executed as normal. The map initialization will resume once the halted script reaches the end.
- Any other initialization: The script file will be halted, but all other scripts will initialize as normal. If another script depends on the script being halted, it will most likely throw an error.
WSCode initializers offer additional diagnostic options, such as the INIT_DUMP, which will write the last map script being initialized into a text file in the CustomMapData folder, potentially preserving your sanity when trying to find the cause of a game crash. Once you have identified the map script responsible, add a breakpoint with WSCode.BreakHere() and go through line by line until the game crahses. If it fails to crash, this probably means that you are calling a Blizzard native before it is safe to do so.
How to use WSCode Initializers
To use a WSCode initializer, wrap your function in a WSCode.Init call, similar to TotalInitialization:
Lua:
WSCode.InitGlobal(function()
--Do stuff
end)The initializers and their order of execution are:
- WSCode.InitMain
- WSCode.InitGlobal
- WSCode.InitTrig
- WSCode.InitMap
- WSCode.InitFinal
Unlike TotalInitialization, WSCode initializers do not use a Require function. Instead, the order of execution is solely determined by the order in which the initializers are called. Therefore, we must control the order of execution of the root of our script files. This is done with the ---@require token. This command tells the Parse function to wait with executing the current map script until all requirements have been executed.
Using WSCode, a script file may look like this:
Lua:
---@beginFile Cataclysm
---@require SpellLibrary, TriggerUtils?
do
local MAX_RANGE = 500*SpellLibrary.RangeModifier --Guaranteed to execute after SpellLibrary, so the SpellLibrary table exists.
WSCode.InitGlobal(function()
SpellLibary.CreateSpell("Cataclysm") --Any InitGlobal function inside SpellLibrary executes first, so the CreateSpell function is guaranteed to work properly.
if TriggerUtils then
TriggerUtils.CreateTrigger("Cataclysm")
end
end)
endIn the unlikely case that you need even more control over the order of initialization, you can set the optional onWhichPass parameter.
Using WSCode initializers has the downside that the map now depends on WSCode to function and it cannot be removed anymore without additional work. The best way to handle this problem is to use the FINALIZE_MAP_SCRIPT option in WSCode and reimport the merged map script or to use the equivalent tool in the WSCodeFileHandler external script.
InitEssential
WSCode.InitEssential is an initializer variant that should be used for libraries that the map or the editor cannot function without. It is executed right before InitFinal and is executed even if the initialization has been halted with a breakpoint.
In-Game Code Editing
WSCode is an excellent tool to prototype code snippets and adjust numbers quickly. Tabs you create and code you write into those tabs are saved between game sessions, so you don't have to worry about losing your progress by not writing in your external editor. You can also pull files from your map script or from Preload files with the respective buttons at the bottom. If your changes require a map restart, you can use the Quick Restart button at the bottom to save the altered map script, then restart the map.
Adjusting numbers
Variables stored in tables or as upvalues can be adjusted directly with the Assign button in the context menu. You can also recompile your library with the numbers adjusted. However, this may break things if you're not careful.
Manipulating game objects
If you want to, for example, fine-tune a special effect or fix the position a frame, you can use the Expose button in the context menu. This will store the selected variable in a global, which you can then manipulate in a second tab.
Fixing functions
Redefining buggy functions requires a complete recompilation of the library. Adjust the faulty logic inside the function, then press Execute to recompile the library. This will have, in many situations, unintended side effects and cause errors unless the compiler setting that protect against those errors are correctly set:
- The Smart Persist flag ensures that upvalues inside the library are not overwritten with uninitialized values. For example, you could have a data table that is initialized as an empty table and filled by external function calls. If the Smart Persist flag is not set, it would overwrite that table and delete all stored data when you recompile the library.
- WRAP_IN_CALLER_FUNCTIONS must be enabled in the config and the library must have already been compiled in debug mode the first time. Otherwise, existing function references will not be correctly updated to the newly compiled function.
- The Run Init flag must be disabled in most cases to make sure handles are not created a second time.
One important problem to solve is how you keep the changes you make in-game synced with your external files. One way to solve this problem is to do all changes in your external IDE, then copy & paste the entire modified script files into the in-game editor. If a pasted chunk contains a "beginFile" anywhere, it is recognized as a script file and automatically added to the appropriate tab, overwriting the code previously stored in that tab. All you need to do is focus the editor's text field anywhere and press Ctrl + V.
Wrapping GUI Triggers
If your map has GUI triggers in it, you can choose to wrap them with the Parse function, so that they can be pulled into the editor just like any Lua script.
The lowest script file in your map will be compiled above all GUI triggers, so if you close the Parse wrap with a script file containing ]===]), only the Lua scripts will be wrapped. To wrap your GUI triggers as well, you have to create a trigger instead with the only content being a custom script line:
-
Custom Script: end ]===]) if true then
Put this trigger below all other triggers to wrap them.
If you disable this trigger and attempt to re-enable it, you will get an error because the syntax checker will think that this is invalid syntax. To circumvent this, you have to remove the custom script line, enable the trigger, then add it again.
To transpile a GUI trigger into debug mode, you can add a custom script line with the content --@debug anywhere in your trigger. Alternatively, you can create a boolean variable called wsdebug and add the line
-
Set VariableSet wsdebug = true
to the trigger.
Enabling Quick Restart
Quick Restart is a powerful feature that allows you to do changes to your map script and reload the map without recompiling your map or relaunching Warcraft 3. This is achieved by dumping the map script you edited in the in-game editor to a text file, then discarding the default map script and loading the script from that text file instead.
This feature is disabled by default because it is not compatible with the default version of TotalInitialization that most people are using. You can enable it in the config.
Because the Preload natives that are used to read from text files are not available in the Lua root, reading in the map script is postponed until main. Even if the map script is loaded regularly, reading it in is postponed as WS Code cannot know whether to use the regular map script or the one from the text file until it reads that information from another text file. This does not cause any issues that I know of, but there might be some scripts that rely on being initialized in the Lua root.
To make your map compatible with Quick Restart, update your TotalInitialization to the custom version by @Insanity_AI or, better yet, switch to WSCode initializers. Whether Quick Restart works with that version of TotalInit is untested, but it should work in theory. WSCode initializers are guaranteed to work.
Using Quick Restart
Once Quick Restart is enabled, the Quick Restart button appears in the bottom row. Note that, if you accidentally restart the map from the F10 menu out of habit, all changes made since the last regular map launch are lost.
The best way to make quick changes and keep them synced with your IDE is to edit the script files in your IDE, then copy&paste them into the editor before restarting.
Debugging replays
Quick Restart also allows you to modify the map script before watching a replay, mostly by adding print statements that help you investigate bugs that appeared in the game prior. Modifying the map script by changing the game logic or creating additional objects will most likely desync the replay and is not advised.
来源:Hiveworkshop · hiveworkshop.com