/*
* 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.client.graphics;
import aphelion.client.graphics.world.ActorShip;
import aphelion.client.graphics.world.MapEntities;
import aphelion.client.graphics.world.Projectile;
import aphelion.client.net.SingleGameConnection;
import aphelion.shared.event.TickEvent;
import aphelion.shared.gameconfig.*;
import aphelion.shared.gameconfig.GCDocumentation.GCDOCUMENTATION_TYPE;
import aphelion.shared.net.game.GameProtocolConnection;
import aphelion.shared.net.game.GameS2CListener;
import aphelion.shared.net.protobuf.GameOperation;
import aphelion.shared.net.protobuf.GameS2C;
import aphelion.shared.physics.EnvironmentConf;
import aphelion.shared.physics.PhysicsEnvironment;
import aphelion.shared.physics.entities.ActorPublic;
import aphelion.shared.swissarmyknife.Point;
import aphelion.shared.swissarmyknife.SwissArmyKnife;
import java.lang.ref.WeakReference;
import java.util.logging.Logger;
import javax.annotation.Nonnull;
/**
*
* @author Joris
*/
public class RenderDelay implements GameS2CListener, TickEvent
{
private static final Logger log = Logger.getLogger("aphelion.client.graphics");
private final MapEntities mapEntities;
private final PhysicsEnvironment physicsEnv;
private boolean initialized = false;
private boolean subscribed = false;
private GCInteger delay = GCIntegerFixed.ZERO;
private GCInteger latencyRatio = GCIntegerFixed.ZERO;
private GCBoolean projectiles = GCBooleanFixed.FALSE;
private GCBoolean maximizeLocalTime = GCBooleanFixed.FALSE;
private GCInteger updateShipEvery = GCIntegerFixed.ZERO;
private GCInteger updateProjectileEvery = GCIntegerFixed.ZERO;
public RenderDelay(@Nonnull PhysicsEnvironment physicsEnv, @Nonnull MapEntities mapEntities)
{
this.physicsEnv = physicsEnv;
this.mapEntities = mapEntities;
}
public void subscribeListeners(@Nonnull SingleGameConnection connection)
{
connection.addListener(this);
subscribed = true;
}
public boolean isInitialized()
{
return initialized;
}
public boolean isSubscribed()
{
return subscribed;
}
static
{
GCDocumentation.put(
"render-delay",
GCDOCUMENTATION_TYPE.TICK_NON_NEGATIVE,
false,
"A fixed amount of render delay to always apply to ships.");
GCDocumentation.put(
"render-delay-latency-ratio",
GCDOCUMENTATION_TYPE.RATIO,
false,
"Adds a permille of the remote ships latency to its render delay.");
GCDocumentation.put(
"render-delay-projectiles",
GCDOCUMENTATION_TYPE.BOOLEAN,
false,
"If set, projectiles will use a dynamically calculated render delay based on nearby ships.");
GCDocumentation.put(
"render-delay-maximize-local-time",
GCDOCUMENTATION_TYPE.BOOLEAN,
false,
"If set, the render delay of projectiles will climb towards zero whenever possible. " +
"If not set the render delay will remain high after having passed a remote ship.");
GCDocumentation.put(
"render-delay-update-ship-delay-every-ticks",
GCDOCUMENTATION_TYPE.TICK_NON_NEGATIVE,
false,
"When the desired render delay of a ship suddenly changes, this setting will help smooth it out over time. " +
"Every x ticks, add/substract one tick to the actual render delay. Use x = zero to disable.");
GCDocumentation.put(
"render-delay-update-projectile-delay-every-ticks",
GCDOCUMENTATION_TYPE.TICK_NON_NEGATIVE,
false,
"When the desired render delay of a projectile suddenly changes, this setting will help smooth it out over time. " +
"Every x ticks, add/substract one tick to the actual render delay. Use x = zero to disable. " +
"If this setting is set too high, projectiles might appear to pass through remote ships.");
}
public void init(@Nonnull ActorPublic localActor)
{
if (localActor == null)
{
throw new IllegalArgumentException();
}
initialized = true;
delay = localActor.getActorConfigInteger("render-delay");
latencyRatio = localActor.getActorConfigInteger("render-delay-latency-ratio");
projectiles = localActor.getActorConfigBoolean("render-delay-projectiles");
maximizeLocalTime = localActor.getActorConfigBoolean("render-delay-maximize-local-time");
updateShipEvery = localActor.getActorConfigInteger("render-delay-update-ship-delay-every-ticks");
updateProjectileEvery = localActor.getActorConfigInteger("render-delay-update-projectile-delay-every-ticks");
}
public void calculateRenderAtTick(@Nonnull ActorShip ship)
{
ActorPublic actor = ship.getActor();
ship.renderDelay_value.setUpdateDelay(updateShipEvery.get());
if (this.delay.get() <= 0 && this.latencyRatio.get() <= 0)
{
ship.renderDelay_value.set(0);
}
long createdAgo = physicsEnv.getTick() - actor.getCreatedAt();
ship.renderDelay_current = SwissArmyKnife.clip(
ship.renderDelay_value.get(),
0,
physicsEnv.getConfig().HIGHEST_DELAY);
if (ship.renderDelay_current > createdAgo)
{
ship.renderDelay_current = (int) createdAgo;
if (ship.renderDelay_current < 0)
{
ship.renderDelay_current = 0;
}
}
ship.renderingAt_tick = physicsEnv.getTick() - ship.renderDelay_current;
}
public void calculateRenderAtTick(@Nonnull Projectile projectile)
{
projectile.renderDelay_value.setUpdateDelay(updateProjectileEvery.get());
ActorShip localShip = mapEntities.getLocalShip();
if (!this.projectiles.get())
{
projectile.renderDelay_value.set(0);
}
else
{
// the closest ship excluding the local one
// all actors should have been updated at this point
ActorShip closest = mapEntities.findNearestActor(projectile.pos, false);
if (closest == null || localShip == null)
{
projectile.renderDelay_value.set(0);
}
else
{
boolean switchedShip = false;
if (projectile.renderDelay_basedOn == null
|| projectile.renderDelay_basedOn.get() != closest)
{
switchedShip = true;
projectile.renderDelay_basedOn = new WeakReference<>(closest);
}
/* p = local player
* r = remote player
* e = entity (projectile)
* r' = the shadow of the player r (the position that is
* dead reckoned up the current time)
*
* δ(x, y) is the distance between x en y
* d(x, y) is the render delay of y on the screen of x
* d(p, e) = 0 if δ(p , e') = 0
* d(p, e) = d(p,r) if δ(r', e') = 0
*
* d(p, e) = d(p, r) * max(0, 1 - δ(r', e') / δ(p, r) )
* sqrt(a) / sqrt(b) = sqrt(a / b)
*/
Point diff = new Point();
diff.set(closest.shadowPosition);
diff.sub(projectile.shadowPosition);
float distSq_rShadow_e = diff.lengthSquared();
diff.set(localShip.pos);
diff.sub(closest.pos);
float distSq_p_r = diff.lengthSquared();
double renderDelay =
closest.renderDelay_value.get() *
Math.max(0, 1 - Math.sqrt(distSq_rShadow_e / distSq_p_r));
if (Double.isNaN(renderDelay))
{
renderDelay = 0;
}
renderDelay = Math.round(renderDelay);
if (maximizeLocalTime.get())
{
projectile.renderDelay_value.set((int) renderDelay);
}
else
{
Point prevPos = new Point(projectile.shadowPosition_prev);
prevPos.sub(localShip.pos);
Point nextPos = new Point(projectile.shadowPosition);
nextPos.sub(localShip.pos);
boolean movingAway = nextPos.lengthSquared() > prevPos.lengthSquared();
if (movingAway)
{
// if the distance to the local ship is increasing:
// only increase the render delay, do not decrease it.
// unless the calculation has switched to a different ship
if (switchedShip || renderDelay > projectile.renderDelay_value.getDesired())
{
projectile.renderDelay_value.set((int) renderDelay);
}
}
else
{
projectile.renderDelay_value.set((int) renderDelay);
}
}
}
}
// The smoothed render delay
projectile.renderDelay_current = SwissArmyKnife.clip(
projectile.renderDelay_value.get(),
0,
physicsEnv.getConfig().HIGHEST_DELAY);
projectile.renderingAt_tick = physicsEnv.getTick() - projectile.renderDelay_current;
}
/** Call this method to update the render delay of the given player when a move for him is received.
* If <strong>this</strong> object has been registered as a GameS2CListener, this is done for you.
* @param pid
* @param firstMoveTick If this move message contains multiple moves, this should be the tick of the first move
*/
public void receivedMove(int pid, long firstMoveTick)
{
ActorShip ship = mapEntities.getActorShip(pid);
if (ship == null)
{
return;
}
// Use the tick of the first move
// This way the render delay does not continuesly drift because
// of the delayed move update mechanism (SEND_MOVE_DELAY,
// which is very similar to Nagle's algorithm).
if (!ship.renderDelay_value.hasBeenSet() || firstMoveTick > ship.renderDelay_mostRecentMove)
{
ship.renderDelay_mostRecentMove = firstMoveTick;
ship.renderDelay_mostRecentMoveLatency = physicsEnv.getTick() - firstMoveTick;
calculateDesiredDelay(ship);
}
}
private void calculateDesiredDelay(@Nonnull ActorShip ship)
{
if (ship.isLocalPlayer())
{
ship.renderDelay_value.set(0);
return;
}
long desired = ship.renderDelay_mostRecentMoveLatency;
desired = desired * latencyRatio.get() / GCInteger.RATIO;
desired += this.delay.get();
ship.renderDelay_value.set((int) desired);
}
@Override
public void gameS2CMessage(@Nonnull GameProtocolConnection game, @Nonnull GameS2C.S2C s2c, long receivedAt)
{
// Calculate the render delay for ships
for (GameOperation.ActorMove msg : s2c.getActorMoveList())
{
if (msg.getDirect())
{
receivedMove(msg.getPid(), msg.getTick());
}
}
}
@Override
public void tick(long tick)
{
for (ActorShip ship : mapEntities.ships())
{
calculateDesiredDelay(ship);
}
}
}