/*
* 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.Deadlock;
import aphelion.shared.event.LoopEvent;
import aphelion.shared.event.TickEvent;
import aphelion.shared.event.TickedEventLoop;
import aphelion.shared.gameconfig.*;
import aphelion.shared.net.protobuf.GameOperation;
import aphelion.shared.physics.entities.ActorPublic;
import aphelion.shared.physics.entities.ProjectilePublic;
import aphelion.shared.physics.events.pub.EventPublic;
import aphelion.shared.physics.operations.*;
import aphelion.shared.physics.valueobjects.PhysicsMovement;
import aphelion.shared.physics.valueobjects.PhysicsWarp;
import aphelion.shared.resource.ResourceDB;
import aphelion.shared.swissarmyknife.ThreadSafe;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.ReentrantLock;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* This class runs states in two threads in order to make sure one thread never blocks for long.
* The first thread tick()s and creates this object will only run a single
* state (state 0) that does not perform timewarps on its own.
* A second thread is spawned by this object that runs multiple states and performs timewarps.
* After having completed such a timewarp the state 0 of thread 2 is copied into the state 0 of
* thread 1.
* All getter will act upon the first thread. This means game play will continue smoothly,
* even during timewarps.
* @author Joris
*/
public class DualRunnerEnvironment implements TickEvent, LoopEvent, PhysicsEnvironment
{
private static final Logger log = Logger.getLogger("aphelion.shared.physics");
private final TickedEventLoop mainLoop;
private long lastLoopSync;
private final MyThread thread;
final SimpleEnvironment environment;
private final SimpleEnvironment[] envs;
private boolean firstTick = true;
private final ReentrantLock syncEnvsLock;
private final AtomicLong needsStateReset = new AtomicLong();
private long tryStateReset_lock_timeout = 0;
private static final long TRYSTATERESET_LOCKTIMEOUT_START = 2_000_000L;
private static final long TRYSTATERESET_LOCKTIMEOUT_PERTICK = 20_000L; // 10ms lock after 5 seconds of trying
private volatile long env_tickCount = 0;
private long nextOpSeq = 1;
private long stateResets = 0;
public final EnvironmentConf econfig_single;
public final EnvironmentConf econfig_thread;
public DualRunnerEnvironment(TickedEventLoop loop, PhysicsMap map)
{
this.mainLoop = loop;
environment = new SimpleEnvironment(new EnvironmentConf(true, 1234), map, false);
thread = new MyThread(new SimpleEnvironment(new EnvironmentConf(false), map, true));
envs = new SimpleEnvironment[] {environment, thread.environment};
econfig_single = environment.econfig;
econfig_thread = thread.environment.econfig;
// ^true = allow threads to call add operation methods
syncEnvsLock = new ReentrantLock();
}
@ThreadSafe
@Override
public EnvironmentConf getConfig()
{
return econfig_thread;
}
@Override
public void tick(long tick)
{
this.tick();
}
@Override
public void loop(long systemNanoTime, long sourceNanoTime)
{
if (!firstTick) // Syncing on the first tick is done elsewhere
{
tryStateReset();
if (lastLoopSync == 0)
{
lastLoopSync = systemNanoTime;
}
if (systemNanoTime - lastLoopSync > 250_000_000L) // 250ms
{
try
{
if (syncEnvsLock.tryLock(2, TimeUnit.MILLISECONDS))
{
try
{
doLoopSync();
}
finally
{
syncEnvsLock.unlock();
}
}
}
catch (InterruptedException ex)
{
Thread.currentThread().interrupt();
return;
}
}
}
}
private void tryStateReset()
{
final long stateReset = this.needsStateReset.get();
if (stateReset > 0)
{
try
{
// try for 2 milliseconds by default
// if we do not acquire the lock, start to increase the timeout every tick (see tick())
long timeout = tryStateReset_lock_timeout == 0
? TRYSTATERESET_LOCKTIMEOUT_START
: tryStateReset_lock_timeout;
if (syncEnvsLock.tryLock(timeout, TimeUnit.NANOSECONDS))
{
try
{
tryStateReset_lock_timeout = 0;
resetState();
this.needsStateReset.addAndGet(-stateReset);
doLoopSync();
}
finally
{
syncEnvsLock.unlock();
}
}
else
{
tryStateReset_lock_timeout = TRYSTATERESET_LOCKTIMEOUT_START;
}
}
catch (InterruptedException ex)
{
Thread.currentThread().interrupt();
return;
}
}
}
private void doLoopSync() // call with lock
{
// mainLoop might get synced because of clock sync
// so also synchronize the loop in the thread.
// add half a tick length so that the threads can be scheduled better
if (thread.loop != null)
{
thread.loop.synchronize(mainLoop.currentNano() + EnvironmentConf.TICK_LENGTH / 2, mainLoop.currentTick());
}
lastLoopSync = System.nanoTime();
}
@Override
public void tick()
{
if (firstTick)
{
firstTick = false;
log.log(Level.INFO, "Starting dual runner thread...");
thread.createLoop(mainLoop);
thread.syncLoop(mainLoop);
thread.start();
}
if (tryStateReset_lock_timeout > 0)
{
tryStateReset_lock_timeout += TRYSTATERESET_LOCKTIMEOUT_PERTICK;
}
environment.tick();
env_tickCount = environment.getTick();
//System.out.printf("%d: %4d => %4d (M)\n", java.util.Objects.hashCode(mainLoop), (mainLoop.currentNano()/1_000_000L), env_tickCount);
}
public void done()
{
log.log(Level.INFO, "Stopping dual runner environment...");
thread.done();
}
/** Wait until the internal thread has caught up with the loop on the main thread.
* This is used for test cases
*
* @return The tick the internal thread is now at.
* This is never greater than the tick of the main thread (it should now be equal).
* @throws java.lang.InterruptedException
*/
long waitForThreadToCatchup() throws InterruptedException
{
while(true)
{
syncEnvsLock.lock();
try
{
if (thread.environment.getTick() >= this.environment.getTick())
{
return thread.environment.getTick();
}
}
finally
{
syncEnvsLock.unlock();
}
Thread.sleep(1);
}
}
/** Wait until all pending operations have been polled.
* This is used for test cases
*
* @param minCount Wait until this many operations have been polled
* @return The total amount of operations polled so far
* @throws java.lang.InterruptedException
*/
long waitForThreadParsedOperations(long minCount) throws InterruptedException
{
while(true)
{
syncEnvsLock.lock();
try
{
long count = thread.environment.polledAddOperationsCount.get();
if (count >= minCount)
{
return count;
}
}
finally
{
syncEnvsLock.unlock();
}
Thread.sleep(1);
}
}
long tryResetStateNow() throws InterruptedException
{
syncEnvsLock.lock();
try
{
tryStateReset();
return stateResets;
}
finally
{
syncEnvsLock.unlock();
}
}
private void resetState() // (call with lock held)
{
long tickBeforeReset = environment.getTick();
++stateResets;
long start = System.nanoTime();
environment.trailingStates[0].timewarp(thread.environment.trailingStates[0]);
long end = System.nanoTime();
log.log(Level.WARNING, "Completed dual runner reset in {0}ms", new Object[]{
(end - start) / 1_000_000.0,
});
if (environment.getTick() != tickBeforeReset)
{
throw new AssertionError("timewarp() should tick until we are current again");
}
}
@Override
public void skipForward(long tick)
{
if (thread.isAlive())
{
throw new IllegalStateException("This method may only be used before tick()ing");
}
environment.skipForward(tick);
thread.environment.skipForward(tick);
}
@Override
public long getTick()
{
return environment.getTick();
}
@Override
public long getTickedAt()
{
return environment.getTickedAt();
}
@Override
public ActorPublic getActor(int pid)
{
return environment.getActor(pid);
}
@Override
public ActorPublic getActor(int pid, boolean nofail)
{
return environment.getActor(pid, nofail);
}
@Override
public Iterator<ActorPublic> actorIterator()
{
return environment.actorIterator();
}
@Override
public Iterable<ActorPublic> actorIterable()
{
return environment.actorIterable();
}
@Override
public int getActorCount()
{
return environment.getActorCount();
}
@Override
public Iterator<ProjectilePublic> projectileIterator()
{
return environment.projectileIterator();
}
@Override
public Iterable<ProjectilePublic> projectileIterable()
{
return environment.projectileIterable();
}
@Override
public int calculateProjectileCount()
{
return environment.calculateProjectileCount();
}
@Override
public ConfigSelection newConfigSelection()
{
return environment.newConfigSelection();
}
@Override
public PhysicsMap getMap()
{
return environment.getMap();
}
@Override
public Iterator<EventPublic> eventIterator()
{
return environment.eventIterator();
}
@Override
public Iterable<EventPublic> eventIterable()
{
return environment.eventIterable();
}
@Override
public GCInteger getGlobalConfigInteger(String name)
{
return environment.getGlobalConfigInteger(name);
}
@Override
public GCString getGlobalConfigString(String name)
{
return environment.getGlobalConfigString(name);
}
@Override
public GCBoolean getGlobalConfigBoolean(String name)
{
return environment.getGlobalConfigBoolean(name);
}
@Override
public GCIntegerList getGlobalConfigIntegerList(String name)
{
return environment.getGlobalConfigIntegerList(name);
}
@Override
public GCStringList getGlobalConfigStringList(String name)
{
return environment.getGlobalConfigStringList(name);
}
@Override
public GCBooleanList getGlobalConfigBooleanList(String name)
{
return environment.getGlobalConfigBooleanList(name);
}
@Override
public GCImage getGlobalConfigImage(String name, ResourceDB db)
{
return environment.getGlobalConfigImage(name, db);
}
public long getTimewarpCount()
{
return thread.timewarpCountLastSeen;
}
public long getResetCount()
{
return this.stateResets;
}
private OperationKey getNextOperationKey()
{
return new OperationKey(nextOpSeq++);
}
@Override
@ThreadSafe
public void loadConfig(long tick, String fileIdentifier, List yamlDocuments)
{
OperationKey key = getNextOperationKey();
for (SimpleEnvironment env : envs)
{
LoadConfig op = new LoadConfig(env, key);
op.tick = tick;
op.fileIdentifier = fileIdentifier;
op.yamlDocuments = yamlDocuments;
boolean ret = env.addOperation(op); // no fail
assert ret;
}
}
@Override
@ThreadSafe
public void unloadConfig(long tick, String fileIdentifier)
{
OperationKey key = getNextOperationKey();
for (SimpleEnvironment env : envs)
{
UnloadConfig op = new UnloadConfig(env, key);
op.tick = tick;
op.fileIdentifier = fileIdentifier;
boolean ret = env.addOperation(op); // no fail
assert ret;
}
}
@Override
@ThreadSafe
public void actorNew(long tick, int pid, long seed, String ship)
{
OperationKey key = getNextOperationKey();
for (SimpleEnvironment env : envs)
{
ActorNew op = new ActorNew(env, key);
op.tick = tick;
op.pid = pid;
op.seed = seed;
op.ship = ship;
boolean ret = env.addOperation(op);
assert ret;
}
}
@Override
@ThreadSafe
public void actorSync(GameOperation.ActorSync sync)
{
OperationKey key = getNextOperationKey();
for (SimpleEnvironment env : envs)
{
assert !env.econfig.server;
ActorSync op = new ActorSync(env, key);
op.tick = sync.getTick();
op.pid = sync.getPid();
op.sync = sync;
boolean ret = env.addOperation(op);
assert ret; // atleast the oldest state must accept the sync
}
}
@Override
@ThreadSafe
public void actorModification(long tick, int pid, String ship)
{
OperationKey key = getNextOperationKey();
for (SimpleEnvironment env : envs)
{
ActorModification op = new ActorModification(env, key);
op.tick = tick;
op.pid = pid;
op.ship = ship;
boolean ret = env.addOperation(op);
assert ret;
}
}
@Override
@ThreadSafe
public void actorRemove(long tick, int pid)
{
OperationKey key = getNextOperationKey();
for (SimpleEnvironment env : envs)
{
ActorRemove op = new ActorRemove(env, key);
op.tick = tick;
op.pid = pid;
boolean ret = env.addOperation(op);
assert ret;
}
}
@Override
@ThreadSafe
public boolean actorWarp(long tick, int pid, boolean hint, int x, int y, int x_vel, int y_vel, int rotation)
{
OperationKey key = getNextOperationKey();
boolean okay = true;
for (SimpleEnvironment env : envs)
{
ActorWarp op = new ActorWarp(env, key);
op.tick = tick;
op.pid = pid;
op.hint = hint;
op.warp = new PhysicsWarp(x, y, x_vel, y_vel, rotation);
okay = env.addOperation(op) && okay;
}
return okay;
}
@Override
@ThreadSafe
public boolean actorWarp(
long tick, int pid, boolean hint, int x, int y, int x_vel, int y_vel, int rotation, boolean has_x, boolean has_y, boolean has_x_vel, boolean has_y_vel, boolean has_rotation)
{
OperationKey key = getNextOperationKey();
boolean okay = true;
for (SimpleEnvironment env : envs)
{
ActorWarp op = new ActorWarp(env, key);
op.tick = tick;
op.pid = pid;
op.hint = hint;
op.warp = new PhysicsWarp(
has_x ? x : null,
has_y ? y : null,
has_x_vel ? x_vel : null,
has_y_vel ? y_vel : null,
has_rotation ? rotation : null);
okay = env.addOperation(op) && okay;
}
return okay;
}
@Override
@ThreadSafe
public boolean actorMove(long tick, int pid, PhysicsMovement move)
{
OperationKey key = getNextOperationKey();
boolean okay = true;
for (SimpleEnvironment env : envs)
{
ActorMove op = new ActorMove(env, key);
op.tick = tick;
op.pid = pid;
op.move = move;
if (move == null)
{
throw new IllegalArgumentException();
}
okay = env.addOperation(op) && okay;
}
return okay;
}
@Override
@ThreadSafe
public boolean actorWeapon(
long tick, int pid, WEAPON_SLOT weapon_slot,
boolean hint_set,
int hint_x, int hint_y,
int hint_x_vel, int hint_y_vel,
int hint_snapped_rotation)
{
OperationKey key = getNextOperationKey();
boolean okay = true;
for (SimpleEnvironment env : envs)
{
ActorWeaponFire op = new ActorWeaponFire(env, key);
op.tick = tick;
op.pid = pid;
op.weapon_slot = weapon_slot;
if (weapon_slot == null)
{
throw new IllegalArgumentException();
}
if (hint_set)
{
op.hint_set = hint_set;
op.hint_x = hint_x;
op.hint_y = hint_y;
op.hint_x_vel = hint_x_vel;
op.hint_y_vel = hint_y_vel;
op.hint_snapped_rot = hint_snapped_rotation;
}
okay = env.addOperation(op) && okay;
}
return okay;
}
@Override
@ThreadSafe
public boolean actorWeapon(long tick, int pid, WEAPON_SLOT weapon_slot)
{
OperationKey key = getNextOperationKey();
boolean okay = true;
for (SimpleEnvironment env : envs)
{
ActorWeaponFire op = new ActorWeaponFire(env, key);
op.tick = tick;
op.pid = pid;
op.weapon_slot = weapon_slot;
if (weapon_slot == null)
{
throw new IllegalArgumentException();
}
okay = env.addOperation(op) && okay;
}
return okay;
}
@Override
@ThreadSafe
public boolean weaponSync(long tick,
int owner_pid,
String weaponKey,
GameOperation.WeaponSync.Projectile[] projectiles,
long syncKey)
{
OperationKey key = getNextOperationKey();
boolean okay = true;
for (SimpleEnvironment env : envs)
{
assert !env.econfig.server;
// do not modify "projectiles" after calling this method
WeaponSync op = new WeaponSync(env, key);
op.tick = tick;
op.pid = owner_pid;
op.weaponKey = weaponKey;
op.syncProjectiles = projectiles;
op.syncKey = syncKey;
okay = env.addOperation(op) && okay;
}
return okay;
}
private final class MyThread extends Thread implements TickEvent, LoopEvent
{
TickedEventLoop loop;
Deadlock.DeadlockTicker deadlockTicker;
final SimpleEnvironment environment;
volatile long timewarpCountLastSeen = 0;
MyThread(SimpleEnvironment env)
{
this.environment = env;
setDaemon(true);
}
/** Set the loop and sync with it properly.
* Do not set this unless you are actually going to start ticking.
*/
void createLoop(TickedEventLoop syncWith)
{
if (this.isAlive())
{
throw new IllegalStateException();
}
loop = new TickedEventLoop(syncWith, 0);
deadlockTicker = loop;
loop.addTickEvent(this);
loop.addLoopEvent(this);
}
@ThreadSafe
void syncLoop(TickedEventLoop syncWith)
{
syncEnvsLock.lock();
try
{
long syncNano = syncWith.currentNano();
// This thread is spawned during the first physics tick.
// This means we are 1 tick behind! So subtract a full tick length.
// But running at exactly the same time is not optimal for performance,
// we are probably sleeping between ticks, so only subtract half a tick.
assert EnvironmentConf.TICK_LENGTH % 2 == 0;
syncNano -= EnvironmentConf.TICK_LENGTH / 2;
this.loop.synchronize(syncNano, syncWith.currentTick());
}
finally
{
syncEnvsLock.unlock();
}
}
@Override
public void run()
{
log.log(Level.INFO, "Started dual runner thread with {0} states", new Object[] {
this.environment.getConfig().TRAILING_STATES
});
setName("DualRunnerEnvironment-"+this.getId());
loop.run();
}
@ThreadSafe
public void done()
{
if (!this.isAlive())
{
return;
}
loop.interrupt();
this.interrupt();
try
{
this.join();
}
catch (InterruptedException ex)
{
Thread.currentThread().interrupt();
}
}
@Override
public void loop(long systemNanoTime, long sourceNanoTime)
{
if (environment.hasThreadedAddOperation())
{
syncEnvsLock.lock();
try
{
environment.pollThreadedAddOperation(deadlockTicker);
}
finally
{
syncEnvsLock.unlock();
}
}
}
@Override
public void tick(long tick)
{
syncEnvsLock.lock();
boolean hasLock = true;
try
{
while (environment.getTick() >= DualRunnerEnvironment.this.env_tickCount)
{
// We are ticking ahead of the environment in the main thread.
// This is not okay because when the environment in the main thread
// resets to this one, it will be a tick ahead of what it was previously!
// The reverse situation is okay because we can catch up by calling tick()
// an extra time.
environment.pollThreadedAddOperation(deadlockTicker);
hasLock = false;
syncEnvsLock.unlock();
deadlockTicker.tickDeadlock();
try
{
Thread.sleep(1);
}
catch (InterruptedException ex)
{
Thread.currentThread().interrupt();
return;
}
syncEnvsLock.lock();
hasLock = true;
}
environment.tick(deadlockTicker);
//System.out.printf("%d: %4d => %4d (T)\n", Objects.hashCode(this.loop), (loop.currentNano()/1_000_000L), environment.getTick());
if (timewarpCountLastSeen != environment.getTimewarpCount())
{
timewarpCountLastSeen = environment.getTimewarpCount();
DualRunnerEnvironment.this.needsStateReset.addAndGet(1);
log.log(Level.WARNING, "Timewarp just occured: needsStateReset");
}
}
finally
{
if (hasLock)
{
syncEnvsLock.unlock();
}
}
}
}
}