/* * 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.gameconfig.ConfigSelection; import aphelion.shared.gameconfig.GameConfig; import aphelion.shared.map.MapClassic; import aphelion.shared.physics.entities.*; import aphelion.shared.physics.events.Event; import aphelion.shared.physics.events.pub.ProjectileExplosionPublic.EXPLODE_REASON; import aphelion.shared.physics.operations.Operation; import aphelion.shared.physics.operations.OperationKey; import aphelion.shared.swissarmyknife.EntityGrid; import aphelion.shared.physics.valueobjects.PhysicsPoint; import aphelion.shared.swissarmyknife.LinkedListEntry; import aphelion.shared.swissarmyknife.LinkedListHead; import aphelion.shared.swissarmyknife.SwissArmyKnife; import java.util.*; import java.util.logging.Level; import java.util.logging.Logger; /** * * @author Joris */ public class State { private static final Logger log = Logger.getLogger("aphelion.shared.physics"); private static final int ACTOR_INITIALCAPACITY = 64; private static final int PROJECTILE_INITIALCAPACITY = ACTOR_INITIALCAPACITY * 1024; public final EnvironmentConf econfig; public final int id; public final SimpleEnvironment env; public final Collision collision = new Collision(); /** how far (ticks) in the past is this state? */ public final long delay; /** Use operation hints in this state? (such as the x,y,x_vel,y_vel hint in the weapon fire packet ) */ public final boolean allowHints; public final boolean isLast; public long tick_now; public boolean needTimewarpToThisState; public final HashMap<ActorKey, Actor> actors = new HashMap<>(ACTOR_INITIALCAPACITY); // actor id -> entity public final ArrayList<Actor> actorsList = new ArrayList<>(ACTOR_INITIALCAPACITY); public final LinkedList<Actor> actorsRemovedDuringReset = new LinkedList<>(); public final HashMap<ProjectileKey, Projectile> projectiles = new HashMap<>(PROJECTILE_INITIALCAPACITY); public LinkedListHead<Projectile> projectilesList = new LinkedListHead<>(); public final LinkedListHead<Projectile> forceEmitterList = new LinkedListHead<>(); // todo ensure that projectiles are not removed in this list too soon public final LinkedListHead<MapEntity> dirtyPositionPathList = new LinkedListHead<>(); public final LinkedListHead<Operation> history = new LinkedListHead<>(); // ordered by tick ascending public final LinkedListHead<Operation> todo = new LinkedListHead<>(); // ordered by tick ascending public final HashMap<OperationKey, Operation> operations = new HashMap<>(); /** This grid is a quick way to find out what entities are at what position. * It is only valid for "tick_now" */ public final EntityGrid<MapEntity> entityGrid = new EntityGrid<>(32768, 1024 * MapClassic.TILE_PIXELS / 32768); public final GameConfig config = new GameConfig(); public final ConfigSelection globalConfig = config.newSelection(); public long config_lastModification = 0; // edge case: 0 is used as a "not set" value here. however 0 is also a valid tick public HashSet<Integer> unknownActorRemove = new HashSet<>(); // Assumes PIDs are unique. ActorNew is never called twice with the same PID. State(SimpleEnvironment env, int state_id, long delay, boolean allowHints, boolean isLast) { this.env = env; this.econfig = env.econfig; this.id = state_id; this.delay = delay; this.tick_now = -delay; this.allowHints = allowHints; this.isLast = isLast; } public boolean isForeign(State other) { return this.env != other.env; } public boolean isForeign(MapEntity en) { return this.isForeign(en.state); } private void setActorSmoothPositionBaseline() { for (Actor actor : actorsList) { actor.smoothHistory.updateBaseLine(); } } /** if a late weapon has emitted forces, or if a late move has been applied, reexecute dead reckon here. */ public void doReexecuteDirtyPositionPath() { Iterator<MapEntity> it = dirtyPositionPathList.iterator(); while (it.hasNext()) { MapEntity en = it.next(); it.remove(); // the first tick that is not dirty long tick = en.dirtyPositionPathTracker.getFirstDirtyTick() - 1; if (tick < en.posHistory.getLowestTick()) { tick = en.posHistory.getLowestTick(); en.dirtyPositionPathTracker.setFirstDirtyTick(tick + 1); } en.posHistory.get(en.pos.pos, tick); en.velHistory.get(en.pos.vel, tick); assert en.pos.pos.set; assert en.pos.vel.set; if (en instanceof Projectile) { } else if (en instanceof Actor) { Actor actor = (Actor) en; actor.rot.points = actor.rotHistory.getX(tick); actor.rot.snapped = actor.rotHistory.getY(tick); } else { assert false; } // this method is called in between tick()s // so we also need to resimulate the current tick of the state en.performDeadReckoning(env.getMap(), tick + 1, tick_now - tick, true); assert en.dirtyPositionPathTracker.getFirstDirtyTick() > this.tick_now; } dirtyPositionPathList.clear(); } private void tickActors() { Iterator<Actor> itActor = this.actorsList.iterator(); while (itActor.hasNext()) { Actor actor = itActor.next(); assert actor.state == this; actor.updatedPosition(tick_now); if (actor.moveHistory.getMostRecentTick() < this.tick_now) { // make sure old moves can be cleaned up (and that getRelative always matches) actor.moveHistory.setHistory(tick_now, null); } if (actor.isRemovedSet()) { // Can we really remove the actor now? if (env.tick_now - actor.removedAt_tick > econfig.HIGHEST_DELAY) { boolean activeSomewhere = false; for (int s = 0; s < actor.crossStateList.length; ++s) { if (actor.crossStateList[s] != null && !actor.crossStateList[s].isRemoved(this.tick_now)) { activeSomewhere = true; break; } } if (!activeSomewhere) { // really remove it now for (int s = 0; s < econfig.TRAILING_STATES; ++s) { State state = env.trailingStates[s]; if (state == this) { Actor stateActor = state.actors.remove(actor.key); assert stateActor == actor; itActor.remove(); // actorsList // this method also tries to remove it from actorsList // however it is already gone stateActor.hardRemove(tick_now); } else { Actor stateActor = state.actors.remove(actor.key); if (stateActor != null) { stateActor.hardRemove(tick_now); } } } } } continue; // skip dead reckoning } if (actor.isDead(tick_now)) { if (this.tick_now >= actor.spawnAt_tick) { PhysicsPoint spawn = new PhysicsPoint(); actor.dead.setAbsoluteValue(0, actor.spawnAt_tick, 0); if (actor.empUntil_tick != null && actor.empUntil_tick >= actor.spawnAt_tick) { actor.empUntil_tick = actor.spawnAt_tick - 1; } actor.findSpawnPoint(spawn, actor.spawnAt_tick); spawn.multiply(PhysicsMap.TILE_PIXELS); spawn.add(PhysicsMap.TILE_PIXELS / 2); actor.pos.pos.set(spawn); actor.pos.vel.set(0, 0); actor.rot.points = actor.randomRotation(actor.spawnAt_tick); actor.rot.snapped = PhysicsMath.snapRotation(actor.rot.points, actor.config.rotationPoints.get()); actor.energy.addAbsoluteValue(Actor.ENERGY_SETTER.OTHER.id, this.tick_now, actor.getMaxEnergy()); actor.updatedPosition(tick_now); } // Keep updating the position even if the actor is dead // This makes it possible to show an explosion animation that follow path of the actor } actor.performDeadReckoning(env.getMap(), tick_now, 1, false); } } private void tickProjectiles() { LinkedListEntry<Projectile> linkProjectile_next; for (LinkedListEntry<Projectile> linkProjectile = this.projectilesList.first; linkProjectile != null; linkProjectile = linkProjectile_next) { Projectile projectile = linkProjectile.data; linkProjectile_next = linkProjectile.next; assert projectile.state == this; projectile.updatedPosition(tick_now); // do not bother if the projectile index is not 0 // in this case, it has already been checked (thanks to combined) if (projectile.isRemovedSet()) { // NOTE: this removal checking has been optimized to turn // "n * (n-1)" checks into "n" checks (in the case of coupled // projectiles). The downside is that it is a bit harder to // understand. // Can we really remove the projectile now? // do not bother if the projectile index is not 0, // it has already been checked if (projectile.configIndex == 0 && env.tick_now - projectile.removedAt_tick > econfig.HIGHEST_DELAY) { boolean activeSomewhere = false; ACTIVE_CHECK: for (Projectile coupledProjectile : projectile.coupled) { for (int s = 0; s < coupledProjectile.crossStateList.length; ++s) { if (coupledProjectile.crossStateList[s] != null && !coupledProjectile.crossStateList[s].isRemoved(this.tick_now)) { activeSomewhere = true; break ACTIVE_CHECK; } } } if (!activeSomewhere) { // really remove it now for (Projectile coupledProjectile : projectile.coupled) { if (linkProjectile_next == coupledProjectile.projectileListLink_state) { // make sure we do not break the loop linkProjectile_next = linkProjectile_next.next; } for (int s = 0; s < coupledProjectile.crossStateList.length; ++s) { if (coupledProjectile.crossStateList[s] != null) { Projectile stateProj = ((Projectile) coupledProjectile.crossStateList[s]); stateProj.hardRemove(tick_now); // NOTE: hardRemove modifies coupled, the list we are iterating over. // However the iterator of LinkedListEntry supports removing // the current object we are iterating over (but only the current). } } } } } continue; // do not perform dead reckoning etc for removed projectiles } // at this point the projectile is _not_ removed // keep the projectile stored everywhere until it has been removed in every state // this way you can figure out when it was removed etc if (tick_now >= projectile.expiresAt_tick) { projectile.explodeWithoutHit(tick_now, EXPLODE_REASON.EXPIRATION); projectile.softRemove(tick_now); continue; } projectile.performDeadReckoning(env.getMap(), tick_now, 1, false); } } private void tickProjectilesAfterActor() { for (Projectile projectile : this.projectilesList) { assert projectile.state == this; projectile.tickProjectileAfterActor(tick_now); } } private void tickOperations(boolean lateSync) { LinkedListEntry<Operation> linkOp = this.todo.first; while (linkOp != null) // go over all todo's with op.tick <= tick_now { assert linkOp.head == this.todo; Operation op = linkOp.data; assert op.env == this.env; if (op.tick > tick_now) { break; // the todo (and history) lists are ordered. So there is nothing to-do anymore } if (op.isLateSyncOperation() != lateSync) { linkOp = linkOp.next; continue; } LinkedListEntry<Operation> nextOp = linkOp.next; long late = tick_now - op.tick; if (!op.execute(this, late > 0, late)) { needTimewarpToThisState = true; log.log(Level.WARNING, "{0}: Inconsistency in queued operation {1}. (lateSync {2})", new Object[]{ econfig.logString, op.getClass().getName(), lateSync }); } operationToHistory(op); // done linkOp = nextOp; } } private void tickActorsLate() { for (Actor actor : actorsList) { assert actor.state == this; actor.tickEnergy(); } } private void tickForceEmitters() { for (Projectile proj : forceEmitterList) { proj.emitForce(tick_now); } } void tick(long tick_now) { this.tick_now = tick_now; setActorSmoothPositionBaseline(); // Update the position of projectiles first, // this is needed because the collision between actor and tiles is done // while updating the actor position, not while update the projectile position tickProjectiles(); // 1. Dead reckoning // 2. Actor cleanup // 3. Respawning (executed before any operations so // that the server may override the spawn location by sending an // ActorWarp at the same tick as the respawn) tickActors(); tickProjectilesAfterActor(); tickOperations(false); tickActorsLate(); // Now that all positions are up to date, emit forces // (which are applied next tick) tickForceEmitters(); doReexecuteDirtyPositionPath(); tickOperations(true); // sync operations config.tick(tick_now); // used for cleanup } /** Lookup an actor that was removed as part of the current timewarp. * This list is cleared after the timewarp has been completed. * If an actor is recreated as part of a timewarp, a new actor object * should not be created. * Otherwise references (external to physics) might break. * The method used here works properly because PIDs may not be reused. * @param pid * @param tick The tick that should represent the new creation tick for this actor * @return An actor reset to default values, as if the constructor had just been called. * Or null in which case you need to create a new Actor. */ public Actor getActorRemovedDuringReset(int pid, long tick) { Actor actor = null; for (Actor removedActor : this.actorsRemovedDuringReset) { if (removedActor.pid == pid) { actor = removedActor; break; } } if (actor != null) { assert actor.removedDuringReset; actor.resetToEmpty(tick); actor.removedDuringReset = false; return actor; } return null; } private MapEntity[] findCrossStateListForActor(Actor foreignActor) { if (!this.isForeign(foreignActor)) { return foreignActor.crossStateList; } for (int s = env.trailingStates.length-1; s >= 0; --s) { State state = env.trailingStates[s]; if (state == this) { continue; } Actor localActor = state.actors.get(foreignActor.key); if (localActor != null) { return localActor.crossStateList; } } return new MapEntity[env.econfig.TRAILING_STATES]; } private MapEntity[] findCrossStateListForProjectile(Projectile foreignProjectile) { if (!this.isForeign(foreignProjectile)) { return foreignProjectile.crossStateList; } for (int s = env.trailingStates.length-1; s >= 0; --s) { State state = env.trailingStates[s]; if (state == this) { continue; } Projectile localProjectile = state.projectiles.get(foreignProjectile.key); if (localProjectile != null) { return localProjectile.crossStateList; } } return new MapEntity[env.econfig.TRAILING_STATES]; } /** Reset to the given state and simulate enough times so that we are current again. * @param older The state to reset to, foreign states are allowed. */ public void timewarp(State older) { timewarp(older, Deadlock.noopTicker); } /** Reset to the given state and simulate enough times so that we are current again. * @param older The state to reset to, foreign states are allowed. * @param deadlockTicker */ public void timewarp(State older, Deadlock.DeadlockTicker deadlockTicker) { long wasTick = this.tick_now; this.resetTo(older); long tick = this.tick_now + 1; while (tick <= wasTick) { deadlockTicker.tickDeadlock(); this.tick(tick); tick++; } this.actorsRemovedDuringReset.clear(); assert this.tick_now == wasTick; } /** * Copy everything from the other state (Actors, Projectiles, config, etc) * (but not operations) to this one. * @param older The state to reset to, foreign states are allowed. */ @SuppressWarnings("unchecked") private void resetTo(State older) { if (older.tick_now > this.tick_now) { throw new IllegalArgumentException(); } long old_tick_now = this.tick_now; this.tick_now = older.tick_now; if (config_lastModification != older.config_lastModification) { // A config change is very rare, so do not waste time if it has not been modified. config.resetTo(older.config); config_lastModification = older.config_lastModification; config.applyChanges(); } this.dirtyPositionPathList.clear(); // Reset actors { Iterator<Actor> actorIt = this.actorsList.iterator(); while (actorIt.hasNext()) { Actor actorMine = actorIt.next(); Actor actorOther = older.actors.get(actorMine.key); // actor in my state does not exist in the other state, so remove it if (actorOther == null) { actorMine.softRemove(old_tick_now); actorIt.remove(); // actorsList actorMine.hardRemove(old_tick_now); actorMine.removedDuringReset = true; this.actorsRemovedDuringReset.add(actorMine); continue; } actorMine.resetTo(actorOther); assert actorMine.state == this; } } // reset actors that are in the other state, but not in mine if (this.actors.size() != older.actors.size()) { // NOTE: At the moment this branch never executes. // The only way for this condition to occur is when // ActorNew is able to execute in state 1, but not in state 0. // Unlike ActorWeapon & projectiles there is no way for this // to occur. // Please make a test case for this branch if this condition // ever becomes possible (for example when a weapon launches // a fake player, and the firing of that weapon depends on energy // or a fire delay). for (Actor actorOther : older.actorsList) { Actor actorMine = this.actors.get(actorOther.key); // actor that is in the other state, does not exist in mine if (actorMine == null) { actorMine = this.getActorRemovedDuringReset(actorOther.pid, tick_now); if (actorMine == null) { actorMine = new Actor(this, findCrossStateListForActor(actorOther), actorOther.pid, actorOther.createdAt_tick); } actorMine.resetTo(actorOther); assert actorMine.state == this; } } } // Reset projectiles { LinkedListHead<Projectile> oldProjectiles = this.projectilesList; this.forceEmitterList.clear(); this.projectilesList = new LinkedListHead<>(); // loop over all the projectiles in the state we are resetting to LinkedListEntry<Projectile> entry = older.projectilesList.first; while (entry != null) { Projectile projectileOther = entry.data; Projectile projectileMine = projectileOther.findInOtherState(this); if (projectileMine == null) { // pretend that the owner is null for now, resetTo() will resolve this projectileMine = new Projectile( projectileOther.key, // key is immutable this, findCrossStateListForProjectile(projectileOther), null, projectileOther.createdAt_tick, null, projectileOther.configIndex); } else { // remove from the old list (oldProjectiles) so that // the old list contains all projectiles that are not in "other" when we are done looping projectileMine.projectileListLink_state.remove(); this.projectiles.remove(projectileMine.key); } this.projectilesList.append(projectileMine.projectileListLink_state); this.projectiles.put(projectileMine.key, projectileMine); projectileMine.resetTo(projectileOther); assert projectileMine.state == this; if (projectileMine.isForceEmitter()) { projectileMine.forceEmitterListLink_state.remove(); this.forceEmitterList.append(projectileMine.forceEmitterListLink_state); } entry = entry.next; } // these are all the projectiles that are not in the other state entry = oldProjectiles.first; while (entry != null) { LinkedListEntry<Projectile> next = entry.next; entry.data.hardRemove(tick_now); entry.data.removedDuringReset = true; entry = next; } if (SwissArmyKnife.assertEnabled) { for (Projectile p : this.projectilesList) { p.coupled.assertCircularConsistency(); } } } { LinkedListEntry<Operation> linkStart = null; LinkedListEntry<Operation> linkEnd = this.history.last; // find the first operation that should now be in the todo for (LinkedListEntry<Operation> linkOp = this.history.first; linkOp != null; linkOp = linkOp.next ) { Operation op = linkOp.data; assert op.env == this.env; if (op.tick > older.tick_now) { linkStart = linkOp; break; } else { Operation resetToOperation = op; if (this.isForeign(older)) { resetToOperation = older.operations.get(op.key); } if (resetToOperation != null) { // reset the execution history for operations that are // in the history for both states op.resetExecutionHistory(this, older, resetToOperation); } } } if (linkStart != null) { this.todo.prependForeignRange(linkStart, linkEnd); // reset the execution history for operations that are now // in the todo list for the newer state for (LinkedListEntry<Operation> linkOp = linkStart; true; linkOp = linkOp.next ) { Operation op = linkOp.data; op.placedBackOnTodo(this); assert op.env == this.env; if (linkOp == linkEnd) { break; } } } if (EnvironmentConf.testCaseAssertions) { todo.assertConsistency(); history.assertConsistency(); } } // reset the event history for (LinkedListEntry<Event> linkEv = env.eventHistoryList.first, linkNext; linkEv != null; linkEv = linkNext) { linkNext = linkEv.next; Event event = linkEv.data; assert event.link == linkEv; assert event.env == this.env; Event resetToEvent = event; if (this.isForeign(older)) { resetToEvent = older.env.eventHistory.get(event.key); } if (resetToEvent == null) { event.resetToEmpty(this); } else { event.resetExecutionHistory(this, older, resetToEvent); } boolean occurredSomewhere = false; for (int s = 0; s < econfig.TRAILING_STATES; ++s) { if (event.hasOccurred(s)) { occurredSomewhere = true; break; } } if (!occurredSomewhere) { event.remove(); } } if (this.isForeign(older)) { for (Event otherEvent : older.env.eventHistoryList) { Event myEvent = env.eventHistory.get(otherEvent.key); if (myEvent == null) { // foreign environment has an event we do not know aboot. myEvent = otherEvent.cloneWithoutHistory(this.env); myEvent.resetExecutionHistory(this, older, otherEvent); env.registerEvent(myEvent); } } } unknownActorRemove = (HashSet<Integer>) older.unknownActorRemove.clone(); } void addOperation(Operation op) { LinkedListEntry<Operation> opLink, link; assert op.env == this.env; opLink = op.link[this.id]; assert opLink.head == null; assert opLink.previous == null; assert opLink.next == null; operations.put(op.key, op); if (op.tick <= this.tick_now) // late operation { // The operation is late here even if tick_now == op.tick if (!op.execute(this, true, this.tick_now - op.tick)) { log.log(Level.WARNING, "{0}: Inconsistency in late operation {1}", new Object[]{ econfig.logString, op.getClass().getName() }); needTimewarpToThisState = true; } operationToHistory(op); return; } link = this.todo.last; while (link != null) { // If 2 operations occur on the same time, // use the priority to determine which takes precedence // (lower priority is executed first) // if the priority is the same, the first to be // added takes precedence if (link.data.tick < op.tick || (link.data.tick == op.tick && link.data.comparePriority(op) <= 0) ) { link.append(opLink); if (EnvironmentConf.testCaseAssertions) { todo.assertConsistency(); history.assertConsistency(); } return; } link = link.previous; } // went through the entire todo list, todo list is either empty or this operation is the oldest this.todo.prepend(opLink); if (EnvironmentConf.testCaseAssertions) { todo.assertConsistency(); history.assertConsistency(); } } private void operationToHistory(Operation op) { LinkedListEntry<Operation> opLink, historyLink; assert op.env == this.env; opLink = op.link[this.id]; historyLink = this.history.last; while (historyLink != null) { if (historyLink.data.tick < op.tick || (historyLink.data.tick == op.tick && historyLink.data.comparePriority(op) <= 0) ) { historyLink.appendForeignRange(opLink, opLink); if (EnvironmentConf.testCaseAssertions) { todo.assertConsistency(); history.assertConsistency(); } return; } historyLink = historyLink.previous; } history.prependForeignRange(opLink, opLink); if (EnvironmentConf.testCaseAssertions) { todo.assertConsistency(); history.assertConsistency(); } } @Override public String toString() { return this.getClass().getSimpleName() + ": " + this.id; } private boolean isPointFree(int tileX, int tileY, int freeRadius) { for (int x = tileX - freeRadius; x <= tileX + freeRadius; ++x) { for (int y = tileY - freeRadius; y <= tileY + freeRadius; ++y) { if (env.getMap().physicsIsSolid(x, y)) { return false; } } } return true; } public void findRandomPointOnMap( PhysicsPoint result, long tick, long seed, int tileX, int tileY, int radius, int freeRadius) { int minX = SwissArmyKnife.clip(tileX - radius, env.getMap().physicsGetMapLimitMinimum(), env.getMap().physicsGetMapLimitMaximum()); int maxX = SwissArmyKnife.clip(tileX + radius, env.getMap().physicsGetMapLimitMinimum(), env.getMap().physicsGetMapLimitMaximum()); int diffX = maxX - minX + 1; int minY = SwissArmyKnife.clip(tileY - radius, env.getMap().physicsGetMapLimitMinimum(), env.getMap().physicsGetMapLimitMaximum()); int maxY = SwissArmyKnife.clip(tileY + radius, env.getMap().physicsGetMapLimitMinimum(), env.getMap().physicsGetMapLimitMaximum()); int diffY = maxY - minY + 1; int seedHigh = (int) (seed >> 32); // high bits int seedLow = (int) seed; // low bits int lowTick = (int) tick; // low bits int x, y; int i = 0; do { x = (Math.abs(SwissArmyKnife.jenkinMix(seedHigh, lowTick, i)) % diffX) + minX; y = (Math.abs(SwissArmyKnife.jenkinMix(seedLow, lowTick, i)) % diffY) + minY; i += 2; if (i == 5000) { result.set(tileX, tileY); return; } } while (!isPointFree(x, y, freeRadius)); result.set(x, y); } }