Tutorial on how to create and use macros on ArcadeZero
This document serves as both a beginner's guide and a reference document on how to use and create macros for ArcadeZero v3.3 and later. If you are not interetested in creating macros, but only using ready-made ones, feel free to skip any code here. But for those who wishes to create their own macros this guide will assume basic familiarity with the scripting language used - Lua.
I highly suggest that you will read through the whole thing anyway, to be able to troubleshoot any macros yourself. If you're new or would like a refresher on the basic of Lua, please refer to the official tutorial: https://www.lua.org/pil/contents.html
Tools you will need:
- ArcadeZero v3.3 or later, or any forks of ArcadeZero that supports macros
- For using ready-made macros: .lua macro scripts obtained from other sources.
- For creating macros: a sufficiently good IDE / text editor is recommended (such as VSCode)
This document will be divided into two parts:
- Using macro: where concepts and basic usages are explained
- Creating macro: how to create your own macro
- Reference: a comprehensive of all available classes and methods
Macros are external, customizable scripts made for automating tasks. They are essentially mini-programs that will run within Arcade, and will tell Arcade 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.
macros are defined within a single .lua
script file (support for other script file referencing is coming soon), and they must be placed within the Macros
folder of your Arcade installation. For example, if your Arcade is installed at D:\Programs\Arcade\
, then place your .lua
script, mirror.lua
for example, in: D:\Programs\Arcade\Macros\mirror.lua
.
Within your ArcadeZero, locate the Macro button on the bottom right corner (the button with a puzzle piece icon). The Macro selection menu should show up. You will see any registered macros in here, and if you don't, press the refresh button (top right of the menu).
On Arcade'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. You can click on the name of the macro you want to use and the macro will start. Exactly what happens next depends on the script (that's the whole point!), so if you aren't sure please contact the script's writer(s).
Any invalid script will prompt an error message on the screen. If you see one, that just means that that macro will not be usable. Either fix it yourself, or again, contact the script's writer(s) for support.
If you saw the documentation for Arcade's Scenecontrol, we'll be doing something similar here! We'll walk through a few example, with detailed explanation on what the code does, and some caveats you have to take note of.
This section is only meant to get the reader familiar with the process and logic of writing a macro script. If you want a comprehensive list of all things you can do with a macro, refer to Part 3.
This extremely basic example will teach you the basic structure of a macro. There's quite a lot to be unpacked here actually!
-- helloWorld.lua
addMacro("helloWorld", function()
notify("Hello World!")
end)
Let's see what this does first! Be sure to save this file in the Macros
folder of your ArcadeZero installation folder, then boot up ArcadeZero. Open your ArcadeZero, and select the macro helloWorld
as instructed in 2.2 and 2.3. You should see the a toast notification (top middle) displaying the text "Hello World!".
Let's unpack what's happening here.
All macros will be registered into Arcade with the function addMacro
. Note that you can have as many of these addMacro()
as you want, and Arcade will register all of them.
- The first parameter specify the macro name, that will display as buttons within the Macro selection menu. In this case, it's
helloWorld
- The second parameter is actually a function, which gives instruction on what should happen each time the macro is run. You might not be very familiar with the concept of passing functions as argument into other functions, but this is the only instance that you will need to do this, so don't worry too much
- Within the function body, we give a single instruction: notify the user with the message "Hello World!"
NOTE! Macro names must be unique. If you try to create another macro with "helloWorld" within the same script, or in another script, an error will be thrown and only the first macro will be accepted.
For the exploratives out there, try these out for yourself:
- Passing numbers to
notify
- Passing more than 1 arguments to
notify
- Try
log
instead ofnotify
- Try joining strings with
..
(look up what it does in the lua tutorial!) - Removing the macro (try to look it up in part 3!)
We'll jump right into the next task, which will probably be the most common task you'll do when working with macros - creating notes.
The last example didn't require opening an actual project. But since you'll be working with a chart file here, prepare a blank test project to play around.
Let's look at a simple script first:
-- addNote.lua
addMacro("addNote", function()
local tap = Event.tap(1000, 1, 0)
-- A tap note at 1000ms, lane 1, timingGroup 0
local command = tap.save()
-- Command is an action of saving the tap note
command.commit()
-- Actually save the tap note
end)
That was quite a lot of steps wasn't it, but trust me they all have a purpose. In fact you will probably never write a macro to create a single note. We'll see what all those steps are for in a bit, but for now, I'll have you fail on purpose.
FYI the above can be shortened to
-- addNote.lua addMacro("addNote", function() tap = Event.tap(1000, 1, 0) tap.save().commit() end)or even
-- addNote.lua addMacro("addNote", function() Event.tap(1000, 1, 0).save().commit() end)Which do the same thing but with less code.
Now let's try adding multiple notes, and while we're at it, let's create other note types as well
-- addNote.lua
addMacro("addNote", function()
local tap = Event.tap(1000, 1, 0)
local command = tap.save()
command.commit()
local hold = Event.hold(1000, 2000, 1, 0)
-- A hold note at 1000ms, last until 2000ms, lane 1, timingGroup 0
hold.save().commit()
local arc = 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
)
arc.save().commit()
local trace = Event.arc(
3000, 1, 1, -- Start at 3000ms, at x=1,y=1
4000, 0, 0, -- End at 4000ms, at x=0, y=0
true, -- Is a trace
0, -- Color blue
'sisi', -- Type sisi
0 -- Timing group 0
)
-- FYI you don't have to add so many line breaks,
-- but I still recommend keeping your code easy to read.
trace.save().commit()
local arctap = Event.arctap(2500, trace)
-- Arctaps are a bit tricky! They are naturally bound to a trace
-- So you must create a trace before creating an arctap
arctap.save().commit()
end)
There you go, all 5 main note types. Go ahead and run the macro, and you should see 5 notes appear in your chart. Perfect! (right?)
Okay here's the problem. Your current macro created 5 separate undo items, so it takes 5 times pressing CTRL+Z to undo everything your macro did. Imagine if you want to write a macro that changes hundreds of notes at once, that'd be nightmarish to use.
Let's fix that right now!
-- addNote.lua
addMacro("addNote", function()
local batchCommand = Command.create()
local tap = Event.tap(1000, 1, 0)
batchCommand.add(tap.save())
local hold = Event.hold(1000, 2000, 1, 0)
batchCommand.add(hold.save())
local arc = Event.arc(
2000, 0, 1,
3000, 1, 1,
false,
0,
's',
0
)
batchCommand.add(arc.save())
local trace = Event.arc(
3000, 1, 1,
4000, 0, 0,
true,
0,
'sisi',
0
)
batchCommand.add(trace.save())
-- You can also shorten like this
batchCommand.add(
Event.arctap(3500, trace).save()
)
batchCommand.commit()
end)
Run it, and you should see that one undo will delete all 5 notes! Similar for redo as well.
Try these out for yourself:
- Use a for loop to add notes
- Try different parameters for notes. Be careful with timingGroups though, as specifying an invalid timing group will cause an error
- Pass a string into
Command.create()
(e.g.Command.create("test macro")
).
Our next example is utterly useless. This macro will send your chart to the heaven realm by wiping every single note out of existence. (not really since you have backups, but just for fun)
You'll need to do two things here: getting all the notes in the chart, and deleting them. Let's look at the script:
-- sakuzyo.lua
addMacro("sakuzyoBeam", function()
local allNotes = Event.query(EventSelectionConstraint.create().any())
local batchCommand = Command.create("SELF DESTRUCTION")
for i = 1, #allNotes["tap"], 1 do
batchCommand.add(allNotes["tap"][i].delete())
end
for i = 1, #allNotes["hold"], 1 do
batchCommand.add(allNotes["hold"][i].delete())
end
for i = 1, #allNotes["arc"], 1 do
batchCommand.add(allNotes["arc"][i].delete())
end
for i = 1, #allNotes["arctap"], 1 do
batchCommand.add(allNotes["arctap"][i].delete())
end
for i = 1, #allNotes["timing"], 1 do
batchCommand.add(allNotes["timing"][i].delete())
end
for i = 1, #allNotes["camera"], 1 do
batchCommand.add(allNotes["camera"][i].delete())
end
batchCommand.commit()
end)
A few things worth noticing here
- You use
Event.query
to gather the notes within the active chart file. - You must pass into
Event.query
aEventSelectionConstraint
, which will limit what notes will be returned to us. Here we specify asany()
, which means anything goes. - The query returns 6 different arrays of 6 different event types. You can access them with
["{type}"]
- Here we're looping over all of them and add a delete command for each note to the batch command, hence deleting everything. Note a special exception for timing: you can't delete timing with starting time of 0ms (Arcade will just ignore your request).
You'll see EventSelectionConstraint
again later on, so try get familiar with them now.
We'll go back to example 2. This time instead of creating notes at 1000ms, let's allow specifying where we will create the notes.
-- addNote.lua
addMacro("addNoteParam", function()
local request = TrackInput.requestTiming() -- Ask user to specify a timing
coroutine.yield() -- Wait for response
timing = request.result["timing"]
local batchCommand = Command.create()
local tap = Event.tap(timing, 1, 0)
batchCommand.add(tap.save())
local hold = Event.hold(timing, timing + 1000, 1, 0)
batchCommand.add(hold.save())
local arc = Event.arc(
timing + 1000, 0, 1,
timing + 2000, 1, 1,
false,
0,
's',
0
)
batchCommand.add(arc.save())
local trace = Event.arc(
timing + 2000, 1, 1,
timing + 3000, 0, 0,
true,
0,
'sisi',
0
)
batchCommand.add(trace.save())
-- You can also shorten like this
batchCommand.add(
Event.arctap(timing + 2500, trace).save()
)
batchCommand.commit()
end)
Let's focus on the snippet that matters the most here
local request = TrackInput.requestTiming() -- Ask user to specify a timing
coroutine.yield() -- Wait for response
local timing = request.result["timing"]
This snippet's purpose is to get input from the macro user, specifically, where the user wants to create our pattern at. TrackInput.RequestTiming()
will switch Arcade into timing input mode, where the user have to click on the track (similar to how you click on a track to create a tap note, for example), to specify the timing.
Since the user will take some time to do so, and your lua code runs immediately, we use coroutine.yield()
to suspense the macro's execution. Arcade will resume our macro once the user has inputted the requested information (or in this case, after they clicked on the track where they want to create our pattern).
The request
variable is where Arcade will pass that information to our macro, and after coroutine.yield()
we can be sure that that information is valid, and we proceed with retrieving it with timing = request.result["timing"]
. That timing variable now contain the timing that the user specified.
The rest is simple! We just offset our pattern with whatever timing
is. Go ahead and give the macro a try!
Try these out for yourself:
- Try
TrackInput.requestPosition(timing)
. This is a bit tricker to use, you have to pass in the timing parameter which will specify where the vertical input plane will be placed. Normally you use aTrackInput.requestTiming()
then feed the result toTrackInput.requestPosition(timing)
- Try requesting and waiting twice
Let's apply the same "request & wait" steps back to example 5. This time we'll create a confirmation box, to make sure that our user didn't misclick and accidentally wipe their chart file for no reason.
This time we're using DialogInput
-- sakuzyo.lua
addMacro("sakuzyoBeam", function()
local codeField =
DialogField.create("code")
.setLabel("Confirmation")
.setTooltip("Your confirmation code is 61616")
.setHint("INPUT CONFIRMATION CODE")
.textField(FieldConstraint.create().integer())
local request =
DialogInput
.withTitle("INITIATING SELF DESTRUCTION")
.requestInput({codeField})
coroutine.yield()
local inputtedCode = request.result["code"]
if inputtedCode != "61616" then
notify("Destruction sequence shut down")
return
end
local allNotes = Event.query(EventSelectionConstraint.create().any())
local batchCommand = Command.create("SELF DESTRUCTION")
for i = 1, #allNotes["tap"], 1 do
batchCommand.add(allNotes["tap"][i].delete())
end
for i = 1, #allNotes["hold"], 1 do
batchCommand.add(allNotes["hold"][i].delete())
end
for i = 1, #allNotes["arc"], 1 do
batchCommand.add(allNotes["arc"][i].delete())
end
for i = 1, #allNotes["arctap"], 1 do
batchCommand.add(allNotes["arctap"][i].delete())
end
for i = 1, #allNotes["timing"], 1 do
batchCommand.add(allNotes["timing"][i].delete())
end
for i = 1, #allNotes["camera"], 1 do
batchCommand.add(allNotes["camera"][i].delete())
end
batchCommand.commit()
end)
Dialog boxes are considerably more complicated. For once you can create multiple field within the same dialog box, and then read values from all of them. Second you have to make sure our user can only input the right format (like integer only, or maybe we allow both integer and decimal but not alphabetical characters...). These complications are all dealt with in DialogField
This snippet
local codeField =
DialogField.create("code")
.setLabel("Confirmation code")
.setTooltip("Your confirmation code is 61616")
.setHint("INPUT CONFIRMATION CODE")
.textField(FieldConstraint.create().integer())
reads:
codeField
is a field of a dialog box- It's created with an identification key of "code"
- The label of the field is "Confirmation code"
- When user hovers over it, the tooltip displays as "Your confirmation code is 61616"
- When nothing is inputted yet, the hint (gray text that disappears once something is typed in) displays as "INPUT YOUR CONFIRMATION CODE".
- This field is a text field, with a constraint that the content must be an integer.
This snippet
local request =
DialogInput
.withTitle("INITIATING SELF DESTRUCTION")
.requestInput({codeField})
similar to example 4, will show our dialog to the user. This dialog has a title of "INITIATING SELF DESTRUCTION", and has one field, which is our codeField
!
NOTE! You can create more than one field within a single dialog. Example
DialogInput.withTitle("Test").requestInput({field1, field2, field2})Also there are dropdown and checkbox fields as well, check out the reference document!
It might sound daunting but just mess around with it in Arcade and see what everything does. You'll get the hang of it sooner or later!
coroutine.yield()
local inputtedCode = request.result["code"]
if inputtedCode != "61616" then
notify("Destruction sequence shut down")
return
end
Finally our code waits for user input, then it retrieves the inputted code through the key "code" (we chose this key in DialogField.create("code")
). Then the rest is standard at this point.
Our next example is going to be practical, and is actually a lot easier than example 3.5. We'll make an amygdata arc converter, right within Arcade!
addMacro("amygdata", function()
local request = EventSelectionInput.requestSingleEvent(
EventSelectionConstraint.create().solidArc()
) -- Ask user to select a single arc note
coroutine.yield() -- Wait for response
local arc = request.result["arc"][1]
batchCommand = Command.create("conversion to amygdata")
local arcLength = Context.beatLengthAt(arc.timing) / Context.beatlineDensity
-- Create the arc
for timing = arc.timing, arc.endTiming, arcLength do
endTiming = math.min(timing + arcLength, arc.endTiming)
if (math.abs(endTiming - timing) <= 1) then break end
startXY = arc.positionAt(timing)
endXY = arc.positionAt(endTiming)
batchCommand.add(
Event.arc(
timing,
startXY,
endTiming,
endXY,
false,
arc.color,
arc.type,
arc.timingGroup
).save()
)
end
batchCommand.add(
arc.delete()
)
batchCommand.commit()
end)
The most noteworthy part is again, the user input part
local request = EventSelectionInput.requestSingleEvent(
EventSelectionConstraint.create().solidArc()
) -- Ask user to select a single arc note
-- voidArc() will limit to traces only
coroutine.yield() -- Wait for response
local arc = request.result["arc"][1]
...
batchCommand.add(
arc.delete()
)
We're this time asking the user to select a single arc note from the chart, as a form of asking for input values. We then retrieve the arc's timing, position, etc., then create our on arcs based on those information. Finally we delete the inputted arc, and our conversion is complete!
Please note that you still have to specify
request.result["arc"][1]
to gather the arc's data.request.result
alone won't work.
The last thing to point out is the Context
class, which provides very useful information about the current chart and settings, such as BPM, currently active arc color, currently active arc type, etc. Be sure to check out the reference document for everything you can do with this class!
Try this out for yourself:
- Convert this macro to process multiple arcs at once (hint:
EventSelectionInput.requestEvents(..)
) - Try to make it work for traces as well (hint:
.arc()
combines.solidArc()
and.voidArc()
) - This is not quite amygdata arc yet! If the arc's starting y coordinate and ending y coordinate is the same, then no "arc beams" will be created. Try expanding on this!
Starting from ArcadeZero v3.3.2, addMacro
now properly works within another macro. This opens up interesting possibilities.
Let's create a very interesting macro this time, stash
. We will create a macro that will save the users selection into a stash, and automatically add a macro that let them paste it!
This will be a rather long and complicated section that will require knowledge from all the previous examples combined. The approach will be rather different, as instead of showing you the code right away, we'll walk through the process of thinking through the problem together (there will be the full code available at the bottom of this section for your convenience).
So let's get started. Let's first decide how exactly our macro will function in concrete steps:
- Our user will select our macro
- Then our user will select the notes to be stored into a stash, then press enter to confirm the selection
- A dialog box will appear, asking for the name of the stash
- A new macro with the specified name will be created
- This macro when selected will prompt our user to select a timing point
- The stash associated with that macro will be pasted on that timing point
Each step is harder to implement than the previous one. So let's go through them in order:
First let's declare our macro (step 1):
addMacro("stash", function()
end)
And step 2 is also very easy, we just request a selection:
...
local eventRequest = EventSelectionInput.requestEvents(
EventSelectionConstraint.create().any(),
"Select notes to be included in your stash. Press ENTER to confirm")
coroutine.yield()
local events = eventRequest.result
...
So now we captured the user's selections into events. That's step 2 complete!
At this point we need to create a dialog box that ask for the stash's name. We know how to do this!
...
local dialogRequest = DialogInput.withTitle("Stash creation").requestInput({
DialogField
.create("name")
.setLabel("Stash's name")
.setHint("Enter anything")
.setTooltip("Your stash name will be included in the newly created macro's name")
})
coroutine.yield()
local name = dialogRequest.result["name"]
...
Very nice. The code I wrote here are deliberately different in syntax, but in essence they do the same thing.
Now it's all unknown territory! Let's first consider our problems:
- We first need to create a new macro, that acts differently, from the same macro.
- We need to store the different sets of notes somewhere, and our different macro will have to retrieve the right set back to actually paste back the notes.
Let's tackle them one by one. Well, let's do some testing first, and see what will happen when we create a macro after retrieving the name
...
local name = dialogRequest.result["name"]
addMacro(name, function() end)
...
Run the macro, and whatever we type into the input box, a macro with the same name gets created. It's kind of hard to navigate actually, but we'll come back to this problem later. For now we know that addMacro
works inside a macro.
Now we need the macros to do different things. And the only thing that differentiate different macros is their name. We now wonder if our macro can actually retrieve this information. Let's test it out!
...
local name = dialogRequest.result["name"]
addMacro(name, function()
notify(name)
end)
...
Run it, and when we run our new macro, the notification actually output back whatever we typed into the dialog box. This is amazing! Our stash
macro is now a macro generator, and our first problem is solved.
The second problem is a bit tricky, you'll need to be familiar with how lua tables work (or if you're familiar with hash map, or python dictionary, then you can look up how to do it in lua). If you're a bit shaky with tables, then refer to the official Lua tutorial linked at the top.
This is one way to do it, but we will create a global variable that will keep track of different stashes's name, and their selection
storedStashes = {}
addMacro("stash", ...)
You're free to try out what will happens if storedStashes = {}
is written in the body of our stash
macro (spoiler: it's reset every time we run our macro)
Anyway, every time we create a macro, we'll also add a new entry to storedStashes
like this
storedStashes = {}
addMacro("stash", function()
...
local events = eventRequest.result
...
local name = dialogRequest.result["name"]
storedStashes[name] = events
addMacro(name, function()
notify(name)
end)
...
end)
Now let's test out if our new macro can properly retrieve the stash's data. Let's write a simple test that output the number of taps within the stash
addMacro(name, function()
local stash = storedStashes[name]
notify(#stash["tap"])
end)
Give it a try and you'll see that the generated macro will output different number of taps. This is actually step 4 complete!
Only step 5 and 6 now. While it might seem simple, there's a lot of caveats left to be considered.
- We need the minimum timestamps of all notes in the stash
- We need to copy each note and change their timing, or endTiming, to an offsetted value
- We need to save all of them in a batch command
- Worst of all, handling arctaps seems like a pain!
There are many ways to solve these, but the fastest way would require us to backtrack a little. Let's change how we store our stashes a little bit so this step becomes easier. Let's change
local events = eventRequest.result
...
storedStashes[name] = events
to
-- We don't need this anymore
-- local events = eventRequest.result
newStash = {}
newStash.events = eventRequest.resultCombined
newStash.arctaps = eventRequest.result["arctap"]
storedStashes[name] = newStash
resultCombined
by the way contains all note types mixed into one table. We will find this useful.
arctaps
are stored separately since we'll need to handle them differently than other note types.
Alright, let's finish it up! First we require the timing point to paste the stash at, and create a batch command while we're at it
addMacro("stash."..name, function()
local stash = storedStashes[name]
local timingRequest = TrackInput.requestTiming(false, "Select where to paste your stash")
coroutine.yield()
local timing = timingRequest.result["timing"]
local batchCommand = Command.create("pasting stash "..name)
-- Code to copy notes goes here
batchCommand.commit()
end)
Then all that's left is copying the notes over. We'll make use of event.is
to determine the note type. For arctaps, we'll add them alongside arcs with a nested for loop like so:
...
local batchCommand = Command.create("pasting stash "..name)
local allEvents = stash.events
local arctaps = stash.arctaps
local origin = allEvents[1].timing -- The minimum timing of all notes
local displace = timing - origin -- The amount to shift all notes by
for i = 1, #allEvents, 1 do
local event = allEvents[i].copy()
event.timing = displace + event.timing
if event.is('long') then -- Notes with endTiming also have to have this value updated
event.endTiming = displace + event.endTiming
end
batchCommand.add(event.save())
if event.is('arc') then
local arc = allEvents[i]
-- Look for arctaps belonging to this arc to copy
for i = 1, #arctaps, 1 do
local arctap = stash.arctaps[i]
if arctap.arc == arc then
local arctapCopy = arctap.copy();
arctapCopy.arc = event
arctapCopy.timing = displace + arctap.timing
batchCommand.add(arctapCopy.save())
end
end
end
end
batchCommand.commit()
...
Here it's important we save the note first before any arctap (otherwise the arctap has nowhere to attach to).
After much effort, our code is finally functional. But let's take it one step further by improving the user experiene. We'll make our newly created macros more distinguished by making use of Unity's rich text formatting!
Let's change how we generate our macro from
addMacro("stash."..name, ...)
to
addMacro("<color=#2E86AB>stash."..name.."</color>", ...)
This will give our macro name in the Macro selection window a fancy dark blue color, which makes it very easy to distinguish. Feel free to change the color to your liking.
However you will observe a problem and that is your macros' order is changed. That's because although the first letter <
aren't displayed, they're still part of your macro's name, and so it'll be sorted accordingly. To solve this, use addMacroWithSort
.
addMacroWithSort("<color=#2E86AB>stash."..name.."</color>", "stash."..name,...)
The ordering should be fixed now. You can even modify this to allow newer stashes to be places last.
Here's the final code:
storedStashes = {}
addMacroWithSort("<color=#2E86AB>stash."..name.."</color>", "stash."..name, function()
-- Request for user's selection
local eventRequest = EventSelectionInput.requestEvents(
EventSelectionConstraint.create().any(),
"Select notes to be included in your stash. Press ENTER to confirm")
coroutine.yield()
-- Request for stash's name
local dialogRequest = DialogInput.withTitle("Stash creation").requestInput({
DialogField
.create("name")
.setLabel("Name")
.setHint("Enter anything")
.setTooltip("Your stash name will be included in the newly created macro's name")
})
coroutine.yield()
local name = dialogRequest.result["name"]
newStash = {}
newStash.events = eventRequest.resultCombined
newStash.arctaps = eventRequest.result["arctap"]
storedStashes[name] = newStash
notify(#newStash.events)
addMacro("stash."..name, function()
local stash = storedStashes[name]
local timingRequest = TrackInput.requestTiming(false, "Select where to paste your stash")
coroutine.yield()
local timing = timingRequest.result["timing"]
local batchCommand = Command.create("pasting stash "..name)
local allEvents = stash.events
local arctaps = stash.arctaps
local origin = allEvents[1].timing -- The minimum timing of all notes
local displace = timing - origin -- The amount to shift all notes by
for i = 1, #allEvents, 1 do
local event = allEvents[i].copy()
event.timing = displace + event.timing
if event.is('long') then -- Notes with endTiming also have to have this value updated
event.endTiming = displace + event.endTiming
end
batchCommand.add(event.save())
if event.is('arc') then
local arc = allEvents[i]
-- Look for arctaps belonging to this arc to copy
for i = 1, #arctaps, 1 do
local arctap = stash.arctaps[i]
if arctap.arc == arc then
local arctapCopy = arctap.copy();
arctapCopy.arc = event
arctapCopy.timing = displace + arctap.timing
batchCommand.add(arctapCopy.save())
end
end
end
end
batchCommand.commit()
end)
end)
Try this for yourself:
- Add a macro to remove all stashes
- Display the macro name with more information, such as note count
- Experiment with different rich text formatting. Check out the official documentation here: https://docs.unity3d.com/2018.3/Documentation/Manual/StyledText.html
- Expand the macro to support different bpm
In fact, the idea of manipulating macros with macros themselves are super powerful, I'm sure you will have other amazing ideas as well!
- Remember to always use
local
for assigning local variable - Error message can be hard to read within Arcade. You can always open the
Error Log
(middle left hand side) - If you plan to share your macros, consider prefixing your macro with something unique (my own macros follow the format
zero.{category}.{name}
) to avoid collision - Any time you see
a["b"]
, you can alternatively writea.b
(this is a lua feature). For example,request.result["arc"]
can berequest.result.arc
Function | Description | Output |
---|---|---|
addMacro(string macro, function macroDef) | Register a macro | Nil |
addMacroWithSort(string macro, string sortKey, function macroDef) | Register a macro with a custom sort key | Nil |
removeMacro(string macro) | Unregister a macro | Nil |
log(object content) | Output content to the log file | Nil |
notify(object content) | Output content to the toast notification | Nil |
xy(number x, number y) | Return an XY coordinate with specified x y value | XY |
toNumber(string s) | Convert string to a number, or 0 if fails | number |
toBool(string s) | Convert string to a bool, or false if fails | bool |
Static property | Description | Type |
---|---|---|
offset | The current audio offset of this chart (ms) | number |
beatlineDensity | The current beatline density setting of Arcade | number |
baseBpm | The current base bpm setting of Arcade | number |
songLength | The length of current song (ms) | number |
allArcColors | String list of available arc colors (which is just "Blue", "Red", "Green")) | Table (of strings) |
currentArcColor | Current default arc color, as an index of the list above | number |
allArcTypes | String list of available arc types ("b", "s", ...) | Table (of strings) |
currentArcType | Current default arc type | string |
currentIsVoidMode | true means creating traces by default, false means creating arcs by default | bool |
currentTimingGroup | Currently active timing group | bool |
timingGroupCount | Number of timing groups in the current chart | bool |
language | The currently used language (one of "zh_Hans", "en", "jp" or "zh_TW") | string |
Static method | Description | Output |
---|---|---|
beatLengthAt(number timing, number timingGroup = 0) | Length of a beat at specified timing and within a timing group (ms) | number |
bpmAt(number timing, number timingGroup = 0) | Bpm value at specified timing and within a timing group (ms) | number |
divisorAt(number timing, number timinggroup = 0) | Divisor value at specified timing and within a timingg group (ms) | number |
Static method | Description | Output |
---|---|---|
tap(number timing, number lane, number timingGroup = 0) | Create a tap note's description | LuaTap |
hold(number timing, number endTiming, number lane, number timingGroup = 0 | Create a hold note's description | LuaHold |
arc(number timing, number startX, number startY, number endTiming, number endX, number endY, bool isVoid=false, number color=0, string type='s', number timingGroup=0) | Create an arc note's description | LuaArc |
arc(number timing, XY startXY, number endTiming, XY endXY, bool isVoid=false, number color=0, string type='s', number timingGroup=0) | Create an arc note's description | LuaArc |
arctap(number timing, LuaArc arc) | Create an arctap's description | LuaArcTap |
timing(number timing, number bpm, number divisor, number timingGroup=0) | Create a timing event's description | LuaTiming |
camera(number timing, number x=0, number y=0, number z=0, number rx=0, number ry=0, number rz=0, string type='reset', number duration=1, number timingGroup=0) | Create a camera event's description | LuaCamera |
createTimingGroup(number bpm, number divisor) | Create a new timing group with specified bpm and divisor as it's base timing event. Returns the newly created timing group | number |
query(EventSelectionConstraint constraint) | Query for all notes in the chart that satisfies the specified constraint | Table |
getCurrentSelection(EventSelectionConstraint constraint = null) | Query for all notes in the currently selected notes. Optionally provide a constraint to only query for notes that satisfies the specified constraint | Table |
setSelection(Table notes) | Set the selected notes to the provided list of notes | nil |
Property | Description | Type |
---|---|---|
x | The horizontal x coordinate (arc unit) | number |
y | The vertical y coordinate (arc unit) | number |
Method | Description | Output |
---|---|---|
mirrorX(float axis = 0.5f) | Returns a coordinate flipped horizontally along the specified x coordinate | XY |
mirrorY(float axis = 0.5f) | Returns a coordinate flipped vertically along the specified y coordinate | XY |
toString() | Returns a string representation. Used for logging | string |
Also supports operator overloading
xy1 = xy(1,2)
xy2 = xy(3,4)
log(xy1 + xy2)
log(xy1 - xy2)
log(xy1 * 3)
log(3 * xy1)
log(xy1 / 2)
Static method | Description | Output |
---|---|---|
requestTiming(bool showVertical = false, string notification = null) | Request the user to input a timing value (similar to creating a tap note). Optionally enable vertical grid, and provide a different toast notification | TrackRequest |
requestPosition(int timing, string notification = null) | Request the user to input a position value (similar to creating an arc note). The grid will be positioned according to the provided timing value. Optionally provide a different toast notification | TrackRequest |
requestLane(string notification = null) | Request the user to select a track lane. Optionally provide a different toast notification | TrackRequest |
Static method | Description | Output |
---|---|---|
requestSingleEvent(EventSelectionConstraint constraint) | Request the user to select a single event that satisfies the constraint | EventSelectionRequest |
requestEvents(EventSelectionConstraint constraint) | Request the user to select any number of events that satisfies the constraint. The user must press enter to confirm the selection | EventSelectionRequest |
Static method | Description | Output |
---|---|---|
withTitle(string title) | Create a dialog input with specified title | DialogInput |
Method | Description | Output |
---|---|---|
requestInput({DialogField field1, DialogField field2,..}) | Build the dialog with the specified list of fields | DialogRequest |
Property | Description | Type |
---|---|---|
result["timing"] | Returned timing value | number |
result["x"] | Returned x coordinate value | number |
result["y"] | Returned y coordinate value | number |
result["lane"] | Returned lane value | number |
Property | Description | Type |
---|---|---|
result["tap"] | Returned list of taps (sorted by timing) | Table (of LuaTap) |
result["hold"] | Returned list of holds (sorted by timing) | Table (of LuaHold) |
result["arc"] | Returned list of arcs and traces (sorted by timing) | Table (of LuaArc) |
result["arctap"] | Returned list of arctaps (sorted by timing) | Table (of LuaArcTap) |
result["timing"] | Returned list of timing events (sorted by timing) | Table (of LuaTiming) |
result["camera"] | Returned list of camera events (sorted by timing) | Table (of LuaCamera) |
resultCombined | Returned list of all events (sorted by timing) | Table (of LuaChartEvent) |
Property | Description | Type |
---|---|---|
result | Map from a field's key to user inputted field value on said field. E.g result["key1"] return input for field DialogField.create("key1") | Table |
Property | Description | Type |
---|---|---|
key | The field's key | string |
label | The field's label, appears on the left column of a dialog | string |
hint | The field's hint, appears on empty text fields only | string |
tooltip | The field's tooltip, appears on hover | string |
defaultValue | The field's default value | dynamic |
dropdownOptions | The different options for a dropdown menu | Table (of dynamic) |
fieldConstraint | The field's constraint for a text field | FieldConstraint |
Static method | Description | Output |
---|---|---|
create(string key) | Create a field with specified key | DialogField |
Method | Description | Output |
---|---|---|
setLabel(string label) | Set the field's label | DialogField |
setTooltip(string tooltip) | Set the field's tooltip | DialogField |
setHint(string hint) | Set the field's hint | DialogField |
defaultTo(dynamic value) | Set the field's default value | DialogField |
textField(FieldConstraint constraint) | Convert field to a text field that only accept inputs satisfying the specified constraint | DialogField |
dropdownMenu(dynamic option1, dynamic option2,...) | Convert field to a dropdown menu with specified option values | DialogField |
checkbox() | Convert field to a checkbox | DialogField |
description(string message = nil) | Convert a field to a description field (only used for displaying text, not for receiving input). If you don't specify the message here, it's label will be used instead |
DialogField |
Property | Description | Type |
---|---|---|
timing | The event's timing (ms) | number |
timingGroup | The event's timing group | number |
attached | Whether this lua representation has a real note in the chart attached to it | number |
Method | Description | Type |
---|---|---|
copy() | Create a copy of the event (not attached) | LuaChartEvent |
save() | Create a command that saves the event to the chart | LuaChartCommand |
delete() | Create a command that deletes the event from the chart | LuaChartCommand |
is(string type) | Returns whether this event is of a given type ('tap', 'arc', 'floor', 'short',...) | bool |
Classes from 5.1 to 5.6 inherits all properties and methods mentioned in 5.0.
Property | Description | Type |
---|---|---|
lane | The tap note's lane | number |
Property | Description | Type |
---|---|---|
endTiming | The hold note's end timing (ms) | number |
lane | The hold note's lane | number |
Property | Description | Type |
---|---|---|
startXY | The arc note's starting XY coordinate | XY |
endXY | The arc note's ending XY coordinate | XY |
endTiming | The arc note's end timing (ms) | number |
type | The arc note's arc type ("b", "s", ...) | string |
color | The arc note's color | number |
isVoid | true means a trace, false means an arc | bool |
startX(readonly) | The arc note's starting X coordinate | number |
startY(readonly) | The arc note's starting Y coordinate | number |
endX(readonly) | The arc note's ending X coordinate | number |
endY(readonly) | The arc note's ending Y coordinate | number |
Method | Description | Output |
---|---|---|
positionAt(number timing, bool clamp = true) | Arc's XY coordinate position at timing. Clamping will limit the resulting point to either ends of the arc | XY |
xAt(number timing, bool clamp = true) | Arc's x coordinate position at timing. Clamping will limit the resulting point to either ends of the arc | number |
yAt(number timing, bool clamp = true) | Arc's y coordinate position at timing. Clamping will limit the resulting point to either ends of the arc | number |
Property | Description | Type |
---|---|---|
arc | The parent arc note | LuaArc |
Property | Description | Type |
---|---|---|
bpm | The timing event's bpm value | number |
divisor | The timing event's divisor value | number |
Property | Description | Type |
---|---|---|
mx | The camera event's x coordinate displacement | number |
my | The camera event's y coordinate displacement | number |
mz | The camera event's z coordinate displacement | number |
rx | The camera event's x axis rotation | number |
ry | The camera event's y axis rotation | number |
rz | The camera event's z axis rotation | number |
duration | The camera event's duration | number |
type | The camera event's easing type("reset", "qi", "qo", ...) | string |
Static method | Description | Type |
---|---|---|
create(string name) | Create an empty chart edit command with specified name | LuaChartCommand |
Method | Description | Type |
---|---|---|
add(LuaChartCommand target) | Merge all commands from target into this instance | Nil |
commit() | Execute any included commands | Nil |
Static method | Description | Type |
---|---|---|
create() | Create an default FieldConstraint (that accepts anything) | FieldConstraint |
Method | Description | Type |
---|---|---|
any() | Set constraint to accept all characters | FieldConstraint |
float() | Set constraint to accept decimal numbers only | FieldConstraint |
integer() | Set constraint to accept integers only | FieldConstraint |
gEqual(number value) | Set constraint to accept number greater than or equal to value | FieldConstraint |
lEqual(number value) | Set constraint to accept number less than or equal to value | FieldConstraint |
greater(number value) | Set constraint to accept number greater than value | FieldConstraint |
less(number value) | Set constraint to accept number less than value | FieldConstraint |
custom(function dynamic->bool, string message = "Invalid") | Specify a custom constraint. Invalidates all other constraints. Optionally specify a feedback message | FieldConstraint |
union(FieldConstraint otherConstraint) | Combines two constraints (similar to an operator OR) | FieldConstraint |
getConstraintDescription() | Get the auto-generated description | string |
Static method | Description | Type |
---|---|---|
create() | Create an default EventSelectionConstraint (that accepts anything) | EventSelectionConstraint |
Method | Description | Type |
---|---|---|
any() | Set constraint to accept all events | EventSelectionConstraint |
tap() | Set constraint to accept tap events only | EventSelectionConstraint |
hold() | Set constraint to accept hold events only | EventSelectionConstraint |
arc() | Set constraint to accept arc and trace events only | EventSelectionConstraint |
solidArc() | Set constraint to accept arc events only | EventSelectionConstraint |
voidArc() | Set constraint to accept traces events only | EventSelectionConstraint |
arctap() | Set constraint to accept arctap events only | EventSelectionConstraint |
timing() | Set constraint to accept timing events only | EventSelectionConstraint |
camera() | Set constraint to accept camera events only | EventSelectionConstraint |
floor() | Set constraint to accept tap and hold events only | EventSelectionConstraint |
sky() | Set constraint to accept arc, trace and arctap events only | EventSelectionConstraint |
short() | Set constraint to accept tap and arctap events only | EventSelectionConstraint |
long() | Set constraint to accept hold, arc and trace events only | EventSelectionConstraint |
judgeable() | Set constraint to tap, hold, arc and arctap events only | EventSelectionConstraint |
fromTiming(number timing) | Set constraint to accept events with timing greater than or equal to value | EventSelectionConstraint |
toTiming(number timing) | Set constraint to accept events with timing less than or equal to value | EventSelectionConstraint |
ofTimingGroup(number group) | Set constraint to accept events within the specified timing group | EventSelectionConstraint |
custom(function dynamic->bool, string message = "Invalid") | Specify a custom constraint. Invalidates all other constraints. Optionally specify a feedback message | EventSelectionConstraint |
union(FieldConstraint otherConstraint) | Combines two constraints (similar to an operator OR) | EventSelectionConstraint |
getConstraintDescription() | Get the auto-generated description | string |