femboyindustries / gimmick-theme

A minimal NotITG v4.3.0 theme focused on developer simplicity and modern solutions
2 stars 0 forks source link

gimmick

A minimal NotITG v4.3.0 theme focused on developer simplicity and modern solutions.

Gimmick is meant to be partly a playground, and partly an example for other developers aiming to make a theme with no idea where to begin.

Why is it called "gimmick"?

Because I really don't know if any of what I'm doing is going to stick. The stuff presented here is highly experimental and prone to breaking under pressure!

Developer documentation

Working with gimmick can be a doozy because of how far away it is from typical theming or even typical Stepmania code. This is why this section exists: to note specific quirks you may encounter.

Actors

For actor initialization, gimmick uses actor235, a work-in-progress (mostly complete) port of Uranium Template's actor system. For the most part, you can refer to its documentation, but it won't always align since actor235 has been greatly modified and rewritten to fit working within a theme.

This section will note the differences between actor235 and Uranium Template's actor initialization, aswell as quirks to look out for that are relevant within gimmick.

Contexts

actor235 introduces the concept of contexts. A context, abstractly speaking, is some context in which actors can be initialized. Actors cannot be initialized outside of a context, as there's no way for you to load an actor during runtime, so you will need a Context in order to initialize actors.

In gimmick, Contexts are created during screen initialization, and are only usable in said screen initialization. Usually this means that you'll be given a Context whenever you define a screen:

gimmick.ActorScreen('CoolScreen', function(self, ctx, scope)
  -- `ctx` here is your Context
  local quad = ctx:Quad()  
end)

You can only define actors through a Context:

local quad = ctx:Quad()
local sprite = ctx:Sprite(...)
local shader = ctx:Shader(...)
-- and so on, and so forth

ActorFrame associations are also always handled through a Context:

local frame = ctx:ActorFrame()
local child = ctx:Quad()
ctx:addChild(frame, child)

Do not try and store the Context, as after the function you're given it in passes and initialization completes, it is locked and accessing it will error.

-- DON'T DO THIS
local quad = ctx:Quad()
quad:addcommand('Init', function()
  local otherQuad = ctx:Quad()
end)
-- NO NO
local storedContext

return function(ctx)
  storedContext = ctx
end

Working with proxied actors

Unlike Uranium Template, actor235 does not expose any functions to work with actors to account for their proxied nature (such as setShader and similar). Instead, you are (for the time being) entrusted with taking care of proxied actors yourself.

To access the raw actor of a proxied actor, import actor235 and use the Proxy module:

local actor235 = require 'gimmick.lib.actor235'

local shader = ctx:Shader('Shaders/penis.frag')

actor235.Proxy.getRaw(shader) --> RageShaderProgram or nil
-- nil is returned when it is not yet initialized

-- You could use this, for instance, like this:
sprite:SetShader(actor235.Proxy.getRaw(shader))

-- However, keep in mind InitCommands will also return the raw actor for a
-- simpler way to achieve the same:
shader:addcommand('Init', function(a)
  sprite:SetShader(a)
end)
-- !! BE WARY OF LOAD ORDER !!, because `shader`'s InitCommand here could be ran
-- before `sprite`'s, depending on the order in which they're defined.

Other notes

ActorFrame transforms only apply to children when the children are drawn within the ActorFrame's drawfunction. Drawing the ActorFrame's children outside of its drawfunction, or drawing other ActorFrames' children in another drawfunction should be considered undefined behavior to avoid discovering weird issues down the line.

local quad = ctx:Quad()
quad:xywh(0, 0, 32, 32)

local frame = ctx:ActorFrame()
frame:xy(100, 100)
ctx:addChild(frame, quad)

local parentlessQuad = ctx:Quad()
parentlessQuad:xywh(0, 0, 32, 32)

self:SetDrawFunction(function()
  -- **Undefined behavior**, but should render at x0 y0
  quad:Draw()
  -- OK, renders at x0 y0
  parentlessQuad:Draw()
end)

frame:SetDrawFunction(function()
  -- OK, renders at x100 y100
  quad:Draw()
  -- **Undefined behavior**, but should render at x100 y100
  parentlessQuad:Draw()
end)

Scopes

Alongside contexts, you'll be passed along a Scope during initialization. A scope is a generalization of everything specific to a screen. Inside it is:

Global events

Global events can be listened to and created with the event global listener, or listened to with any event instances in a scope (preferable to use when possible).

keypress(device: InputDevice, key: string)

A raw keypress. device corresponds to an InputDevice. For instance to detect keyboard presses, do:

event:on('keypress', function(device, key)
  if device == InputDevice.Key then
    -- ...
  end
end)

For keyboards, reference RageKeySymToString for possible values of key. You can likely find the other possible values of key for other device types within the same file.

keyrelease(device: InputDevice, key: string)

Same as keypress, except triggered when the key is no longer held.

press(pn: number, button: string)

A processed button press, corresponding to the actions you can bind in the game. See m_szButtonNames for possible values.

release(pn: number, button: string)

Same as release, except triggered when the key is no longer held.

resize(dw: number, dh: number)

Triggered whenever the window resizes. Uses the display resolution instead of the usual screen resolution. Common usecase is to recreate AFTs with fresh sizes.

warn(msg: string)

A warning; not necessarily a user-facing one. Calls to the warn global will send this event. Currently only displayed in the console, but could potentially be shown to the user later on.

Notable functions

Make sure to look through util.lua for the rest; there's plenty of handy things to be found there.

File structure

Folders will sometimes have readme.md files in them documenting how they're meant to be used. Be sure to read those for full details on where things should go.

Folders at the root should be minimized. Folders that are at the root at the folder not required by Stepmania's theme structure (and aren't gimmick or relate to repository structure) are currently considered to be moved to a better spot later on.

Be sure to try and deduplicate files whenever possible with .redirs.