Glyphweft Studio / Documentation

Glyphweft file formats

The Glyphweft file format reference, included here for Studio's projects, assets and editors.

20 chapters Complete web edition

All files are UTF-8 text that can be edited in any text editor. JSON files allow // comments and trailing commas, property names are not case-sensitive, and unknown properties are errors (so a misspelt field is reported, not ignored). The engine writes every format in one canonical form: two-space indents, short arrays on one line, properties in the order the tables below list them, and every value that equals its default left out. Writing a file twice gives the same bytes; comments are not kept (see the Engine manual's Writing files).

Each Glyphweft format has its own extension (.gwanim, .gwworld, .gwcolors and the rest), not a double one like .gwanim.json: Windows only looks at the last extension, and a name of its own lets Glyphweft Studio be the program that opens these files. Most of them are JSON inside. The repository and the game template include .vscode/settings.json, which tells VS Code to show them as JSON (with comments); in another editor, set the same association. glyphweft.json, the game's settings, keeps .json.

Glyph art: name.gwart + name.gwcolors#

A piece of art is two files side by side: the glyphs, and a JSON sidecar with size, pivot and colours. The glyphs and the colours are kept apart, so the glyph file stays readable as ASCII art.

name.gwart — the glyphs#

A rectangle of printable ASCII (0x20–0x7E): exactly size[1] lines of exactly size[0] characters. Whitespace is significant and never trimmed. A newline after the last line is optional. Tabs and non-ASCII characters are errors. Each error names the file, line and column, for example:

Code example Plain text
art/nature/bush.gwart:4: line is 28 characters wide, "size" says 26.
art/nature/bush.gwart:6:5: tab character; use spaces.

name.gwcolors — the sidecar#

Code example JSON
{
  "schemaVersion": 1,
  "id": "characters.hero.down",
  "size": [30, 34],
  "transparent": " ",
  "origin": [15, 33],
  "defaultForeground": "#BAC6D1",
  "defaultBackground": "#101418",
  "colorsByGlyph": { "o": { "foreground": "#1C2C48", "background": "#F2F6FA" } },
  "palette": { "a": "#E2B090", "b": "#6A3A1E", "c": "transparent" },
  "foregroundMap": ["  ab  ", "..."],
  "backgroundMap": ["   c  ", "..."],
  "sockets": [ { "id": "head", "x": 15, "y": 3 } ]
}
Field Meaning
schemaVersion 1
id Name of the art; defaults to the file name
size [width, height] in glyph cells (required)
transparent The character that means "no glyph" (default space)
origin The pivot cell: the cell placed at the drawing position, e.g. between a character's feet
defaultForeground, defaultBackground Colours of glyph cells with nothing more specific (white, transparent if absent)
colorsByGlyph Colours for every cell showing a given glyph
palette Colour keys used by the maps: one character each, or two with "mapKeyWidth": 2
foregroundMap, backgroundMap One row per glyph row, one key per cell (a space, or two with width 2, means "not set")
glowMap One row per glyph row; any character other than a space marks a cell that gives off its own light (windows, fire, lamps). Lighting leaves glowing cells at full brightness at night
sockets Named attachment points (where a hat, weapon or effect goes)
sizes Optional: the same art drawn at other sizes ahead of time, for the tilted view to pick by distance: a list of { "scale", "file" }, scale above 0 and at most 4 but not 1 (0.5 is half as large), file another .gwart relative to this one (a leading / means the asset folder). At most 8, no scale twice; written smallest first

Colours are #RRGGBB, #RRGGBBAA or "transparent".

Colour precedence, highest first: foregroundMap/backgroundMap > colorsByGlyph > defaults.

Transparent cells draw nothing, not even the default background. The exception is a cell that a backgroundMap key gives a background: it becomes a background-only cell. A background of "transparent" on a glyph cell removes the default background for that cell.

Colour maps let dense art give every cell its own colour without one JSON object per cell. With one-character keys a map can use 94 colours; with "mapKeyWidth": 2 it can use 8,836.

Canonical form#

The engine writes art in one canonical form (ArtFormat.Write, used by Glyphweft Studio later):

  • the defaults are the most common colours of the glyph cells;
  • the maps hold only cells that differ from the defaults;
  • palette keys are assigned in order of first use (a–z, A–Z, 0–9, then punctuation);
  • the key width is 2 only when more than 94 colours are needed.

Reading the canonical form back gives exactly the same cells, and writing it again gives the same text.

Animation sets: name.gwanim#

The clips of one character or effect: sequences of .gwart frames with timing, events and directions.

Code example JSON
{
  "schemaVersion": 1,
  "id": "hero",
  "fps": 8,
  "clips": {
    "idle.down": { "fps": 4, "frames": ["hero/idle_down_0.gwart", "hero/idle_down_1.gwart"] },
    "walk.down": { "frames": [
      { "art": "hero/walk_down_0.gwart", "ms": 105, "events": ["step"] },
      { "art": "hero/walk_down_1.gwart", "ms": 105 }
    ] },
    "walk.right": { "frames": ["hero/walk_right_0.gwart", "hero/walk_right_1.gwart"] },
    "attack.down": { "mode": "once", "next": "idle", "frames": [
      { "art": "hero/attack_down_0.gwart", "ms": 110 },
      { "art": "hero/attack_down_1.gwart", "ms": 70, "events": ["swing"] },
      { "art": "hero/attack_down_2.gwart", "ms": 120, "events": ["hit"], "offset": [1, 0] }
    ] }
  }
}
Field Meaning
schemaVersion 1
id Name of the set; defaults to the file name
fps Frame rate for frames without ms (default 8); a clip can set its own
clips Clips by name: {name} for every direction, {name}.{direction}, or {name}.{heading}
mode loop (default), once (holds the last frame, or plays next), pingpong (forwards, then backwards)
next Clip to play when a once clip ends
frames A path, or { "art", "ms", "events", "offset", "flipX" }
events Names raised when the frame starts; game code decides what they do (nothing is executed)
offset Moves the frame by whole cells [x, y] (mirrored with the frame)
flipX Draws this frame mirrored
sizes Optional: the same set drawn at other sizes ahead of time, for the tilted view to pick by distance: a list of { "scale", "file" } as for art, each file another .gwanim with the same clips and the same number of frames in each

Paths are relative to the .gwanim file; a path starting with / is relative to the asset folder.

Directions are down (the front), downright, right, upright, up (the back), upleft, left and downleft. To find the clip for a facing, the engine tries in order:

  1. that direction;
  2. the mirror image of the opposite side (left plays right mirrored, so most sets author five directions and get eight);
  3. for a diagonal, its left/right side (or that side's mirror), then its up/down side;
  4. the plain {name};
  5. the front, {name}.down.

Headings, for things seen straight down (the overhead view) that turn to any angle, are h0 (facing up the screen) to h31, turning clockwise 11.25 degrees each: h8 faces right, h16 down, h24 left. Any of them may be left out; a game turned to a heading in degrees shows the nearest one the clip has, else the plain {name}.

Validation reports, with the file, clip and frame: missing frame files, durations of 0 ms or less, unknown directions, headings and modes, next clips that do not exist, and unknown fields (in a frame object too).

Canonical form: a frame that sets nothing but its art is written as just its path.

Models: name.gwmodel#

A model asset: one of the art kit's models and the values of its parameters. Studio's model editor writes it; building it writes ordinary art next to it, named after it: a character's animation sets (name.gwanim, frames in name/, and name_sword.gwanim for a sword layer), or one piece of art (name.gwart). Games use the built files and never read the model file.

Code example JSON
{
  "schemaVersion": 1,
  "model": "runefox",
  "parameters": {
    "fur": "#9FB6D8",
    "ears": 1.4,
    "sword": false
  }
}
Field Meaning
schemaVersion 1
model runefox, person or tree (glyphweft-artkit models lists them with their parameters)
parameters values by name: colours as "#RRGGBB", numbers in the parameter's range, true/false, or one of a choice's names; a parameter left out keeps its default

Validation reports, with the file: an unknown model, unknown parameters, and values of the wrong kind or out of range. Canonical form: every parameter, in the model's order.

Worlds: name.gwworld and name.gwmap#

A world is tile ground of unlimited size with objects standing on it. Hand-made maps (.gwmap) are placed in it, and an optional generator makes everything around them, as far as anyone walks.

Code example JSON
{
  "schemaVersion": 1,
  "tile": [8, 6],
  "tiles": {
    "grass": { "texture": "art/ground/grass.gwart" },
    "water": { "texture": "art/ground/water.gwart", "solid": true, "surface": "water", "edge": "#B8A47A", "edgeWidth": 3 }
  },
  "default": "grass",
  "objects": {
    "oak": { "art": "art/nature/oak.gwart", "sway": 1, "collider": [-12, -10, 24, 12] },
    "cottage": { "art": "art/buildings/cottage.gwart", "snowTop": 0.5 },
    "flowers": { "art": "art/nature/flowers.gwart" }
  },
  "maps": [
    { "file": "village.gwmap", "at": [-32, -20], "legend": {
      ".": "grass",
      "~": "water",
      "O": { "tile": "grass", "object": "oak" },
      "h": { "tile": "grass", "object": "cottage", "flipX": true, "offset": [4, 0] },
      "@": { "tile": "grass", "spawn": "hero" }
    } }
  ],
  "generator": {
    "seed": 7,
    "layers": [ { "tile": "water", "scale": 34, "below": 0.27 } ],
    "scatter": [ { "object": "oak", "on": "grass", "density": 0.01 } ]
  }
}
Field Meaning
tile A tile's size in glyph cells [width, height]; 8 x 6 cells of 3 x 4 px is a 24 x 24 px square
tiles Ground types. texture is a seamless .gwart that every tile of the type takes its cells from; solid blocks movement; edge and edgeWidth (cells) give a band of colour where the type meets another (a sandy bank); surface is ground (the default: it gets wet and snowed on), water (rain makes rings on it, snow does not settle) or lava (stays dry and clear of snow); animate makes it move and join makes it join up with its neighbours (both below); for the side view, platform makes a one-way platform (stood on from above, passed through from below and the sides; not with solid) and open a tile with no texture that draws nothing, so the background shows (the sky); for the tilted view, height (0 to 512 world pixels, default 0) makes a tile stand up as a wall, its top drawn that high and its sides as faces, and face (only with a height) is the seamless .gwart its faces take their cells from (default: its texture). Other views draw a standing tile flat. For the 3D views, ceiling is the seamless .gwart over the tile at the camera's ceiling height (default: none, open to the sky)
default The tile everywhere nothing else is placed
objects Things that stand on the ground: art placed by its pivot, an optional collider [x, y, width, height] in pixels from the pivot (a tree blocks at its trunk, not its whole crown), and optional lights, effects and sounds; sway (0..4) is how much it bends in the wind (a tree 1, grass 1.5), snowTop (0..1) how much of its top snow covers whole (a roof 0.5), bob (0..4 cells) how far it rises and sinks when it floats; headings (true or false) whether its art turns, for the overhead view (its art is drawn facing up, and its 32 headings sit beside it as {name}_h0.gwart to {name}_h31.gwart, as the art kit writes them; each is checked); tags (a list) every object of the type has, and behaviours (below)
effects A list of { "effect", "offset": [x, y], "rate", "night" }: particle effects (by name, from a .gweffects file) the object keeps giving off, such as chimney smoke; rate is particles per second (0 = the effect's own), night: true runs it only after dark
sounds A list of { "sound", "offset": [x, y], "volume", "night" }: looping sounds (by name, from a .gwsounds file) the object keeps making, such as a campfire's crackle, heard within the sound's range of the listener; volume (0..4, default 1) multiplies the sound's own, night: true plays it only after dark. A sound that does not loop is an error in the log, and is not played
lights A list of { "offset": [x, y], "color", "radius", "intensity", "flicker", "night" }: offset in pixels from the pivot (mirrored when the object is flipped), radius in pixels, flicker 0..1 for flames, and night: true for lamps that come on at dusk
maps Hand-made maps: file (relative to the world file), at (the map's top-left tile) and legend
legend One character per entry: a tile name, or { "tile", "object", "flipX", "offset" (pixels), "spawn" (a start point name) }
prefabs Groups of objects placed as one: { "name": { "parts": [ { "object", "at": [x, y], "flipX" } ] } }, each part's at in pixels from the prefab's pivot. Changing a prefab changes every copy
placed Objects at exact positions: a list of { "object", "at": [x, y], "flipX" }, at in world pixels (where the pivot stands); or a prefab, { "prefab", "at", "flipX" } (mirroring a prefab mirrors where its parts stand, and each part). They stay even inside a map's area; Studio's world editor writes these. An object (not a prefab) can also have a heading in degrees (0 up, clockwise; drawn with the nearest heading when its type turns), a name (unique in the world, for World.Find), tags besides its type's, and behaviours besides its type's (one its type has too gets its settings over the type's)
behaviours On an object type or a placed object: a list of { "name", "settings" }. name is a C# class in the game marked [Behaviour("name")]; settings gives its public properties values ({ "Key": "brass", "Locked": false, "Hinge": [2, 3], "Paint": "#AA3322" }: text, numbers, true or false, an enum's name, [x, y], a colour). A behaviour the game hasn't got, a setting it hasn't got, or a value of the wrong kind is reported by the game with the object's name, and the rest still runs
spawns Named start points at exact positions: { "name": [x, y] } in world pixels. Games find them with Spawn("name"), after any a map's letters give
sounds Looping sounds placed on their own, where there is nothing to see (a river bend, wind on a hill): a list of { "sound", "at": [x, y], "volume", "night" }, at in world pixels, the rest as an object's sounds. Studio's world editor writes these
background Optional, for the side view: layers behind the ground, farthest first, each { "art", "distance", "y", "fill" }. art repeats left and right; distance (1 to 100, default 2) divides how far it scrolls (1 moves with the ground, 8 hardly at all); y is where its top is in world pixels when the camera's top is at 0; fill (#RRGGBB, optional) colours everything below it
camera Optional. For the tilted view: lean, the camera's lean from looking straight down (10 to 80 degrees, default 50), and distance, how far it is from the point it looks at (100 to 5000 world pixels, default 480; nearer makes the perspective stronger). For the 3D views: fov (40 to 120 degrees across the screen, default 75), eye (the first person's eye height, 1 to 1000 world pixels, default 100), behind and above (how far behind the hero and how high the third person follows, default 400 and 220) and ceiling (the height of ceilings over tiles with ceiling art, 0 for none to 2000, default 144)
generator Optional. layers: the first whose condition holds picks the tile, from smooth noise (0..1) with features about scale tiles across, below or above a level. scatter: puts an object on a tile of type on with probability density

A tile's join makes it join up with neighbours of its own type, like a road, a wall or a stream:

Code example JSON
"road": { "texture": "art/ground/dirt.gwart", "join": { "under": "grass", "style": "path", "shapes": ["art/ground/road_join/road_0.gwart", "...16 in all"] } }
Field Meaning
under The tile type it lies on, shown where a shape leaves the tile empty. It must not join up itself. For the ragged borders between other ground types, the joining tile counts as this one
style How the art kit drew the shapes: path (narrow, a soft edge; joined on every side it is a crossroads), channel (wider, a dark bank) or wall (widest, lit along its top, dark along its foot); a channel or wall joined on every side fills the tile. Studio draws them again this way
shapes 16 art paths, each a tile in size, picked by which sides touch the same type: up 1, right 2, down 4, left 8, added up (the first stands alone, the last joins on every side). A cell with a clear background takes the background of the ground beneath

A tile's animate (every field optional):

Code example JSON
"water": { "texture": "art/ground/water.gwart", "solid": true, "surface": "water", "edge": "#B8A47A", "edgeWidth": 3,
  "animate": { "frames": ["art/ground/water_1.gwart", "art/ground/water_2.gwart"], "fps": 2.5, "shimmer": 0.5, "glints": 0.05,
               "shore": "#D6E8F0", "waves": 3.2, "shoreWidth": 3, "reflect": true } }
Field Default Meaning
frames none More textures, played in turn after the tile's own
fps 4 Frames per second
flow [0, 0] Cells per second the texture slides: x towards the east, y towards the south
shimmer 0 0..1: slow waves of light and dark over the surface
glints 0 0..1: how many cells sparkle at a time
glint "#E6F5FF" The sparkle's colour
shore none Foam colour of waves rolling in to the tile's edge
waves 3 Seconds between waves
shoreWidth 3 Cells out from the edge where the waves start (1..8)
reflect false Things standing by it are mirrored in it
light none { "color", "radius", "intensity" }: light the tile gives off its surroundings (lava)

.gwmap is plain text with one character per tile. Spaces count, every row must be the same width, and every character must be in the legend. Errors give the line and column:

Code example Plain text
village.gwmap:12:30: 'x' is not in the legend.
village.gwmap:7: row is 63 characters wide, the first row is 64.

Objects from a map stand at the bottom centre of their tile (plus offset). A map replaces whatever the generator made inside its area. The same seed always makes the same world, whichever part is visited first.

Particle effects: name.gweffects#

Code example JSON
{
  "schemaVersion": 1,
  "effects": {
    "fire": {
      "blend": "add", "glow": true, "glyphs": "*^'.", "colors": ["#FFD070F0", "#FF9028D0", "#E04810A0", "#40080000"],
      "life": [0.4, 0.75], "speed": [30, 60], "direction": -90, "spread": 30, "gravity": -70, "drag": 1.4,
      "size": [9, 2], "rate": 46, "jitter": 7,
      "light": { "color": "#FF8030", "radius": 40, "intensity": 0.22 }
    }
  }
}

Comments and trailing commas are allowed. Unknown fields are errors. Every field of an effect is optional:

Field Default Meaning
glyphs "*+:." Printable ASCII glyphs from dense to light; a particle steps through them as it fades
colors ["#FFFFFF", "#FFFFFF00"] #RRGGBB or #RRGGBBAA colours from birth to death, blended evenly
life [0.5, 1] Seconds, [min, max] (or one number in a list)
speed [10, 40] Starting speed in pixels per second, [min, max]
direction -90 Degrees: 0 is right, 90 down the screen, -90 up
spread 360 Degrees around direction
gravity 0 Pixels per second squared down the screen; negative rises
drag 0 How quickly speed fades, per second
wind 0 How much the wind passed to Update carries the particle (1 = fully)
size [1, 1] Radius in pixels at birth and at death; 2 or less is one cell
jitter 0 Pixels of random spread around the spawn point
blend "alpha" alpha covers what is behind, add brightens it
glow false Keeps full brightness at night
layer "air" ground draws under objects and actors, air over them
count 10 Particles in one Burst
rate 10 Particles per second for emitters and object effects
light none { "color", "radius", "intensity" }: each particle lights its surroundings, fading with it

Weather: name.gwweather#

Code example JSON
{
  "schemaVersion": 1,
  "start": "clear",
  "blend": 10,
  "look": { "rain": "#C4D4ECD8", "snow": "#F2F6FFE8", "fog": "#B8C0CC", "snowGround": "#F2F6FC", "puddle": "#36435A", "lightning": "#E0E8FF" },
  "states": {
    "clear": { "cloud": 0.1, "wind": 10, "gust": 0.3, "seconds": [60, 120], "next": ["cloudy", "fog"] },
    "storm": { "cloud": 1, "rain": 1, "fog": 0.15, "wind": 95, "gust": 0.8, "lightning": 7, "seconds": [25, 50], "next": ["rain"] }
  }
}

Comments and trailing commas are allowed. Unknown fields are errors.

Field Default Meaning
start the first state The state the weather starts in
blend 10 Seconds a change of weather takes
look see above Colours: rain and snow (with alpha), fog, snowGround (snow lying on the ground and objects), puddle, lightning
states At least one, by name

Every field of a state is optional:

Field Default Meaning
cloud 0 0..1: dims and greys the daylight; part cloud casts drifting shadows
rain 0 0..1: how hard it rains
snow 0 0..1: how hard it snows
fog 0 0..1: how thick the fog banks are
wind 0 Pixels per second towards the east (negative: towards the west), -300..300
gust 0 0..1: how strongly the wind comes and goes
lightning 0 Strikes per minute
seconds [60, 120] How long the state lasts before the weather may change by itself, [min, max]
next none The states that may follow when the weather changes by itself (Weather.Auto)

Sounds: name.gwsounds#

Code example JSON
{
  "schemaVersion": 1,
  "sounds": {
    "hit": { "pitch": [0.85, 1.15], "max": 6, "voices": [
      { "wave": "square", "freq": [380, 70], "duty": 0.35, "attack": 0.001, "decay": 0.14, "lowpass": [5000, 900], "volume": 0.45 },
      { "wave": "noise", "freq": [9000], "attack": 0.001, "decay": 0.07, "volume": 0.55 } ] },
    "rain": { "bus": "ambient", "loop": true, "length": 3, "voices": [
      { "wave": "noise", "freq": [30000], "lowpass": [2600], "highpass": 500, "volume": 0.3 } ] },
    "door": { "file": "sounds/door.wav" },
    "town": { "file": "music/town.ogg", "bus": "music", "loop": true, "loopStart": 6.4, "max": 1 }
  }
}

Comments and trailing commas are allowed. Unknown fields are errors. A game can load several sounds files; a sound named like one read before replaces it (a game's own file, loaded after the shared one, can change a shared sound). A sound has either voices or file:

Field Default Meaning
voices Synthesizer voices, added together (below)
file A recording (asset path): WAV (PCM 8, 16 or 24 bit, or 32-bit float), Ogg Vorbis or MP3; mono or stereo (more channels keep front left and right), any sample rate (resampled to 44,100 Hz)
volume 1 Volume of the sound
pitch [1, 1] Each play picks a speed between these (1.1 is a little higher and shorter)
vary [1, 1] Each play picks a volume between these
max 6 How many may play at once; another one replaces the oldest
bus "sfx" sfx, ambient, music or ui: which volume setting it follows
loop false Voices: a seamless loop of length seconds (0.25..60, default 2), played steadily with no envelope or slide. A file: the recording plays round and round
loopStart 0 Needs loop: where the loop starts again after the first time through, in seconds, from 0 to the sound's length, so an intro plays once
range 0 World pixels at which the sound, placed in the world, fades to nothing (0 = the mixer's 900)

A voice (every field optional):

Field Default Meaning
wave "square" sine, triangle, square, saw, noise or crackle
freq [440] Hz, [hz] or [start, end] sliding over the voice. For noise, how often it changes (low is rumbly, 30000 is hiss); for crackle, clicks per second
duty 0.5 Square waves: the part of each cycle that is high (0.25 is thinner and nasal)
attack, decay, hold, release 0.005, 0.1, 0, 0.05 Envelope, in seconds: up to full volume, down to sustain, held there, then down to nothing
sustain 0 0..1: the level after the decay; 0 makes a blip that ends with the decay
volume 1 The voice's volume
vibrato none [depth in semitones, rate in Hz]: pitch wobble
tremolo none [depth 0..1, rate in Hz]: volume wobble
lowpass none [hz] or [start, end]: cuts the highs (a sweep from high to low closes like a wah)
highpass 0 Hz: cuts the lows
delay 0 Seconds before the voice starts (two-note jingles)

Music: name.gwmusic#

Code example JSON
{
  "schemaVersion": 1,
  "bpm": 84,
  "steps": 2,
  "beats": 4,
  "volume": 0.9,
  "instruments": {
    "lead": { "volume": 0.22, "pan": -0.15, "voices": [ { "wave": "triangle", "attack": 0.012, "decay": 0.15, "sustain": 0.45, "release": 0.3 } ] },
    "kick": { "volume": 0.4, "voices": [ { "wave": "sine", "freq": [150, 45], "decay": 0.18 } ] }
  },
  "patterns": {
    "a": { "lead": "E5 - - - D5 C5 - - | A4 - - - C5 - - -",
           "kick": "x . . . x . . . | x . . . x . . ." }
  },
  "song": ["a", "a"]
}
Field Meaning
bpm Beats per minute (20..400)
steps Steps per beat (1..16): 2 makes each step an eighth note, 4 a sixteenth
beats Beats per bar (1..16, default 4; 3 for a waltz). Where the composer draws bar lines and writes |
volume Volume of the whole piece
instruments Voices as in sound files, plus volume and pan (-1 left .. 1 right). An instrument's freq is relative to C4 (261.63 Hz, the default): each note plays it at the note's pitch
patterns By name: one track per instrument, all with the same number of steps
song The patterns in the order they play; the piece loops

A track is tokens separated by spaces, one per step: a note (C4, F#3, Bb5) starts on its step, - holds the note before for another step, . is a rest, x is a hit at the instrument's own pitch (drums), and | marks a bar (it is skipped). A track plays one note at a time. The piece is rendered once into a stereo loop; notes still ringing at the end carry on into the start, so the loop has no seam.

Data tables: name.gwtable + name.gwschema#

A game's own data (items, monsters, recipes, buildings, moves, its settings) is kept in tables, one file each, in a folder of the assets (normally data/). Each table has a schema beside it, named the same, that says what its rows hold. GameTables.Load(assets) reads every table in the folder, checks every row against its schema, then every reference to another table's rows and every asset path, and reports every problem at once with its file, row and field.

A schema:

Code example JSON
{
  "schemaVersion": 1,
  "title": "Monsters",
  "about": "What the hero fights.",
  "fields": {
    "title": { "type": "text", "about": "The name players see." },
    "life": { "type": "int", "min": 1 },
    "damage": { "type": "range", "min": 0, "whole": true },
    "anim": { "type": "asset", "kind": "anim" },
    "tint": { "type": "colour", "optional": true },
    "drops": { "type": "map", "key": { "type": "ref", "table": "items" }, "of": { "type": "number", "min": 0, "max": 1 }, "default": {} }
  }
}
Property Meaning
schemaVersion 1
title the table's name for people (default: the file name)
about one line on what the table is for
single true for a table of settings: it holds exactly one row
fields the fields every row has, in order, each with its type

Every field has a type, and may have optional (a row may leave it out), default (the value a row that leaves it out gets; the field is then optional) and about (one line on what it means, for Studio's forms). The types:

Type Value Its own properties
text "Skeleton"
int a whole number min, max
number a number min, max
bool true or false
colour "#RRGGBB"
range [low, high], low not above high min, max (both ends), whole (whole numbers)
asset a path from the assets folder kind: art, anim, model, world, effects, sounds, music or weather (the file must have that extension)
ref the name of a row table: the table it comes from (this one or another)
choice one of a list of words options
list [ ... ] of: the field its values are; length: how many it must hold
map { "key": value, ... } key: text (the default), int (with min, max), ref or choice; of: the field its values are
record { ... } with fields of its own fields, as in the schema

A table:

Code example JSON
{
  "schemaVersion": 1,
  "rows": {
    "skeleton": { "title": "Skeleton", "life": 16, "damage": [1, 4], "anim": "anim/skeleton.gwanim", "drops": { "potion": 0.14 } },
    "bone_lord": { "title": "The Bone Lord", "life": 260, "damage": [7, 13], "anim": "anim/bone_lord.gwanim", "tint": "#C0A0FF" }
  }
}
  • rows: each row by name, in the order the game sees them. Row, field and table names are letters, digits, _ and -, starting with a letter or digit.
  • A field the schema does not name is an error; so is a missing field that is neither optional nor has a default.
  • A table of settings ("single": true) holds one row; its name is up to you.
  • Problems read like data/monsters.gwtable: row "skeleton" "life": 0 must be at least 1. or data/units.gwtable: row "archer" "tech": "archeryy" is not a row of techs (it has pottery, archery, ...).
  • A game reads rows into its own C# classes with table.Read<T>() (fields map to properties by name, catchRate to CatchRate), or value by value with table["skeleton"]["life"].Int. What only the code knows (a building kind the game must have, levels that must rise) the game checks itself, reporting it the same way.

Dialogue: name.gwdialogue#

A conversation, or every conversation of one character, as nodes. A conversation starts at start and stops at each node with a line for the game to show; nodes without a line only change the story and go on.

Code example JSON
{
  "schemaVersion": 1,
  "speaker": "Professor Elm",
  "start": "hello",
  "nodes": {
    "hello": { "branch": ["later", "first"] },
    "later": { "text": "How is {starter} doing?", "if": ["starter"] },
    "first": { "text": "Which wildling will you take?",
               "choices": [ { "text": "Emberkit", "to": "emberkit" }, { "text": "Let me think" } ] },
    "emberkit": { "set": ["starter"], "do": "give_starter emberkit", "next": "thanks" },
    "thanks": { "text": "Look after each other.", "add": { "visits": 1 } }
  }
}
Field Meaning
speaker who speaks when a node does not say
start the node a conversation starts at
nodes the nodes, by name (letters, digits, _ and -)

A node's fields, all optional:

Field Meaning
speaker who says its line
text the line; {word} is filled in by the game (or from the story's numbers). Without a line the node only changes things and goes on.
next the node after it
branch nodes to go on to: the first whose if holds (none: the conversation ends)
choices [{ "text", "to", "if" }]: what the player may answer; to leads on (none: the conversation ends); if offers it only when its conditions hold. A node with choices needs a line.
if when a branch may take this node: every condition must hold
set, clear flags it sets and clears as it is entered
add { "name": amount }: added to numbers as it is entered
do an action the game carries out as it is entered ("heal", "give_starter emberkit"); the game decides what its actions mean
at [x, y]: where Studio draws its box

A node goes on by next, branch or choices, only one of them. Conditions are flag (set), !flag (not set), or a number compared with a whole number: coins >= 20, visits == 3 (==, !=, <, <=, >, >=). Flags and numbers belong to the story (StoryState); a game can lend it numbers of its own, such as coins.

Quests: name.gwquests#

Code example JSON
{
  "schemaVersion": 1,
  "quests": {
    "partner": {
      "title": "A first partner",
      "about": "Every trainer starts with a wildling.",
      "stages": [
        { "text": "See Professor Elm in the town square.", "done": ["starter"] },
        { "text": "Beat your rival on Route 1.", "done": ["rival"] }
      ]
    }
  }
}
Field Meaning
title the quest's name in the journal
about one line on what it is about
if it begins when these conditions hold (none: from the start)
stages [{ "text", "done" }]: what the journal says to do, and the conditions that finish it

A quest's stage is the first one not done yet; it is done when every stage is. The same conditions as dialogue.

Texts: name.gwtext#

The words a game shows, in every language it has, named by "texts" in glyphweft.json:

Code example JSON
{
  "schemaVersion": 1,
  "languages": [ { "code": "en", "name": "English" }, { "code": "af", "name": "Afrikaans" } ],
  "texts": {
    "title.play": { "en": "New game", "af": "Nuwe speletjie" },
    "hud.gold": { "en": "{gold} gold", "af": "{gold} goud" },
    "elder.hello": { "en": "Hello, Runefox." }
  }
}
Field Meaning
languages the languages, at least one, in order: a code (en, af, pt-BR: two or three small letters, a country after a dash) and the name players see. The first is the one every text needs
texts each text by name (small letters, digits, ., _ and -), with its words in each language that has them

A text a language lacks shows in the first language. A text can hold values ({gold}), filled in where it is shown, and may use the accented letters the fonts draw (see the Engine manual's Fonts); other characters are errors. Screens (.gwui), dialogue and quests name a text as {@title.play}; the asset check reports a name the table does not have, with its file and line. Problems name the text: runeglyph.gwtext: "only.af" has no English text (the first language's text is the one every row needs).

Input actions: name.gwinput#

A game's input actions, named by "actions" in glyphweft.json:

Code example JSON
{
  "schemaVersion": 1,
  "actions": {
    "up": { "keys": ["W", "Up"], "about": "Walk up" },
    "act": { "keys": ["Z", "Space", "Enter"], "about": "Talk, read on, choose" }
  }
}
Field Meaning
actions each action by name (letters, digits, _, - and .), in the order a settings screen lists them
keys its keys and mouse buttons, at least one: A, Space, Escape, F3, Ctrl+Z, MouseLeft ...
about what it does, for people

An action the game's code also defines (Actions.Define) keeps the file's keys. Problems name the action: game.gwinput: action "jump": 'Spcae': 'Spcae' is not a key or mouse button (...).

Screens: name.gwui#

A menu or HUD of the engine's glyph widgets. C# binds the actions its buttons name and sets the values its text and bars show; nothing in a layout runs code.

Code example JSON
{
  "schemaVersion": 1,
  "overlay": true,
  "back": "resume",
  "values": { "gold": 0 },
  "widgets": [
    { "type": "label", "anchor": "topRight", "at": [1, 1], "text": "{gold} gold" },
    {
      "type": "panel",
      "anchor": "center",
      "size": [30, 0],
      "padding": [2, 1, 2, 1],
      "title": "Paused",
      "children": [
        {
          "type": "stack",
          "size": [26, 0],
          "children": [
            { "type": "button", "text": "Back to the game", "action": "resume", "primary": true },
            { "type": "button", "text": "Quit the game", "action": "quit" }
          ]
        }
      ]
    }
  ]
}
Field Default Meaning
overlay false Drawn over the scene below (a pause menu), which it dims; otherwise the screen is its own
dim 0.55 How much an overlay darkens what is below, 0..1
background none The colour behind a screen that is not an overlay (UiScene clears to it; dark when unset)
back none The action Esc runs
values none Values the screen starts with, by name: true or false, a number, text, or a list of text. Studio previews with them
widgets The widgets, in drawing order

Every widget has a type and may have:

Field Meaning
name For C# to find it (screen.Find<Button>("play")); names are unique in a file
anchor topLeft (the default), top, topRight, left, center, right, bottomLeft, bottom or bottomRight of its parent
at [x, y] in UI cells from its anchor, counted inwards from a right or bottom edge (ignored inside stacks)
size [width, height] in UI cells; 0 fits the content
padding Space inside its edge: [all sides] or [left, top, right, bottom]
showIf, enabledIf A value that must be true for it to show, or to respond; "!name" turns it round. A value is true when it is true, a number other than 0, text that is not empty, or a list with rows
tooltip Text shown when the mouse rests on it

and the fields of its kind:

Type Fields What it is
label text, color, align (left, center, right) Text; with a width it wraps
title text, color, scale (1..4, default 2) Big letters, each pixel of the glyph font a block of cells
panel title, border (default true), background (#RRGGBB or #RRGGBBAA), children A filled, framed area
window title, closable, movable (both default true), action (run when it closes), children A panel with a title bar
stack direction (column or row), spacing, children Lines its children up
button text, action (needed), primary Runs its action when clicked, or with Enter or Space
checkbox text, value (needed), action Shows and changes a true-or-false value
slider value (needed), min, max (default 1), step (default 0.05), action Shows and changes a number
bar value (needed, 0..1), text, color A filled bar
textbox value (needed), placeholder, action (run on Enter) One line of text the player types
list items (a value holding a list of text, needed), value (the chosen row), action (run on double-click or Enter) Rows to choose from
image art (needed) Glyph art at full detail; without a size, as big as the art. "{name}" alone may name a value holding art made in code (a save's thumbnail)
slot icon, count, hint, action (run on a click) An inventory or hotbar slot

In text, tooltip, art, icon, count and hint, {name} shows a value and {name:format} formats a number (.NET formats: {life:0%}, {gold:N0}). A field a kind does not take is an error, said with where it is: hud.gwui: widgets[2].children[0] "play": a button has no "value" (it takes name, anchor, ...).

Saves: name.gwsave and name.gwinfo#

A save slot is two files in the player's folder for the game (saves/): the save, and its information for lists of saves. Both are written in the game's saves format: plain JSON, gzip-compressed JSON, or encrypted. A plain save:

Code example JSON
{
  "schemaVersion": 1,
  "game": "glyphweft-walkabout",
  "info": { "title": "Day 1, 17:52", "saved": "2026-10-20T10:38:02Z", "playSeconds": 62.4, "detail": "clear, walking",
            "thumbnail": { "width": 48, "height": 20, "glyphs": [ "..." ], "colors": [ "..." ] } },
  "sections": {
    "world": { "tiles": [ { "x": 5, "y": 8, "type": "dirt" } ], "removed": [ { "kind": "oak", "x": 132, "y": 191 } ],
               "added": [ { "kind": "crate", "x": 140, "y": 215, "flipX": false } ] },
    "time": { "hours": 17.86, "day": 0 },
    "walkabout": { "actors": [ { "name": "hero", "pos": [140.5, 190], "facing": 4 } ], "torch": true, "zoom": 1, "played": 62.4 }
  }
}
  • info.thumbnail: rows of glyphs, and rows of colours with 12 hex digits a cell (foreground, then background).
  • A section is any JSON value the game wrote. The engine writes world (WorldChanges), time, weather, inventory, and for a simulation simulation (clock, random state, timers, entity ids) plus one section per component type (simulation.machine: entity ids as index, generation pairs, and the values in packed order).
  • Values: [x, y] for vectors and points, [index, generation] for entity ids, "#RRGGBB" for colours.
  • Compressed files are gzip. Encrypted files start with GWSE and a version byte, then a 12-byte nonce and a 16-byte AES-GCM tag, then the compressed JSON, encrypted with a key made from the game's passphrase and id (PBKDF2, SHA-256, 100,000 rounds).
  • The previous save of a slot is kept as name.gwsave.bak.

Asset packs: assets.gwpack#

An exported game's assets in one file beside it, written by the export (you do not edit it). The game reads it in place of its assets folder, and a folder given after it with --assets overrides its files (a patch). It is binary, little-endian:

  • GWPACK, a version byte (1) and a flags byte (1 = encrypted).
  • The number of files (32-bit), then for each: its asset path (a UTF-8 string after its length, 7 bits a byte), where its data starts in the file (64-bit), how many bytes of data it has (32-bit) and how long the file is (32-bit).
  • Then the data, file after file. Each file is deflated; encrypted, it is a 12-byte nonce, a 16-byte AES-GCM tag and the cipher text, with the asset path as associated data. The key is the SHA-256 of glyphweft pack, a zero byte and the game's id, so encryption stops casual copying, not a determined person.

Asset paths are not case-sensitive. A pack that is not one, an unsupported version, an encrypted pack opened without a key and a file the key does not open are reported with the pack's name.

The player's settings: settings.json#

In the player's folder for the game. Every field is optional; the ones present replace the game's settings.

Code example JSON
{ "schemaVersion": 1, "window": [1600, 900], "fullscreen": false, "vSync": true, "pixelScale": 1,
  "audio": { "master": 0.8, "music": 0.5, "sfx": 1, "ambient": 0.8, "ui": 0.8, "mute": false },
  "input": { "attack": ["Space", "MouseRight"] }, "language": "af" }

language is the language the player chose, a code of the game's texts; without it (or when the texts no longer have it) the game shows their first language.

Project settings: glyphweft.json#

See the Engine manual.

Studio projects: Name.gwproject#

How Glyphweft Studio opens a game. It is named after the game (RuneCrawler.gwproject) and sits next to the game's .csproj and its glyphweft.json; a project folder has exactly one.

Code example JSON
{
  "schemaVersion": 1,
  "name": "RuneCrawler",
  "game": "RuneCrawler.csproj",
  "assets": ["../../samples/assets", "assets"],
  "startupScene": "TitleScene",
  "platforms": ["win-x64", "linux-x64"]
}
Field Meaning
schemaVersion 1
name The game's name
game The game's C# project, relative to this file
assets The asset folders, relative to this file, merged in order: later folders replace files of earlier ones (default ["assets"])
startupScene The C# class name of the scene the game starts in
platforms What the game is exported for: win-x64, win-arm64, linux-x64, linux-arm64, osx-x64, osx-arm64 (default ["win-x64"])

Checking a project reports, naming the file: an empty name or startup scene, a game that is not an existing .csproj, missing asset folders, unknown platforms, and a missing glyphweft.json.

Input scripts: .gws#

See the Engine manual.