// High-level MoonBit wrappers for the TIC-80 WebAssembly API.
///|
/// A button on one of the four TIC-80 gamepads.
///
/// The explicit tags are the button IDs expected by the TIC-80 host ABI.
pub(all) enum Button {
P1Up = 0
P1Down = 1
P1Left = 2
P1Right = 3
P1A = 4
P1B = 5
P1X = 6
P1Y = 7
P2Up = 8
P2Down = 9
P2Left = 10
P2Right = 11
P2A = 12
P2B = 13
P2X = 14
P2Y = 15
P3Up = 16
P3Down = 17
P3Left = 18
P3Right = 19
P3A = 20
P3B = 21
P3X = 22
P3Y = 23
P4Up = 24
P4Down = 25
P4Left = 26
P4Right = 27
P4A = 28
P4B = 29
P4X = 30
P4Y = 31
} derive(Debug, Eq)
///|
fn Button::to_int(self : Button) -> Int = "%identity"
///|
/// Returns whether the selected gamepad button is currently pressed.
///
/// The result remains true for as long as the button is held. Use `btnp` when
/// only a new press, or a controlled key-repeat style event, should be reported.
pub fn btn(button : Button) -> Bool {
@raw.btn(button.to_int()) != 0
}
///|
/// Returns whether any button on any of the four gamepads is currently pressed.
///
/// As with `btn`, the result remains true while at least one button is held.
pub fn any_btn() -> Bool {
@raw.btn(-1) != 0
}
///|
/// An immutable snapshot of all 32 TIC-80 gamepad buttons.
#valtype
pub struct ButtonState {
priv bits : UInt
} derive(Debug, Eq)
///|
/// Captures the current state of all four gamepads in one host call.
///
/// The returned value does not change as input changes; call `button_state`
/// again to obtain a new snapshot.
pub fn button_state() -> ButtonState {
{ bits: @raw.btn(-1).reinterpret_as_uint(), }
}
///|
/// Returns whether the selected button was held in this snapshot.
pub fn ButtonState::held(self : ButtonState, button : Button) -> Bool {
(self.bits & (1U << button.to_int())) != 0U
}
///|
/// Returns whether this snapshot contains any held button.
pub fn ButtonState::any(self : ButtonState) -> Bool {
self.bits != 0U
}
///|
/// Returns whether `button` was newly pressed, with optional repeat timing.
///
/// Without `hold` and `period`, this is true only when the button is pressed in
/// the current frame but was not pressed in the previous frame. Both timing
/// values are measured in 60 Hz ticks. After `hold` ticks, a still-held button
/// produces another true result every `period` ticks. For example,
/// `btnp(button, hold=120, period=6)` starts repeating after two seconds and
/// then repeats ten times per second. Leaving both values at -1 disables repeat.
pub fn btnp(button : Button, hold? : Int = -1, period? : Int = -1) -> Bool {
@raw.btnp(button.to_int(), hold, period) != 0
}
///|
/// Returns whether any gamepad button was newly pressed since the previous
/// frame. This convenience wrapper does not enable repeat timing.
pub fn any_btnp() -> Bool {
@raw.btnp(-1, -1, -1) != 0
}
///|
/// Restricts subsequent drawing to the rectangle at `(x, y)` with size
/// `w` by `h`.
///
/// Pixels drawn outside this viewport are not visible. The clipping rectangle
/// remains active until another call to `clip` or `clip_reset`.
pub fn clip(x : Int, y : Int, w : Int, h : Int) -> Unit {
@raw.clip(x, y, w, h)
}
///|
/// Restores the clipping rectangle to the entire screen.
pub fn clip_reset() -> Unit {
@raw.clip(-1, -1, -1, -1)
}
///|
/// Clears the entire screen with `color`, which defaults to palette color 0.
pub fn cls(color? : Int = 0) -> Unit {
@raw.cls(color)
}
///|
/// Draws a filled circle of `radius` in `color`, centered at `(x, y)`.
///
/// TIC-80 rasterizes the circle with the Bresenham algorithm. Use `circb` to
/// draw only its circumference.
pub fn circ(x : Int, y : Int, radius : Int, color : Int) -> Unit {
@raw.circ(x, y, radius, color)
}
///|
/// Draws a one-pixel circle circumference of `radius` in `color`, centered at
/// `(x, y)`.
///
/// TIC-80 rasterizes the circle with the Bresenham algorithm. Use `circ` for a
/// filled circle.
pub fn circb(x : Int, y : Int, radius : Int, color : Int) -> Unit {
@raw.circb(x, y, radius, color)
}
///|
/// Draws a filled ellipse in `color`, centered at `(x, y)` with horizontal and
/// vertical radii `a` and `b`.
///
/// TIC-80 rasterizes the ellipse with the Bresenham algorithm. Use `ellib` to
/// draw only its border.
pub fn elli(x : Int, y : Int, a : Int, b : Int, color : Int) -> Unit {
@raw.elli(x, y, a, b, color)
}
///|
/// Draws a one-pixel ellipse border in `color`, centered at `(x, y)` with
/// horizontal and vertical radii `a` and `b`.
///
/// TIC-80 rasterizes the ellipse with the Bresenham algorithm. Use `elli` for
/// a filled ellipse.
pub fn ellib(x : Int, y : Int, a : Int, b : Int, color : Int) -> Unit {
@raw.ellib(x, y, a, b, color)
}
///|
/// Interrupts cartridge execution and returns to the TIC-80 console after the
/// current `TIC` callback ends.
pub fn exit() -> Unit {
@raw.exit()
}
///|
/// Returns whether one of a sprite's eight user-defined flags is set.
///
/// `sprite_id` ranges from 0 through 511 and `flag` ranges from 0 through 7.
/// Flag meanings are defined entirely by the cartridge; for example, a game
/// might use one flag for solid tiles and another for hazards. Panics when
/// either argument is outside its valid range.
pub fn fget(sprite_id : Int, flag : Int) -> Bool {
check_sprite_flag_args(sprite_id, flag)
@raw.fget(sprite_id, flag) != 0
}
///|
/// Sets or clears one of a sprite's eight user-defined flags.
///
/// `sprite_id` ranges from 0 through 511 and `flag` ranges from 0 through 7.
/// TIC-80 does not prescribe meanings for these flags: a cartridge might use
/// flag 0 for invisible sprites or flag 6 for sprites that should be scaled.
/// The change affects runtime RAM; use `sync` with the `Flags` section and
/// `to_cart=true` to copy runtime flags back to a cartridge resource bank.
/// Panics when either argument is outside its valid range.
pub fn fset(sprite_id : Int, flag : Int, value : Bool) -> Unit {
check_sprite_flag_args(sprite_id, flag)
@raw.fset(sprite_id, flag, value)
}
///|
fn check_sprite_flag_args(sprite_id : Int, flag : Int) -> Unit {
if sprite_id < 0 || sprite_id >= 512 {
abort("sprite id must be between 0 and 511")
}
if flag < 0 || flag >= 8 {
abort("sprite flag must be between 0 and 7")
}
}
///|
/// Draws ASCII text using a custom raster font stored in foreground sprites and
/// returns the rendered width.
///
/// `char_width` and `char_height` select each glyph's dimensions, `fixed`
/// selects fixed-width layout, `scale` enlarges the glyphs, and `alt` selects
/// the alternate 128-character glyph set. Every palette index in
/// `transparent_colors` is treated as transparent; an empty value draws the
/// font opaquely. Use `print`
/// for TIC-80's configured built-in font and `trace` for console output.
/// Panics if `text` contains a non-ASCII code unit.
pub fn font(
text : StringView,
x : Int,
y : Int,
transparent_colors? : Bytes = b"",
char_width? : Int = -1,
char_height? : Int = -1,
fixed? : Bool = false,
scale? : Int = -1,
alt? : Bool = false,
) -> Int {
font_ascii(
encode_c_text(text),
x,
y,
transparent_colors~,
char_width~,
char_height~,
fixed~,
scale~,
alt~,
)
}
///|
/// Draws ASCII bytes using a custom raster font and returns the rendered width,
/// without ASCII encoding or validation.
///
/// This is the low-overhead counterpart of `font`. The caller must ensure that
/// `text` contains only ASCII bytes. The wrapper appends the NUL terminator
/// required by TIC-80 when it is missing; an already terminated value is passed
/// without copying. An earlier NUL truncates the text. The remaining arguments
/// have the same meaning as in `font`.
pub fn font_ascii(
text : Bytes,
x : Int,
y : Int,
transparent_colors? : Bytes = b"",
char_width? : Int = -1,
char_height? : Int = -1,
fixed? : Bool = false,
scale? : Int = -1,
alt? : Bool = false,
) -> Int {
let text = terminate_c_text(text)
@raw.font(
text,
x,
y,
transparent_colors,
transparent_colors.length(),
char_width,
char_height,
fixed,
scale,
alt,
)
}
///|
/// A TIC-80 keyboard key.
///
/// The explicit tags are the key codes expected by the TIC-80 host ABI.
pub(all) enum Key {
A = 1
B = 2
C = 3
D = 4
E = 5
F = 6
G = 7
H = 8
I = 9
J = 10
K = 11
L = 12
M = 13
N = 14
O = 15
P = 16
Q = 17
R = 18
S = 19
T = 20
U = 21
V = 22
W = 23
X = 24
Y = 25
Z = 26
Digit0 = 27
Digit1 = 28
Digit2 = 29
Digit3 = 30
Digit4 = 31
Digit5 = 32
Digit6 = 33
Digit7 = 34
Digit8 = 35
Digit9 = 36
Minus = 37
Equals = 38
LeftBracket = 39
RightBracket = 40
Backslash = 41
Semicolon = 42
Apostrophe = 43
Grave = 44
Comma = 45
Period = 46
Slash = 47
Space = 48
Tab = 49
Return = 50
Backspace = 51
Delete = 52
Insert = 53
PageUp = 54
PageDown = 55
Home = 56
End = 57
Up = 58
Down = 59
Left = 60
Right = 61
CapsLock = 62
Ctrl = 63
Shift = 64
Alt = 65
Escape = 66
F1 = 67
F2 = 68
F3 = 69
F4 = 70
F5 = 71
F6 = 72
F7 = 73
F8 = 74
F9 = 75
F10 = 76
F11 = 77
F12 = 78
Numpad0 = 79
Numpad1 = 80
Numpad2 = 81
Numpad3 = 82
Numpad4 = 83
Numpad5 = 84
Numpad6 = 85
Numpad7 = 86
Numpad8 = 87
Numpad9 = 88
NumpadPlus = 89
NumpadMinus = 90
NumpadMultiply = 91
NumpadDivide = 92
NumpadEnter = 93
NumpadPeriod = 94
} derive(Debug, Eq)
///|
fn Key::to_int(self : Key) -> Int = "%identity"
///|
/// Returns whether the selected keyboard key is currently pressed.
///
/// The result remains true for as long as the key is held. Use `keyp` when only
/// a new press, or a controlled key-repeat style event, should be reported.
pub fn key(key : Key) -> Bool {
@raw.key(key.to_int()) != 0
}
///|
/// Returns whether any keyboard key is currently pressed.
pub fn any_key() -> Bool {
@raw.key(-1) != 0
}
///|
/// Returns whether `key` was newly pressed, with optional repeat timing.
///
/// Without `hold` and `period`, this is true only when the key is pressed in the
/// current frame but was not pressed in the previous frame. Both timing values
/// are measured in 60 Hz ticks. After `hold` ticks, a still-held key produces
/// another true result every `period` ticks. Leaving both values at -1 disables
/// repeat. This is the keyboard counterpart of `btnp`.
pub fn keyp(key : Key, hold? : Int = -1, period? : Int = -1) -> Bool {
@raw.keyp(key.to_int(), hold, period) != 0
}
///|
/// Returns whether any keyboard key was newly pressed since the previous frame.
/// This convenience wrapper does not enable repeat timing.
pub fn any_keyp() -> Bool {
@raw.keyp(-1, -1, -1) != 0
}
///|
/// Draws a straight line in `color` from `(x0, y0)` to `(x1, y1)`.
pub fn line(
x0 : Float,
y0 : Float,
x1 : Float,
y1 : Float,
color : Int,
) -> Unit {
@raw.line(x0, y0, x1, y1, color)
}
///|
/// A sprite or map tile flip applied by TIC-80.
pub(all) enum Flip {
NoFlip = 0
Horizontal = 1
Vertical = 2
Both = 3
} derive(Debug, Eq)
///|
fn Flip::to_int(self : Flip) -> Int = "%identity"
///|
/// A clockwise sprite or map tile rotation applied by TIC-80.
pub(all) enum Rotation {
NoRotation = 0
Clockwise90 = 1
Clockwise180 = 2
Clockwise270 = 3
} derive(Debug, Eq)
///|
fn Rotation::to_int(self : Rotation) -> Int = "%identity"
///|
/// The current tile state passed to a map remap callback, and the replacement
/// state returned by it.
///
/// The callback may replace `tile_id`, apply a `flip`, apply a clockwise
/// `rotate`, or return the value unchanged.
#valtype
pub(all) struct MapRemapResult {
tile_id : Int
flip : Flip
rotate : Rotation
} derive(Debug, Eq)
///|
/// Draws a rectangular region of TIC-80's tile map at a screen position.
///
/// The map contains 8 by 8 pixel cells and can be up to 240 cells wide by 136
/// cells high. `(x, y)` is the first map cell, `(w, h)` is the region size in
/// cells, and `(sx, sy)` is its destination in screen pixels. The effective
/// defaults selected by -1 are `(x, y) = (0, 0)`, `(w, h) = (30, 17)`, and
/// `scale = 1`. Every palette index in `transparent_colors` is skipped; an
/// empty value draws tiles opaquely.
///
/// When present, `remap` receives the current tile state and its map
/// coordinates, and returns the state to draw. It can replace, flip, rotate, or
/// hide tiles for this draw without changing map RAM, which is useful for
/// animated tiles, doors, and object-spawn markers. The callback must be
/// capture-free because it is stored as a WebAssembly function reference.
/// Use `mset` for persistent runtime-map changes.
///
/// Map cells are stored sequentially from byte address `0x08000`; adjacent
/// rows are 240 bytes apart. For example, the cell below the top-left cell is
/// at `0x08000 + 240`, or `0x080f0`.
pub fn map(
x? : Int = -1,
y? : Int = -1,
w? : Int = -1,
h? : Int = -1,
sx? : Int = 0,
sy? : Int = 0,
transparent_colors? : Bytes = b"",
scale? : Int = -1,
remap? : FuncRef[(MapRemapResult, Int, Int) -> MapRemapResult],
) -> Unit {
let map_data : @raw.MapData? = match remap {
None => None
Some(remap) =>
Some({
trampoline: map_remap_trampoline_ref,
user_data: map_remap_to_int(remap),
result_buffer: FixedArray::make(12, b'\x00'),
})
}
@raw.map(
x,
y,
w,
h,
sx,
sy,
transparent_colors,
transparent_colors.length(),
scale,
map_data,
)
}
///|
let map_remap_trampoline_ref : FuncRef[
(Int, Int, Int, FixedArray[Byte]) -> Unit,
] = fn(remap, x, y, result_buffer) {
let current : MapRemapResult = {
tile_id: result_buffer[0].to_int(),
flip: decode_flip(decode_i32_le(result_buffer, 4)),
rotate: decode_rotation(decode_i32_le(result_buffer, 8)),
}
let replacement = map_remap_from_int(remap)(current, x, y)
result_buffer[0] = replacement.tile_id.to_byte()
encode_i32_le(result_buffer, 4, replacement.flip.to_int())
encode_i32_le(result_buffer, 8, replacement.rotate.to_int())
}
///|
fn map_remap_to_int(
remap : FuncRef[(MapRemapResult, Int, Int) -> MapRemapResult],
) -> Int = "%identity"
///|
fn map_remap_from_int(
remap : Int,
) -> FuncRef[(MapRemapResult, Int, Int) -> MapRemapResult] = "%identity"
///|
fn decode_flip(value : Int) -> Flip {
match value & 3 {
0 => NoFlip
1 => Horizontal
2 => Vertical
_ => Both
}
}
///|
fn decode_rotation(value : Int) -> Rotation {
match value & 3 {
0 => NoRotation
1 => Clockwise90
2 => Clockwise180
_ => Clockwise270
}
}
///|
fn decode_i32_le(buffer : FixedArray[Byte], offset : Int) -> Int {
buffer[offset].to_int() |
(buffer[offset + 1].to_int() << 8) |
(buffer[offset + 2].to_int() << 16) |
(buffer[offset + 3].to_int() << 24)
}
///|
fn encode_i32_le(buffer : FixedArray[Byte], offset : Int, value : Int) -> Unit {
buffer[offset] = value.to_byte()
buffer[offset + 1] = (value >> 8).to_byte()
buffer[offset + 2] = (value >> 16).to_byte()
buffer[offset + 3] = (value >> 24).to_byte()
}
///|
/// Copies `length` bytes within TIC-80's 96 KiB RAM.
///
/// `dest` and `src` are byte addresses. The source and destination ranges may
/// overlap. A negative length or a range outside TIC-80 RAM is ignored. This
/// function is useful for moving runtime sprites, maps, sounds, and other
/// cartridge data, but does not copy MoonBit heap objects or `Bytes` values.
pub fn memcpy(dest : Int, src : Int, length : Int) -> Unit {
@raw.memcpy(dest, src, length)
}
///|
/// Fills `length` bytes of TIC-80 RAM with `value`.
///
/// `address` is a byte address. A negative length or a range outside TIC-80 RAM
/// is ignored. This can modify any runtime resource represented in TIC-80 RAM,
/// but does not operate on MoonBit heap objects or `Bytes` values.
pub fn memset(address : Int, value : Byte, length : Int) -> Unit {
@raw.memset(address, value, length)
}
///|
/// Returns the sprite ID stored at map coordinates `(x, y)`.
pub fn mget(x : Int, y : Int) -> Int {
@raw.mget(x, y)
}
///|
/// Changes the sprite ID at map coordinates `(x, y)` in runtime map RAM.
///
/// The change normally lasts only while the cartridge is running. To save it
/// to a cartridge resource bank, call `sync` with the `Map` section and
/// `to_cart=true`.
pub fn mset(x : Int, y : Int, tile_id : Int) -> Unit {
@raw.mset(x, y, tile_id)
}
///|
/// A snapshot of the mouse cursor, wheel, and button state.
///
/// `x` and `y` are screen coordinates. `scroll_x` and `scroll_y` are signed
/// wheel movements for the current frame. `left`, `middle`, and `right` report
/// whether the corresponding button is pressed.
pub(all) struct MouseState {
x : Int
y : Int
scroll_x : Int
scroll_y : Int
left : Bool
middle : Bool
right : Bool
} derive(Debug, Eq)
///|
/// Returns an independent snapshot of the current mouse coordinates, wheel
/// movement, and button state.
pub fn mouse() -> MouseState {
let buffer = FixedArray::make(9, b'\x00')
@raw.mouse(buffer)
decode_mouse_state(buffer)
}
///|
fn decode_mouse_state(buffer : FixedArray[Byte]) -> MouseState {
{
x: decode_i16_le(buffer, 0),
y: decode_i16_le(buffer, 2),
scroll_x: decode_i8(buffer[4]),
scroll_y: decode_i8(buffer[5]),
left: buffer[6] != b'\x00',
middle: buffer[7] != b'\x00',
right: buffer[8] != b'\x00',
}
}
///|
fn decode_i16_le(buffer : FixedArray[Byte], offset : Int) -> Int {
let bits = buffer[offset].to_int() | (buffer[offset + 1].to_int() << 8)
if (bits & 0x8000) != 0 {
bits - 0x10000
} else {
bits
}
}
///|
fn decode_i8(byte : Byte) -> Int {
let bits = byte.to_int()
if (bits & 0x80) != 0 {
bits - 0x100
} else {
bits
}
}
///|
/// Starts playing one of the eight tracks created in TIC-80's Music Editor.
///
/// `track` ranges from 0 through 7. By default playback starts at the beginning
/// of the track, loops after its final populated frame, does not sustain notes
/// across frame boundaries, and uses the tempo and speed stored in the track.
/// Use `frame` and `row` to start at a specific position, or `tempo` and `speed`
/// to override the track settings. Call `music_stop` to stop playback.
/// Panics if `track` is outside the range 0 through 7.
pub fn music(
track : Int,
frame? : Int = -1,
row? : Int = -1,
loop_? : Bool = true,
sustain? : Bool = false,
tempo? : Int = -1,
speed? : Int = -1,
) -> Unit {
check_music_track(track)
@raw.music(track, frame, row, loop_, sustain, tempo, speed)
}
///|
fn check_music_track(track : Int) -> Unit {
if track < 0 || track >= 8 {
abort("music: track must be between 0 and 7")
}
}
///|
/// Stops the currently playing music and resets its channels.
///
/// This does not stop sound effects started independently with `sfx`.
pub fn music_stop() -> Unit {
@raw.music(-1, -1, -1, false, false, -1, -1)
}
///|
/// Returns the palette color at screen coordinates `(x, y)`.
pub fn pix(x : Int, y : Int) -> Byte {
@raw.pix(x, y, -1)
}
///|
/// Sets the pixel at screen coordinates `(x, y)` to `color`.
pub fn pix_set(x : Int, y : Int, color : Int) -> Unit {
ignore(@raw.pix(x, y, color))
}
///|
/// Reads one byte from TIC-80 RAM at the byte address `address`.
///
/// Returns zero when `address` is outside TIC-80 RAM. Use `peek4`, `peek2`, or
/// `peek1` when the address is expressed in narrower units.
pub fn peek(address : Int) -> Byte {
@raw.peek(address, 8)
}
///|
/// Reads one four-bit nibble from TIC-80 RAM.
///
/// `address` is a nibble index rather than a byte address. Addresses `2 * n`
/// and `2 * n + 1` select the low and high nibbles of byte `n`, respectively.
/// Returns zero when the address is outside TIC-80 RAM.
pub fn peek4(address : Int) -> Byte {
@raw.peek4(address)
}
///|
/// Reads one two-bit value from TIC-80 RAM.
///
/// `address` is a two-bit-field index. Addresses `4 * n` through `4 * n + 3`
/// select the four fields of byte `n` from least to most significant. Returns
/// zero when the address is outside TIC-80 RAM.
pub fn peek2(address : Int) -> Byte {
@raw.peek2(address)
}
///|
/// Reads one bit from TIC-80 RAM.
///
/// `address` is a bit index. Addresses `8 * n` through `8 * n + 7` select the
/// bits of byte `n` from least to most significant. Returns zero when the
/// address is outside TIC-80 RAM.
pub fn peek1(address : Int) -> Byte {
@raw.peek1(address)
}
///|
/// Reads a value from a persistent-memory slot.
///
/// A cartridge has 256 unsigned 32-bit slots, suitable for high scores, level
/// progress, achievements, and other small saved values. `index` ranges from 0
/// through 255. Panics when it is outside that range.
///
/// By default the save is associated with the cartridge hash, so changing the
/// cartridge can create a different save namespace. Set `saveid:` in cartridge
/// metadata to keep a stable namespace across cartridge updates.
pub fn pmem(index : Int) -> UInt {
check_pmem_index(index)
@raw.pmem(index, -1L)
}
///|
/// Writes a persistent-memory slot and returns its previous value.
///
/// Values are unsigned 32-bit integers. `index` ranges from 0 through 255.
/// Panics when it is outside that range. See `pmem` for save identity details.
pub fn pmem_set(index : Int, value : UInt) -> UInt {
check_pmem_index(index)
@raw.pmem(index, value.to_int64())
}
///|
fn check_pmem_index(index : Int) -> Unit {
if index < 0 || index >= 256 {
abort("persistent-memory index must be between 0 and 255")
}
}
///|
/// Writes one byte to TIC-80 RAM at the byte address `address`.
///
/// Writes outside TIC-80 RAM are ignored. Use `poke4`, `poke2`, or `poke1`
/// when the address is expressed in narrower units.
pub fn poke(address : Int, value : Byte) -> Unit {
@raw.poke(address, value.to_int(), 8)
}
///|
/// Writes one four-bit nibble to TIC-80 RAM.
///
/// `address` uses the nibble indexing described by `peek4`; only the low four
/// bits of `value` are stored. Writes outside TIC-80 RAM are ignored.
pub fn poke4(address : Int, value : Byte) -> Unit {
@raw.poke4(address, value.to_int())
}
///|
/// Writes one two-bit value to TIC-80 RAM.
///
/// `address` uses the two-bit-field indexing described by `peek2`; only the
/// low two bits of `value` are stored. Writes outside TIC-80 RAM are ignored.
pub fn poke2(address : Int, value : Byte) -> Unit {
@raw.poke2(address, value.to_int())
}
///|
/// Writes one bit to TIC-80 RAM.
///
/// `address` uses the bit indexing described by `peek1`; only the lowest bit
/// of `value` is stored. Writes outside TIC-80 RAM are ignored.
pub fn poke1(address : Int, value : Byte) -> Unit {
@raw.poke1(address, value.to_int())
}
///|
/// Draws ASCII text using TIC-80's configured font and returns its width.
///
/// When `fixed` is true, every character occupies an equal-width box, so a
/// narrow glyph such as `i` advances by the same amount as `w`. When false,
/// proportional glyph widths are used with one pixel of spacing. `scale`
/// enlarges the text, and `small_font` selects TIC-80's small built-in font.
/// Use `font` for a custom raster font or `trace` for console output. Panics if
/// `text` contains a non-ASCII code unit.
pub fn print(
text : StringView,
x? : Int = 0,
y? : Int = 0,
color? : Int = 15,
fixed? : Bool = false,
scale? : Int = 1,
small_font? : Bool = false,
) -> Int {
print_ascii(encode_c_text(text), x~, y~, color~, fixed~, scale~, small_font~)
}
///|
/// Draws ASCII bytes using TIC-80's configured font and returns the rendered
/// width, without ASCII encoding or validation.
///
/// This is the low-overhead counterpart of `print`. The caller must ensure that
/// `text` contains only ASCII bytes. The wrapper appends the NUL terminator
/// required by TIC-80 when it is missing; an already terminated value is passed
/// without copying. An earlier NUL truncates the text. The remaining arguments
/// have the same meaning as in `print`.
pub fn print_ascii(
text : Bytes,
x? : Int = 0,
y? : Int = 0,
color? : Int = 15,
fixed? : Bool = false,
scale? : Int = 1,
small_font? : Bool = false,
) -> Int {
@raw.print(terminate_c_text(text), x, y, color, fixed, scale, small_font)
}
///|
/// Draws a filled rectangle in `color` at `(x, y)` with size `w` by `h`.
/// Use `rectb` when only a one-pixel border is needed.
pub fn rect(x : Int, y : Int, w : Int, h : Int, color : Int) -> Unit {
@raw.rect(x, y, w, h, color)
}
///|
/// Draws a one-pixel rectangle border in `color` at `(x, y)` with size `w` by
/// `h`. Use `rect` for a filled rectangle.
pub fn rectb(x : Int, y : Int, w : Int, h : Int, color : Int) -> Unit {
@raw.rectb(x, y, w, h, color)
}
///|
/// One of TIC-80's four sound-effect channels.
///
/// The explicit tags are the channel IDs expected by the TIC-80 host ABI.
pub(all) enum SfxChannel {
Channel0 = 0
Channel1 = 1
Channel2 = 2
Channel3 = 3
} derive(Debug, Eq)
///|
fn SfxChannel::to_int(self : SfxChannel) -> Int = "%identity"
///|
/// Plays one of the 64 effects created in TIC-80's SFX Editor.
///
/// `note` is a combined note number from 0 through 95, with twelve notes per
/// octave. The values in each octave are C, C sharp, D, D sharp, E, F, F sharp,
/// G, G sharp, A, A sharp, and B; flat names are not represented separately.
/// For example, 14 is D in the second octave. When `note` is omitted, the note
/// stored in the selected effect is used.
/// `duration` is measured in 60 Hz ticks; -1 plays continuously. The left and
/// right volumes range from 0 through 15. When `speed` is omitted, the speed
/// stored in the selected effect is used; explicit speeds range from -4 through
/// 3. Call `sfx_stop` to stop an effect on a channel.
///
/// Panics if `id` is outside the range 0 through 63.
pub fn sfx(
id : Int,
note? : Int,
duration? : Int = -1,
channel? : SfxChannel = Channel0,
volume_left? : Int = 15,
volume_right? : Int = 15,
speed? : Int,
) -> Unit {
if id < 0 || id >= 64 {
abort("sfx: id must be between 0 and 63")
}
let combined_note = match note {
None => read_sfx_default_note(id)
Some(note) => note
}
let (raw_note, raw_octave) = split_sfx_note(combined_note)
let raw_speed = match speed {
None => 8
Some(speed) => speed
}
@raw.sfx(
id,
raw_note,
raw_octave,
duration,
channel.to_int(),
volume_left,
volume_right,
raw_speed,
)
}
///|
/// Stops the sound effect playing on `channel`.
///
/// This does not stop music playback or effects on the other channels.
pub fn sfx_stop(channel? : SfxChannel = Channel0) -> Unit {
@raw.sfx(-1, 0, 0, -1, channel.to_int(), 15, 15, 8)
}
///|
fn read_sfx_default_note(id : Int) -> Int {
// TIC-80 stores 64 66-byte effects at 0x100e4. The final two bytes hold
// the effect's octave, speed, note, and stereo flags.
let metadata_address = 0x100e4 + id * 66 + 64
decode_sfx_default_note(peek(metadata_address), peek(metadata_address + 1))
}
///|
fn decode_sfx_default_note(metadata0 : Byte, metadata1 : Byte) -> Int {
let octave = metadata0.to_int() & 0x07
let note = metadata1.to_int() & 0x0f
note + octave * 12
}
///|
fn split_sfx_note(note : Int) -> (Int, Int) {
(note % 12, note / 12)
}
///|
/// Draws a sprite, or a rectangular region of sprites, at `(x, y)`.
///
/// `id` selects the top-left sprite. `w` and `h` select a composite rectangular
/// region in sprite units. `scale = 2`, for example, draws each 8 by 8 sprite
/// in a 16 by 16 pixel area. `flip` mirrors the result and `rotate` rotates it
/// clockwise in 90-degree steps. Every palette index in
/// `transparent_colors` is skipped; an empty value draws the sprite opaquely.
pub fn spr(
id : Int,
x : Int,
y : Int,
transparent_colors? : Bytes = b"",
scale? : Int = 1,
flip? : Flip = NoFlip,
rotate? : Rotation = NoRotation,
w? : Int = 1,
h? : Int = 1,
) -> Unit {
@raw.spr(
id,
x,
y,
transparent_colors,
transparent_colors.length(),
scale,
flip.to_int(),
rotate.to_int(),
w,
h,
)
}
///|
/// A cartridge resource section that can be transferred by `sync`.
///
/// Each variant corresponds to one bit in TIC-80's resource synchronization
/// mask.
pub(all) enum SyncSection {
Tiles
Sprites
Map
Sfx
Music
Palette
Flags
Screen
} derive(Debug, Eq)
///|
fn SyncSection::to_int(self : SyncSection) -> Int = "%identity"
///|
/// Synchronizes cartridge resources with runtime memory. An empty `sections`
/// list selects every section. By default, data is copied from bank 0 into
/// runtime memory; set `to_cart` to copy runtime data back to the bank. Each
/// call transfers the requested tiles, sprites, map, sound effects, music,
/// palette, flags, and/or screen data.
///
/// TIC-80 Pro cartridges provide eight resource banks. Each resource section
/// can be synchronized at most once per frame; later calls in the same frame
/// may still transfer sections not selected earlier. Calling `sync(bank=0)`
/// with no sections restores all runtime resources from bank 0. Cartridge code
/// is not loaded by `sync`; TIC-80 handles code-bank loading automatically.
pub fn sync(
sections? : ArrayView[SyncSection] = [],
bank? : Int = 0,
to_cart? : Bool = false,
) -> Unit {
@raw.sync(encode_sync_mask(sections), bank, to_cart)
}
///|
fn encode_sync_mask(sections : ArrayView[SyncSection]) -> Int {
sections.fold(init=0, (mask, section) => mask | (1 << section.to_int()))
}
///|
/// Writes an ASCII message to the TIC-80 console and then panics.
///
/// This is useful for reporting a fatal error before trapping the cartridge.
/// `color` has the same meaning and default as in `trace`. Panics while
/// encoding the message if it contains a non-ASCII code unit.
pub fn[T] abort(message : StringView, color? : Int = -1) -> T {
abort_ascii(encode_c_text(message), color~)
}
///|
/// Writes an ASCII message to the TIC-80 console and then panics, without ASCII
/// encoding or validation.
///
/// This is the low-overhead counterpart of `abort`. The caller must ensure that
/// `message` contains only ASCII bytes. The wrapper appends the NUL terminator
/// required by TIC-80 when it is missing; an already terminated value is passed
/// without copying. An earlier NUL truncates the message. `color` has the same
/// meaning and default as in `trace`.
pub fn[T] abort_ascii(message : Bytes, color? : Int = -1) -> T {
trace_ascii(message, color~)
panic()
}
///|
/// Returns the elapsed time in milliseconds since the cartridge started.
///
/// This is useful for animation, elapsed-time tracking, and timed events.
pub fn time() -> Float {
@raw.time()
}
///|
/// Returns the current Unix timestamp in seconds.
///
/// The timestamp counts seconds since 1970-01-01 00:00:00 UTC and can be used
/// for cartridge state that evolves between play sessions.
pub fn tstamp() -> UInt {
@raw.tstamp()
}
///|
/// Writes ASCII text to the TIC-80 console in `color`.
///
/// This is intended for debugging. The default color is palette color 15; use
/// the console's `cls` command to clear accumulated trace output. Panics if
/// `text` contains a non-ASCII code unit.
pub fn trace(text : StringView, color? : Int = -1) -> Unit {
trace_ascii(encode_c_text(text), color~)
}
///|
/// Writes ASCII bytes to the TIC-80 console without ASCII encoding or
/// validation.
///
/// This is the low-overhead counterpart of `trace`. The caller must ensure that
/// `text` contains only ASCII bytes. The wrapper appends the NUL terminator
/// required by TIC-80 when it is missing; an already terminated value is passed
/// without copying. An earlier NUL truncates the text. `color` has the same
/// meaning and default as in `trace`.
pub fn trace_ascii(text : Bytes, color? : Int = -1) -> Unit {
@raw.trace(terminate_c_text(text), color)
}
///|
fn encode_c_text(text : StringView) -> Bytes {
@ascii.encode(text) + b"\x00"
}
///|
fn terminate_c_text(text : Bytes) -> Bytes {
if text.length() > 0 && text[text.length() - 1] == b'\x00' {
text
} else {
text + b"\x00"
}
}
///|
/// Draws a triangle filled with `color` using the three supplied vertices.
pub fn tri(
x1 : Float,
y1 : Float,
x2 : Float,
y2 : Float,
x3 : Float,
y3 : Float,
color : Int,
) -> Unit {
@raw.tri(x1, y1, x2, y2, x3, y3, color)
}
///|
/// Draws a one-pixel triangle border in `color` using the three supplied
/// vertices.
pub fn trib(
x1 : Float,
y1 : Float,
x2 : Float,
y2 : Float,
x3 : Float,
y3 : Float,
color : Int,
) -> Unit {
@raw.trib(x1, y1, x2, y2, x3, y3, color)
}
///|
/// The texture sampled by `ttri`.
///
/// `TileSheet` samples image RAM through the active tile or sprite-sheet
/// segment. `TileMap` samples map RAM and resolves its tile IDs through image
/// RAM. `OtherVideoBank` samples the framebuffer of the video bank that is not
/// currently selected by `vbank_set`.
pub(all) enum TextureSource {
TileSheet = 0
TileMap = 1
OtherVideoBank = 2
} derive(Debug, Eq)
///|
fn TextureSource::to_int(self : TextureSource) -> Int = "%identity"
///|
/// Draws a triangle textured from `texture_source`.
///
/// The `u` and `v` coordinates are interpreted in the coordinate space of the
/// selected source. Image RAM and map RAM are treated as single large images,
/// and the coordinates address pixels rather than sprite IDs. For example, the
/// top-left corner of sprite 2 is at `(u, v) = (16, 0)`.
///
/// `z1`, `z2`, and `z3` provide per-vertex depth for perspective correction;
/// triangles whose vertices have different depths can otherwise appear
/// distorted. Set `depth` to use TIC-80's depth buffer. When `OtherVideoBank`
/// is selected, `ttri` samples whichever video bank is not currently selected:
/// it normally samples `Bank1` while drawing to `Bank0`, and samples `Bank0`
/// while drawing to `Bank1`. Every palette index in `transparent_colors` is
/// skipped; an empty value draws the texture opaquely.
pub fn ttri(
x1 : Float,
y1 : Float,
x2 : Float,
y2 : Float,
x3 : Float,
y3 : Float,
u1 : Float,
v1 : Float,
u2 : Float,
v2 : Float,
u3 : Float,
v3 : Float,
texture_source? : TextureSource = TileSheet,
transparent_colors? : Bytes = b"",
z1? : Float = 0.0,
z2? : Float = 0.0,
z3? : Float = 0.0,
depth? : Bool = false,
) -> Unit {
@raw.ttri(
x1,
y1,
x2,
y2,
x3,
y3,
u1,
v1,
u2,
v2,
u3,
v3,
texture_source.to_int(),
transparent_colors,
transparent_colors.length(),
z1,
z2,
z3,
depth,
)
}
///|
/// One of TIC-80's two 16 KiB video-memory banks.
///
/// Drawing commands and direct VRAM access operate on the selected bank.
/// `Bank1` is composited over `Bank0`; pixels matching `Bank1`'s clear color
/// are transparent and reveal `Bank0`. Video banks are always available and
/// are unrelated to the cartridge resource banks selected by `sync`.
pub(all) enum VideoBank {
Bank0 = 0
Bank1 = 1
} derive(Debug, Eq)
///|
fn VideoBank::to_int(self : VideoBank) -> Int = "%identity"
///|
/// Returns the currently selected video bank without changing it.
///
/// Use `vbank_set` when subsequent drawing or direct VRAM operations should
/// target a different bank.
pub fn vbank() -> VideoBank {
decode_video_bank(@raw.vbank(-1))
}
///|
/// Selects the video bank used by subsequent drawing and direct VRAM
/// operations, and returns the previously selected bank.
///
/// The return value makes temporary switching straightforward: save it, draw
/// into another bank, then pass it back to `vbank_set` to restore the previous
/// target. This only switches VRAM; it does not select a cartridge resource
/// bank for `sync`.
pub fn vbank_set(bank : VideoBank) -> VideoBank {
decode_video_bank(@raw.vbank(bank.to_int()))
}
///|
fn decode_video_bank(value : Byte) -> VideoBank {
if value == b'\x00' {
Bank0
} else {
Bank1
}
}