luv

Workshop wiki

knobs.lisp

luvcraft/knobs.lisp

system luvcraft/core · 33 definitions · on GitHub

Knobs: the live values of the game, as objects a gadget can turn.

The grading specials in RENDER.LISP, the sky's clock, the player's stride, the terminal's emissions, a literal in a shader: each has always been meant to be retuned from a live SLY eval. A knob is that value given a name, a declared quantity (so its unit is the one the arithmetic checks, and the one the metabar prints), a range and a step, a place to live -- a special, or a slot reached through SESSION -- and, the part that matters, one definite answer to "and then what": what has to happen for the change to be seen.

That answer is the knob's class. A plain knob is read every frame and needs nothing. A knob mixing in a REALIZATION -- the terminal's, which bakes its values into glyph instances and must rebuild them; the streaming window's, which must be re-centred -- says so by its class, and realize-knob dispatches on it. A knob folded into shader source needs no class of its own: the shader parser folds any knob's name into a literal through shader-source-value and remembers that it did, and the live pipeline rebuilds when the value it folded no longer holds.

The protocol lives here, early; the knobs themselves are defined beside what they tune -- define-knob in RENDER.LISP for grading, SKY.LISP for the clock, SHADERS.LISP for the folded literals, and so on -- so a knob is found where its value is used. Reading a knob in a hot loop is a slot lookup and a funcall; take the reading once, at frame scope, and keep the dense loop dense.

in-package#:luvcraft

--------------------------------------------------------------------- The knob.

defclassknob
name:initarg:name:readerknob-name
label:initarg:label:readerknob-label
group:initarg:group:initform:grading:readerknob-group
documentation:initarg:documentation:initformnil:readerknob-documentation
declaration:initarg:declaration:initformnil:readerknob-declaration:documentation

The represented-value declaration: representation type and quantity, and so the unit.

unit-label:initarg:unit-label:initformnil:documentation"An explicit suffix for the value, overriding the unit's own."
reader:initarg:reader:readerknob-reader
writer:initarg:writer:readerknob-writer
:documentation

One live value: where it lives, what it measures, and -- by its class -- what SESSION must do after a change for the change to show.

defclassscalar-knob
minimum:initarg:minimum:readerknob-minimum
maximum:initarg:maximum:readerknob-maximum
step:initarg:step:readerknob-step
:documentation

A knob over a real, bounded and quantized.

defclassswitch-knob
:documentation

A knob over a generalized boolean: on or off.

defvar*knobs*'
"Every defined knob, in definition order."
defunregister-knob

Add or replace knob in *KNOBS*, keeping the definition order.

let
existing
position
knob-nameknob
*knobs*:key#'knob-name
ifexisting
setf
nthexisting*knobs*
knob
setf*knobs*
append*knobs*
listknob
knob
defunfind-knob
name

The knob named name, or NIL.

findname*knobs*:key#'knob-name
defparameter*knob-group-order*'
:grading:sun:sky:shadows:camera:player:streaming:critters:gnome:cat:riding:terminal:text
"The order the metabar shows groups in; a group not named here follows."
defunknob-groups

The groups the knobs fall in: those in *KNOB-GROUP-ORDER* first, in that order, then the rest in order of first appearance.

let
present
remove-duplicates
mapcar#'knob-group*knobs*
:from-endt
append
remove-if-not
lambda
group
membergrouppresent
*knob-group-order*
remove-if
lambda
group
present
defunknobs-in-group
group
removegroup*knobs*:key#'knob-group:test-not#'eq

--------------------------------------------------------------------- Realization: what has to happen after a change.

defgenericrealize-knob
knobsession
:documentation

Do what session needs after knob's value changed for the change to show. The primary method on knob does nothing: a value the renderer reads every frame is realized by the next frame. A realization mixin adds a method.

:method-combinationprogn
defmethodrealize-knobprogn
session
declare
ignoreknobsession
nil

The realizations themselves are defined beside what they realize: terminal-realization in TERMINAL-WALL.LISP, residency-realization in STREAMING.LISP.

--------------------------------------------------------------------- Values.

defgenericknob-value
knobsession
:documentation

knob's current value in session.

:method
session
funcall
knob-readerknob
session
defgenericcoerce-knob-value
knobvaluesession
:documentation

value made fit for knob's place: clamped and of the place's type.

:method
valuesession
let
clamped
max
knob-minimumknob
min
knob-maximumknob
value
current
typecasecurrent
integer
roundclamped
float
coerceclamped
type-ofcurrent
tclamped
:method
valuesession
declare
ignoresession
andvaluet
defunset-knob-value
knobvaluesession

Set knob to value, made fit, and realize it in session.

Returns the value actually set.

let
fit
funcall
knob-writerknob
fitsession
fit
defgenericstep-knob
knobsessiondirection&optionalmultiplier
:documentation

Move knob by direction (+1 or -1) times multiplier steps in session.

:method
sessiondirection&optional
multiplier1

Land on a multiple of the step so a run of nudges stays tidy.

let*
step
knob-stepknob
current
target
*step
round
+
/currentstep
*directionmultiplier
set-knob-valueknobtargetsession
:method
sessiondirection&optionalmultiplier
declare
ignoremultiplier

Right turns it on, left turns it off, so a nudge is idempotent.

set-knob-valueknob
pluspdirection
session
defuntoggle-knob
knobsession

Flip a switch knob.

defgenericknob-fraction
knobsession
:documentation

Where knob's value sits in its range, 0 to 1.

:method
let
minimum
knob-minimumknob
maximum
knob-maximumknob
max0.0
min1.0
/
-minimum
max1e-9
-maximumminimum
:method
if1.00.0

--------------------------------------------------------------------- Units, as the metabar prints them.

defunknob-unit

The unit knob's value is measured in, as a designator: a unit name for one named unit to the first power, else a list of (NAME EXPONENT), else NIL for the identity.

alexandria:when-let*
unit
luv.arithmetic:quantity-specification-unitspecification
let
factors
mapcar
lambda
factor
list
carfactor
cdrfactor
cond
nullfactors
nil
and
null
restfactors
eql1
second
firstfactors
first
firstfactors
tfactors
defgenericunit-abbreviation
unit
:documentation

The short spelling of the named unit, as it appears after a value or inside a compound unit. The default spells the unit's name; the common ones abbreviate.

:method
unitsymbol
string-downcaseunit
:method
unit
eql:one
""
:method
unit
eql:second
"s"
:method
unit
eql:minute
"min"
:method
unit
eql:hour
"h"
:method
unit
eql:radian
"rad"
:method
unit
eql:milliradian
"mrad"
:method
unit
eql:degree
"°"
:method
unit
eql:cell
"cell"
:method
unit
eql:percent
"%"
defununit-label
unit

The suffix a value in unit is printed with, space included where one is wanted. unit is a unit designator: a name, or a list of (NAME EXPONENT).

flet
power
nameexponent
formatnil"~A~A"
case
absexponent
1""
2"²"
3"³"
t
formatnil"^~D"
absexponent
cond
nullunit
""
equnit:one
""
equnit:degree
"°"
symbolpunit
formatnil" ~A"
conspunit
let
above
remove-if-not#'pluspunit:key#'second
below
remove-if-not#'minuspunit:key#'second
formatnil" ~{~A~^·~}~@[/~{~A~^·~}~]"
mapcar
lambda
factor
apply#'powerfactor
above
andbelow
mapcar
lambda
factor
apply#'powerfactor
below
t
formatnil" ~(~A~)"unit
defunknob-unit-label

What follows knob's value when printed.

defgenericformat-knob-value
knobsession
:documentation

knob's value as the control shows it.

:method

At the precision of the step.

let*
if
or
integerpvalue
formatnil"~D~A"
roundvalue
label
formatnil"~,vF~A"
max0
ceiling
valuelabel
:method
if"on""off"

--------------------------------------------------------------------- Defining one.

defmacrodefine-knob
name
&keylabel
group:grading
documentationquantity
type'single-float
unit-labelminimummaximumstep
place

Define name as a knob of CLASS over the setf-able place.

place may refer to SESSION, so a knob can live on the session -- its sky clock, its player -- as well as in a special. quantity is a quantity plist as define-quantity-constant takes, (:quantity ... :unit ...); it and type make the knob's declaration, which value-declaration-for also answers for name. MINIMUM, MAXIMUM, and step bound and quantize what a control may set on a scalar knob; a switch knob wants none. How a change is realized is CLASS's business.

let
value
gensym"VALUE"
query
gensym"NAME"
source-form`
define-knob,name
:quantity,quantity:type,type
,place
`
progn,@
whenquantity`
defmethodluv.arithmetic:value-declaration-for
,query
eql',name
declare
ignore,query
load-time-value
register-knob
make-instance,class:name',name:label,
orlabel
substitute#\Space#\-
string-downcasename
:group,group:documentation,documentation:declaration,:unit-label,unit-label,@
whenminimum`
:minimum,minimum
,@
whenmaximum`
:maximum,maximum
,@
whenstep`
:step,step
:reader
lambda
session
declare
ignorablesession
,place
:writer
lambda
,valuesession
declare
ignorablesession
setf,place,value

--------------------------------------------------------------------- Knobs in shader source.

A knob named in a shader body folds to a literal of the knob's quantity; the pipeline that folded it rebuilds when the value moves. Only a knob whose place needs no session can stand in a shader, since the shader is parsed with none.

defmethodluv.shader:shader-source-value
namesymbol
let
if
values
knob-declarationknob
t
valuesnilnilnil

--------------------------------------------------------------------- Actions: things the game can be told to do, given a name and a label so a gadget can offer them as buttons.

defclassaction
name:initarg:name:readeraction-name
label:initarg:label:readeraction-label
function:initarg:function:readeraction-function
:documentation

One named verb over a session.

defvar*actions*'
"Every defined action, in definition order."
defmacrodefine-action
name
&keylabel
&bodybody

Define name as an action: body runs with SESSION bound.

`
register-action
make-instance'action:name',name:label,
orlabel
string-downcasename
:function
lambda
session
declare
ignorablesession
,@body
defunregister-action
let
existing
position
action-nameaction
*actions*:key#'action-name
ifexisting
setf
nthexisting*actions*
action
action
defunfind-action
name
findname*actions*:key#'action-name
defunrun-action
actionsession

Do action in session.

funcall
action-functionaction
session

--------------------------------------------------------------------- The metabar hook.

defgenerictoggle-luvcraft-metabar
session
:documentation

Slide session's metabar of knobs in or out, returning true when one is available. LUVCRAFT/MCCLIM supplies the session method.

defmethodtoggle-luvcraft-metabar
sessiont
declare
ignoresession
nil