Skip to content

Commit 798c45f

Browse files
author
DavidQ
committed
Finish Asteroids manifest-driven render path cleanup - PR_26133_127-asteroids-manifest-render-path-cleanup
1 parent 12e64bf commit 798c45f

9 files changed

Lines changed: 163 additions & 47 deletions
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# PR_26133_127 Asteroids Manifest Render Path Cleanup
2+
3+
## Summary
4+
- Replaced Asteroids attract/demo-only manifest ID names with shared Object Vector Studio V2 object IDs: bullet, ship, large/medium/small asteroid, large UFO, and small UFO.
5+
- Required manifest geometry now explicitly includes `object.asteroids.small-ufo` with the other six runtime objects.
6+
- Gameplay ship, ship lives, UFO, bullets, and asteroids now render with explicit `objectId` plus `requireManifestBinding: true`.
7+
- Demo and attract ship/UFO/asteroid rendering now uses the same shared render keys and manifest object IDs as gameplay.
8+
9+
## Runtime Render Path Audit
10+
- Removed active `attractShip`, `attractUfo`, and `attractAsteroid` render paths from Asteroids runtime code.
11+
- No separate demo/attract object geometry definitions remain in Asteroids runtime code.
12+
- Remaining direct renderer draws are non-object UI/effect paths: starfield background pixels, HUD/menu text, attract text panels, demo trail pixels, pause/initials overlays, particle effects, and ship debris fragments.
13+
- Ship debris was left unchanged because it is an explosion effect, not a demo/attract object geometry source.
14+
15+
## Preserved Intentional Behavior
16+
- Ship flame flicker state/shape data was not changed.
17+
- Asteroid manifest geometry and scale tuning were not changed.
18+
- Manifest-authored style colors remain sourced from Object Vector Studio V2 objects.
19+
- No fallback/default vector maps or hardcoded object vector maps were added.
20+
21+
## Validation
22+
- PASS: targeted Asteroids manifest/gameplay/demo/bullet node validation:
23+
- `AsteroidsValidation`
24+
- `AsteroidsPresentation`
25+
- `AsteroidsVectorTransforms`
26+
- `AsteroidsPlatformDemo`
27+
- `AsteroidsAssetReferenceAdoption`
28+
- PASS: targeted Workspace Manager V2/Asteroids Playwright validation:
29+
- `npx playwright test tests/playwright/tools/WorkspaceManagerV2.spec.mjs --project=playwright --workers=1 --reporter=list -g "loads Object Vector Studio V2 runtime assets into Asteroids gameplay rendering"`
30+
- PASS: `git diff --check`
31+
- Playwright impacted: Yes. Validates Asteroids Object Vector runtime asset loading, attract/demo shared manifest IDs, gameplay manifest rendering, UFO/ship/asteroid render counts, and bullet runtime rendering.
32+
- Full samples smoke test skipped as requested; this PR is limited to Asteroids render-path cleanup.
33+
34+
## Coverage
35+
- Updated `docs/dev/reports/playwright_v8_coverage_report.txt` and `docs/dev/reports/coverage_changed_js_guardrail.txt` to list the changed Asteroids runtime JS files from the targeted Playwright V8 coverage output.
36+
37+
## Manual Validation
38+
- Open `/games/Asteroids/index.html`.
39+
- Let attract mode enter title/demo phases; expected: ship, UFO, and all asteroids render from manifest-authored Object Vector Studio V2 objects.
40+
- Start gameplay and fire bullets at multiple ship angles; expected: bullets use `object.asteroids.bullet` styling/geometry and rotate with fire direction.
41+
- Confirm large, medium, and small asteroids preserve manifest-authored colors/scale tuning.

games/Asteroids/game/AsteroidsAttractAdapter.js

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ AsteroidsAttractAdapter.js
77
import { clamp } from '../../../src/shared/utils/mathUtils.js';
88
import { ASTEROIDS_OBJECT_GEOMETRY_IDS } from './asteroidsObjectGeometryManifest.js';
99

10-
const ATTRACT_ASTEROID_RENDER_BINDINGS = Object.freeze({
10+
const ASTEROID_RENDER_BINDINGS = Object.freeze({
1111
large: Object.freeze({
1212
objectId: ASTEROIDS_OBJECT_GEOMETRY_IDS.asteroidLarge,
1313
objectKey: 'asteroidLarge',
@@ -105,12 +105,12 @@ export default class AsteroidsAttractAdapter {
105105
}
106106

107107
drawManifestAsteroid(renderer, sizeKey, options = {}) {
108-
const binding = ATTRACT_ASTEROID_RENDER_BINDINGS[sizeKey];
108+
const binding = ASTEROID_RENDER_BINDINGS[sizeKey];
109109
if (!binding) {
110110
return;
111111
}
112112

113-
this.scene?.drawObjectVectorAsset?.(renderer, 'attractAsteroid', {
113+
this.scene?.drawObjectVectorAsset?.(renderer, 'asteroids', {
114114
...this.scene.objectVectorTagOptions(binding.objectKey),
115115
elapsedMs: this.scene.objectVectorPlaybackMs,
116116
fps: 12,
@@ -152,11 +152,12 @@ export default class AsteroidsAttractAdapter {
152152
});
153153
}
154154

155-
this.scene?.drawObjectVectorAsset?.(renderer, 'attractShip', {
155+
this.scene?.drawObjectVectorAsset?.(renderer, 'ship', {
156156
...this.scene.objectVectorTagOptions('ship'),
157157
elapsedMs: this.scene.objectVectorPlaybackMs,
158158
fps: 12,
159-
objectId: ASTEROIDS_OBJECT_GEOMETRY_IDS.attractShip,
159+
objectId: ASTEROIDS_OBJECT_GEOMETRY_IDS.ship,
160+
requireManifestBinding: true,
160161
rotation: -0.28,
161162
scale: 1.1,
162163
stateId: 'idle',
@@ -245,11 +246,12 @@ export default class AsteroidsAttractAdapter {
245246

246247
const x = 480 + Math.cos(this.demoTime * 0.7) * 220;
247248
const y = 340 + Math.sin(this.demoTime * 1.1) * 130;
248-
this.scene?.drawObjectVectorAsset?.(renderer, 'attractShip', {
249+
this.scene?.drawObjectVectorAsset?.(renderer, 'ship', {
249250
...this.scene.objectVectorTagOptions('ship'),
250251
elapsedMs: this.scene.objectVectorPlaybackMs,
251252
fps: 12,
252-
objectId: ASTEROIDS_OBJECT_GEOMETRY_IDS.attractShip,
253+
objectId: ASTEROIDS_OBJECT_GEOMETRY_IDS.ship,
254+
requireManifestBinding: true,
253255
rotation: Math.sin(this.demoTime * 0.9) * 1.2,
254256
stateId: 'idle',
255257
x,
@@ -271,11 +273,12 @@ export default class AsteroidsAttractAdapter {
271273
x: 430 + Math.sin((this.demoTime * 0.92) + 2.2) * 154,
272274
y: 318 + Math.cos((this.demoTime * 0.72) + 2.7) * 78,
273275
});
274-
this.scene?.drawObjectVectorAsset?.(renderer, 'attractUfo', {
276+
this.scene?.drawObjectVectorAsset?.(renderer, 'ufo', {
275277
...this.scene.objectVectorTagOptions('ufoLarge'),
276278
elapsedMs: this.scene.objectVectorPlaybackMs,
277279
fps: 12,
278-
objectId: ASTEROIDS_OBJECT_GEOMETRY_IDS.attractUfo,
280+
objectId: ASTEROIDS_OBJECT_GEOMETRY_IDS.ufoLarge,
281+
requireManifestBinding: true,
279282
stateId: 'active',
280283
x: 480 + Math.cos(this.demoTime * 0.43) * 280,
281284
y: 284,

games/Asteroids/game/AsteroidsGameScene.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -754,6 +754,8 @@ export default class AsteroidsGameScene extends Scene {
754754
...this.objectVectorTagOptions(objectKey),
755755
elapsedMs: this.objectVectorPlaybackMs,
756756
fps: 12,
757+
objectId: this.objectVectorRuntimeObjectValidation.objectsByKey[objectKey]?.id || "",
758+
requireManifestBinding: true,
757759
stateId: "active",
758760
x: this.world.ufo.x,
759761
y: this.world.ufo.y,
@@ -791,6 +793,8 @@ export default class AsteroidsGameScene extends Scene {
791793
...this.objectVectorTagOptions("ship"),
792794
elapsedMs: this.objectVectorPlaybackMs,
793795
fps: 12,
796+
objectId: this.objectVectorRuntimeObjectValidation.objectsByKey.ship?.id || "",
797+
requireManifestBinding: true,
794798
rotation: this.world.ship.angle,
795799
stateId: this.world.ship.thrusting && this.session.mode === 'playing' ? "move" : "idle",
796800
x: this.world.ship.x,
@@ -880,6 +884,8 @@ export default class AsteroidsGameScene extends Scene {
880884
...this.objectVectorTagOptions("ship"),
881885
elapsedMs: this.objectVectorPlaybackMs,
882886
fps: 12,
887+
objectId: this.objectVectorRuntimeObjectValidation.objectsByKey.ship?.id || "",
888+
requireManifestBinding: true,
883889
rotation: -Math.PI / 2,
884890
scale: 1.05,
885891
stateId: "idle",

games/Asteroids/game/asteroidsObjectGeometryManifest.js

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,19 @@ export const ASTEROIDS_OBJECT_GEOMETRY_IDS = Object.freeze({
1010
asteroidMedium: 'object.asteroids.medium-asteroid',
1111
asteroidSmall: 'object.asteroids.small-asteroid',
1212
bullet: 'object.asteroids.bullet',
13-
attractShip: 'object.asteroids.ship',
14-
attractUfo: 'object.asteroids.large-ufo',
13+
ship: 'object.asteroids.ship',
14+
ufoLarge: 'object.asteroids.large-ufo',
15+
ufoSmall: 'object.asteroids.small-ufo',
1516
});
1617

1718
export const ASTEROIDS_REQUIRED_MANIFEST_GEOMETRY_IDS = Object.freeze([
1819
ASTEROIDS_OBJECT_GEOMETRY_IDS.asteroidLarge,
1920
ASTEROIDS_OBJECT_GEOMETRY_IDS.asteroidMedium,
2021
ASTEROIDS_OBJECT_GEOMETRY_IDS.asteroidSmall,
21-
ASTEROIDS_OBJECT_GEOMETRY_IDS.attractShip,
22-
ASTEROIDS_OBJECT_GEOMETRY_IDS.attractUfo,
2322
ASTEROIDS_OBJECT_GEOMETRY_IDS.bullet,
23+
ASTEROIDS_OBJECT_GEOMETRY_IDS.ship,
24+
ASTEROIDS_OBJECT_GEOMETRY_IDS.ufoLarge,
25+
ASTEROIDS_OBJECT_GEOMETRY_IDS.ufoSmall,
2426
]);
2527

2628
function isRecord(value) {

tests/games/AsteroidsAssetReferenceAdoption.test.mjs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -63,12 +63,12 @@ export async function run() {
6363
assert.deepEqual(
6464
payload.objects.find((object) => object.id === "object.asteroids.ship").shapes.find((shape) => shape.tool === "polygon").geometry.points,
6565
[
66-
{ x: 14, y: 0 },
67-
{ x: -10, y: -8 },
68-
{ x: -6, y: -3 },
69-
{ x: -6, y: 3 },
70-
{ x: -10, y: 8 },
71-
{ x: 14, y: 0 },
66+
{ x: 15.4, y: 0 },
67+
{ x: -11, y: -8.8 },
68+
{ x: -6.6, y: -3.3 },
69+
{ x: -6.6, y: 3.3 },
70+
{ x: -11, y: 8.8 },
71+
{ x: 15.4, y: 0 },
7272
],
7373
);
7474
assert.equal(payload.objects.find((object) => object.id === "object.asteroids.large-ufo").shapes[0].tool, "polyline");

tests/games/AsteroidsPresentation.test.mjs

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,15 +245,74 @@ function testAsteroidsAttractAsteroidsUseManifestObjectsAndStyles() {
245245
assert.equal(objectIds.includes('object.asteroids.small-asteroid'), true);
246246
assert.equal(objectIds.includes('object.asteroids.large-ufo'), true);
247247
[
248+
'object.asteroids.ship',
248249
'object.asteroids.large-asteroid',
249250
'object.asteroids.medium-asteroid',
250251
'object.asteroids.small-asteroid',
252+
'object.asteroids.large-ufo',
251253
].forEach((objectId) => {
252254
const calls = renderCalls.filter((call) => call.objectId === objectId);
253255
assert.equal(calls.length > 0, true);
254256
assert.equal(calls.every((call) => call.requireManifestBinding), true);
255257
assert.equal(calls.every((call) => call.stroke === styleByObjectId.get(objectId)), true);
256258
});
259+
assert.equal(scene.objectVectorRenderCounts.attractAsteroid, undefined);
260+
assert.equal(scene.objectVectorRenderCounts.attractShip, undefined);
261+
assert.equal(scene.objectVectorRenderCounts.attractUfo, undefined);
262+
assert.equal(scene.objectVectorRenderCounts.asteroids > 0, true);
263+
assert.equal(scene.objectVectorRenderCounts.ship > 0, true);
264+
assert.equal(scene.objectVectorRenderCounts.ufo > 0, true);
265+
}
266+
267+
function testAsteroidsGameplayObjectsUseSharedManifestBindings() {
268+
const renderCalls = [];
269+
const scene = new AsteroidsGameScene(createAsteroidsTestSceneOptions({
270+
objectVectorAssets: createObjectVectorAssetSet(),
271+
objectVectorRuntime: createObjectVectorRuntime(renderCalls),
272+
}));
273+
scene.session.start(1);
274+
scene.attractController.active = false;
275+
scene.world.asteroids = [
276+
scene.world.createAsteroidFromState({ angle: 0.12, size: 3, x: 120, y: 160 }),
277+
scene.world.createAsteroidFromState({ angle: -0.28, size: 2, x: 240, y: 260 }),
278+
scene.world.createAsteroidFromState({ angle: 0.47, size: 1, x: 360, y: 320 }),
279+
];
280+
scene.world.bullets = [];
281+
scene.world.ufoBullets = [];
282+
scene.world.ufo = scene.world.createUfoEntity('large', scene.world.wave);
283+
scene.world.ufo.x = 520;
284+
scene.world.ufo.y = 180;
285+
286+
const renderer = {
287+
drawRect() {},
288+
strokeRect() {},
289+
drawPolygon() {},
290+
drawLine() {},
291+
drawCircle() {},
292+
drawText() {},
293+
};
294+
295+
scene.render(renderer);
296+
[
297+
'object.asteroids.ship',
298+
'object.asteroids.large-asteroid',
299+
'object.asteroids.medium-asteroid',
300+
'object.asteroids.small-asteroid',
301+
'object.asteroids.large-ufo',
302+
].forEach((objectId) => {
303+
const calls = renderCalls.filter((call) => call.objectId === objectId);
304+
assert.equal(calls.length > 0, true, `${objectId} should render from the manifest`);
305+
assert.equal(calls.every((call) => call.requireManifestBinding), true, `${objectId} should require manifest binding`);
306+
});
307+
assert.equal(renderCalls.some((call) => call.objectId === 'object.asteroids.ship' && call.stateId === 'idle'), true);
308+
309+
scene.world.ufo = scene.world.createUfoEntity('small', scene.world.wave);
310+
scene.world.ufo.x = 580;
311+
scene.world.ufo.y = 220;
312+
scene.render(renderer);
313+
const smallUfoCalls = renderCalls.filter((call) => call.objectId === 'object.asteroids.small-ufo');
314+
assert.equal(smallUfoCalls.length > 0, true);
315+
assert.equal(smallUfoCalls.every((call) => call.requireManifestBinding), true);
257316
}
258317

259318
function testAsteroidsGameplayBulletsUseManifestObjectGeometry() {
@@ -360,6 +419,7 @@ export function run() {
360419
testAsteroidsGameOverQualifyingScoreInitialsFlow();
361420
testAsteroidsMenuHighScoreUsesLeaderboardTop();
362421
testAsteroidsAttractAsteroidsUseManifestObjectsAndStyles();
422+
testAsteroidsGameplayObjectsUseSharedManifestBindings();
363423
testAsteroidsGameplayBulletsUseManifestObjectGeometry();
364424
testAsteroidsGameplayRenderDoesNotCoverBackgroundLayer();
365425
}

tests/games/AsteroidsValidation.test.mjs

Lines changed: 21 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -163,14 +163,16 @@ export async function run() {
163163
assert.equal(ASTEROIDS_OBJECT_GEOMETRY_IDS.asteroidLarge, 'object.asteroids.large-asteroid');
164164
assert.equal(ASTEROIDS_OBJECT_GEOMETRY_IDS.asteroidMedium, 'object.asteroids.medium-asteroid');
165165
assert.equal(ASTEROIDS_OBJECT_GEOMETRY_IDS.asteroidSmall, 'object.asteroids.small-asteroid');
166-
assert.equal(ASTEROIDS_OBJECT_GEOMETRY_IDS.attractShip, 'object.asteroids.ship');
167-
assert.equal(ASTEROIDS_OBJECT_GEOMETRY_IDS.attractUfo, 'object.asteroids.large-ufo');
166+
assert.equal(ASTEROIDS_OBJECT_GEOMETRY_IDS.ship, 'object.asteroids.ship');
167+
assert.equal(ASTEROIDS_OBJECT_GEOMETRY_IDS.ufoLarge, 'object.asteroids.large-ufo');
168+
assert.equal(ASTEROIDS_OBJECT_GEOMETRY_IDS.ufoSmall, 'object.asteroids.small-ufo');
168169
assert.equal(objectGeometry.objectsById.get(ASTEROIDS_OBJECT_GEOMETRY_IDS.bullet).id, 'object.asteroids.bullet');
169170
assert.equal(objectGeometry.objectsById.get(ASTEROIDS_OBJECT_GEOMETRY_IDS.asteroidLarge).id, 'object.asteroids.large-asteroid');
170171
assert.equal(objectGeometry.objectsById.get(ASTEROIDS_OBJECT_GEOMETRY_IDS.asteroidMedium).id, 'object.asteroids.medium-asteroid');
171172
assert.equal(objectGeometry.objectsById.get(ASTEROIDS_OBJECT_GEOMETRY_IDS.asteroidSmall).id, 'object.asteroids.small-asteroid');
172-
assert.equal(objectGeometry.objectsById.get(ASTEROIDS_OBJECT_GEOMETRY_IDS.attractShip).id, 'object.asteroids.ship');
173-
assert.equal(objectGeometry.objectsById.get(ASTEROIDS_OBJECT_GEOMETRY_IDS.attractUfo).id, 'object.asteroids.large-ufo');
173+
assert.equal(objectGeometry.objectsById.get(ASTEROIDS_OBJECT_GEOMETRY_IDS.ship).id, 'object.asteroids.ship');
174+
assert.equal(objectGeometry.objectsById.get(ASTEROIDS_OBJECT_GEOMETRY_IDS.ufoLarge).id, 'object.asteroids.large-ufo');
175+
assert.equal(objectGeometry.objectsById.get(ASTEROIDS_OBJECT_GEOMETRY_IDS.ufoSmall).id, 'object.asteroids.small-ufo');
174176
ASTEROIDS_REQUIRED_MANIFEST_GEOMETRY_IDS.forEach((id) => {
175177
assert.equal(objectGeometry.objectsById.has(id), true, `required Asteroids manifest geometry id ${id} should resolve`);
176178
});
@@ -189,12 +191,12 @@ export async function run() {
189191
assert.deepEqual(
190192
getAsteroidsObjectGeometryPoints(objectGeometry, 'ship'),
191193
[
192-
{ x: 14, y: 0 },
193-
{ x: -10, y: -8 },
194-
{ x: -6, y: -3 },
195-
{ x: -6, y: 3 },
196-
{ x: -10, y: 8 },
197-
{ x: 14, y: 0 },
194+
{ x: 15.4, y: 0 },
195+
{ x: -11, y: -8.8 },
196+
{ x: -6.6, y: -3.3 },
197+
{ x: -6.6, y: 3.3 },
198+
{ x: -11, y: 8.8 },
199+
{ x: 15.4, y: 0 },
198200
],
199201
);
200202
assert.deepEqual(
@@ -244,12 +246,12 @@ export async function run() {
244246
const shipObject = loadAsteroidsObjectVectorPayload().objects.find((object) => object.id === 'object.asteroids.ship');
245247
const shipHull = shipObject.shapes.find((shape) => shape.tool === 'polygon');
246248
assert.deepEqual(shipHull.geometry.points, [
247-
{ x: 14, y: 0 },
248-
{ x: -10, y: -8 },
249-
{ x: -6, y: -3 },
250-
{ x: -6, y: 3 },
251-
{ x: -10, y: 8 },
252-
{ x: 14, y: 0 },
249+
{ x: 15.4, y: 0 },
250+
{ x: -11, y: -8.8 },
251+
{ x: -6.6, y: -3.3 },
252+
{ x: -6.6, y: 3.3 },
253+
{ x: -11, y: 8.8 },
254+
{ x: 15.4, y: 0 },
253255
]);
254256
const largeUfoObject = loadAsteroidsObjectVectorPayload().objects.find((object) => object.id === 'object.asteroids.large-ufo');
255257
assert.equal(largeUfoObject.shapes[0].tool, 'polyline');
@@ -288,13 +290,13 @@ export async function run() {
288290
assert.equal(legacySharedGeometryValidation.errors.some((message) => message.includes('vectorMaps')), true);
289291
const missingShipObjectManifest = structuredClone(manifestPayload);
290292
missingShipObjectManifest.tools['object-vector-studio-v2'].objects = missingShipObjectManifest.tools['object-vector-studio-v2'].objects
291-
.filter((object) => object.id !== ASTEROIDS_OBJECT_GEOMETRY_IDS.attractShip);
293+
.filter((object) => object.id !== ASTEROIDS_OBJECT_GEOMETRY_IDS.ship);
292294
const missingShipObjectValidation = loadAsteroidsObjectGeometryFromManifest(missingShipObjectManifest);
293295
assert.equal(missingShipObjectValidation.ok, false);
294-
assert.equal(missingShipObjectValidation.errors.some((message) => message.includes(ASTEROIDS_OBJECT_GEOMETRY_IDS.attractShip)), true);
296+
assert.equal(missingShipObjectValidation.errors.some((message) => message.includes(ASTEROIDS_OBJECT_GEOMETRY_IDS.ship)), true);
295297
const missingSmallUfoObjectManifest = structuredClone(manifestPayload);
296298
missingSmallUfoObjectManifest.tools['object-vector-studio-v2'].objects = missingSmallUfoObjectManifest.tools['object-vector-studio-v2'].objects
297-
.filter((object) => object.id !== 'object.asteroids.small-ufo');
299+
.filter((object) => object.id !== ASTEROIDS_OBJECT_GEOMETRY_IDS.ufoSmall);
298300
const missingSmallUfoObjectValidation = loadAsteroidsObjectGeometryFromManifest(missingSmallUfoObjectManifest);
299301
assert.equal(missingSmallUfoObjectValidation.ok, false);
300302
assert.equal(

tests/games/AsteroidsVectorTransforms.test.mjs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,9 @@ export function run() {
3131
ship.angle = Math.PI / 2;
3232
const shipPoints = ship.getPoints();
3333
assert.equal(shipPoints.length, 6);
34-
assertPointClose(shipPoints[0], { x: 100, y: 214 });
35-
assertPointClose(shipPoints[1], { x: 108, y: 190 });
36-
assertPointClose(shipPoints[4], { x: 92, y: 190 });
34+
assertPointClose(shipPoints[0], { x: 100, y: 215.4 });
35+
assertPointClose(shipPoints[1], { x: 108.8, y: 189 });
36+
assertPointClose(shipPoints[4], { x: 91.2, y: 189 });
3737

3838
const asteroid = new Asteroid(320, 240, 3, () => 0.5, asteroidGeometryProfiles);
3939
asteroid.angle = 0;

0 commit comments

Comments
 (0)