/* * Aphelion * Copyright (c) 2013 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 (for example by appending a copyright notice). * * 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.physics.entities.*; import aphelion.shared.physics.operations.*; import aphelion.shared.event.TickEvent; import aphelion.shared.gameconfig.*; import aphelion.shared.net.protobuf.GameOperation; import aphelion.shared.physics.events.*; import aphelion.shared.physics.events.pub.EventPublic; import aphelion.shared.physics.operations.pub.OperationPublic; import aphelion.shared.physics.valueobjects.PhysicsMovement; import aphelion.shared.physics.valueobjects.PhysicsShipPosition; import aphelion.shared.physics.valueobjects.PhysicsWarp; import aphelion.shared.resource.ResourceDB; import aphelion.shared.swissarmyknife.LinkedListEntry; import aphelion.shared.swissarmyknife.LinkedListHead; import aphelion.shared.swissarmyknife.ThreadSafe; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Objects; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.atomic.AtomicLong; import java.util.logging.Level; import java.util.logging.Logger; import javax.annotation.Nullable; /** * Physics Engine based on TSS. * * All position values are in pixels * 1024. The Top Left corner of the screen is considered 0,0. All time values are in * ticks (10ms by default) Actor ids (pid) are unique, and should be for a while (if a player leaves, do not immediately * reuse his pid). * * All position values (pos, velocity) must be between -(2^30-1) and 2^30-1; * * Trailing State Synchronisation: * * + Operations arrive by network / or are generated at the client. * * + An operation is * placed on the pending list for each trailing state (PhysicsState). * * + Late operations: These are executed immediately. (operation.tick lte state.tick_now). * * + Early operations: These are placed on a todo list (operation.tick gt state.tick_now). * * + As an operation executes, it stores the changes it made to the game state. This is stored for each trailing state. * * + At some point (there are multiple viable strategies), each trailing state looks at the changes in game state that * an operation produced. These changes are compared with changed recorded at the directly previous state. * * + Very late operations: If an operation is very late (so late that it does not get added to at least 1 state todo * list, at least 2 on the server). It is ignored. This means lost weapons, etc. * * + Very early operations: If an operation is very early (based on a setting). It is ignored, this is to prevent * cheating. * * + Any operation that is older than the last trailing state is removed (HISTORY_DURATION) * * + Movement operations consist of the ActorWarp(position,velocity,rotation,time) and the * ActorMove(up,down,left,right,time) operations. ActorWarp may only be generated by the server. And is sent upon spawn, * special items and periodically to prevent synchronization mistakes. ActorMove is generated by the client and consists * of what keys the player pressed for that tick. ActorMove is sent to all clients. Multiple moves on the same tick are * ignored. * * * + Weapon fire consists of the WeaponFire(position,velocity,weaponid,time) operation. In states where the operation is * late, it is executed using the position and velocity values. in these states, there is no check for cheating. And the * "current" position of the projectile is made up to date using dead reckoning. In states where the operation is early, * position and velocity are ignored. The position and direction of weapon fire are determined by the movement commands * the player has sent. Things like weapon delay and energy are also checked to prevent cheating. * * Operations vs Events: * + An operation is reexecuted in a timewarp (added back to the todo list). An event is never reexecuted * unless the original cause occurs again. * + Both operations and events have a single instance for the entire environment. Operations are placed in a * todo or history list for each state. Events only in a history list. * + Both execute consistency checks. * + Operations always have an external cause (user hits a key, something recieved over the network). * Events always have an internal cause (previous operation triggered a weapon fire, the event is the * projectile hitting someone). * * @author Joris */ public class SimpleEnvironment implements TickEvent, PhysicsEnvironment { private static final Logger log = Logger.getLogger("aphelion.shared.physics"); public final EnvironmentConf econfig; long tick_now; long tickedAt_nano; private long lastTimewarp_nano = 0; private final PhysicsMap map; final State[] trailingStates; //Delay = index * TRAILING_STATE_DURATION final LinkedListHead<Event> eventHistoryList = new LinkedListHead<>(); // ordered by the order of appending final HashMap<EventKey, Event> eventHistory = new HashMap<>(); /** If set, addOperation will use this queue instead of handling the operation directly. */ private final ConcurrentLinkedQueue<Operation> threadedAddOperation; final AtomicLong polledAddOperationsCount = new AtomicLong(0); // used in test cases private long nextOpSeq = 1; private final AtomicLong nextOpSeqSafe = new AtomicLong(1); private long timewarps = 0; /** should only be set by test cases, TODO: get rid of me and use doReexecuteDirtyPositionPath instead */ @Deprecated public boolean testcaseImmediateMove; public SimpleEnvironment(boolean server, PhysicsMap map) { this(new EnvironmentConf(server), map, false); } public SimpleEnvironment(EnvironmentConf econfig, PhysicsMap map, boolean threadedAddOperation) { this.econfig = econfig; this.map = map; trailingStates = new State[econfig.TRAILING_STATES]; for (int s = 0; s < econfig.TRAILING_STATES; s++) { trailingStates[s] = new State( this, s, econfig.FIRST_STATE_DELAY + s * econfig.TRAILING_STATE_DELAY, s < econfig.TRAILING_STATES - 2, // do not allow hints in the last 2 trailing states s == econfig.TRAILING_STATES - 1 // force late config execution in the last trailing state ); trailingStates[s].tick_now = this.tick_now - trailingStates[s].delay; } if (threadedAddOperation) { this.threadedAddOperation = new ConcurrentLinkedQueue<>(); } else { this.threadedAddOperation = null; } } @Override public EnvironmentConf getConfig() { return econfig; } /** Use this method to find an event before you construct a new one to * make sure it has not been created already. * @param key * @return */ public @Nullable Event findEvent(EventKey key) { return this.eventHistory.get(key); } /** Register the occurrence of an event. * This should only be called by physics. * Call me anytime you execute an event. * @param event */ public void registerEvent(Event event) { assert event.env == this; if (event.inEnvList) { return; } assert !eventHistory.containsKey(event.key); event.inEnvList = true; eventHistoryList.append(event.link); eventHistory.put(event.key, event); } public void unregisterEvent(Event event) { assert event.env == this; event.link.remove(); eventHistory.remove(event.key); event.inEnvList = false; } public @Nullable Event findForeignEvent(Event event) { if (event.env == this) { return event; } else { return this.eventHistory.get(event.key); } } @Override public void skipForward(long tick) { if (this.tick_now != 0) { throw new IllegalStateException(); } this.tick_now = tick; } @Override public long getTick() { return tick_now; } @Override public long getTickedAt() { return tickedAt_nano; } public long getTick(int stateid) { if (stateid < 0 || stateid >= econfig.TRAILING_STATES) { throw new IllegalArgumentException("Invalid state"); } return trailingStates[stateid].tick_now; } public int getState(long tick) { int ticks_ago = (int) (this.tick_now - tick); int state_id = ticks_ago / econfig.TRAILING_STATE_DELAY; if (state_id < 0 || state_id >= econfig.TRAILING_STATES) { return -1; } return state_id; } @Override public void tick(long eventloop_tick) // interface { tick(); } public static int debug_current_state = -1; public boolean hasThreadedAddOperation() { return threadedAddOperation != null && !threadedAddOperation.isEmpty(); } public void pollThreadedAddOperation() { pollThreadedAddOperation(Deadlock.noopTicker); } public void pollThreadedAddOperation(Deadlock.DeadlockTicker deadlockTicker) { if (threadedAddOperation != null) { Operation op; while ((op = threadedAddOperation.poll()) != null) { assert op.env == this; polledAddOperationsCount.getAndAdd(1); for (int s = 0; s < econfig.TRAILING_STATES; s++) { trailingStates[s].addOperation(op); } deadlockTicker.tickDeadlock(); } } } @Override public void tick() { tick(Deadlock.noopTicker); } private boolean firstTick = true; public void tick(Deadlock.DeadlockTicker deadlockTicker) { if (firstTick) { firstTick = false; log.log(Level.INFO, "{0}: First tick for environment", econfig.logString); econfig.log(); } pollThreadedAddOperation(deadlockTicker); doReexecuteDirtyPositionPath(); ++tick_now; tickedAt_nano = System.nanoTime(); for (int s = 0; s < econfig.TRAILING_STATES; s++) { debug_current_state = s; long tick = this.tick_now - this.trailingStates[s].delay; this.trailingStates[s].tick(tick); } consistencyCheck(deadlockTicker); removeOldHistory(); } public void doReexecuteDirtyPositionPath() { for (int s = 0; s < econfig.TRAILING_STATES; s++) { debug_current_state = s; this.trailingStates[s].doReexecuteDirtyPositionPath(); } } public boolean consistencyCheck() { return consistencyCheck(Deadlock.noopTicker); } public boolean consistencyCheck(Deadlock.DeadlockTicker deadlockTicker) { for (int s = econfig.TRAILING_STATES - 1; s > 0; --s) { State older = this.trailingStates[s]; State newer = this.trailingStates[s - 1]; assert older.env == this; assert newer.env == this; if (!older.needTimewarpToThisState) { older.needTimewarpToThisState = !areStatesConsistent(older, newer); } if (older.needTimewarpToThisState) { if (this.lastTimewarp_nano == 0 || this.tickedAt_nano - this.lastTimewarp_nano >= econfig.TIMEWARP_EVERY_NANO) { // state is not consistent with the older state // fix all higher states timewarp(s); return false; } } } return true; } /** Reset all states to the given state and re-simulate. * @param stateid The state to reset lower (more recent) states to */ public void timewarp(int stateid) { timewarp(stateid, Deadlock.noopTicker); } /** Reset all states to the given state and re-simulate. * @param stateid The state to reset lower (more recent) states to * @param deadlockTicker */ public void timewarp(int stateid, Deadlock.DeadlockTicker deadlockTicker) { long start = System.nanoTime(); ++timewarps; for (int s = stateid; s > 0; --s) { State older = this.trailingStates[s]; State newer = this.trailingStates[s - 1]; older.needTimewarpToThisState = false; newer.timewarp(older, deadlockTicker); } long end = System.nanoTime(); log.log(Level.WARNING, "Time Warp {0}: to state {1} in {2}ms. Tick {3} to {4}.", new Object[] { econfig.logString, stateid, (end - start) / 1_000_000.0, this.trailingStates[0].tick_now, this.trailingStates[stateid].tick_now, }); // At the end of execution, so that if timewarps become long running, // they will not cause a timewarp right again. lastTimewarp_nano = System.nanoTime(); } /** The number of timewarps performed since the start of environment. * @return */ public long getTimewarpCount() { return timewarps; } private void removeOldHistory() // (or recycle) { LinkedListEntry<Operation> linkOp, linkOpNext; LinkedListEntry<Event> linkEv, linkEvNext; Operation op; State oldestState; // If an operation has been executed in the oldest state, // it can be removed. oldestState = this.trailingStates[econfig.TRAILING_STATES - 1]; linkOp = oldestState.history.first; while (linkOp != null) { linkOpNext = linkOp.next; op = linkOp.data; assert op.env == this; if (op.tick < this.tick_now - econfig.KEEP_OPERATIONS_FOR_TICKS) { // remove the operation everywhere for (int s = 0; s < econfig.TRAILING_STATES; ++s) { trailingStates[s].operations.remove(op.key); op.link[s].remove(); } } else { // do not remove operations which were just added to the history in the oldest state // (op.tick == oldestState.tick_now) // remove it next tick break; } linkOp = linkOpNext; } linkEv = this.eventHistoryList.first; while (linkEv != null) { linkEvNext = linkEv.next; Event event = linkEv.data; assert event.env == this; if (event.isOld(this.tick_now - econfig.KEEP_EVENTS_FOR_TICKS)) { event.remove(); } linkEv = linkEvNext; } } /** * Full consistency check for a state */ private boolean areStatesConsistent(State older, State newer) { assert newer.id == older.id - 1; // Go through the actors of the newer state // There is no need to verify if older state has actors we do not have; // The operation history check will take care of this. PhysicsShipPosition newerPosition = new PhysicsShipPosition(); PhysicsShipPosition olderPosition = new PhysicsShipPosition(); for (int i = 0; i < newer.actorsList.size(); ++i) { Actor actorNewer = newer.actorsList.get(i); if (actorNewer.createdAt_tick <= older.tick_now) { // this works because histories overlap actorNewer.getHistoricPosition(newerPosition, older.tick_now, false); actorNewer.getHistoricPosition(olderPosition, older.tick_now, true); // note if this method is not called every tick, all the tick since // the last call of areStatesConsistent() will have to be checked, // instead of only the current if (!olderPosition.equals(newerPosition)) { log.log(Level.WARNING, "{0}: Inconsistency in position/velocity/rotation, actor {1}", new Object[]{ econfig.logString, actorNewer.pid }); return false; } if (actorNewer.getHistoricEnergy(older.tick_now, false) != actorNewer.getHistoricEnergy(older.tick_now, true)) { log.log(Level.WARNING, "{0}: Inconsistency in energy, actor {1}", new Object[]{ econfig.logString, actorNewer.pid }); return false; } } } // Go through the history of the newer state for (LinkedListEntry<Operation> linkOp = newer.history.first; linkOp != null; linkOp = linkOp.next) { assert linkOp.head == newer.history; if (!linkOp.data.isConsistent(older, newer)) { log.log(Level.WARNING, "{0}: Inconsistency in operation {1}", new Object[]{ econfig.logString, linkOp.data.getClass().getName() }); return false; } } // go through the events of the newer state for (LinkedListEntry<Event> linkEv = eventHistoryList.first; linkEv != null; linkEv = linkEv.next) { if (!linkEv.data.isConsistent(older, newer)) { log.log(Level.WARNING, "{0}: Inconsistency in event {1}", new Object[]{ econfig.logString, linkEv.data.getClass().getName() }); return false; } } return true; } /** . * * @return false if the operation was too old and has been ignored */ @ThreadSafe boolean addOperation(Operation operation) { final long now = this.tick_now; assert operation.env == this; if (operation.ignorable && operation.tick <= now - econfig.HIGHEST_DELAY) { log.log(Level.WARNING, "Operation about actor {1} at {2} dropped, too old ({0}). now = {3}", new Object[] { econfig.logString, operation.getPid(), operation.getTick(), now }); return false; } if (threadedAddOperation == null) { for (int s = 0; s < econfig.TRAILING_STATES; s++) { trailingStates[s].addOperation(operation); } } else { threadedAddOperation.add(operation); } return true; } @Override public ActorPublic getActor(int pid) { return getActor(pid, false); } @Override public ActorPublic getActor(int pid, boolean nofail) { return getActor(pid, 0, nofail); } /** * Look up an actor by pid. The returned value is a wrapper (PhysicsActor) around the real actor class * (PhysicsActorPrivate). This wrapper is unaffected by things such as temporary removal of the actor. * * @param pid * @param nofail if set, an actor wrapper is always returned, even if the actor does not exist at the moment. * @param stateid A state id, 0 for the most current state. TRAILING_STATES-1 for the oldest. * This lets you get a wrapper before the actual actor creation operation has been executed (which may take a * while, depending on the timestamp). * @return */ public ActorPublic getActor(int pid, int stateid, boolean nofail) { Actor actor; State state; if (stateid < 0 || stateid >= econfig.TRAILING_STATES) { throw new IllegalArgumentException("Invalid state"); } if (pid == 0) { // pid 0 is a special value that is never assigned // always return null even if nofail is set return null; } state = trailingStates[stateid]; actor = state.actors.get(new ActorKey(pid)); if (actor == null) { if (nofail) { return new ActorPublicImpl(pid, state); } else { return null; } } return actor.publicWrapper; } @Override public Iterator<ActorPublic> actorIterator() { return actorIterator(0); } /** Loop over all the known actors in a given state using an iterator. * It is OK to reference PhysicsActorPublic for long periods of time. * @param stateid A state id, 0 for the most current state. TRAILING_STATES-1 for the oldest. * @return A read only iterator. Only use this iterator on the main thread, do not store the iterator. */ public Iterator<ActorPublic> actorIterator(int stateid) { State state; if (stateid < 0 || stateid >= econfig.TRAILING_STATES) { throw new IllegalArgumentException("Invalid state"); } state = trailingStates[stateid]; return new ActorIterator(state.actorsList.iterator(), state); } /** Loop over all the known actors in a given state using an iterator. * It is OK to reference PhysicsActorPublic for long periods of time. * @return A read only iterator. Only use this iterator on the main thread, do not store the iterator. */ @Override public Iterable<ActorPublic> actorIterable() { return actorIterable(0); } /** Loop over all the known actors in a given state using an iterator. * It is OK to reference PhysicsActorPublic for long periods of time. * @param stateid A state id, 0 for the most current state. TRAILING_STATES-1 for the oldest. * @return A read only iterator. Only use this iterator on the main thread, do not store the iterator. */ public Iterable<ActorPublic> actorIterable(int stateid) { if (stateid < 0 || stateid >= econfig.TRAILING_STATES) { throw new IllegalArgumentException("Invalid state"); } final State state = trailingStates[stateid]; return new Iterable<ActorPublic>() { @Override public Iterator<ActorPublic> iterator() { return new ActorIterator(state.actorsList.iterator(), state); } }; } @Override public int getActorCount() { return getActorCount(0); } /** Return the actor count, including those who have been soft deleted. * This is equal to the number of actors iterated by actorIterator() * @param stateid * @return */ public int getActorCount(int stateid) { State state; if (stateid < 0 || stateid >= econfig.TRAILING_STATES) { throw new IllegalArgumentException("Invalid state"); } state = trailingStates[stateid]; assert state.actors.size() == state.actorsList.size(); return state.actors.size(); } @Override public Iterator<ProjectilePublic> projectileIterator() { return projectileIterator(0); } @SuppressWarnings("unchecked") public Iterator<ProjectilePublic> projectileIterator(int stateid) { State state; if (stateid < 0 || stateid >= econfig.TRAILING_STATES) { throw new IllegalArgumentException("Invalid state"); } state = trailingStates[stateid]; return (Iterator<ProjectilePublic>) (Object) state.projectilesList.iteratorReadOnly(); } @Override public Iterable<ProjectilePublic> projectileIterable() { return projectileIterable(0); } @SuppressWarnings("unchecked") public Iterable<ProjectilePublic> projectileIterable(int stateid) { if (stateid < 0 || stateid >= econfig.TRAILING_STATES) { throw new IllegalArgumentException("Invalid state"); } final State state = trailingStates[stateid]; return new Iterable<ProjectilePublic>() { @Override public Iterator<ProjectilePublic> iterator() { return (Iterator<ProjectilePublic>) (Object) state.projectilesList.iteratorReadOnly(); } }; } @Override public int calculateProjectileCount() { return calculateProjectileCount(0); } /** Return the projectile count, including those who have been soft deleted. * This is equal to the number of projectiles iterated by projectileIterator() * This method is O(n) time and primarily intended for test cases. * @param stateid * @return */ public int calculateProjectileCount(int stateid) { if (stateid < 0 || stateid >= econfig.TRAILING_STATES) { throw new IllegalArgumentException("Invalid state"); } final State state = trailingStates[stateid]; return state.projectilesList.calculateSize(); } /** Loop over all the operations in the todo list of a state. * @return A read only iterator. Only use this iterator on the main thread, do not * store the iterator or any of its values after iterating. */ public Iterator<OperationPublic> todoListIterator() { return todoListIterator(0); } /** Loop over all the operations in the todo list of a state. * @param stateid A state id, 0 for the most current state. TRAILING_STATES-1 for the oldest. * @return A read only iterator. Only use this iterator on the main thread, do not * store the iterator or any of its values after iterating. */ @SuppressWarnings("unchecked") public Iterator<OperationPublic> todoListIterator(int stateid) { State state; if (stateid < 0 || stateid >= econfig.TRAILING_STATES) { throw new IllegalArgumentException("Invalid state"); } state = trailingStates[stateid]; return (Iterator<OperationPublic>) (Object) state.todo.iteratorReadOnly(); } @Override public ConfigSelection newConfigSelection() { return trailingStates[0].config.newSelection(); } private OperationKey getNextOperationKey() { if (threadedAddOperation == null) { return new OperationKey(nextOpSeq++); } else { return new OperationKey(nextOpSeqSafe.getAndIncrement()); } } @Override @ThreadSafe public void loadConfig(long tick, String fileIdentifier, List yamlDocuments) { LoadConfig op = new LoadConfig(this, getNextOperationKey()); op.tick = tick; op.fileIdentifier = fileIdentifier; op.yamlDocuments = yamlDocuments; boolean ret = addOperation(op); // no fail assert ret; } @Override @ThreadSafe public void unloadConfig(long tick, String fileIdentifier) { UnloadConfig op = new UnloadConfig(this, getNextOperationKey()); op.tick = tick; op.fileIdentifier = fileIdentifier; boolean ret = addOperation(op); // no fail assert ret; } @Override @ThreadSafe public void actorNew(long tick, int pid, long seed, String ship) { ActorNew op = new ActorNew(this, getNextOperationKey()); op.tick = tick; op.pid = pid; op.seed = seed; op.ship = ship; boolean ret = addOperation(op); assert ret; } @Override @ThreadSafe public void actorSync(GameOperation.ActorSync sync) { assert !this.econfig.server; ActorSync op = new ActorSync(this, getNextOperationKey()); op.tick = sync.getTick(); op.pid = sync.getPid(); op.sync = sync; boolean ret = addOperation(op); assert ret; // atleast the oldest state must accept the sync } @Override @ThreadSafe public void actorModification(long tick, int pid, String ship) { ActorModification op = new ActorModification(this, getNextOperationKey()); op.tick = tick; op.pid = pid; op.ship = ship; boolean ret = addOperation(op); assert ret; } @Override @ThreadSafe public void actorRemove(long tick, int pid) { ActorRemove op = new ActorRemove(this, getNextOperationKey()); op.tick = tick; op.pid = pid; boolean ret = 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) { ActorWarp op = new ActorWarp(this, getNextOperationKey()); op.tick = tick; op.pid = pid; op.hint = hint; op.warp = new PhysicsWarp(x, y, x_vel, y_vel, rotation); return addOperation(op); } @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) { ActorWarp op = new ActorWarp(this, getNextOperationKey()); 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); return addOperation(op); } @Override @ThreadSafe public boolean actorMove(long tick, int pid, PhysicsMovement move) { ActorMove op = new ActorMove(this, getNextOperationKey()); op.tick = tick; op.pid = pid; op.move = move; if (move == null) { throw new IllegalArgumentException(); } try { return addOperation(op); } finally { if (this.testcaseImmediateMove) { doReexecuteDirtyPositionPath(); } } } @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) { ActorWeaponFire op = new ActorWeaponFire(this, getNextOperationKey()); 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; } return addOperation(op); } @Override @ThreadSafe public boolean actorWeapon(long tick, int pid, WEAPON_SLOT weapon_slot) { ActorWeaponFire op = new ActorWeaponFire(this, getNextOperationKey()); op.tick = tick; op.pid = pid; op.weapon_slot = weapon_slot; if (weapon_slot == null) { throw new IllegalArgumentException(); } return addOperation(op); } @Override @ThreadSafe public boolean weaponSync(long tick, int owner_pid, String weaponKey, GameOperation.WeaponSync.Projectile[] projectiles, long syncKey) { assert !econfig.server; // do not modify "projectiles" after calling this method WeaponSync op = new WeaponSync(this, getNextOperationKey()); op.tick = tick; op.pid = owner_pid; op.weaponKey = weaponKey; op.syncProjectiles = projectiles; op.syncKey = syncKey; return addOperation(op); } @Override public PhysicsMap getMap() { return map; } @Override public Iterator<EventPublic> eventIterator() { return (Iterator<EventPublic>) (Object) this.eventHistoryList.iteratorReadOnly(); } @Override public Iterable<EventPublic> eventIterable() { return new Iterable<EventPublic>() { @Override public Iterator<EventPublic> iterator() { return (Iterator<EventPublic>) (Object) eventHistoryList.iteratorReadOnly(); } }; } @Override public GCInteger getGlobalConfigInteger(String name) { return trailingStates[0].globalConfig.getInteger(name); } @Override public GCString getGlobalConfigString(String name) { return trailingStates[0].globalConfig.getString(name); } @Override public GCBoolean getGlobalConfigBoolean(String name) { return trailingStates[0].globalConfig.getBoolean(name); } @Override public GCIntegerList getGlobalConfigIntegerList(String name) { return trailingStates[0].globalConfig.getIntegerList(name); } @Override public GCStringList getGlobalConfigStringList(String name) { return trailingStates[0].globalConfig.getStringList(name); } @Override public GCBooleanList getGlobalConfigBooleanList(String name) { return trailingStates[0].globalConfig.getBooleanList(name); } @Override public GCImage getGlobalConfigImage(String name, ResourceDB db) { return trailingStates[0].globalConfig.getImage(name, db); } public GCInteger getGlobalConfigInteger(int state, String name) { return trailingStates[state].globalConfig.getInteger(name); } public GCString getGlobalConfigString(int state, String name) { return trailingStates[state].globalConfig.getString(name); } public GCBoolean getGlobalConfigBoolean(int state, String name) { return trailingStates[state].globalConfig.getBoolean(name); } public GCIntegerList getGlobalConfigIntegerList(int state, String name) { return trailingStates[state].globalConfig.getIntegerList(name); } public GCStringList getGlobalConfigStringList(int state, String name) { return trailingStates[state].globalConfig.getStringList(name); } public GCBooleanList getGlobalConfigBooleanList(int state, String name) { return trailingStates[state].globalConfig.getBooleanList(name); } public GCImage getGlobalConfigImage(int state, String name, ResourceDB db) { return trailingStates[state].globalConfig.getImage(name, db); } }