Implement the RCO animation system and focus handling

Add the 11 RCO animation record types (MoveTo/Recolour/Resize/Fade/
FireEvent/...), an AnimFactory, and populate the resource animtable. Anim
play() applies the final state immediately so the menu lays out correctly.
Also add setInterval overloads (5+ params), setFocus(VsmxBaseObject), and
animation offsets (animX/Y/Z) to position objects, fixing the dispatch
warnings that were silently stalling the menu.
This commit is contained in:
2026-09-16 08:16:34 -04:00
parent 5d3ccd65fe
commit 62d61bad45
17 changed files with 341 additions and 2 deletions
@@ -0,0 +1,39 @@
// SPDX-License-Identifier: GPL-3.0-or-later
package dev.quoije.umd.rco;
import dev.quoije.umd.rco.anim.Anim;
import dev.quoije.umd.rco.anim.BaseAnim;
import dev.quoije.umd.rco.anim.DelayAnim;
import dev.quoije.umd.rco.anim.FadeAnim;
import dev.quoije.umd.rco.anim.FireEventAnim;
import dev.quoije.umd.rco.anim.LockAnim;
import dev.quoije.umd.rco.anim.MoveToAnim;
import dev.quoije.umd.rco.anim.RecolourAnim;
import dev.quoije.umd.rco.anim.ResizeAnim;
import dev.quoije.umd.rco.anim.RotateAnim;
import dev.quoije.umd.rco.anim.SlideOutAnim;
import dev.quoije.umd.rco.anim.UnlockAnim;
/** Creates RCO animation records by type id. */
public final class AnimFactory {
private AnimFactory() {
}
public static BaseAnim newAnim(int type) {
return switch (type) {
case 1 -> new Anim();
case 2 -> new MoveToAnim();
case 3 -> new RecolourAnim();
case 4 -> new RotateAnim();
case 5 -> new ResizeAnim();
case 6 -> new FadeAnim();
case 7 -> new DelayAnim();
case 8 -> new FireEventAnim();
case 9 -> new LockAnim();
case 10 -> new UnlockAnim();
case 11 -> new SlideOutAnim();
default -> null;
};
}
}
@@ -73,6 +73,7 @@ public final class RcoFile {
private final Map<Integer, String> events = new LinkedHashMap<>(); private final Map<Integer, String> events = new LinkedHashMap<>();
private final Map<Integer, ImageObject> images = new LinkedHashMap<>(); private final Map<Integer, ImageObject> images = new LinkedHashMap<>();
private final Map<Integer, BaseObject> objects = new LinkedHashMap<>(); private final Map<Integer, BaseObject> objects = new LinkedHashMap<>();
private final Map<Integer, BaseObject> anims = new LinkedHashMap<>();
private final byte[] buffer; private final byte[] buffer;
private int offset; private int offset;
@@ -109,6 +110,9 @@ public final class RcoFile {
for (BaseObject object : objects.values()) { for (BaseObject object : objects.values()) {
object.init(initContext); object.init(initContext);
} }
for (BaseObject anim : anims.values()) {
anim.init(initContext);
}
ok = true; ok = true;
} catch (RuntimeException e) { } catch (RuntimeException e) {
ok = false; ok = false;
@@ -148,6 +152,10 @@ public final class RcoFile {
return objects; return objects;
} }
public Map<Integer, BaseObject> anims() {
return anims;
}
public static String tableName(int id) { public static String tableName(int id) {
return switch (id) { return switch (id) {
case TABLE_MAIN -> "MAIN"; case TABLE_MAIN -> "MAIN";
@@ -305,6 +313,14 @@ public final class RcoFile {
if (dataLength > 0) { if (dataLength > 0) {
entry.data = readBytes(dataLength); entry.data = readBytes(dataLength);
} }
BaseObject anim = AnimFactory.newAnim(entry.type);
if (anim != null) {
if (entry.data != null) {
anim.read(new RcoContext(entry.data, 0, events, images, objects));
}
anims.put(entryOffset, anim);
entry.obj = anim;
}
} }
case TABLE_TEXT -> { case TABLE_TEXT -> {
if (entry.type == 1) { if (entry.type == 1) {
@@ -0,0 +1,6 @@
// SPDX-License-Identifier: GPL-3.0-or-later
package dev.quoije.umd.rco.anim;
/** A generic animation container (type 1): no direct effect, plays its children. */
public class Anim extends BaseAnim {
}
@@ -0,0 +1,46 @@
// SPDX-License-Identifier: GPL-3.0-or-later
package dev.quoije.umd.rco.anim;
import dev.quoije.umd.rco.object.BaseObject;
import dev.quoije.umd.rco.object.BasePositionObject;
import dev.quoije.umd.rco.type.ObjectType;
import dev.quoije.umd.rco.vsmx.interpreter.VsmxBaseObject;
import dev.quoije.umd.rco.vsmx.interpreter.VsmxNativeObject;
import dev.quoije.umd.rco.vsmx.objects.Resource;
import java.util.function.Consumer;
/**
* Base of RCO animation records. {@link #play} applies the animation (and its
* children) to the target. This port applies the final state immediately
* rather than tweening over time; the menu still lays out correctly.
*/
public class BaseAnim extends BaseObject {
/** Plays this animation and all its child animations on the target. */
public void play(VsmxBaseObject thisObject, VsmxBaseObject object) {
doPlay(object);
if (thisObject.hasPropertyValue(Resource.childrenName)) {
VsmxBaseObject children = thisObject.getPropertyValue(Resource.childrenName);
for (String name : children.getPropertyNames()) {
VsmxBaseObject child = children.getPropertyValue(name);
if (child instanceof VsmxNativeObject nativeObject && nativeObject.getObject() instanceof BaseAnim anim) {
anim.play(child, object);
}
}
}
}
/** Applies this animation's effect to the given target. */
protected void doPlay(VsmxBaseObject object) {
}
/** Resolves an object reference and applies the action to the target. */
protected void applyTo(ObjectType ref, Consumer<BasePositionObject> action) {
BasePositionObject target = ref == null ? null : ref.getPositionObject();
if (target != null) {
action.accept(target);
}
}
}
@@ -0,0 +1,11 @@
// SPDX-License-Identifier: GPL-3.0-or-later
package dev.quoije.umd.rco.anim;
import dev.quoije.umd.rco.ObjectField;
import dev.quoije.umd.rco.type.FloatType;
/** A timed delay between animations (type 7). */
public class DelayAnim extends BaseAnim {
@ObjectField(order = 1) public FloatType time;
}
@@ -0,0 +1,25 @@
// SPDX-License-Identifier: GPL-3.0-or-later
package dev.quoije.umd.rco.anim;
import dev.quoije.umd.rco.ObjectField;
import dev.quoije.umd.rco.type.FloatType;
import dev.quoije.umd.rco.type.IntType;
import dev.quoije.umd.rco.type.ObjectType;
import dev.quoije.umd.rco.vsmx.interpreter.VsmxBaseObject;
/** Fades the referenced object to the given transparency (type 6). */
public class FadeAnim extends BaseAnim {
@ObjectField(order = 1) public ObjectType ref;
@ObjectField(order = 2) public FloatType duration;
@ObjectField(order = 3) public IntType accelMode;
@ObjectField(order = 4) public FloatType transparency;
@Override
protected void doPlay(VsmxBaseObject object) {
applyTo(ref, target -> {
target.alphaScale.setFloatValue(transparency.getFloatValue());
target.onDisplayUpdated();
});
}
}
@@ -0,0 +1,19 @@
// SPDX-License-Identifier: GPL-3.0-or-later
package dev.quoije.umd.rco.anim;
import dev.quoije.umd.rco.ObjectField;
import dev.quoije.umd.rco.type.EventType;
import dev.quoije.umd.rco.vsmx.interpreter.VsmxBaseObject;
/** Fires the referenced event script (type 8). */
public class FireEventAnim extends BaseAnim {
@ObjectField(order = 1) public EventType event;
@Override
protected void doPlay(VsmxBaseObject object) {
if (event != null && event.getEvent() != null) {
object.getInterpreter().interpretScript(object, event.getEvent());
}
}
}
@@ -0,0 +1,11 @@
// SPDX-License-Identifier: GPL-3.0-or-later
package dev.quoije.umd.rco.anim;
import dev.quoije.umd.rco.ObjectField;
import dev.quoije.umd.rco.type.IntType;
/** Locks input (type 9). */
public class LockAnim extends BaseAnim {
@ObjectField(order = 1) public IntType unknownInt0;
}
@@ -0,0 +1,29 @@
// SPDX-License-Identifier: GPL-3.0-or-later
package dev.quoije.umd.rco.anim;
import dev.quoije.umd.rco.ObjectField;
import dev.quoije.umd.rco.type.FloatType;
import dev.quoije.umd.rco.type.IntType;
import dev.quoije.umd.rco.type.ObjectType;
import dev.quoije.umd.rco.vsmx.interpreter.VsmxBaseObject;
/** Moves the referenced object by the given animation offset (type 2). */
public class MoveToAnim extends BaseAnim {
@ObjectField(order = 1) public ObjectType ref;
@ObjectField(order = 2) public FloatType duration;
@ObjectField(order = 3) public IntType accelMode;
@ObjectField(order = 4) public FloatType x;
@ObjectField(order = 5) public FloatType y;
@ObjectField(order = 6) public FloatType z;
@Override
protected void doPlay(VsmxBaseObject object) {
applyTo(ref, target -> {
target.animX = x.getFloatValue();
target.animY = y.getFloatValue();
target.animZ = z.getFloatValue();
target.onDisplayUpdated();
});
}
}
@@ -0,0 +1,31 @@
// SPDX-License-Identifier: GPL-3.0-or-later
package dev.quoije.umd.rco.anim;
import dev.quoije.umd.rco.ObjectField;
import dev.quoije.umd.rco.type.FloatType;
import dev.quoije.umd.rco.type.IntType;
import dev.quoije.umd.rco.type.ObjectType;
import dev.quoije.umd.rco.vsmx.interpreter.VsmxBaseObject;
/** Recolours the referenced object (type 3). */
public class RecolourAnim extends BaseAnim {
@ObjectField(order = 1) public ObjectType ref;
@ObjectField(order = 2) public FloatType duration;
@ObjectField(order = 3) public IntType accelMode;
@ObjectField(order = 4) public FloatType red;
@ObjectField(order = 5) public FloatType green;
@ObjectField(order = 6) public FloatType blue;
@ObjectField(order = 7) public FloatType alpha;
@Override
protected void doPlay(VsmxBaseObject object) {
applyTo(ref, target -> {
target.redScale.setFloatValue(red.getFloatValue());
target.greenScale.setFloatValue(green.getFloatValue());
target.blueScale.setFloatValue(blue.getFloatValue());
target.alphaScale.setFloatValue(alpha.getFloatValue());
target.onDisplayUpdated();
});
}
}
@@ -0,0 +1,29 @@
// SPDX-License-Identifier: GPL-3.0-or-later
package dev.quoije.umd.rco.anim;
import dev.quoije.umd.rco.ObjectField;
import dev.quoije.umd.rco.type.FloatType;
import dev.quoije.umd.rco.type.IntType;
import dev.quoije.umd.rco.type.ObjectType;
import dev.quoije.umd.rco.vsmx.interpreter.VsmxBaseObject;
/** Resizes the referenced object (type 5). */
public class ResizeAnim extends BaseAnim {
@ObjectField(order = 1) public ObjectType ref;
@ObjectField(order = 2) public FloatType duration;
@ObjectField(order = 3) public IntType accelMode;
@ObjectField(order = 4) public FloatType width;
@ObjectField(order = 5) public FloatType height;
@ObjectField(order = 6) public FloatType depth;
@Override
protected void doPlay(VsmxBaseObject object) {
applyTo(ref, target -> {
target.scaleWidth.setFloatValue(width.getFloatValue());
target.scaleHeight.setFloatValue(height.getFloatValue());
target.scaleDepth.setFloatValue(depth.getFloatValue());
target.onDisplayUpdated();
});
}
}
@@ -0,0 +1,18 @@
// SPDX-License-Identifier: GPL-3.0-or-later
package dev.quoije.umd.rco.anim;
import dev.quoije.umd.rco.ObjectField;
import dev.quoije.umd.rco.type.FloatType;
import dev.quoije.umd.rco.type.IntType;
import dev.quoije.umd.rco.type.ObjectType;
/** Rotates the referenced object (type 4). */
public class RotateAnim extends BaseAnim {
@ObjectField(order = 1) public ObjectType ref;
@ObjectField(order = 2) public FloatType duration;
@ObjectField(order = 3) public IntType accelMode;
@ObjectField(order = 4) public FloatType x;
@ObjectField(order = 5) public FloatType y;
@ObjectField(order = 6) public FloatType z;
}
@@ -0,0 +1,19 @@
// SPDX-License-Identifier: GPL-3.0-or-later
package dev.quoije.umd.rco.anim;
import dev.quoije.umd.rco.ObjectField;
import dev.quoije.umd.rco.type.FloatType;
import dev.quoije.umd.rco.type.IntType;
import dev.quoije.umd.rco.type.ObjectType;
/** Slides the referenced object out (type 11). */
public class SlideOutAnim extends BaseAnim {
@ObjectField(order = 1) public ObjectType ref;
@ObjectField(order = 2) public FloatType duration;
@ObjectField(order = 3) public IntType accelMode;
@ObjectField(order = 4) public FloatType xspeed;
@ObjectField(order = 5) public FloatType yspeed;
@ObjectField(order = 6) public FloatType xcompress;
@ObjectField(order = 7) public FloatType ycompress;
}
@@ -0,0 +1,11 @@
// SPDX-License-Identifier: GPL-3.0-or-later
package dev.quoije.umd.rco.anim;
import dev.quoije.umd.rco.ObjectField;
import dev.quoije.umd.rco.type.IntType;
/** Unlocks input (type 10). */
public class UnlockAnim extends BaseAnim {
@ObjectField(order = 1) public IntType unknownInt0;
}
@@ -32,13 +32,18 @@ public class BasePositionObject extends BaseObject {
@ObjectField(order = 114) public IntType iconOffset; @ObjectField(order = 114) public IntType iconOffset;
@ObjectField(order = 115) public EventType onInit; @ObjectField(order = 115) public EventType onInit;
/** Animation offsets (added to the authored position). */
public float animX;
public float animY;
public float animZ;
@Override @Override
public int getX() { public int getX() {
int parentX = 0; int parentX = 0;
if (getParent() instanceof BasePositionObject positionParent) { if (getParent() instanceof BasePositionObject positionParent) {
parentX = positionParent.getX(); parentX = positionParent.getX();
} }
return parentX + posX.getIntValue(); return parentX + posX.getIntValue() + Math.round(animX);
} }
@Override @Override
@@ -47,7 +52,7 @@ public class BasePositionObject extends BaseObject {
if (getParent() instanceof BasePositionObject positionParent) { if (getParent() instanceof BasePositionObject positionParent) {
parentY = positionParent.getY(); parentY = positionParent.getY();
} }
return parentY + posY.getIntValue(); return parentY + posY.getIntValue() + Math.round(animY);
} }
@Override @Override
@@ -94,6 +99,10 @@ public class BasePositionObject extends BaseObject {
} }
} }
public void setFocus(VsmxBaseObject object) {
setFocus();
}
public void setPos(VsmxBaseObject object, VsmxBaseObject x, VsmxBaseObject y) { public void setPos(VsmxBaseObject object, VsmxBaseObject x, VsmxBaseObject y) {
posX.setFloatValue(x.getFloatValue()); posX.setFloatValue(x.getFloatValue());
posY.setFloatValue(y.getFloatValue()); posY.setFloatValue(y.getFloatValue());
@@ -3,6 +3,7 @@ package dev.quoije.umd.rco.type;
import dev.quoije.umd.rco.RcoContext; import dev.quoije.umd.rco.RcoContext;
import dev.quoije.umd.rco.object.BaseObject; import dev.quoije.umd.rco.object.BaseObject;
import dev.quoije.umd.rco.object.BasePositionObject;
import dev.quoije.umd.rco.object.ImageObject; import dev.quoije.umd.rco.object.ImageObject;
/** /**
@@ -60,6 +61,13 @@ public class BaseReferenceType extends BaseType {
return object; return object;
} }
public BasePositionObject getPositionObject() {
if (object instanceof BasePositionObject positionObject) {
return positionObject;
}
return null;
}
public ImageObject getImage() { public ImageObject getImage() {
return image; return image;
} }
@@ -121,6 +121,18 @@ public class Timer extends BaseNativeObject {
return setInterval(object, function, interval, new VsmxBaseObject[]{param1, param2, param3}); return setInterval(object, function, interval, new VsmxBaseObject[]{param1, param2, param3});
} }
public VsmxBaseObject setInterval(VsmxBaseObject object, VsmxBaseObject function, VsmxBaseObject interval, VsmxBaseObject param1, VsmxBaseObject param2, VsmxBaseObject param3, VsmxBaseObject param4) {
return setInterval(object, function, interval, new VsmxBaseObject[]{param1, param2, param3, param4});
}
public VsmxBaseObject setInterval(VsmxBaseObject object, VsmxBaseObject function, VsmxBaseObject interval, VsmxBaseObject param1, VsmxBaseObject param2, VsmxBaseObject param3, VsmxBaseObject param4, VsmxBaseObject param5) {
return setInterval(object, function, interval, new VsmxBaseObject[]{param1, param2, param3, param4, param5});
}
public VsmxBaseObject setInterval(VsmxBaseObject object, VsmxBaseObject function, VsmxBaseObject interval, VsmxBaseObject param1, VsmxBaseObject param2, VsmxBaseObject param3, VsmxBaseObject param4, VsmxBaseObject param5, VsmxBaseObject param6) {
return setInterval(object, function, interval, new VsmxBaseObject[]{param1, param2, param3, param4, param5, param6});
}
public void clearInterval(VsmxBaseObject object, VsmxBaseObject id) { public void clearInterval(VsmxBaseObject object, VsmxBaseObject id) {
int idValue = id.getPropertyValue(0).getIntValue(); int idValue = id.getPropertyValue(0).getIntValue();
synchronized (timers) { synchronized (timers) {