-
-
Notifications
You must be signed in to change notification settings - Fork 56
Macro
This article serves as both a beginner's guide and a reference document on how to use and create macros for ArcCreate Editor. If you are not interetested in creating macros, but only using ready-made ones, feel free to read part 1 and skip the rest. But for those who wishes to create their own macros, this guide will assume basic familiarity with the scripting language used - Lua. Please refer to the official tutorial for Lua as necessary.
Macros are external, customizable scripts made for automating tasks. They are essentially mini-programs that will run within ArcCreate, and will tell ArcCreate what to do based on different actions and inputs from the user.
Macros are great for common tasks that are tedious and time consuming to do manually but its steps can easily be described. Examples include: Offsetting notes position, splitting arcs into segments, generating decorative traces, etc.
By default ArcCreate come bundled with some default macros. But if you want to install more macros made by other users, then refer to the instructions below.
Macros are defined within .lua
script files, and they must be placed within the Macros
folder of your ArcCreate installation. You can easily navigate to this folder by opening ArcCreate, then in Toolbox
, click the arrow on the Macro
section, then select Open macros folder
.
After inserting .lua
script files into the correct folder, select Reload macro scripts
, you will be able to see all registerd macros listed in the macro list.
On ArcCreate's startup, and any time you choose to refresh the macro list, the program will look for all valid .lua
script within the Macros
folder, as located above.
Navigate through the folders to find the macro you want to use, and click on it to start the macro. Exactly what happens next depend on the macro's content, so if you aren't sure, contact the script's writer(s).
The default ArcCreate macros come with a help dialog (Built-in macros > Help
), please refer to this for instruction on all built-in macros.
This section serves as a tutorial for getting started with creating macro scripts.
ArcCreate can automatically generate lua files to support autocompletetion in text editors such as VSCode. Within the expanded Macro list, select Generate EmmyLua
to generate the files.
It's recommended that you use the addon Lua
by sumneko, and within VSCode, simply open the generated lib.lua and autocompletion should work. If you're bothered by the warnings that pop up in the generated lib.lua, you can disable all warnings for the file.
The generated lib.lua can also serve as an up-to-date API documentation as well, complete with description about each method and their parameters.
The first example will be extremely basic. The goal is to understand the basic structure of a macro.
First, navigate to the Macros folder as located in Part 1, then create a file named helloWorld.lua
and write the following:
File:
helloWorld.lua
Macro.new("helloWorld")
.withName("Hello World!")
.withDefinition(function()
notify("Hello")
end)
.add()
Be sure to save this file, then within ArcCreate choose Reload macro scripts
. You should be able to see a new macro pop up in the list with the name "Hello World!". If you press it, a text message pops up in the notification tray saying "Hello".
Let's unpack what's happening here, line by line.
Macro.new("helloWorld")
...
First, we create a new macro with the identifier helloWorld
. This identifier must be unique, attempting to add another macro with the same identifier will instead remove the old one. Additionally, macros in the list are sorted by their identifier.
...
.withName("Hello World!")
...
Next, we give it a name to display in the macro list, Hello World!
.
...
.withDefinition(function()
...
end)
...
This syntax might look weird if you're unfamiliar with closures, in which case, you only have to care that you should put code that should be run whenever the macro is activated within ...
Here, the macro code is:
...
notify("Hello")
...
This prints the message to the notification tray in ArcCreate.
After all of this, don't forget to run .add()
at the end in order for the macro to be properly registered. (without .add()
, nothing will happen)
...
.withDefinition(function()
...
end)
.add()
Note: You can give your macro an icon so it's more easily identifiable in the editor
Macro.new("helloWorld") .withIcon("e887") -- This gives the macro a question mark icon ...The string "e887" is the material icon unicode string corresponing to the icon "Help". You can look for icons at https://fonts.google.com/icons. After selecting the icon you want, the value you should copy is under "Code point"
Note: you can define different properties of a macro in any order. Just make sure to start with defining the identifier with
.new()
and end with registering the macro with.add()
.This is valid:
Macro.new("helloWorld") .withName("Hello World!") .withIcon("e887") .withDefinition(function() notify("Hello") end) .add()So is this:
Macro.new("helloWorld") .withDefinition(function() notify("Hello") end) .withIcon("e887") .withName("Hello World!") .add()
We'll jump right into the next example, which will probably be the most common task you'll do when working with macros - creating notes.
Since you'll be working with a chart here, prepare a test project to play around. Then create addNote.lua
where we will put our code for this example.
Let's create the macro first, we'll borrow the code from Example 1.
Macro.new("addNote")
.withName("Add note")
.withIcon("e145")
.withDefinition(function()
end)
.add()
We're using the icon e145 which is the "Plus" icon. The macro definition is left empty, we'll add code to it soon.
Now, we'll explore how to create each note type. First, tap note can be created like so:
...
.withDefinition(function()
-- Defines a new tap note at 1000ms, on lane 1, timing group 0
local tap = Event.tap(1000, 1, 0)
-- Create a command that saves the tap note
local command = tap.save()
-- Execute the command
command.commit()
end)
...
We start by defining a note with Event.tap(1000, 1, 0)
, then storing it in the variable tap
.
Then by calling .save()
, you creates a command that saves the defined note into the chart. At this point, this command has not been run yet, and it's not until you call .commit()
that the tap is actually added into the current chart.
Note: the code above can be shortened to the following. We skip assigning to variables and instead chain commands directly.
... .withDefinition(function() Event.tap(1000, 1, 0).save().commit() end)
It might seem unnecessary have to explicitly call .commit()
to run a command, but the reason is that we want to be able to create commands can add multiple notes, and the user will be able to undo all of them at once. Let's do it the wrong way first:
...
.withDefinition(function()
-- First tap note
local tap1 = Event.tap(1000, 1, 0)
local command1 = tap1.save()
command1.commit()
-- Second tap note
local tap2 = Event.tap(1000, 2, 0)
local command2 = tap2.save()
command2.commit()
end)
...
If you try running this code, you will find that it runs completely fine and will create two tap notes at lane 1 and 2 as expected. However, if you undo, then only one note at the time is removed from the chart. Imagine if your macro creates hundred of notes at once, this will be very inconvenient to have to undo hundred of times to completely revert a change.
Let's do it the right way this time. The key is that we use one command to add multiple notes.
...
.withDefinition(function()
-- Create an empty command
local command = Command.create("Test command")
local tap1 = Event.tap(1000, 1, 0)
command.add(tap1.save())
local tap2 = Event.tap(1000, 2, 0)
command.add(tap2.save())
command.commit()
end)
...
This time, we don't commit each changes right away, but store them inside of the variable command
. At the end, by calling .commit()
, we are able to execute both commands at once and have it registered as a single undo-able action in the ArcCreate Editor.
Note: the code above isn't the only way to write this. For example, you can shorten the code a bit, this is how you'll most likely see macros being written:
... .withDefinition(function() local command = Command.create("Test command") command.add(Event.tap(1000, 1, 0).save()) command.add(Event.tap(1000, 2, 0).save()) command.commit() end) ...Alternatively you can provide an array of commands like this. This is a newer syntax so you won't see it being used much.
... .withDefinition(function() Command.create("Test command", { Event.tap(1000, 1, 0).save(), Event.tap(1000, 2, 0).save() }) .commit() end) ...
Let's now try to create other note types
...
.withDefinition(function()
local command = Command.create("Test command")
command.add(Event.tap(1000, 1, 0).save())
command.add(Event.hold(1000, 2000, 2, 0).save())
command.add(Event.arc(
2000, 0, 1, -- Start at 2000ms, at x=0,y=1
3000, 1, 1, -- End at 3000ms, at x=1, y=1
false, -- Is an arc
0, -- Color blue
's', -- Type s
0 -- Timing group 0
).save())
-- Sky note/arctap are a bit tricky, they require a trace to be created first.
local trace = Event.arc(
3000, 0, 1, -- Start at 2000ms, at x=0,y=1
4000, 1, 1, -- End at 3000ms, at x=1, y=1
true, -- Is a trace
0, -- Color blue
'si', -- Type si
0 -- Timing group 0
)
local arctap = Event.arctap(3500, trace)
command.add(trace.save())
command.add(arctap.save())
command.commit()
end)
...
That's all 5 main note types. You can add timing, camera and scenecontrol events too, feel free to browse the EmmyLua file for documentation.
Complete code:
The next macro will delete every single notes present in the chart. (It's fine since you can undo it anyway)
You'll need to do two things here: getting all the notes in the chart, and deleting them. Let's tackle them one by one. First create removeNotes.lua
and define our macro:
Macro.new("removeAllNote")
.withName("Remove all notes")
.withIcon("e872")
.withDefinition(function()
end)
.add()
First let's try to retrieve the notes. The function to call is Event.query()
.
...
.withDefinition(function()
local allNotes = Event.query(EventSelectionConstraint.create().any())
end)
...
We have to supply what notes we want to Event.query()
. In this case, we want all notes so we use .any()
Note: You can query for specific types of notes too. For example, if you want to delete all arcs and traces:
Event.query(EventSelectionConstraint.create().arc())We'll explore more about EventSelectionConstraint in later chapters.
Now let's confirm that we actually have the notes data stored in the allNotes
variable.
local allNotes = Event.query(EventSelectionConstraint.create().any())
local tapNotes = allNotes.tap
notify(#tapNotes) -- #tapNotes is lua syntax for length of the array
If you try running this now, you'll find that the number of tap notes currently in the chart will be outputted into the notification tray. Try doing this for the other note types (hold
, arc
, arctap
, timing
, camera
, scenecontrol
) if you want to.
Let's now delete the notes.
local command = Command.create("Delete all notes")
-- Get the array of tap notes
local tapNotes = allNotes.tap
-- Iterate over each tap note in the array
for _, tap in ipair(tapNotes) do
-- Add the delete command
command.add(tap.delete())
end
-- Shorter version
for _, hold in ipair(allNotes.hold) do
command.add(hold.delete())
end
for _, arctap in ipair(allNotes.arctap) do
command.add(arctap.delete())
end
for _, arc in ipair(allNotes.arc) do
command.add(arc.delete())
end
for _, timing in ipair(allNotes.timing) do
command.add(timing.delete())
end
for _, sc in ipair(allNotes.scenecontrol) do
command.add(sc.delete())
end
for _, cam in ipair(allNotes.camera) do
command.add(cam.delete())
end
command.commit()
This macro does exactly what we want: within a single command deleting every note of every note type in the chart.
Note: if you're good at lua, you can shorten the code above further like this
local command = Command.create("Delete all notes") for _, eventType in ipair({"tap", "hold", "arc", "arctap", "timing", "camera", "scenecontrol"}) do for _, event in ipair(allNotes[eventType]) do command.add(event.delete()) end end command.commit()
Complete code:
We'll go back creating notes again, but we'll allow the user to specify where the notes will be created instead.
Our macro will ask the user for the timing and position, and then will generate a trace art pattern onto the selected position. First, let's handle the retrieving user input part first.
Create traceArt.lua
, and create our macro as usual
Macro.new("traceArt")
.withName("Generate trace art")
.withIcon("f1d1")
.withDefinition(function()
end)
.add()
We'll use the TrackInput
class to request both timing and position from the user.
local timingRequest = TrackInput.requestTiming()
coroutine.yield() -- VERY IMPORTANT. Pause script until the user provides timing selection.
local timing = timingRequest.result.timing
notify(timing)
We first make a request for timing selection with TrackInput.requestTiming()
. Then we pause the script until this request is complete, after which we can retrieve the selected timing value.
Try running this code, you should see that the notify()
line will output the correct timing value.
The next step is similar, we'll request for the position on the vertical grid instead.
local timingRequest = TrackInput.requestTiming()
coroutine.yield()
local timing = timingRequest.result.timing
local positionRequest = TrackInput.requestPosition(timing)
coroutine.yield()
local positionX = positionRequest.result.x
local positionY = positionRequest.result.y
notify(positionX.."-"..positiony)
The most important difference is that we need to provide the timing at which we want to grab the vertical position of.
Now, with all the parameters necessary, let's create the trace art. We'll make a simple diamond trace, but you can do anything you want here.
local timingRequest = TrackInput.requestTiming()
coroutine.yield()
local timing = timingRequest.result.timing
local positionRequest = TrackInput.requestPosition(timing)
coroutine.yield()
local positionX = positionRequest.result.x
local positionY = positionRequest.result.y
local timingGroup = Context.currentTimingGroup
local command = Command.create("Diamond trace art")
command.add(Event.arc(
timing, positionX - 0.25, positionX
timing, positionY, positionY + 0.5
true, 0, 's', timingGroup))
command.add(Event.arc(
timing, positionX, positionX + 0.25
timing, positionY + 0.5, positionY
true, 0, 's', timingGroup))
command.add(Event.arc(
timing, positionX + 0.25, positionX
timing, positionY, positionY - 0.5
true, 0, 's', timingGroup))
command.add(Event.arc(
timing, positionX, positionX - 0.25
timing, positionY - 0.5, positionY
true, 0, 's', timingGroup))
command.commit()
One extra feature was added, in that we don't simply add the notes to the base timing group, but we retrieve the currently editing timing group with Context.currentTimingGroup
.
If you try running this macro now, you will see the diamond trace pattern being generated around the selected timing and position.
Complete code:
TrackInput works well for what its intended to capture, timing and position. But for more complex input it's better to request the data from user in the form of a dialogue.
We will apply the same "request & wait" steps back to example 4. Create dialogTest.lua
this time, then let's define the macro as usual
Macro.new("dialogTest")
.withName("Dialog Test")
.withDefinition(function()
end)
.add()
DialogInput
is a lot more complex than TrackInput. It allows you to create a dialog with many input fields, and then after that, you can retrieve the typed data from the fields.
Let's make a dialog with a single text field first to get an idea of how it works. A text field is an input box where user can type text in.
local nameField =
DialogField.create("nameField")
.setLabel("Name")
.setTooltip("Type your name here")
.setHint("Name...")
.textField()
Label is the text shown to the left of the field. Tooltip is text shown when user hover over the field, and hint is temporary text shown on the field when it's empty.
Note the call to DialogField.create()
, here we passed "nameField" which is the identifier of this field. Each field added to a dialogue must have a unique identifier, because later on we will use this identifier to retrieve the text typed into this dialog field.
With the field, we can now create the dialogue
...
local dialogRequest =
DialogInput
.withTitle("Test dialog")
.requestInput({nameField})
We create a dialog with the title "Test dialog", and then request with a list of fields - in which case the list only contains one field we defined earlier.
Now we pause the script similar to Example 4, then retrieve the typed input and print it out to the user.
...
local dialogRequest =
DialogInput
.withTitle("Test dialog")
.requestInput({nameField})
coroutine.yield()
local name = dialogRequest.result["nameField"]
notify("Hello, " .. name)
Remember the string we passed to DialogField.create()
? We use the same string to access the correct value in dialogRequest.result
.
This works for basic text input, but if we want to request numbers from the user then it would be nice to only allow number input. Let's modify the code a little
local ageField =
DialogField.create("ageField")
.setLabel("Age")
.setTooltip("Type your age here")
.setHint("Age...")
.textField(FieldConstraint.create().integer())
local dialogRequest =
DialogInput
.withTitle("Age confirmation")
.requestInput({ageField})
coroutine.yield()
local age = dialogRequest.result["ageField"]
local agePlusOne = tonumber(age) + 1
notify("In 1 year, your age will be " .. agePlusOne)
There are other advanced things you can do with FieldConstraint too, feel free to browse the auto-generated EmmyLua doc for more information.
Complete code:
Our next example is going to be practical, we'll make macro that converts an arc into dense arcs made of multiple segments. We will be working with retrieving note selection this time.
Make a macro again, at arcSplit.lua
Macro.new("arcSplit")
.withName("Split arcs")
.withDefinition(function()
end)
.add()
We will request the user to select an arc, and then we will do further processing on the selected arc note. We will make use of EventSelectionInput
for this.
local request = EventSelectionInput.requestSingleEvent(
EventSelectionConstraint.create().solidArc()
) -- Ask user to select a single solid arc note
coroutine.yield() -- Wait for response
local arc = request.result.arc[1]
The structure should look very familiar to you now. This time in the result, we use request.result.arc[1]
, since request.result.arc
returns an array of arc notes that might contain more than one note. [1]
specifies we want the first item
Note: Lua indexes from 1 instead of 0 like most languages.
Here we've constrained the selection to only solid arc notes, excluding traces. If you want to include traces as well, use EventSelectionConstraint.create().arc()
With the selected arc stored in the variable arc
, we can do further processing with it
local command = Command.create("Split arc")
-- We will calculate the length of each segment to be based on the beatline density
local arcLength = Context.beatLengthAt(arc.timing) / Context.beatlineDensity
-- Create the arc segments
for timing = arc.timing, arc.endTiming, arcLength do
endTiming = math.min(timing + arcLength, arc.endTiming)
-- If the segment length is too short then stop creating segments
if (math.abs(endTiming - timing) <= 1) then break end
-- Grab the positions from the original arc
startXY = arc.positionAt(timing)
endXY = arc.positionAt(endTiming)
-- Add the segment
command.add(
Event.arc(
timing, startXY,
endTiming, endXY,
false,
arc.color,
arc.type,
arc.timingGroup
).save()
)
end
-- Delete the original arc
command.add(arc.delete())
command.commit()
The core idea remains simple, but a few other methods and properites are being used here that hasn't been introduced before. You can more details about all of them in the EmmyLua files as usual.
Complete code:
ArcCreate v1.2 introduces the ability to undo/redo timing group actions. The macro Lua api has also been updated to accomodate for this change. Now timing group actions can also be added to commands just like how regular notes are.
Let's explore this ability by creating a timing group with a single tap note inside in this example. Create timingGroupTest.lua
:
Macro.new("timingGroup")
.withName("Split arcs")
.withDefinition(function()
end)
.add()
First, create the timing group.
local group = Event.createTimingGroup()
Note: if you want to add more timing group properties, the syntax is exactly like .aff format
local group = Event.createTimingGroup("noinput,arcresolution=2")Refer to timing group documentation for more information
Of course, as it is the group is not added to the chart yet. We have to call save and commit, just like regular notes. Before that, let's set up our command and tap note.
local command = Command.create()
local group = Event.createTimingGroup()
local tap = Event.tap(1000, 1)
We ran into a problem, what number do we use for the tap's timing group number? Since the timing group is not yet added to the chart, we don't know what its group number is.
Note: If you try to assign a number that's not yet of a valid timing group to a note, it'll throw an error. So guessing the number isn't possible either.
The answer is to tell ArcCreate to assign the timing group number after the group is added to the chart. Use withTimingGroup()
like below to achieve this:
local command = Command.create()
local group = Event.createTimingGroup()
local tap = Event.tap(1000, 1)
command.add(group.save())
command.add(tap.save().withTimingGroup(group))
command.commit()
This code will successfully create a new timing group and tap note belonging to the group. When the command is commited, ArcCreate will first add the group to the chart, then assign its group number to the tap note, and then finally add the note.
Complete code:
- Remember to always use
local
for assigning local variable - Error message can be hard to read within ArcCreate. You can open the log file by going to
%localappdata%low/Arcthesia/ArcCreate/Player.log
on Windows (if you use other OS, figure it out yourself) - If you plan to share your macros, consider prefixing your macro with something unique (default macros follow the format
zero.{category}.{name}
) to avoid collision - Any time you see
a["b"]
, you can alternatively writea.b
and vice versa (this is a lua feature). For example,request.result["arc"]
can be written asrequest.result.arc