Выделить CorpseManager
This commit is contained in:
+2
-2
@@ -49,10 +49,10 @@
|
||||
{"_type":"issue","id":"goon-game-ui-vslice","title":"Базовый HUD и multitouch controls","description":"В APK есть направления, jump, restart, pause, deaths и timer.","acceptance_criteria":"Управление работает одновременно двумя касаниями.","spec_id":"docs/DEVELOPMENT_PLAN.md","status":"closed","priority":1,"issue_type":"feature","owner":"gavrilovdev@yandex-team.ru","created_at":"2026-08-17T21:40:00Z","created_by":"E.Gavrilov","updated_at":"2026-08-18T21:01:04Z","closed_at":"2026-08-17T21:41:21Z","close_reason":"Базовый HUD и multitouch controls реализованы.","metadata":{"stages":0,"primary_stage":0},"labels":["baseline","closed","primary-stage-0","stage-0"],"dependencies":[{"issue_id":"goon-game-ui-vslice","depends_on_id":"goon-game-e08","type":"parent-child","created_at":"2026-08-18T00:43:22Z","created_by":"E.Gavrilov","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||
{"_type":"issue","id":"goon-game-camera-polish","title":"Доработать production camera controller","description":"Добавить vertical damping, room transitions, focus target и управляемый shake.","acceptance_criteria":"Камера не скрывает обязательные объекты и не вызывает дискомфорт.","notes":"Stage 5 RC: bounded horizontal/vertical damping, mechanism focus, impact shake, reduced-motion persistence; camera excluded from physics signatures; 35/35 Android gate GREEN.","spec_id":"docs/DEVELOPMENT_PLAN.md","status":"closed","priority":1,"issue_type":"feature","owner":"gavrilovdev@yandex-team.ru","estimated_minutes":960,"created_at":"2026-08-17T21:39:50Z","created_by":"E.Gavrilov","updated_at":"2026-08-18T23:01:45Z","closed_at":"2026-08-18T23:01:45Z","close_reason":"Production camera behavior and reduced-motion acceptance are implemented and regression-tested.","metadata":{"stages":5,"primary_stage":5},"labels":["camera","primary-stage-5","stage-5","vertical-slice"],"dependencies":[{"issue_id":"goon-game-camera-polish","depends_on_id":"goon-game-e06","type":"parent-child","created_at":"2026-08-18T00:43:16Z","created_by":"E.Gavrilov","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||
{"_type":"issue","id":"goon-game-camera-basic","title":"Базовая камера с look-ahead и bounds","description":"В текущем runtime реализовано сглаженное горизонтальное слежение.","acceptance_criteria":"Камера работает во всех 10 уровнях.","spec_id":"docs/DEVELOPMENT_PLAN.md","status":"closed","priority":1,"issue_type":"feature","owner":"gavrilovdev@yandex-team.ru","created_at":"2026-08-17T21:39:45Z","created_by":"E.Gavrilov","updated_at":"2026-08-18T21:01:04Z","closed_at":"2026-08-17T21:41:18Z","close_reason":"Базовое сглаженное слежение реализовано.","metadata":{"stages":0,"primary_stage":0},"labels":["baseline","closed","primary-stage-0","stage-0"],"dependencies":[{"issue_id":"goon-game-camera-basic","depends_on_id":"goon-game-e06","type":"parent-child","created_at":"2026-08-18T00:43:15Z","created_by":"E.Gavrilov","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||
{"_type":"issue","id":"goon-game-puzzle-events","title":"Выделить event-driven PuzzleSystem","description":"Заменить прямую проверку сигналов универсальными каналами событий и явными состояниями DoorController.","acceptance_criteria":"Кнопки, двери и платформы связываются данными; integration-тесты проходят.","spec_id":"docs/DEVELOPMENT_PLAN.md","status":"open","priority":1,"issue_type":"feature","owner":"gavrilovdev@yandex-team.ru","estimated_minutes":1920,"created_at":"2026-08-17T21:39:41Z","created_by":"E.Gavrilov","updated_at":"2026-08-18T21:02:27Z","metadata":{"stages":6,"primary_stage":6},"labels":["architecture","primary-stage-6","puzzle","stage-6"],"dependencies":[{"issue_id":"goon-game-puzzle-events","depends_on_id":"goon-game-e05","type":"parent-child","created_at":"2026-08-18T00:43:13Z","created_by":"E.Gavrilov","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||
{"_type":"issue","id":"goon-game-puzzle-events","title":"Выделить event-driven PuzzleSystem","description":"Заменить прямую проверку сигналов универсальными каналами событий и явными состояниями DoorController.","acceptance_criteria":"Кнопки, двери и платформы связываются данными; integration-тесты проходят.","notes":"2026-08-19: claimed as the next Stage 6 foundation slice after CorpseManager. Planned boundary: deterministic event/signal/door state model, data-driven bindings, GameView adapter, RED-GREEN integration, no level-solution changes.","spec_id":"docs/DEVELOPMENT_PLAN.md","status":"in_progress","priority":1,"issue_type":"feature","assignee":"E.Gavrilov","owner":"gavrilovdev@yandex-team.ru","estimated_minutes":1920,"created_at":"2026-08-17T21:39:41Z","created_by":"E.Gavrilov","updated_at":"2026-08-19T00:43:02Z","started_at":"2026-08-19T00:43:02Z","metadata":{"stages":6,"primary_stage":6},"labels":["architecture","primary-stage-6","puzzle","stage-6"],"dependencies":[{"issue_id":"goon-game-puzzle-events","depends_on_id":"goon-game-e05","type":"parent-child","created_at":"2026-08-18T00:43:13Z","created_by":"E.Gavrilov","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||
{"_type":"issue","id":"goon-game-puzzle-vslice","title":"Базовые puzzle objects vertical slice","description":"Работают pressure/toggle/timed switches, doors и moving platforms.","acceptance_criteria":"Существующие уровни используют все базовые типы.","spec_id":"docs/DEVELOPMENT_PLAN.md","status":"closed","priority":1,"issue_type":"feature","owner":"gavrilovdev@yandex-team.ru","created_at":"2026-08-17T21:39:39Z","created_by":"E.Gavrilov","updated_at":"2026-08-18T21:01:05Z","closed_at":"2026-08-17T21:41:17Z","close_reason":"Базовые кнопки, двери и платформы реализованы.","metadata":{"stages":0,"primary_stage":0},"labels":["baseline","closed","primary-stage-0","stage-0"],"dependencies":[{"issue_id":"goon-game-puzzle-vslice","depends_on_id":"goon-game-e05","type":"parent-child","created_at":"2026-08-18T00:43:11Z","created_by":"E.Gavrilov","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||
{"_type":"issue","id":"goon-game-hazards-vslice","title":"Опасности vertical slice","description":"Реализованы spikes, saw, pit, freeze и electricity.","acceptance_criteria":"Опасности присутствуют в каталоге 10 уровней и не ломают restart.","spec_id":"docs/DEVELOPMENT_PLAN.md","status":"closed","priority":1,"issue_type":"feature","owner":"gavrilovdev@yandex-team.ru","created_at":"2026-08-17T21:39:35Z","created_by":"E.Gavrilov","updated_at":"2026-08-18T21:01:05Z","closed_at":"2026-08-17T21:41:16Z","close_reason":"Базовый набор опасностей присутствует в 10 уровнях.","metadata":{"stages":0,"primary_stage":0},"labels":["baseline","closed","primary-stage-0","stage-0"],"dependencies":[{"issue_id":"goon-game-hazards-vslice","depends_on_id":"goon-game-e04","type":"parent-child","created_at":"2026-08-18T00:43:09Z","created_by":"E.Gavrilov","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||
{"_type":"issue","id":"goon-game-corpse-manager","title":"Выделить полноценный CorpseManager","description":"Убрать управление телами из монолитного GameView; добавить register/remove/transform/reset, лимит и диагностические счётчики.","acceptance_criteria":"Система имеет отдельный контракт, тесты и не меняет решения существующих уровней.","notes":"2026-08-19: next Stage 6 foundation slice after character state machine. Planned boundary: deep public corpse lifecycle contract, GameView extraction without level-solution changes, test-first register/remove/transform/reset/limit/diagnostics.","spec_id":"docs/DEVELOPMENT_PLAN.md","status":"in_progress","priority":1,"issue_type":"feature","assignee":"E.Gavrilov","owner":"gavrilovdev@yandex-team.ru","estimated_minutes":1440,"created_at":"2026-08-17T21:39:27Z","created_by":"E.Gavrilov","updated_at":"2026-08-19T00:19:59Z","started_at":"2026-08-19T00:19:59Z","metadata":{"stages":6,"primary_stage":6},"labels":["architecture","corpse","primary-stage-6","stage-6"],"dependencies":[{"issue_id":"goon-game-corpse-manager","depends_on_id":"goon-game-e02","type":"parent-child","created_at":"2026-08-18T00:43:04Z","created_by":"E.Gavrilov","metadata":"{}"}],"dependency_count":0,"dependent_count":1,"comment_count":0}
|
||||
{"_type":"issue","id":"goon-game-corpse-manager","title":"Выделить полноценный CorpseManager","description":"Убрать управление телами из монолитного GameView; добавить register/remove/transform/reset, лимит и диагностические счётчики.","acceptance_criteria":"Система имеет отдельный контракт, тесты и не меняет решения существующих уровней.","notes":"2026-08-19: next Stage 6 foundation slice after character state machine. Planned boundary: deep public corpse lifecycle contract, GameView extraction without level-solution changes, test-first register/remove/transform/reset/limit/diagnostics.\n2026-08-19: completed test-first. CorpseManager now owns bounded register/remove/reset/FIFO eviction, composable pinned/frozen/conductive transformations, body save/restore and immutable diagnostics. GameView remains the physics adapter; no physics, geometry or level-signature changes. Final gates: 84 JVM tests; lintDebug/assembleDebug/assembleDebugAndroidTest GREEN; Android 16 unfiltered 37/37, 0 failed/0 skipped, 427.237s; APK ZIP valid, SHA-256 982fcf312f10bf23feb3a8794b708b61002012a3706c94ebb4320fefb36fb889. Evidence: docs/STAGE6_CORPSE_MANAGER.md.","spec_id":"docs/DEVELOPMENT_PLAN.md","status":"closed","priority":1,"issue_type":"feature","assignee":"E.Gavrilov","owner":"gavrilovdev@yandex-team.ru","estimated_minutes":1440,"created_at":"2026-08-17T21:39:27Z","created_by":"E.Gavrilov","updated_at":"2026-08-19T00:43:01Z","started_at":"2026-08-19T00:19:59Z","closed_at":"2026-08-19T00:43:01Z","close_reason":"Acceptance criteria выполнены: отдельный lifecycle-контракт интегрирован, существующие решения сохранены, полная Android-матрица 37/37 зелёная","metadata":{"stages":6,"primary_stage":6},"labels":["architecture","corpse","primary-stage-6","stage-6"],"dependencies":[{"issue_id":"goon-game-corpse-manager","depends_on_id":"goon-game-e02","type":"parent-child","created_at":"2026-08-18T00:43:04Z","created_by":"E.Gavrilov","metadata":"{}"}],"dependency_count":0,"dependent_count":1,"comment_count":0}
|
||||
{"_type":"issue","id":"goon-game-character-states","title":"Завершить state machine персонажа","description":"Добавить явные Interact, Stunned, Turn, Land и animation-state переходы; вынести coyote/input buffer в конфиг.","acceptance_criteria":"Все переходы детерминированы, визуально различимы и имеют тесты.","notes":"2026-08-19: Stage 6 started after accepted Vertical Slice. First vertical slice: explicit deterministic character state machine (Idle/Run/Turn/Jump/Fall/Land/Interact/Stunned/Death), movement timings sourced from PhysicsConfig, GameView integration without physics changes, public automation state, JVM contract and full build gate.\n2026-08-19: TDD complete. Explicit Idle/Run/Turn/Jump/Fall/Land/Interact/Stunned/Death model, configured timings, non-color poses, GameView spawn/update/death/restore/switch/automation integration. Final exact gates: 81 JVM; lint/build; Android 16 unfiltered 36/36 in 720.578s; APK ZIP and SHA d25b7dd0a06cead0d514539773acd0a84ed92bf7c2510b9b880ded2f66508581. Evidence: docs/STAGE6_CHARACTER_STATES.md.","spec_id":"docs/DEVELOPMENT_PLAN.md","status":"closed","priority":1,"issue_type":"feature","assignee":"E.Gavrilov","owner":"gavrilovdev@yandex-team.ru","estimated_minutes":960,"created_at":"2026-08-17T21:39:20Z","created_by":"E.Gavrilov","updated_at":"2026-08-19T00:19:57Z","started_at":"2026-08-18T23:20:45Z","closed_at":"2026-08-19T00:19:57Z","close_reason":"All character transitions are deterministic, visually distinct, configured and proven by JVM plus exact Android 16 integration/regression gates.","metadata":{"stages":6,"primary_stage":6},"labels":["character","future","primary-stage-6","stage-6"],"dependencies":[{"issue_id":"goon-game-character-states","depends_on_id":"goon-game-e01","type":"parent-child","created_at":"2026-08-18T00:43:02Z","created_by":"E.Gavrilov","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||
{"_type":"issue","id":"goon-game-character-basic","title":"Базовый character controller","description":"Движение, остановка, разворот, прыжок, падение и приземление в текущем Canvas runtime.","acceptance_criteria":"Функции присутствуют в APK и покрыты smoke-тестом.","spec_id":"docs/DEVELOPMENT_PLAN.md","status":"closed","priority":1,"issue_type":"feature","owner":"gavrilovdev@yandex-team.ru","created_at":"2026-08-17T21:39:18Z","created_by":"E.Gavrilov","updated_at":"2026-08-18T21:01:05Z","closed_at":"2026-08-17T21:41:12Z","close_reason":"Реализовано в текущем vertical slice.","metadata":{"stages":0,"primary_stage":0},"labels":["baseline","closed","primary-stage-0","stage-0"],"dependencies":[{"issue_id":"goon-game-character-basic","depends_on_id":"goon-game-e01","type":"parent-child","created_at":"2026-08-18T00:43:01Z","created_by":"E.Gavrilov","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||
{"_type":"issue","id":"goon-game-e13","title":"EPIC-13 Optimization","description":"60 FPS target, object pooling, physics budgets и low profile. Разделы 41–44.","acceptance_criteria":"Производительность укладывается в зафиксированные бюджеты на средних и слабых устройствах.","spec_id":"docs/DEVELOPMENT_PLAN.md","status":"open","priority":1,"issue_type":"epic","owner":"gavrilovdev@yandex-team.ru","created_at":"2026-08-17T21:39:10Z","created_by":"E.Gavrilov","updated_at":"2026-08-18T21:03:51Z","metadata":{"stages":"5,7","primary_stage":7},"labels":["epic","performance","primary-stage-7","stage-5","stage-7"],"dependencies":[{"issue_id":"goon-game-e13","depends_on_id":"goon-game-plan","type":"parent-child","created_at":"2026-08-18T00:42:56Z","created_by":"E.Gavrilov","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||
|
||||
@@ -105,10 +105,12 @@ times with identical state signatures. The product owner confirmed completion of
|
||||
external testing on 19 August 2026; the evidence boundary is recorded without
|
||||
invented session data in `docs/STAGE5_HUMAN_PLAYTEST_RESULT.md`. Stage 5 is accepted.
|
||||
|
||||
Stage 6 production work has started. The first completed foundation slice adds a
|
||||
deterministic character state machine with explicit transient and command states;
|
||||
scope and final `81 JVM / 36 Android` evidence are in
|
||||
`docs/STAGE6_CHARACTER_STATES.md`.
|
||||
Stage 6 production work has started. Its first two completed foundation slices add
|
||||
a deterministic character state machine and a bounded `CorpseManager` that owns
|
||||
body registration, transformations, eviction, reset, save and restore. Scope and
|
||||
final evidence are in `docs/STAGE6_CHARACTER_STATES.md` and
|
||||
`docs/STAGE6_CORPSE_MANAGER.md`; the latest unfiltered gate is
|
||||
`84 JVM / 37 Android`.
|
||||
|
||||
The debug APK is written to `app/build/outputs/apk/debug/app-debug.apk`. It is signed with Android's standard debug key and can be installed with:
|
||||
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
package com.projectsacrifice.game;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import android.app.Instrumentation;
|
||||
|
||||
import androidx.test.core.app.ActivityScenario;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import androidx.test.platform.app.InstrumentationRegistry;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public final class CorpseManagerIntegrationTest {
|
||||
@Test
|
||||
public void deathRegistersAndRestartResetsTheOwnedBodyLifecycle() throws Exception {
|
||||
Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
|
||||
try (ActivityScenario<MainActivity> activity =
|
||||
ActivityScenario.launch(MainActivity.class)) {
|
||||
GameView[] view = new GameView[1];
|
||||
activity.onActivity(current -> view[0] = current.gameViewForAutomation());
|
||||
instrumentation.runOnMainSync(() -> {
|
||||
view[0].startLevelForAutomation(10);
|
||||
view[0].setAutomationInput(false, true, false);
|
||||
});
|
||||
|
||||
GameView.AutomationSnapshot died = awaitBody(instrumentation, view[0], 8_000L);
|
||||
CorpseManager.Diagnostics before = diagnostics(instrumentation, view[0]);
|
||||
assertEquals(1, died.corpseCount());
|
||||
assertEquals(1L, before.registered());
|
||||
assertEquals(0L, before.evicted());
|
||||
|
||||
instrumentation.runOnMainSync(view[0]::restartLevelForAutomation);
|
||||
GameView.AutomationSnapshot restarted = snapshot(instrumentation, view[0]);
|
||||
CorpseManager.Diagnostics after = diagnostics(instrumentation, view[0]);
|
||||
|
||||
assertEquals(0, restarted.corpseCount());
|
||||
assertEquals(before.resets() + 1L, after.resets());
|
||||
assertEquals(before.clearedByReset() + 1L, after.clearedByReset());
|
||||
assertTrue(restarted.playerAlive());
|
||||
}
|
||||
}
|
||||
|
||||
private static GameView.AutomationSnapshot awaitBody(
|
||||
Instrumentation instrumentation, GameView view, long timeoutMillis)
|
||||
throws Exception {
|
||||
long deadline = System.currentTimeMillis() + timeoutMillis;
|
||||
GameView.AutomationSnapshot current = snapshot(instrumentation, view);
|
||||
while (System.currentTimeMillis() < deadline) {
|
||||
current = snapshot(instrumentation, view);
|
||||
if (current.corpseCount() == 1) return current;
|
||||
Thread.sleep(24L);
|
||||
}
|
||||
throw new AssertionError("Body was not registered; deaths=" + current.deaths()
|
||||
+ ", corpses=" + current.corpseCount());
|
||||
}
|
||||
|
||||
private static GameView.AutomationSnapshot snapshot(
|
||||
Instrumentation instrumentation, GameView view) {
|
||||
GameView.AutomationSnapshot[] result = new GameView.AutomationSnapshot[1];
|
||||
instrumentation.runOnMainSync(() -> result[0] = view.automationSnapshot());
|
||||
return result[0];
|
||||
}
|
||||
|
||||
private static CorpseManager.Diagnostics diagnostics(
|
||||
Instrumentation instrumentation, GameView view) {
|
||||
CorpseManager.Diagnostics[] result = new CorpseManager.Diagnostics[1];
|
||||
instrumentation.runOnMainSync(
|
||||
() -> result[0] = view.corpseDiagnosticsForAutomation());
|
||||
return result[0];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
package com.projectsacrifice.game;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/** Owns the bounded lifecycle of puzzle bodies for one active level. */
|
||||
public final class CorpseManager {
|
||||
public enum Transformation {
|
||||
PINNED,
|
||||
FROZEN,
|
||||
CONDUCTIVE
|
||||
}
|
||||
|
||||
public static final class Body {
|
||||
final long id;
|
||||
final String deathType;
|
||||
float x;
|
||||
float y;
|
||||
float width = 72f;
|
||||
float height = 54f;
|
||||
float vx;
|
||||
float vy;
|
||||
boolean frozen;
|
||||
boolean conductive;
|
||||
boolean pinned;
|
||||
boolean grounded;
|
||||
int supportIndex = -1;
|
||||
|
||||
private Body(long id, String deathType, float x, float y) {
|
||||
this.id = id;
|
||||
this.deathType = deathType;
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
|
||||
public long id() { return id; }
|
||||
public String deathType() { return deathType; }
|
||||
public float x() { return x; }
|
||||
public float y() { return y; }
|
||||
public float width() { return width; }
|
||||
public float height() { return height; }
|
||||
public float velocityX() { return vx; }
|
||||
public float velocityY() { return vy; }
|
||||
public boolean frozen() { return frozen; }
|
||||
public boolean conductive() { return conductive; }
|
||||
public boolean pinned() { return pinned; }
|
||||
public boolean grounded() { return grounded; }
|
||||
public int supportIndex() { return supportIndex; }
|
||||
|
||||
public Body position(float x, float y) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Body size(float width, float height) {
|
||||
if (width <= 0f || height <= 0f) {
|
||||
throw new IllegalArgumentException("body size must be positive");
|
||||
}
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Body velocity(float x, float y) {
|
||||
vx = x;
|
||||
vy = y;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Body grounded(boolean value) {
|
||||
grounded = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Body supportIndex(int value) {
|
||||
supportIndex = value;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
public static final class Diagnostics {
|
||||
private final long registered;
|
||||
private final long evicted;
|
||||
private final long removed;
|
||||
private final long resets;
|
||||
private final long clearedByReset;
|
||||
|
||||
private Diagnostics(long registered, long evicted, long removed,
|
||||
long resets, long clearedByReset) {
|
||||
this.registered = registered;
|
||||
this.evicted = evicted;
|
||||
this.removed = removed;
|
||||
this.resets = resets;
|
||||
this.clearedByReset = clearedByReset;
|
||||
}
|
||||
|
||||
public long registered() { return registered; }
|
||||
public long evicted() { return evicted; }
|
||||
public long removed() { return removed; }
|
||||
public long resets() { return resets; }
|
||||
public long clearedByReset() { return clearedByReset; }
|
||||
}
|
||||
|
||||
private final int maximumBodies;
|
||||
private final ArrayList<Body> bodies = new ArrayList<>();
|
||||
private long nextId = 1L;
|
||||
private long registered;
|
||||
private long evicted;
|
||||
private long removed;
|
||||
private long resets;
|
||||
private long clearedByReset;
|
||||
|
||||
public CorpseManager(int maximumBodies) {
|
||||
if (maximumBodies < 1) throw new IllegalArgumentException("maximumBodies must be positive");
|
||||
this.maximumBodies = maximumBodies;
|
||||
}
|
||||
|
||||
public Body register(String deathType, float x, float y) {
|
||||
Body body = new Body(nextId++, safeDeathType(deathType), x, y);
|
||||
bodies.add(body);
|
||||
registered++;
|
||||
if (bodies.size() > maximumBodies) {
|
||||
bodies.remove(0);
|
||||
evicted++;
|
||||
}
|
||||
return body;
|
||||
}
|
||||
|
||||
public int count() { return bodies.size(); }
|
||||
|
||||
public boolean remove(Body body) {
|
||||
boolean changed = body != null && bodies.remove(body);
|
||||
if (changed) removed++;
|
||||
return changed;
|
||||
}
|
||||
|
||||
public boolean transform(Body body, Transformation transformation) {
|
||||
if (body == null || transformation == null || !bodies.contains(body)) return false;
|
||||
switch (transformation) {
|
||||
case PINNED:
|
||||
body.pinned = true;
|
||||
break;
|
||||
case FROZEN:
|
||||
body.frozen = true;
|
||||
break;
|
||||
case CONDUCTIVE:
|
||||
body.conductive = true;
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public void reset() {
|
||||
clearedByReset += bodies.size();
|
||||
bodies.clear();
|
||||
resets++;
|
||||
}
|
||||
|
||||
public List<Body> bodies() {
|
||||
return Collections.unmodifiableList(bodies);
|
||||
}
|
||||
|
||||
public Diagnostics diagnostics() {
|
||||
return new Diagnostics(registered, evicted, removed, resets, clearedByReset);
|
||||
}
|
||||
|
||||
private static String safeDeathType(String value) {
|
||||
return value == null || value.trim().isEmpty() ? "GENERIC" : value;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
package com.projectsacrifice.game;
|
||||
|
||||
import com.projectsacrifice.game.CorpseManager.Body;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
@@ -190,18 +192,6 @@ public final class GameView extends View implements Runnable {
|
||||
int facing = 1;
|
||||
}
|
||||
|
||||
private static final class Corpse {
|
||||
float x, y, width = 72f, height = 54f, vx, vy;
|
||||
boolean frozen;
|
||||
boolean conductive;
|
||||
boolean pinned;
|
||||
boolean grounded;
|
||||
int supportIndex = -1;
|
||||
DeathType type;
|
||||
|
||||
RectF rect() { return new RectF(x, y, x + width, y + height); }
|
||||
}
|
||||
|
||||
private static final class PlatformState {
|
||||
float x, y, previousX, previousY, motionTime;
|
||||
final LevelDefinition.Platform definition;
|
||||
@@ -235,7 +225,8 @@ public final class GameView extends View implements Runnable {
|
||||
private final Player player = new Player();
|
||||
private final CharacterStateMachine characterStateMachine =
|
||||
new CharacterStateMachine(PHYSICS);
|
||||
private final ArrayList<Corpse> corpses = new ArrayList<>();
|
||||
private final CorpseManager corpseManager = new CorpseManager(MAX_CORPSES);
|
||||
private final List<Body> corpses = corpseManager.bodies();
|
||||
private final ArrayList<PlatformState> platformStates = new ArrayList<>();
|
||||
private final Map<String, Boolean> toggleSignals = new HashMap<>();
|
||||
private final Map<String, Float> timedSignals = new HashMap<>();
|
||||
@@ -400,7 +391,7 @@ public final class GameView extends View implements Runnable {
|
||||
userPaused = false;
|
||||
pointers.clear();
|
||||
deriveControls();
|
||||
corpses.clear();
|
||||
corpseManager.reset();
|
||||
platformStates.clear();
|
||||
toggleSignals.clear();
|
||||
timedSignals.clear();
|
||||
@@ -517,7 +508,7 @@ public final class GameView extends View implements Runnable {
|
||||
private boolean switchContact(LevelDefinition.Switch item) {
|
||||
int mass = 0;
|
||||
if (player.alive && intersects(player.x, player.y, PLAYER_WIDTH, PLAYER_HEIGHT, item.box)) mass++;
|
||||
for (Corpse corpse : corpses) {
|
||||
for (Body corpse : corpses) {
|
||||
if (intersects(corpse.x, corpse.y, corpse.width, corpse.height, item.box)) mass++;
|
||||
}
|
||||
return mass >= item.requiredMass;
|
||||
@@ -533,7 +524,7 @@ public final class GameView extends View implements Runnable {
|
||||
}
|
||||
for (LevelDefinition.Hazard hazard : level.hazards) {
|
||||
if (id.equals(hazard.relayId)) {
|
||||
for (Corpse corpse : corpses) {
|
||||
for (Body corpse : corpses) {
|
||||
if (corpse.conductive && intersects(corpse.x, corpse.y, corpse.width, corpse.height, hazard.box)) return true;
|
||||
}
|
||||
}
|
||||
@@ -566,7 +557,7 @@ public final class GameView extends View implements Runnable {
|
||||
player.x += support.x - support.previousX;
|
||||
player.y += support.y - support.previousY;
|
||||
}
|
||||
for (Corpse corpse : corpses) {
|
||||
for (Body corpse : corpses) {
|
||||
if (!corpse.frozen && corpse.supportIndex >= 0 && corpse.supportIndex < platformStates.size()) {
|
||||
PlatformState support = platformStates.get(corpse.supportIndex);
|
||||
corpse.x += support.x - support.previousX;
|
||||
@@ -650,8 +641,8 @@ public final class GameView extends View implements Runnable {
|
||||
player.vy = 0f;
|
||||
current = playerRect();
|
||||
}
|
||||
for (Corpse corpse : corpses) {
|
||||
RectF solid = corpse.rect();
|
||||
for (Body corpse : corpses) {
|
||||
RectF solid = corpseRect(corpse);
|
||||
if (!RectF.intersects(current, solid)) continue;
|
||||
if (amount >= 0f) {
|
||||
player.y = solid.top - PLAYER_HEIGHT;
|
||||
@@ -666,7 +657,7 @@ public final class GameView extends View implements Runnable {
|
||||
}
|
||||
|
||||
private void updateCorpses(float dt) {
|
||||
for (Corpse corpse : corpses) {
|
||||
for (Body corpse : corpses) {
|
||||
if (corpse.frozen || corpse.pinned) continue;
|
||||
corpse.vy += GRAVITY * dt;
|
||||
corpse.grounded = false;
|
||||
@@ -680,7 +671,7 @@ public final class GameView extends View implements Runnable {
|
||||
}
|
||||
}
|
||||
|
||||
private boolean settleCorpseOnPressurePlate(Corpse corpse) {
|
||||
private boolean settleCorpseOnPressurePlate(Body corpse) {
|
||||
for (LevelDefinition.Switch item : level.switches) {
|
||||
if (item.type != LevelDefinition.SwitchType.PRESSURE) continue;
|
||||
if (!intersects(corpse.x, corpse.y, corpse.width, corpse.height, item.box)) continue;
|
||||
@@ -691,26 +682,26 @@ public final class GameView extends View implements Runnable {
|
||||
}
|
||||
corpse.vx = 0f;
|
||||
corpse.vy = 0f;
|
||||
corpse.pinned = true;
|
||||
corpseManager.transform(corpse, CorpseManager.Transformation.PINNED);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void resolveCorpseHorizontal(Corpse corpse) {
|
||||
RectF current = corpse.rect();
|
||||
private void resolveCorpseHorizontal(Body corpse) {
|
||||
RectF current = corpseRect(corpse);
|
||||
for (RectF solid : solidRects(false)) {
|
||||
if (!RectF.intersects(current, solid)) continue;
|
||||
if (corpse.vx > 0f) corpse.x = solid.left - corpse.width;
|
||||
else if (corpse.vx < 0f) corpse.x = solid.right;
|
||||
corpse.vx *= -0.18f;
|
||||
current = corpse.rect();
|
||||
current = corpseRect(corpse);
|
||||
}
|
||||
}
|
||||
|
||||
private void resolveCorpseVertical(Corpse corpse) {
|
||||
private void resolveCorpseVertical(Body corpse) {
|
||||
corpse.supportIndex = -1;
|
||||
RectF current = corpse.rect();
|
||||
RectF current = corpseRect(corpse);
|
||||
for (int i = 0; i < platformStates.size(); i++) {
|
||||
RectF solid = platformStates.get(i).rect();
|
||||
if (!RectF.intersects(current, solid)) continue;
|
||||
@@ -723,7 +714,7 @@ public final class GameView extends View implements Runnable {
|
||||
corpse.y = solid.bottom;
|
||||
corpse.vy *= -0.1f;
|
||||
}
|
||||
current = corpse.rect();
|
||||
current = corpseRect(corpse);
|
||||
}
|
||||
for (LevelDefinition.Door door : level.doors) {
|
||||
if (doorOpen(door)) continue;
|
||||
@@ -737,7 +728,7 @@ public final class GameView extends View implements Runnable {
|
||||
corpse.y = solid.bottom;
|
||||
corpse.vy *= -0.1f;
|
||||
}
|
||||
current = corpse.rect();
|
||||
current = corpseRect(corpse);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -745,7 +736,7 @@ public final class GameView extends View implements Runnable {
|
||||
ArrayList<RectF> result = new ArrayList<>();
|
||||
for (PlatformState platform : platformStates) result.add(platform.rect());
|
||||
for (LevelDefinition.Door door : level.doors) if (!doorOpen(door)) result.add(rect(door.box));
|
||||
if (includeCorpses) for (Corpse corpse : corpses) result.add(corpse.rect());
|
||||
if (includeCorpses) for (Body corpse : corpses) result.add(corpseRect(corpse));
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -778,10 +769,8 @@ public final class GameView extends View implements Runnable {
|
||||
cameraController.impact(type == DeathType.PIT ? 12f : 24f);
|
||||
|
||||
if (type != DeathType.PIT) {
|
||||
Corpse corpse = new Corpse();
|
||||
corpse.type = type;
|
||||
corpse.x = player.x;
|
||||
corpse.y = player.y + PLAYER_HEIGHT - corpse.height;
|
||||
Body corpse = corpseManager.register(type.name(), player.x,
|
||||
player.y + PLAYER_HEIGHT - 54f);
|
||||
if (source != null) {
|
||||
corpse.vx = source.impulseX;
|
||||
corpse.vy = source.impulseY;
|
||||
@@ -789,23 +778,21 @@ public final class GameView extends View implements Runnable {
|
||||
if (type == DeathType.SPIKE && source != null) {
|
||||
corpse.y = source.box.y - corpse.height;
|
||||
corpse.vx = corpse.vy = 0f;
|
||||
corpse.pinned = true;
|
||||
corpseManager.transform(corpse, CorpseManager.Transformation.PINNED);
|
||||
} else if (type == DeathType.FREEZE && source != null) {
|
||||
corpse.width = 104f;
|
||||
corpse.height = 104f;
|
||||
corpse.x = source.box.x - 2f;
|
||||
corpse.y = 880f - corpse.height;
|
||||
corpse.vx = corpse.vy = 0f;
|
||||
corpse.frozen = true;
|
||||
corpseManager.transform(corpse, CorpseManager.Transformation.FROZEN);
|
||||
} else if (type == DeathType.ELECTRIC && source != null) {
|
||||
corpse.conductive = true;
|
||||
corpse.x = source.box.x + (source.box.width - corpse.width) * 0.5f;
|
||||
corpse.y = 880f - corpse.height;
|
||||
corpse.vx = corpse.vy = 0f;
|
||||
corpse.pinned = true;
|
||||
corpseManager.transform(corpse, CorpseManager.Transformation.CONDUCTIVE);
|
||||
corpseManager.transform(corpse, CorpseManager.Transformation.PINNED);
|
||||
}
|
||||
corpses.add(corpse);
|
||||
while (corpses.size() > MAX_CORPSES) corpses.remove(0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -841,6 +828,11 @@ public final class GameView extends View implements Runnable {
|
||||
return new RectF(player.x, player.y, player.x + PLAYER_WIDTH, player.y + PLAYER_HEIGHT);
|
||||
}
|
||||
|
||||
private static RectF corpseRect(Body corpse) {
|
||||
return new RectF(corpse.x, corpse.y,
|
||||
corpse.x + corpse.width, corpse.y + corpse.height);
|
||||
}
|
||||
|
||||
private static RectF rect(LevelDefinition.Box box) {
|
||||
return new RectF(box.x, box.y, box.right(), box.bottom());
|
||||
}
|
||||
@@ -902,7 +894,7 @@ public final class GameView extends View implements Runnable {
|
||||
drawHazards(canvas);
|
||||
drawDoors(canvas);
|
||||
drawGoal(canvas);
|
||||
for (Corpse corpse : corpses) drawCorpse(canvas, corpse);
|
||||
for (Body corpse : corpses) drawCorpse(canvas, corpse);
|
||||
VolunteerRig.Skin skin = VolunteerRig.Skin.forLevel(level.number);
|
||||
if (player.alive) {
|
||||
VolunteerRig.Motion motion = VolunteerRig.motionFor(characterStateMachine.state());
|
||||
@@ -1140,7 +1132,7 @@ public final class GameView extends View implements Runnable {
|
||||
canvas.drawCircle(box.centerX() - 6f, box.centerY() - 7f, 7f, paint);
|
||||
}
|
||||
|
||||
private void drawCorpse(Canvas canvas, Corpse corpse) {
|
||||
private void drawCorpse(Canvas canvas, Body corpse) {
|
||||
canvas.save();
|
||||
float cx = corpse.x + corpse.width * 0.5f;
|
||||
float cy = corpse.y + corpse.height * 0.5f;
|
||||
@@ -2280,12 +2272,12 @@ public final class GameView extends View implements Runnable {
|
||||
player.x, player.y, player.vx, player.vy,
|
||||
player.alive, player.onGround, player.coyote,
|
||||
player.jumpBuffer, player.supportIndex, player.facing));
|
||||
for (Corpse corpse : corpses) {
|
||||
for (Body corpse : corpses) {
|
||||
builder.addBody(new GameSessionSnapshot.BodyData(
|
||||
corpse.x, corpse.y, corpse.width, corpse.height,
|
||||
corpse.vx, corpse.vy, corpse.frozen, corpse.conductive,
|
||||
corpse.pinned, corpse.grounded, corpse.supportIndex,
|
||||
corpse.type == null ? DeathType.SPIKE.name() : corpse.type.name()));
|
||||
corpse.deathType()));
|
||||
}
|
||||
ArrayList<Float> motionTimes = new ArrayList<>(platformStates.size());
|
||||
for (PlatformState state : platformStates) motionTimes.add(state.motionTime);
|
||||
@@ -2353,22 +2345,22 @@ public final class GameView extends View implements Runnable {
|
||||
characterStateMachine.reset(player.alive, player.onGround,
|
||||
player.vx, player.vy, player.facing);
|
||||
|
||||
corpses.clear();
|
||||
for (GameSessionSnapshot.BodyData savedBody : snapshot.bodies()) {
|
||||
Corpse corpse = new Corpse();
|
||||
corpse.x = savedBody.x();
|
||||
corpse.y = savedBody.y();
|
||||
corpse.width = savedBody.width();
|
||||
corpse.height = savedBody.height();
|
||||
corpse.vx = savedBody.velocityX();
|
||||
corpse.vy = savedBody.velocityY();
|
||||
corpse.frozen = savedBody.frozen();
|
||||
corpse.conductive = savedBody.conductive();
|
||||
corpse.pinned = savedBody.pinned();
|
||||
corpse.grounded = savedBody.grounded();
|
||||
corpse.supportIndex = savedBody.supportIndex();
|
||||
corpse.type = DeathType.valueOf(savedBody.deathType());
|
||||
corpses.add(corpse);
|
||||
Body corpse = corpseManager.register(savedBody.deathType(),
|
||||
savedBody.x(), savedBody.y())
|
||||
.size(savedBody.width(), savedBody.height())
|
||||
.velocity(savedBody.velocityX(), savedBody.velocityY())
|
||||
.grounded(savedBody.grounded())
|
||||
.supportIndex(savedBody.supportIndex());
|
||||
if (savedBody.frozen()) {
|
||||
corpseManager.transform(corpse, CorpseManager.Transformation.FROZEN);
|
||||
}
|
||||
if (savedBody.conductive()) {
|
||||
corpseManager.transform(corpse, CorpseManager.Transformation.CONDUCTIVE);
|
||||
}
|
||||
if (savedBody.pinned()) {
|
||||
corpseManager.transform(corpse, CorpseManager.Transformation.PINNED);
|
||||
}
|
||||
}
|
||||
|
||||
leftPressed = false;
|
||||
@@ -2452,4 +2444,8 @@ public final class GameView extends View implements Runnable {
|
||||
public void triggerStunForAutomation() {
|
||||
characterStateMachine.stun();
|
||||
}
|
||||
|
||||
public CorpseManager.Diagnostics corpseDiagnosticsForAutomation() {
|
||||
return corpseManager.diagnostics();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
package com.projectsacrifice.game;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
public final class CorpseManagerTest {
|
||||
@Test
|
||||
public void registeringBeyondTheBudgetEvictsTheOldestBodyAndReportsIt() {
|
||||
CorpseManager manager = new CorpseManager(2);
|
||||
CorpseManager.Body first = manager.register("SPIKE", 10f, 20f);
|
||||
CorpseManager.Body second = manager.register("SAW", 30f, 40f);
|
||||
CorpseManager.Body third = manager.register("FREEZE", 50f, 60f);
|
||||
|
||||
assertEquals(2, manager.count());
|
||||
assertSame(second, manager.bodies().get(0));
|
||||
assertSame(third, manager.bodies().get(1));
|
||||
assertEquals(3L, manager.diagnostics().registered());
|
||||
assertEquals(1L, manager.diagnostics().evicted());
|
||||
assertEquals(first.id() + 1L, second.id());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void removeAndResetAreIdempotentAndHaveSeparateDiagnostics() {
|
||||
CorpseManager manager = new CorpseManager(3);
|
||||
CorpseManager.Body first = manager.register("SPIKE", 10f, 20f);
|
||||
manager.register("SAW", 30f, 40f);
|
||||
|
||||
assertTrue(manager.remove(first));
|
||||
assertFalse(manager.remove(first));
|
||||
manager.reset();
|
||||
|
||||
assertEquals(0, manager.count());
|
||||
assertEquals(1L, manager.diagnostics().removed());
|
||||
assertEquals(1L, manager.diagnostics().resets());
|
||||
assertEquals(1L, manager.diagnostics().clearedByReset());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void ownedBodiesExposePhysicsFactsAndComposableTransformations() {
|
||||
CorpseManager manager = new CorpseManager(3);
|
||||
CorpseManager.Body body = manager.register("ELECTRIC", 10f, 20f)
|
||||
.position(30f, 40f)
|
||||
.size(104f, 54f)
|
||||
.velocity(12f, -8f)
|
||||
.grounded(true)
|
||||
.supportIndex(2);
|
||||
|
||||
assertTrue(manager.transform(body, CorpseManager.Transformation.FROZEN));
|
||||
assertTrue(manager.transform(body, CorpseManager.Transformation.CONDUCTIVE));
|
||||
assertTrue(manager.transform(body, CorpseManager.Transformation.PINNED));
|
||||
|
||||
assertEquals(30f, body.x(), 0f);
|
||||
assertEquals(40f, body.y(), 0f);
|
||||
assertEquals(104f, body.width(), 0f);
|
||||
assertEquals(54f, body.height(), 0f);
|
||||
assertEquals(12f, body.velocityX(), 0f);
|
||||
assertEquals(-8f, body.velocityY(), 0f);
|
||||
assertTrue(body.frozen());
|
||||
assertTrue(body.conductive());
|
||||
assertTrue(body.pinned());
|
||||
assertTrue(body.grounded());
|
||||
assertEquals(2, body.supportIndex());
|
||||
|
||||
CorpseManager.Body foreign = new CorpseManager(1).register("SAW", 0f, 0f);
|
||||
assertFalse(manager.transform(foreign, CorpseManager.Transformation.FROZEN));
|
||||
}
|
||||
}
|
||||
@@ -179,10 +179,12 @@ baseline-задачи также имели явного владельца по
|
||||
- принят `goon-game-milestone-production`.
|
||||
|
||||
Статус на 19 августа 2026: Stage 6 начат в изолированном production stream.
|
||||
Первый foundation slice `goon-game-character-states` завершён: явные
|
||||
Idle/Run/Turn/Jump/Fall/Land/Interact/Stunned/Death transitions, configured
|
||||
timings и отдельные non-color animation poses прошли 81 JVM-тест и финальную
|
||||
Android 16 matrix `36/36`. Следующий foundation slice — `CorpseManager`.
|
||||
Завершены первые два foundation slice. `goon-game-character-states` добавил
|
||||
явные Idle/Run/Turn/Jump/Fall/Land/Interact/Stunned/Death transitions.
|
||||
`goon-game-corpse-manager` перенёс регистрацию, transforms, FIFO budget, reset,
|
||||
save/restore и diagnostics тел в отдельного владельца без изменения физики и
|
||||
геометрии. Последний gate: 84 JVM-теста и Android 16 matrix `37/37`.
|
||||
Следующий foundation slice — event-driven `PuzzleSystem`.
|
||||
|
||||
### Этап 7. Техническая стабилизация и release engineering — P0, 4–8 дней
|
||||
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
# Stage 6 CorpseManager
|
||||
|
||||
Date: 19 August 2026
|
||||
|
||||
Beads task: `goon-game-corpse-manager`
|
||||
|
||||
## Contract
|
||||
|
||||
`CorpseManager` is the single owner of bodies in one active level. It provides:
|
||||
|
||||
- bounded registration with stable IDs and deterministic FIFO eviction;
|
||||
- explicit remove and level-reset lifecycle operations;
|
||||
- composable `PINNED`, `FROZEN`, and `CONDUCTIVE` transformations;
|
||||
- body physics facts needed by collision, moving-platform, save, and restore code;
|
||||
- immutable lifecycle diagnostics for automation and regression tests.
|
||||
|
||||
`GameView` remains the high-frequency physics adapter, but it no longer creates,
|
||||
owns, evicts, resets, or restores bodies through an internal collection. The
|
||||
manager exposes an unmodifiable live view, so callers can update owned body facts
|
||||
without being able to change collection membership.
|
||||
|
||||
The body budget and FIFO policy preserve the accepted Vertical Slice behavior.
|
||||
Restart and level changes clear active bodies through the manager; save/restore
|
||||
re-registers bodies and then reapplies their stored transformations. No level
|
||||
geometry, movement, collision constant, or death signature changed.
|
||||
|
||||
## TDD evidence
|
||||
|
||||
Separate RED cycles covered the missing manager contract, removal/reset
|
||||
diagnostics, mutable physics facts plus composable transformations, rejection of
|
||||
foreign bodies, and the missing `GameView` automation hook. Each RED was followed
|
||||
by its focused GREEN before the next behavior was introduced.
|
||||
|
||||
The Android integration test creates a real level-10 saw body through production
|
||||
input, verifies manager registration, restarts the level through the public game
|
||||
surface, and verifies that manager reset owns the cleanup.
|
||||
|
||||
## Final gates
|
||||
|
||||
- JSON validator: `10/10`, format `1`, physics `1`.
|
||||
- JVM: `84`, failures `0`, errors `0`, skipped `0`.
|
||||
- `lintDebug`, `assembleDebug`, `assembleDebugAndroidTest`: GREEN.
|
||||
- focused Android 16 lifecycle: `1/1` GREEN.
|
||||
- final unfiltered Android 16 matrix: `37/37`, failures `0`, errors `0`,
|
||||
skipped `0`, `427.237 s`, on the exact final APK/test APK.
|
||||
- debug APK ZIP integrity: `unzip -t` GREEN.
|
||||
- debug APK SHA-256:
|
||||
`982fcf312f10bf23feb3a8794b708b61002012a3706c94ebb4320fefb36fb889`.
|
||||
|
||||
The dedicated AVD was stopped after the final gate.
|
||||
Reference in New Issue
Block a user