/* * Aphelion * Copyright (c) 2014 Joris van der Wel * * This file is part of Aphelion * * Aphelion is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, version 3 of the License. * * Aphelion is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Aphelion. If not, see <http://www.gnu.org/licenses/>. * * In addition, the following supplemental terms apply, based on section 7 of * the GNU Affero General Public License (version 3): * a) Preservation of all legal notices and author attributions * b) Prohibition of misrepresentation of the origin of this material, and * modified versions are required to be marked in reasonable ways as * different from the original version * * Linking this library statically or dynamically with other modules is making a * combined work based on this library. Thus, the terms and conditions of the * GNU Affero General Public License cover the whole combination. * * As a special exception, the copyright holders of this library give you * permission to link this library with independent modules to produce an * executable, regardless of the license terms of these independent modules, * and to copy and distribute the resulting executable under terms of your * choice, provided that you also meet, for each linked independent module, * the terms and conditions of the license of that module. An independent * module is a module which is not derived from or based on this library. */ package aphelion.shared.physics; import aphelion.shared.event.ManualClockSource; import aphelion.shared.event.TickedEventLoop; import aphelion.shared.gameconfig.GameConfig; import static aphelion.shared.physics.PhysicsTest.MOVE_UP; import aphelion.shared.physics.entities.ActorPublic; import aphelion.shared.physics.entities.ProjectilePublic; import aphelion.shared.physics.events.pub.ActorDiedPublic; import aphelion.shared.physics.events.pub.EventPublic; import aphelion.shared.physics.events.pub.ProjectileExplosionPublic; import aphelion.shared.physics.operations.ActorWeaponFire; import aphelion.shared.physics.operations.Operation; import aphelion.shared.physics.valueobjects.PhysicsPoint; import aphelion.shared.physics.valueobjects.PhysicsPositionVector; import aphelion.shared.swissarmyknife.LinkedListEntry; import java.lang.reflect.Field; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.concurrent.TimeUnit; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import org.junit.Test; /** * * @author Joris */ public class DualRunnerEnvironmentTest extends PhysicsTest { protected TickedEventLoop loop; protected ManualClockSource clock; @Override protected void createEnvironment() { clock = new ManualClockSource(EnvironmentConf.TICK_LENGTH); loop = new TickedEventLoop(EnvironmentConf.TICK_LENGTH, 0, clock); env = new DualRunnerEnvironment(loop, new MapEmpty()); loop.addTickEvent((DualRunnerEnvironment) env); } @Override public void tearDown() { DualRunnerEnvironment env = (DualRunnerEnvironment) this.env; env.done(); // joins try { Field threadField = DualRunnerEnvironment.class.getDeclaredField("thread"); threadField.setAccessible(true); assert ! ((Thread) threadField.get(env)).isAlive(); } catch (Exception ex) { throw new Error(ex); } loop.breakdown(); loop = null; clock = null; super.tearDown(); } private void loopSetup() { loop.setup(); loop.loop(); clock.advanceHalfTick(); //thread will start on the first tick! } @Test(timeout=10000) public void testMoveConsistency1() throws InterruptedException { DualRunnerEnvironment env = (DualRunnerEnvironment) this.env; // Test multiple move's received in the past (but still in order) loopSetup(); env.actorNew(0, 1, 1234, "Warbird"); env.actorWarp(0, 1, false, 0, 0, 1000, 1001, EnvironmentConf.ROTATION_1_2TH); ActorPublic actor = env.getActor(1); clock.advanceTick(20); loop.loop(); // now at tick 20 assertEquals(20, env.getTick()); env.actorMove(10, 1, MOVE_UP); env.actorMove(15, 1, MOVE_UP); // make sure the thread catches up assertEquals(5, env.waitForThreadParsedOperations(5)); assertEquals(20, env.waitForThreadToCatchup()); // Ensure a timewarp can happen (might need to update this if the state length changes) clock.advanceTick(30); loop.loop(); // now at tick 50 assertEquals(50, env.getTick()); // make sure the thread catches up assertEquals(50, env.waitForThreadToCatchup()); assertEquals(0, env.getTimewarpCount()); // no need for a timewarp, there is a faster solution assertVelocity(1000, 1001 + 28 * 2, actor); assertPosition( 50 * 1000, 50 * 1001 + (50-10) * 28 + (50-15) * 28, actor); } @Test(timeout=10000) public void testResetConsistency() throws InterruptedException, NoSuchFieldException, IllegalAccessException { DualRunnerEnvironment env = (DualRunnerEnvironment) this.env; loopSetup(); env.actorNew(0, 1, 1234, "Warbird"); env.actorWarp(0, 1, false, 1000, 2000, 0, 0, EnvironmentConf.ROTATION_1_2TH); env.actorWeapon(2, 1, WEAPON_SLOT.GUN, false, 0, 0, 0, 0, 0); // Execute the 3 operations in both threads clock.advanceTick(2); loop.loop(); assertEquals(2, env.waitForThreadToCatchup()); int offsetY = conf.getInteger("projectile-offset-y").get(); int fireSpeed = conf.getInteger("projectile-speed").get(); // Verify operation history Field fireHistoryField = ActorWeaponFire.class.getDeclaredField("fireHistories"); PhysicsPositionVector opFireHistory = new PhysicsPositionVector(); fireHistoryField.setAccessible(true); LinkedListEntry<Operation> opLink = env.environment.trailingStates[0].history.first; // actor new opLink = opLink.next; // actor warp opLink = opLink.next; // actor weapon ActorWeaponFire opWeaponA = (ActorWeaponFire) opLink.data; assertNotNull(opWeaponA); opFireHistory.set(((ArrayList<PhysicsPositionVector[]>) fireHistoryField.get(opWeaponA)).get(0)[0]); // Position history before reset assertPointEquals(1000, 2000 + offsetY, opFireHistory.pos); assertPointEquals(0, fireSpeed, opFireHistory.vel); // Verify the actor reference does not changed (comparison follows after reset) ActorPublic actorA = env.getActor(1, false); assertNotNull(actorA); // Verify the projectile reference does not changed (comparison follows after reset) Iterator<ProjectilePublic> it = env.projectileIterator(); assertTrue(it.hasNext()); PhysicsPositionVector pos = new PhysicsPositionVector(); assertTrue(it.hasNext()); ProjectilePublic projA = it.next(); assertFalse(it.hasNext()); projA.getPosition(pos); // Projectile location at tick 2, before reset assertEquals(1000, pos.pos.x); assertEquals(2000 + offsetY, pos.pos.y); assertEquals(0, pos.vel.x); assertEquals(fireSpeed, pos.vel.y); it = null; // weapon fire was at tick 2, this should cause can inconsistency env.actorMove(1, 1, MOVE_UP); // Ensure a timewarp can happen clock.advanceTick(env.econfig_thread.TRAILING_STATE_DELAY); loop.loop(); assertEquals(2 + env.econfig_thread.TRAILING_STATE_DELAY, env.waitForThreadToCatchup()); assertEquals(1, env.getTimewarpCount()); assertEquals(1, env.tryResetStateNow()); // Verify the actor reference does not changed (this is what ActorKey is for) ActorPublic actorB = env.getActor(1, false); assertNotNull(actorB); assert actorA == actorB; // Verify actor position after reset assertPosition(1000, 2000 + ((int) env.getTick() - 1) * 28, actorB); // Verify the projectile reference did not change (this is what ProjectileKey is for) it = env.projectileIterator(); assertTrue(it.hasNext()); ProjectilePublic projB = it.next(); assertFalse(it.hasNext()); assert projA == projB; projB.getPosition(pos); // Verify projectile position after the reset assertEquals(1000, pos.pos.x); assertEquals(2000 + offsetY + 28 // ship position + (fireSpeed + 28) * ((int) env.getTick() - 2), pos.pos.y ); assertEquals(0, pos.vel.x); assertEquals(fireSpeed + 28, pos.vel.y); // Verify the operation reference does not change, and // Was the operation history changed properly? opLink = env.environment.trailingStates[0].history.first; // actor new opLink = opLink.next; // actor warp opLink = opLink.next; // actor move opLink = opLink.next; // actor weapon ActorWeaponFire opWeaponB = (ActorWeaponFire) opLink.data; assertNotNull(opWeaponB); assert opWeaponA == opWeaponB; opFireHistory.set(((ArrayList<PhysicsPositionVector[]>) fireHistoryField.get(opWeaponB)).get(0)[0]); assertPointEquals(1000, 2000 + offsetY + 28, opFireHistory.pos); assertPointEquals(0, fireSpeed + 28, opFireHistory.vel); } private ProjectileExplosionPublic testExplosionEventLong_explosion; private ActorDiedPublic testExplosionEventLong_died; private void testExplosionEventLong_assertEvent(boolean afterReset) { DualRunnerEnvironment env = (DualRunnerEnvironment) this.env; int events = 0; for (EventPublic e : env.eventIterable()) { ++events; if (e instanceof ProjectileExplosionPublic) { ProjectileExplosionPublic ev = (ProjectileExplosionPublic) e; // The reference should not change if (testExplosionEventLong_explosion != null) { assert testExplosionEventLong_explosion == ev; } testExplosionEventLong_explosion = ev; assert ev.hasOccurred(0); assertEquals(ACTOR_FIRST, ev.getFireActor(0)); assertEquals(ACTOR_SECOND, ev.getHitActor(0)); assertEquals(afterReset ? 29 : 24, ev.getOccurredAt(0)); PhysicsPoint pos = new PhysicsPoint(); ev.getPosition(0, pos); assertPointEquals(afterReset ? 485664 : 385664, 90, pos); PhysicsPositionVector posv = new PhysicsPositionVector(); // The projectile is removed at the moment of impact, it no longer has a position assertFalse(ev.getProjectile(0).getHistoricPosition(posv, ev.getOccurredAt(0), true)); assertFalse(ev.getProjectile(0).getHistoricPosition(posv, ev.getOccurredAt(0), false)); assertTrue(ev.getProjectile(0).getHistoricPosition(posv, ev.getOccurredAt(0) - 1, true)); assertPointEquals(afterReset ? 475336 : 375336, 90, posv.pos); assertTrue(ev.getProjectile(0).getHistoricPosition(posv, ev.getOccurredAt(0) - 1, false)); assertPointEquals(afterReset ? 475336 : 375336, 90, posv.pos); } else if (e instanceof ActorDiedPublic) { ActorDiedPublic ev = (ActorDiedPublic) e; // The reference should not change if (testExplosionEventLong_died != null) { assert testExplosionEventLong_died == ev; } testExplosionEventLong_died = ev; assertTrue(ev.hasOccurred(0)); assertEquals(afterReset ? 29 : 24, ev.getOccurredAt(0)); assertEquals(ACTOR_SECOND, ev.getDied(0)); assertEquals(testExplosionEventLong_explosion, ev.getCause(0)); } else { assert false; } } assertEquals(2, events); } @Test(timeout=10000) public void testExplosionEventLong() throws InterruptedException { DualRunnerEnvironment env = (DualRunnerEnvironment) this.env; testExplosionEventLong_explosion = null; testExplosionEventLong_died = null; loopSetup(); try { List<Object> yamlDocuments = GameConfig.loadYaml("" + "- weapon-projectiles: 1\n" + " projectile-hit-ship: true\n" + " projectile-angle-relative: true\n" + " projectile-speed: 20000\n" + " projectile-damage: 2000\n" + " ship-energy: 1500\n" ); env.loadConfig(env.getTick() - env.getConfig().HIGHEST_DELAY, "test", yamlDocuments); } catch (Exception ex) { throw new Error(ex); } env.actorNew(1, ACTOR_FIRST, 1234, "warbird"); env.actorWarp(1, ACTOR_FIRST, false, 1000, 90, 0, 0, EnvironmentConf.ROTATION_1_4TH); env.actorNew(1, ACTOR_SECOND, 4321, "warbird"); env.actorWarp(1, ACTOR_SECOND, false, 400000, 90, 0, 0, 0); env.actorWeapon(5, ACTOR_FIRST, WEAPON_SLOT.GUN, false, 0, 0, 0 , 0, 0); clock.advanceTick(2); loop.loop(); assertEquals(2, env.waitForThreadToCatchup()); assertEquals(7, env.waitForThreadParsedOperations(7)); // modify this test case if TRAILING_STATE_DELAY changes assert env.econfig_thread.TRAILING_STATE_DELAY == 32; clock.advanceTick(36); loop.loop(); assertEquals(38, env.waitForThreadToCatchup()); testExplosionEventLong_assertEvent(false); // Should cause an inconsistency (event hits somewhere else now) env.actorWarp(2, ACTOR_SECOND, false, 500000, 90, 0, 0, 0); assertEquals(8, env.waitForThreadParsedOperations(8)); clock.advanceTick(env.econfig_thread.TRAILING_STATE_DELAY); loop.loop(); assertEquals(38 + env.econfig_thread.TRAILING_STATE_DELAY, env.waitForThreadToCatchup()); assertEquals(1, env.getTimewarpCount()); assertEquals(1, env.tryResetStateNow()); testExplosionEventLong_assertEvent(true); } }