diff --git a/editor/CHANGELOG.md b/editor/CHANGELOG.md index b3781dd8..beb81d8c 100644 --- a/editor/CHANGELOG.md +++ b/editor/CHANGELOG.md @@ -1,5 +1,6 @@ # Editor changelog +- A party member who leaves the battle line mid-fight, for example captured or banished by a state that hides them, disappears from the battlefield instead of standing there with their last pose (runtime 20261113.1). Clearing a sprite's bitmap assigned a new texture frame, which Pixi v8 never applies, so the sprite kept drawing the old image; it now returns to an empty texture of its own. Status window, target lists and turn order were already right. - For plugin authors: two methods on `Game_Enemy` to replace instead of working around. `rrActionPatterns()` returns the action patterns an enemy chooses from, and the turn-based, Action and Tactical battle modes now all read them through it, so a plugin that gives an enemy other skills changes one method and every mode follows. `rrEquipSource()` returns the rows an enemy's equipment is first read from, so a plugin can equip an enemy from somewhere other than its database entry. Both return the database lists by default, so nothing changes for a project without such a plugin. (runtime 20261112.1) The full technical list, one file per release or labeled development checkpoint, newest first. The short version of each release is in the [root changelog](../CHANGELOG.md). diff --git a/editor/tests/sprite-null-bitmap.test.cjs b/editor/tests/sprite-null-bitmap.test.cjs new file mode 100644 index 00000000..de8810ea --- /dev/null +++ b/editor/tests/sprite-null-bitmap.test.cjs @@ -0,0 +1,73 @@ +'use strict'; +// Sprite.bitmap = null must stop a sprite drawing its previous bitmap under Pixi v8. +// An actor sprite whose battler leaves battleMembers() mid-battle is cleared this way; +// before the fix it kept rendering that actor's last frame. +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const path = require('node:path'); +const test = require('node:test'); +const vm = require('node:vm'); +const native = require('pixi.js'); + +const runtime = path.resolve(__dirname, '../../runtime'); +const read = file => fs.readFileSync(path.join(runtime, file), 'utf8'); + +function loadSprite() { + const context = { + PIXI: { ...native }, TypeError, Error, Reflect, Float32Array, Uint8Array, ArrayBuffer, + console: { log() {}, warn() {}, error() {} }, location: { search: '' } + }; + context.window = context; + vm.createContext(context); + vm.runInContext(read('libs/pixi_compat.js'), context); + const core = read('reactor_core.js'); + const rectangleStart = core.indexOf('function Rectangle()'); + vm.runInContext(core.slice(rectangleStart, core.indexOf('//----', rectangleStart)), context); + const display = read('reactor_core_display.js'); + vm.runInContext(display.slice(0, display.indexOf('Sprite.prototype._createColorFilter')) + '\nthis.Sprite = Sprite;', context); + return context; +} + +function fakeBitmap(P) { + const baseTexture = P.BaseTexture.fromBuffer(new Uint8Array(64 * 64 * 4), 64, 64); + return { baseTexture, width: 64, height: 64, addLoadListener(callback) { callback(this); } }; +} + +test('clearing a sprite bitmap leaves it on an empty texture of its own', () => { + const context = loadSprite(); + const bitmap = fakeBitmap(context.PIXI); + const sprite = new context.Sprite(bitmap); + assert.equal(sprite.texture.source, bitmap.baseTexture.source); + assert.equal(sprite.texture.uvs.x1, 1); + const drawn = sprite.texture; + + sprite.bitmap = null; + + assert.notEqual(sprite.texture.source, bitmap.baseTexture.source, 'no longer samples the old bitmap'); + assert.equal(sprite.texture.uvs.x1, 0, 'UVs cover nothing'); + assert.deepEqual([sprite.texture.frame.width, sprite.texture.frame.height], [0, 0]); + assert.equal(sprite.texture.__rrSpriteOwned, true); + assert.equal(drawn.destroyed, true, 'the replaced sprite-owned texture is released'); + assert.equal(bitmap.baseTexture.source.destroyed, false, 'the bitmap source itself is untouched'); +}); + +test('a cleared sprite shows its bitmap again when one is assigned', () => { + const context = loadSprite(); + const bitmap = fakeBitmap(context.PIXI); + const sprite = new context.Sprite(bitmap); + sprite.bitmap = null; + sprite.bitmap = bitmap; + assert.equal(sprite.texture.source, bitmap.baseTexture.source); + assert.deepEqual([sprite.texture.frame.width, sprite.texture.frame.height], [64, 64]); +}); + +test('a new sprite without a bitmap keeps its constructor texture', () => { + const context = loadSprite(); + const realCreate = context.PIXICreateTexture; + let created = 0; + context.PIXICreateTexture = (...args) => { created++; return realCreate(...args); }; + const sprite = new context.Sprite(); + assert.equal(created, 1, 'initialize() does not build a second empty texture'); + assert.equal(sprite.texture.__rrSpriteOwned, true); + assert.deepEqual([sprite.texture.frame.width, sprite.texture.frame.height], [0, 0]); +}); diff --git a/runtime/reactor_core_display.js b/runtime/reactor_core_display.js index a28a6577..beaad4df 100644 --- a/runtime/reactor_core_display.js +++ b/runtime/reactor_core_display.js @@ -329,7 +329,26 @@ Sprite.prototype._onBitmapChange = function() { this._bitmap.addLoadListener(this._onBitmapLoad.bind(this)); } else { this._refreshFrame = false; - this.texture.frame = new Rectangle(); + if (PIXI.TextureSource) { + // v8: assigning texture.frame refreshes neither the UVs nor the + // source, so a sprite whose bitmap is cleared kept drawing the old + // bitmap's last frame (an actor sprite left without a battler went + // on showing that actor). Go back to an empty texture of the + // sprite's own, as initialize() starts with. A texture whose frame + // is already 0x0 draws nothing (initialize() calls this with one), + // so it is kept rather than allocated twice. + const old = this.texture; + if (!old || old.frame.width > 0 || old.frame.height > 0) { + const texture = PIXICreateTexture(Sprite._emptyBaseTexture, new Rectangle()); + texture.__rrSpriteOwned = true; + this.texture = texture; + if (old && old.__rrSpriteOwned && !old.destroyed) { + old.destroy(); + } + } + } else { + this.texture.frame = new Rectangle(); + } } }; diff --git a/runtime/reactor_main.js b/runtime/reactor_main.js index 69c968e3..80a8c3dd 100644 --- a/runtime/reactor_main.js +++ b/runtime/reactor_main.js @@ -1,10 +1,10 @@ //============================================================================= // reactor_main.js — RPG Reactor runtime entry point // RPG Reactor runtime version: 0.99.5 -// RPG Reactor runtime revision: 20261112.1 +// RPG Reactor runtime revision: 20261113.1 // The same stamp, reachable from the F12 console: which engine is this // window actually running? Type RPG_REACTOR_RUNTIME_REVISION to see. -globalThis.RPG_REACTOR_RUNTIME_REVISION = "20261112.1"; +globalThis.RPG_REACTOR_RUNTIME_REVISION = "20261113.1"; //============================================================================= const scriptUrls = [ diff --git a/template/Demo/js/reactor_core_display.js b/template/Demo/js/reactor_core_display.js index a28a6577..beaad4df 100644 --- a/template/Demo/js/reactor_core_display.js +++ b/template/Demo/js/reactor_core_display.js @@ -329,7 +329,26 @@ Sprite.prototype._onBitmapChange = function() { this._bitmap.addLoadListener(this._onBitmapLoad.bind(this)); } else { this._refreshFrame = false; - this.texture.frame = new Rectangle(); + if (PIXI.TextureSource) { + // v8: assigning texture.frame refreshes neither the UVs nor the + // source, so a sprite whose bitmap is cleared kept drawing the old + // bitmap's last frame (an actor sprite left without a battler went + // on showing that actor). Go back to an empty texture of the + // sprite's own, as initialize() starts with. A texture whose frame + // is already 0x0 draws nothing (initialize() calls this with one), + // so it is kept rather than allocated twice. + const old = this.texture; + if (!old || old.frame.width > 0 || old.frame.height > 0) { + const texture = PIXICreateTexture(Sprite._emptyBaseTexture, new Rectangle()); + texture.__rrSpriteOwned = true; + this.texture = texture; + if (old && old.__rrSpriteOwned && !old.destroyed) { + old.destroy(); + } + } + } else { + this.texture.frame = new Rectangle(); + } } }; diff --git a/template/Demo/js/reactor_main.js b/template/Demo/js/reactor_main.js index 69c968e3..80a8c3dd 100644 --- a/template/Demo/js/reactor_main.js +++ b/template/Demo/js/reactor_main.js @@ -1,10 +1,10 @@ //============================================================================= // reactor_main.js — RPG Reactor runtime entry point // RPG Reactor runtime version: 0.99.5 -// RPG Reactor runtime revision: 20261112.1 +// RPG Reactor runtime revision: 20261113.1 // The same stamp, reachable from the F12 console: which engine is this // window actually running? Type RPG_REACTOR_RUNTIME_REVISION to see. -globalThis.RPG_REACTOR_RUNTIME_REVISION = "20261112.1"; +globalThis.RPG_REACTOR_RUNTIME_REVISION = "20261113.1"; //============================================================================= const scriptUrls = [