Crash-tan Crash-tan

Major bug fixes inbound!

Crash BASIC 1.57.2 Release Notes

written by Crash-tan

Behavior parameters learned to carry more than numbers, and two old bugs fell
out of the hedge on the way. Somewhere else entirely, MEMORYBUFFER stopped
hiding your redraws.

Then a tug on one loose thread — a particle that looked wrong for a single frame
— unravelled a whole row of them. SCALETO was being forgotten a tick after you
set it, a misspelled image handle drew nothing and said nothing while colliding
at a size it made up, EMITPARTICLE could only ever reach frame 0 of a
spritesheet, and PUTIMAGE had been accepting an ORIGIN it never applied.

The same thread ran all the way into tilemaps, where a BASEMAP had been naming
a tile-set picture that nothing ever loaded — the most patient of these bugs, and
the one that showed you the least.

And two that had nothing to do with any of it: resizing a desktop window did not
resize the picture in it, and a sprite destroyed by a collision handler froze on
screen instead of disappearing.

A PARAMETER CAN NOW BE A NAME OR A POINT, NOT JUST A NUMBER

@param used to mean "a number". That was fine for how fast, how far and how
long — and useless for the arguments that actually differ between one sprite and
the next: who to chase, what to wear, where to turn.

All three now take a parameter, and the default you write declares which kind it
is:

BEHAVIOR "icon"
  ANIMATE @sheet="hero", 0, 1, 2 FRAMERATE 8   ' a STRING parameter
  BORBIT @center=(0, 0) DISTANCE 30            ' a COORDINATE parameter
  BMOVE @dx=0, @dy=0                           ' NUMBER parameters, as always
END BEHAVIOR

SET SPRITE 5 BEHAVIOR "icon", @sheet = "villain", @center = hubPos

One behavior, and every sprite running it can wear a different sheet, orbit a
different point, and chase a different target.

A coordinate takes a parameter whole or one axis at a time:

BROTATE 0 TO 360 DURATION 60 ORIGIN(@ox=0.5, 1)   ' pivot X varies, Y is pinned

Everything that already worked still works and still means the same thing. This
only widens what may go in those slots.

AND @thing = "a string" NO LONGER SILENTLY MEANS ZERO

This is the bug underneath the feature. SET SPRITE 5 BEHAVIOR "icon", @image = "hero" parsed before today, then quietly handed the behavior 0 — because
a parameter could only be a number, and everything else was converted to one on
the way in. Nothing said so. You got a sprite wearing the wrong texture and no
thread to pull.

Now a value the parameter system cannot carry — an array, an object, a function,
a range, a mutation — stops the program and names the parameter and what it was
given. So does passing the wrong kind: the default written in the behavior
says what that parameter is, and handing a number to a text parameter (or a
string to a numeric one) is refused rather than quietly falling back to the
default.

BEHAVIOR "icon"
  BIMAGE @sheet="idle"
END BEHAVIOR

SET SPRITE 5 BEHAVIOR "icon", @sheet = 3
' → @sheet is a STRING parameter of behavior "icon", but was given NUMBER

BEHAVIORS CAN NOW BE POINTED AT A SPRITE

BFOLLOW's target — who to follow — is now a parameter like every other
argument in a behavior:

BEHAVIOR "chase"
  BFOLLOW @target=0 DISTANCE 40 SPEED 2
END BEHAVIOR

SET SPRITE 10 BEHAVIOR "chase", @target = playerA
SET SPRITE 11 BEHAVIOR "chase", @target = playerB

Two chasers, two targets, one behavior.

Before this, DISTANCE, SPEED and ELEVATION all took @param, and the
sprite id did not — so the one thing you actually wanted to vary per sprite was
the one thing baked into the definition. A second chaser meant a second
behavior, identical but for a number, and a fleet of guards meant a behavior per
guard.

Everything you have already written still parses and still means exactly what it
meant: BFOLLOW 0 DISTANCE 40, BFOLLOW enemyId DISTANCE 40, and any
expression you were passing before all work unchanged. This only widens what is
allowed in that slot.

A couple of details worth knowing:

  • The target is resolved when the behavior is activated, like every other
    parameter — so a sprite chases whoever it was pointed at, and re-activating
    with a different @target re-points it.
  • A target that names no sprite simply follows nothing. It does not drift, and
    it does not chase sprite 0 by accident.

SO CAN BORBIT, AND BIMAGE'S FRAME

The same was true of the other two arguments that named a thing rather than a
number, and they got the same treatment:

BEHAVIOR "ring"
  BORBIT SPRITE @center=0 DISTANCE 30 SPEED 2
END BEHAVIOR

SET SPRITE 20 BEHAVIOR "ring", @center = 1   ' orbits the sprite at slot 1
SET SPRITE 21 BEHAVIOR "ring", @center = 2   ' orbits a different one
BEHAVIOR "icon"
  BIMAGE "icons", @frame=0
END BEHAVIOR

SET SPRITE 5 BEHAVIOR "icon", @frame = 2     ' one sheet, one behavior,
SET SPRITE 6 BEHAVIOR "icon", @frame = 7     ' a different face on each sprite

A centre that names no sprite orbits nothing — it does not fall back to spinning
the sprite around itself. A negative frame is left alone rather than wrapping
around to something enormous.

One limit worth knowing, on ANIMATE alone: its frame list (when you leave it
out), FRAMERATE OFSPRITE and AUTOBBOX are all worked out from the image at
the moment the behavior is defined. An image that arrives later, as a
parameter, cannot answer them — so combining the two is a loud error rather than
a guess made from the default image. List the frames and give FRAMERATE a
number, and a parameterized sheet works fine.

ASSIGNING A BEHAVIOR THAT DOESN'T EXIST NOW STOPS THE PROGRAM

It used to do nothing at all. SET SPRITE 5 BEHAVIOR "chase" with no BEHAVIOR "chase" anywhere just… didn't. No message, no mark on the sprite, nothing to
search for. The sprite sat there, and the only way to find out why was to go
looking.

Now it is an error that names the behavior and lists the ones you do have.
PATTERN gets the same treatment, for the same reason.

The version of this that costs a whole afternoon:

CALL SetupEnemies()          ' runs FIRST

BEHAVIOR "chase"             ' ...but this hasn't run yet
  BFOLLOW @target=0 DISTANCE 40
END BEHAVIOR

SUB SetupEnemies()
  SET SPRITE 10 BEHAVIOR "chase"   ' silently did nothing, before today
END SUB

SUB and FUNCTION are hoisted — the whole file's subs are registered
before the first line runs, which is what lets you call one that is written
further down. BEHAVIOR and PATTERN blocks are not; they register when
execution reaches them. So a helper at the top and behaviors below it — a
perfectly natural way to write a program — meant the helper ran first and
assigned behaviors that did not exist yet.

Move the BEHAVIOR block above the code that uses it and it works. The error
says so, and lists what was defined at the time.

A COORDINATE VARIABLE CALLED spriteCenter IS A COORDINATE AGAIN

Found while doing the above, and it had been there a long time:

DIM spriteCenter AS COORDINATE
spriteCenter = (200, 120)

BEHAVIOR "ring"
  BORBIT spriteCenter DISTANCE 40    ' orbit that point
END BEHAVIOR

BORBIT decided which kind of centre you meant by looking for the word
SPRITE anywhere in the line, and the keyword itself matched the start of
any identifier. So a coordinate variable whose name merely began with "sprite"
was read as BORBIT SPRITE, and the orbit silently went around sprite number 0
— or around whatever sprite id the rest of your variable name happened to parse
as.

Now the two forms are told apart by shape, and SPRITE only counts as the
keyword when it is the whole word. If you had a coordinate variable named
around that trap, it works the way it reads.

REBUILDING A MEMORYBUFFER NOW ACTUALLY SHOWS THE NEW PICTURE

Build a buffer, put it on something, rebuild it under the same name — and the
rebuild was invisible. The shape kept wearing the first picture, forever.

BEGIN MEMORYBUFFER "gauge", 64, 8
  LINE (0, 0)-(hp, 8), 2
END MEMORYBUFFER

SET POLYGON 1 BIND bar TEXTURE "gauge"    ' the gauge appears

' later, the player takes a hit:
BEGIN MEMORYBUFFER "gauge", 64, 8
  LINE (0, 0)-(hp, 8), 2
END MEMORYBUFFER                          ' ...and nothing changed on screen

The cruel part was that the buffer agreed with you. POINT and POINTRGB
read the new pixels back correctly, so every check you could make from inside
your program said the redraw had worked. Only the screen disagreed, and it does
not explain itself.

This bit anything that had been given the buffer to draw with — a polygon
TEXTURE, or a layer pointed at the image — and it bit once the buffer was in
use
, which is exactly when you start updating it. A health bar, a minimap, a
damage overlay: build it, show it, and it freezes at whatever it looked like the
first time. Each rebuild also quietly stranded the old texture on the GPU, so a
buffer redrawn every frame leaked one every frame.

Now a rebuild replaces what is drawn as well as what is stored.

Editing a buffer in place still works, and now you can see it. Seeding a
buffer from itself is half of what MEMORYBUFFER is for — build once, add to it
as things change, instead of redrawing the whole picture every time:

BEGIN MEMORYBUFFER "map" FROM "map"
  PSET (px, py), 15                       ' just light the room they entered
END MEMORYBUFFER

That carries the previous contents through exactly as before. The difference is
that the new dot now appears on the map you are looking at.

A PARTICLE NO LONGER FLASHES BEFORE IT LOOKS RIGHT

EMITPARTICLE ... ROTATE(45) SCALE(2, 2) drew the particle once, untouched,
and applied the rotation and the scale on the next tick. One frame of a
square-on, original-size sprite, then a snap into place.

That was invisible in a screenshot and impossible to miss in motion, because it
is worst exactly where it matters: a shower of debris that lives twenty ticks
spends the first of them wrong, and every particle in the burst does it at a
different moment. In a networked game it was worse still — updates arrive more
slowly there, so instead of snapping the particle swept from unrotated to
rotated over a fiftieth of a second, and read as the particles animating into
existence on purpose.

The pose is now settled before the particle is handed over, so the first frame
you see is the right one. Nothing to change in your code.

SCALETO ON A SPRITE ACTUALLY STAYS

SET SPRITE 1, x, y, "hero" SCALETO(64, 32) sized the sprite correctly and then
put it back to its original size a tick later. It looked like the modifier had
been ignored, and if you were setting it every frame — inside a game loop, or
from a behavior — it half-worked, which is a worse thing to debug than not
working at all.

SCALETO names a size, and a size has to be remembered. It was being worked
out at the moment you asked and then thrown away, so the very next frame of
ordinary sprite bookkeeping overwrote it with "no scaling".

It now sticks, on SET SPRITE and UPDATE SPRITE alike, and it works on
particles, where it previously did nothing whatsoever:

EMITPARTICLE (x, y), "spark", "fx", "", 30 SCALETO(64, 32)

SCALE is unchanged — that one is a multiplier and always worked.

NAMING AN IMAGE THAT ISN'T THERE NOW SAYS SO

Misspell an image handle, or draw with one before you load it, and Crash BASIC
used to say nothing at all. The sprite existed, took up a slot, reported a
position, and drew nothing — and for collisions it was treated as 32×32
regardless of what the picture actually was, so it would bump into things at a
size that had no relationship to anything on screen.

That is three different wrong behaviours from one typo, and not one of them
mentions the typo.

Every statement that draws by name now checks: PUTIMAGE, SET SPRITE,
EMITPARTICLE, SET POLYGON, POLYGON, SETIMAGE, SET CURSOR and
GAMEPAD SETIMAGE. You get told which handle, and what to do about it.

Two things are deliberately not errors, because neither is a mistake:

  • SET SPRITE 1, x, y, "" — a sprite with no picture on purpose, used as a
    collider or a parent for other sprites to hang off.
  • Naming an image from server-side code in a CrashNet game. Images belong to the
    players who can draw them; the server refers to them by name and always has.

If you hit this on a game that used to run, the image was never arriving — the
draw was being skipped in silence.

EMITPARTICLE CAN PICK A FRAME

A particle emitted from a spritesheet always used frame 0. Whatever else was on
the sheet was unreachable, so a sheet of six different sparks could give you one
of them.

EMITPARTICLE (x, y), "sparks", "fx", "", 30 FRAME 2

FRAME means on a particle exactly what it means on SET SPRITE. It is not
accepted on a particle emitted from a MUTATION — that one is drawn from its
own points and has no sheet to pick from, and it will tell you so.

PUTIMAGE HAS AN ORIGIN

ORIGIN is how you say which part of the image goes on the coordinate you
gave
. SET SPRITE has understood it for a long time; PUTIMAGE accepted it
and quietly ignored it.

PUTIMAGE (100, 100), "icon" ORIGIN(0.5, 0.5)   ' centred on (100, 100)

(0, 0) is the top-left and stays the default, so nothing you have already
written moves. It is measured against the size actually drawn, so it still lands
correctly alongside SCALE, SCALETO and REGION. It is a different anchor
from the one in ROTATE(angle, ox, oy): this is where the image is placed,
that is where it turns.

REGION AND MAP NOW EXPLAIN THEMSELVES

REGION and MAP belong to PUTIMAGE, which draws immediately and can afford
to crop its source or stamp out a grid of tiles. Writing either on a sprite, a
particle or a polygon used to get you a parser complaint listing the tokens it
would have preferred.

They now say what they are and where to go instead: FRAME to use part of a
spritesheet, and TILEMAP with CREATE MAP to lay out a grid of tiles.

A TILEMAP WITH A BASEMAP DRAWS SOMETHING NOW

This one had been quietly broken since TILEMAP blocks arrived, and it looked
like nothing at all:

TILEMAP "level"
  BASEMAP "level1.map.json"
  TILES 0 INTO ground
END TILEMAP

CREATE MAP "level" AS LAYER 0

A valid map, a valid tile set, a black screen, and no explanation.

The older CREATE MAP("level1.map.json") form has always loaded the tile-set
picture the map file names, working the path out relative to the map. The
TILEMAP / BASEMAP form only wrote the tile set's name down and never
fetched it — so the renderer was handed a map pointing at a picture that had
never been loaded, found nothing under that name, and gave up quietly. Every
number you could inspect from your program was correct. The only wrong thing was
somewhere you cannot look.

Both forms now load it the same way, and a tile set that cannot be read is an
error naming the file it came from, rather than a rectangle of nothing.

If you have been working around this with a bare LOADIMG of the tile set
before CREATE MAP, that still works and is still perfectly reasonable —
loading an image twice costs nothing.

AND CREATE MAP CHECKS ITS TILE SET

TILEMAP declares a map; CREATE MAP puts it on the screen. The picture only
has to exist for the second of those, so declaring a map before loading its art
is still fine — that ordering is common and there is nothing wrong with it.

But drawing a map whose tile set was never loaded now says so, instead of
producing the black rectangle above. That covers all three ways of naming one:
TILESET in a TILEMAP block, TILESET in the inline CREATE MAP FROM form,
and a tile set inherited from a BASEMAP.

RESIZING THE DESKTOP WINDOW WORKS AGAIN

Drag the edge of a CrashPlayer window on the desktop and the picture stayed the
size it was when the window opened. Widen it and you got the old picture in a
bigger frame; the black bars either side never moved to match. Clicking went
wrong at the same moment, because the game was still working out where your
mouse was using the size the window used to be — so buttons stopped being where
they looked.

The scaling maths was never the problem. Every platform computes the same thing:
fit the game's own resolution inside the window, keep its shape, centre it, put
black bars in whatever is left over. The browser build did that on every resize
and so did phones and tablets, where a rotation is a resize. The desktop
simply never asked.
The window told the player it had changed size, the player
passed the message along, and nothing was listening — the code that recomputes
the picture had been written for phones and was not compiled into a desktop
build at all.

Desktop now recomputes on every resize, like everything else always did. Nothing
to change in your game, and the resolution you asked for with SCREEN still
means exactly what it did — a 320×200 game is still a 320×200 game, it is just
correctly centred in whatever window you give it.

A DISABLED SPRITE STAYS DISABLED

Disable a sprite from inside a collision handler — the single most ordinary thing
a collision handler does, an invader dying when a shot lands — and it froze on
screen instead of vanishing. Still visible, still where it was hit, forever.

Each frame builds a description of every sprite and sends it to the renderer.
Collision handlers run after that description is built, so a sprite your
handler disabled was still in it, still described as alive and at its old
position. The renderer takes any such description as proof a sprite exists and
turned it back on — and because the game had by then let go of the sprite, no
later frame ever corrected it. The sprite you destroyed sat there for the rest
of the game.

The description is now brought up to date after handlers run. A sprite that no
longer exists is left out of it, and one your handler hid or moved is described
the way you left it, not the way it was a moment earlier.

Also: a handler could be told it had collided with a sprite that another
handler had already destroyed in the same instant. With a fleet of enemies
sharing one behavior, several collisions in a frame is ordinary, so this was
easy to hit and impossible to see. Those are no longer delivered.

PARTICLES WITH A PATTERN NO LONGER SLOW A GAME DOWN

A particle that expired left its PATTERN behind. Nothing was visibly wrong —
the particle went away exactly as it should — but the pattern it had been
running stayed in the engine, and the engine visits every pattern it holds on
every frame.

The cost of that grows by one for each particle your game has ever emitted. A
game that showers sparks looks fine for a minute and then slowly grinds, and
because the count only ever climbs, it never recovers. Long sessions were worst.

Now a particle takes its pattern with it. So do the other ways a sprite can end:
DISABLE SPRITE, a TILEMAP scene ending, DESTROY POLYGON, FREE IMAGE,
FREE LAYER, and a player leaving a multiplayer room. Hiding a sprite still
keeps its pattern running
— hiding is not destroying, and a hidden sprite that
comes back should carry on where it was.

SHADERS STOP WORKING AFTER A WHILE — FIXED

A sprite with a shader on it holds one of a fixed number of shader slots. The
slot came back when you cleared the shader with SHADER NONE — and only then.
Disabling the sprite did not return it.

So a game that puts a shader on something short-lived — a hit flash, a shielded
enemy, an effect that spawns and dies — leaked one slot every time. After enough
of them the supply was gone, and from that point on no sprite in the game
could take a shader again
, with nothing said about it. Restarting the game was
the only cure.

Disabling a sprite now returns its slot. It also fixes a subtler one: sprite ids
are reused, so a new sprite could inherit a shader from whatever used that id
before it.

EMITPARTICLE SAYS WHEN THERE IS NO ROOM

EMITPARTICLE used to do nothing at all when every particle slot was in use.
Effects would simply stop appearing, with nothing printed and every value your
program could read looking perfectly correct.

It now tells you, and says how many particles are alive, so you can see whether
they are being emitted faster than they expire or something is keeping them
alive.