From 8fe7a6f951ab0c4cfd420efb3a1c741f00ebd263 Mon Sep 17 00:00:00 2001
From: DBotThePony <dbotthepony@yandex.ru>
Date: Thu, 11 Apr 2024 15:06:32 +0700
Subject: [PATCH] Update all methods to not reference global timestep constant
 this sets up ground for variable tick rate

---
 .../kstarbound/client/StarboundClient.kt      |  4 +-
 .../kstarbound/math/PeriodicFunction.kt       |  2 -
 .../serverbound/ClientConnectPacket.kt        |  2 +-
 .../network/syncher/BasicNetworkedElement.kt  | 48 ++--------
 .../syncher/FloatingNetworkedElement.kt       | 25 ++---
 .../network/syncher/InterpolationQueue.kt     | 95 +++++++++++++++++++
 .../network/syncher/NetworkedList.kt          | 28 +-----
 .../network/syncher/NetworkedMap.kt           | 76 +++++----------
 .../network/syncher/NetworkedSignal.kt        | 23 +----
 .../server/IntegratedStarboundServer.kt       |  2 +-
 .../kstarbound/server/ServerConnection.kt     |  2 +-
 .../kstarbound/server/StarboundServer.kt      | 14 +--
 .../kstarbound/server/world/ServerChunk.kt    |  8 +-
 .../server/world/ServerSystemWorld.kt         | 34 +++----
 .../kstarbound/server/world/ServerWorld.kt    |  8 +-
 .../ru/dbotthepony/kstarbound/util/Clocks.kt  | 58 ++++-------
 .../ru/dbotthepony/kstarbound/world/Chunk.kt  |  2 +-
 .../ru/dbotthepony/kstarbound/world/Sky.kt    |  9 +-
 .../kstarbound/world/SystemWorld.kt           |  4 +-
 .../kstarbound/world/TileHealth.kt            |  3 +-
 .../ru/dbotthepony/kstarbound/world/World.kt  | 10 +-
 .../world/entities/AbstractEntity.kt          |  4 +-
 .../world/entities/ActorMovementController.kt | 33 ++++---
 .../kstarbound/world/entities/Animator.kt     |  9 +-
 .../world/entities/DynamicEntity.kt           |  4 +-
 .../world/entities/ItemDropEntity.kt          | 14 +--
 .../world/entities/MovementController.kt      | 38 ++++----
 .../world/entities/tile/TileEntity.kt         |  4 +-
 .../world/entities/tile/WorldObject.kt        | 12 +--
 29 files changed, 272 insertions(+), 303 deletions(-)
 create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/network/syncher/InterpolationQueue.kt

diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/StarboundClient.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/StarboundClient.kt
index fedc064f..1272db60 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/StarboundClient.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/StarboundClient.kt
@@ -703,7 +703,7 @@ class StarboundClient private constructor(val clientID: Int) : BlockableEventLoo
 
 	private fun renderWorld(world: ClientWorld) {
 		updateViewportParams()
-		world.tick()
+		world.tick(Starbound.TIMESTEP)
 
 		stack.clear(Matrix3f.identity())
 
@@ -772,7 +772,7 @@ class StarboundClient private constructor(val clientID: Int) : BlockableEventLoo
 			GLFW.glfwPollEvents()
 
 			if (world != null && Starbound.initialized)
-				world.tick()
+				world.tick(Starbound.TIMESTEP)
 
 			activeConnection?.flush()
 			return
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/math/PeriodicFunction.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/math/PeriodicFunction.kt
index 4970a000..57e4a8ba 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/math/PeriodicFunction.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/math/PeriodicFunction.kt
@@ -2,8 +2,6 @@ package ru.dbotthepony.kstarbound.math
 
 import ru.dbotthepony.kommons.math.linearInterpolation
 import java.util.random.RandomGenerator
-import kotlin.math.PI
-import kotlin.math.sin
 
 data class PeriodicFunction(
 	val period: Double = 1.0,
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/serverbound/ClientConnectPacket.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/serverbound/ClientConnectPacket.kt
index b96b7032..cac34a4e 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/serverbound/ClientConnectPacket.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/serverbound/ClientConnectPacket.kt
@@ -72,7 +72,7 @@ data class ClientConnectPacket(
 
 			connection.receiveShipChunks(shipChunks)
 			connection.send(ConnectSuccessPacket(connection.connectionID, connection.server.serverUUID, connection.server.universe.baseInformation))
-			connection.send(UniverseTimeUpdatePacket(connection.server.universeClock.seconds))
+			connection.send(UniverseTimeUpdatePacket(connection.server.universeClock.time))
 			connection.channel.flush()
 			connection.inGame()
 		}
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/syncher/BasicNetworkedElement.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/syncher/BasicNetworkedElement.kt
index 7fb130a5..df78c034 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/network/syncher/BasicNetworkedElement.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/syncher/BasicNetworkedElement.kt
@@ -10,7 +10,7 @@ import java.util.function.Consumer
 
 open class BasicNetworkedElement<TYPE, LEGACY>(private var value: TYPE, protected val codec: StreamCodec<TYPE>, protected val legacyCodec: StreamCodec<LEGACY>, protected val toLegacy: (TYPE) -> LEGACY, protected val fromLegacy: (LEGACY) -> TYPE) : NetworkedElement(), ListenableDelegate<TYPE> {
 	protected val valueListeners = Listenable.Impl<TYPE>()
-	protected val queue = LinkedList<Pair<Double, TYPE>>()
+	protected val queue = InterpolationQueue<TYPE> { this.value = it; valueListeners.accept(value) }
 	protected var isInterpolating = false
 	protected var currentTime = 0.0
 
@@ -48,17 +48,9 @@ open class BasicNetworkedElement<TYPE, LEGACY>(private var value: TYPE, protecte
 
 	override fun writeInitial(data: DataOutputStream, isLegacy: Boolean) {
 		if (isLegacy) {
-			if (queue.isNotEmpty()) {
-				legacyCodec.write(data, toLegacy(queue.last.second))
-			} else {
-				legacyCodec.write(data, toLegacy(value))
-			}
+			legacyCodec.write(data, toLegacy(queue.last { value }))
 		} else {
-			if (queue.isNotEmpty()) {
-				codec.write(data, queue.last.second)
-			} else {
-				codec.write(data, value)
-			}
+			codec.write(data, queue.last { value })
 		}
 	}
 
@@ -67,19 +59,7 @@ open class BasicNetworkedElement<TYPE, LEGACY>(private var value: TYPE, protecte
 		bumpVersion()
 
 		if (isInterpolating) {
-			// Only append an incoming delta to our pending value list if the incoming
-			// step is forward in time of every other pending value.  In any other
-			// case, this is an error or the step tracking is wildly off, so just clear
-			// any other incoming values.
-			val actualDelay = interpolationDelay + currentTime
-
-			if (interpolationDelay > 0.0 && (queue.isEmpty() || queue.last.first <= actualDelay)) {
-				queue.add(actualDelay to read)
-			} else {
-				value = read
-				queue.clear()
-				valueListeners.accept(read)
-			}
+			queue.push(read, interpolationDelay)
 		} else {
 			value = read
 			valueListeners.accept(read)
@@ -90,38 +70,22 @@ open class BasicNetworkedElement<TYPE, LEGACY>(private var value: TYPE, protecte
 		writeInitial(data, isLegacy)
 	}
 
-	override fun readBlankDelta(interpolationDelay: Double) {
-		// TODO: original engine doesn't override this, is this intentional?
-		// tickInterpolation(interpolationDelay)
-	}
+	override fun readBlankDelta(interpolationDelay: Double) {}
 
 	override fun enableInterpolation(extrapolation: Double) {
 		if (!isInterpolating) {
 			isInterpolating = true
-			queue.clear()
 		}
 	}
 
 	override fun disableInterpolation() {
 		if (isInterpolating) {
 			isInterpolating = false
-
-			if (queue.isNotEmpty()) {
-				value = queue.last.second
-				valueListeners.accept(value)
-			}
-
 			queue.clear()
 		}
 	}
 
 	override fun tickInterpolation(delta: Double) {
-		require(delta >= 0.0) { "Negative interpolation delta: $delta" }
-		currentTime += delta
-
-		while (queue.isNotEmpty() && queue.first.first <= currentTime) {
-			value = queue.removeFirst().second
-			valueListeners.accept(value)
-		}
+		queue.tick(delta)
 	}
 }
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/syncher/FloatingNetworkedElement.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/syncher/FloatingNetworkedElement.kt
index cc010e10..070efe59 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/network/syncher/FloatingNetworkedElement.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/syncher/FloatingNetworkedElement.kt
@@ -70,7 +70,8 @@ class FloatingNetworkedElement(private var value: Double = 0.0, val ops: Ops, va
 		}
 	}
 
-	private val queue = ArrayDeque<Pair<Double, Double>>()
+	private data class QueueEntry(val time: Double, val value: Double)
+	private val queue = ArrayDeque<QueueEntry>()
 
 	var currentTime = 0.0
 		private set
@@ -95,7 +96,7 @@ class FloatingNetworkedElement(private var value: Double = 0.0, val ops: Ops, va
 
 			if (isInterpolating) {
 				queue.clear()
-				queue.add(currentTime to t)
+				queue.add(QueueEntry(currentTime, t))
 			}
 
 			valueListeners.accept(t)
@@ -130,13 +131,13 @@ class FloatingNetworkedElement(private var value: Double = 0.0, val ops: Ops, va
 		queue.clear()
 
 		if (isInterpolating) {
-			queue.add(currentTime to value)
+			queue.add(QueueEntry(currentTime, value))
 		}
 	}
 
 	override fun writeInitial(data: DataOutputStream, isLegacy: Boolean) {
 		if (queue.isNotEmpty())
-			(if (isLegacy) legacyOps else ops).write(data, queue.last().second)
+			(if (isLegacy) legacyOps else ops).write(data, queue.last().value)
 		else
 			(if (isLegacy) legacyOps else ops).write(data, value)
 	}
@@ -146,10 +147,10 @@ class FloatingNetworkedElement(private var value: Double = 0.0, val ops: Ops, va
 
 		if (isInterpolating) {
 			val realDelay = interpolationDelay + currentTime
-			if (queue.last().first > realDelay)
+			if (queue.last().time > realDelay)
 				queue.clear()
 
-			queue.add(realDelay to read)
+			queue.add(QueueEntry(realDelay, read))
 			value = interpolated()
 			valueListeners.accept(value)
 		} else {
@@ -172,7 +173,7 @@ class FloatingNetworkedElement(private var value: Double = 0.0, val ops: Ops, va
 			if (actual < last)
 				queue.clear()
 
-			queue.add(actual to lastPoint)
+			queue.add(QueueEntry(actual, lastPoint))
 
 			val old = value
 			value = interpolated()
@@ -187,7 +188,7 @@ class FloatingNetworkedElement(private var value: Double = 0.0, val ops: Ops, va
 		if (!isInterpolating) {
 			isInterpolating = true
 			queue.clear()
-			queue.add(currentTime to value)
+			queue.add(QueueEntry(currentTime, value))
 		}
 
 		this.extrapolation = extrapolation
@@ -198,7 +199,7 @@ class FloatingNetworkedElement(private var value: Double = 0.0, val ops: Ops, va
 			isInterpolating = false
 
 			if (queue.isNotEmpty()) {
-				value = queue.last().second
+				value = queue.last().value
 			}
 
 			queue.clear()
@@ -210,7 +211,7 @@ class FloatingNetworkedElement(private var value: Double = 0.0, val ops: Ops, va
 		currentTime += delta
 
 		if (isInterpolating && queue.size >= 2) {
-			while (queue.size > 2 && queue[1].first <= currentTime) {
+			while (queue.size > 2 && queue[1].time <= currentTime) {
 				queue.removeFirst()
 			}
 
@@ -229,7 +230,7 @@ class FloatingNetworkedElement(private var value: Double = 0.0, val ops: Ops, va
 
 		val actualTime = time + currentTime
 
-		if (actualTime < queue.first().first) {
+		if (actualTime < queue.first().time) {
 			// extrapolate into past?
 			val (time0, value0) = queue[0]
 			val (time1, value1) = queue[1]
@@ -240,7 +241,7 @@ class FloatingNetworkedElement(private var value: Double = 0.0, val ops: Ops, va
 				diff = 0.0
 
 			return interpolator.interpolate(diff, value0, value1)
-		} else if (actualTime > queue.last().first) {
+		} else if (actualTime > queue.last().time) {
 			// extrapolate into future
 			val (time0, value0) = queue[queue.size - 2]
 			val (time1, value1) = queue[queue.size - 1]
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/syncher/InterpolationQueue.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/syncher/InterpolationQueue.kt
new file mode 100644
index 00000000..1f6027a4
--- /dev/null
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/syncher/InterpolationQueue.kt
@@ -0,0 +1,95 @@
+package ru.dbotthepony.kstarbound.network.syncher
+
+import java.util.LinkedList
+
+class InterpolationQueue<T>(private val visitor: (T) -> Unit) : Iterable<T> {
+	private data class Entry<T>(val time: Double, val value: T) : Comparable<Entry<*>> {
+		override fun compareTo(other: Entry<*>): Int {
+			return time.compareTo(other.time)
+		}
+	}
+
+	private var currentTime = 0.0
+ 	private val queue = LinkedList<Entry<T>>()
+
+	/**
+	 * Clears all interpolated values
+	 */
+	fun drop() {
+		queue.clear()
+	}
+
+	/**
+	 * Applies interpolation queue, and clears it
+	 */
+	fun clear() {
+		try {
+			queue.forEach { visitor(it.value) }
+		} finally {
+			queue.clear()
+		}
+	}
+
+	override fun iterator(): Iterator<T> {
+		return object : Iterator<T> {
+			private val parent = queue.iterator()
+
+			override fun hasNext(): Boolean {
+				return parent.hasNext()
+			}
+
+			override fun next(): T {
+				return parent.next().value
+			}
+		}
+	}
+
+	val isEmpty: Boolean
+		get() = queue.isEmpty()
+
+	val isNotEmpty: Boolean
+		get() = queue.isNotEmpty()
+
+	val size: Int
+		get() = queue.size
+
+	fun last(): T {
+		return queue.last.value
+	}
+
+	inline fun last(orElse: () -> T): T {
+		if (isEmpty)
+			return orElse.invoke()
+		else
+			return last()
+	}
+
+	fun tick(delta: Double) {
+		require(delta >= 0.0) { "Negative interpolation delta: $delta" }
+		currentTime += delta
+
+		while (queue.isNotEmpty() && queue[0].time <= currentTime) {
+			visitor(queue.removeAt(0).value)
+		}
+	}
+
+	fun push(value: T, delay: Double, visitAllIfDeviated: Boolean = true) {
+		val actualTime = delay + currentTime
+
+		// Only append an incoming delta to our pending value list if the incoming
+		// step is forward in time of every other pending value.  In any other
+		// case, this is an error or the step tracking is wildly off, so just clear
+		// any other incoming values.
+		if (queue.isNotEmpty() && queue.last.time > actualTime) {
+			if (visitAllIfDeviated)
+				queue.forEach { visitor(it.value) }
+
+			queue.clear()
+		}
+
+		if (delay > 0L)
+			queue.add(Entry(actualTime, value))
+		else
+			visitor(value)
+	}
+}
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/syncher/NetworkedList.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/syncher/NetworkedList.kt
index ca76b96f..ab1d6ae6 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/network/syncher/NetworkedList.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/syncher/NetworkedList.kt
@@ -19,8 +19,8 @@ class NetworkedList<E>(
 	private val elementsFactory: (Int) -> MutableList<E> = ::ArrayList
 ) : NetworkedElement(), MutableList<E> {
 	private val backlog = ArrayDeque<Pair<Long, Entry<E>>>()
-	private val queue = ArrayDeque<Pair<Double, Entry<E>>>()
 	private val elements = elementsFactory(10)
+	private val queue = InterpolationQueue<Entry<E>> { it.apply(elements) }
 
 	private enum class Type {
 		ADD, REMOVE, CLEAR;
@@ -41,7 +41,6 @@ class NetworkedList<E>(
 
 	private val clearEntry = Entry<E>(Type.CLEAR, 0, KOptional())
 	private var isInterpolating = false
-	private var currentTime = 0.0
 	private var isRemote = false
 	private val listeners = CopyOnWriteArrayList<Runnable>()
 
@@ -56,7 +55,7 @@ class NetworkedList<E>(
 	}
 
 	private fun latestState(): List<E> {
-		if (queue.isEmpty()) {
+		if (queue.isEmpty) {
 			return elements
 		} else {
 			val copy = elementsFactory(elements.size)
@@ -64,9 +63,7 @@ class NetworkedList<E>(
 			for (v in elements)
 				copy.add(v)
 
-			for ((_, e) in queue)
-				e.apply(copy)
-
+			queue.forEach { it.apply(copy) }
 			return copy
 		}
 	}
@@ -126,17 +123,7 @@ class NetworkedList<E>(
 					}
 
 					if (isInterpolating) {
-						val actualTime = interpolationDelay + currentTime
-
-						if (queue.isNotEmpty() && queue.last().first >= actualTime) {
-							queue.forEach { it.second.apply(elements) }
-							queue.clear()
-						}
-
-						if (interpolationDelay > 0.0)
-							queue.add(actualTime to entry)
-						else
-							entry.apply(elements)
+						queue.push(entry, interpolationDelay)
 					} else {
 						entry.apply(elements)
 					}
@@ -186,16 +173,11 @@ class NetworkedList<E>(
 
 	override fun disableInterpolation() {
 		isInterpolating = false
-		queue.forEach { it.second.apply(elements) }
 		queue.clear()
 	}
 
 	override fun tickInterpolation(delta: Double) {
-		currentTime += delta
-
-		while (queue.isNotEmpty() && queue.first().first <= currentTime) {
-			queue.removeFirst().second.apply(elements)
-		}
+		queue.tick(delta)
 	}
 
 	override fun hasChangedSince(version: Long): Boolean {
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/syncher/NetworkedMap.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/syncher/NetworkedMap.kt
index d970136d..283119af 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/network/syncher/NetworkedMap.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/syncher/NetworkedMap.kt
@@ -50,12 +50,12 @@ class NetworkedMap<K, V>(
 	init {
 		map.addListener(object : ListenableMap.MapListener<K, V> {
 			override fun onClear() {
-				if (isReading) return
+				if (isReading > 0) return
 				check(!isRemote) { "This map is not owned by this side" }
 
 				// this is fragile (due to interpolation fuckery, we remove everything before applying delayed changes),
 				// but let's hope it doesn't break
-				delayed.clear()
+				queue.clear()
 				backlog.add(currentVersion() to clearEntry)
 
 				purgeBacklog()
@@ -63,7 +63,7 @@ class NetworkedMap<K, V>(
 			}
 
 			override fun onValueAdded(key: K, value: V) {
-				if (isReading) return
+				if (isReading > 0) return
 				check(!isRemote) { "This map is not owned by this side" }
 				backlog.add(currentVersion() to Entry(Action.ADD, KOptional(nativeKey.copy(key)), KOptional(nativeValue.copy(value))))
 				purgeBacklog()
@@ -71,7 +71,7 @@ class NetworkedMap<K, V>(
 			}
 
 			override fun onValueRemoved(key: K, value: V) {
-				if (isReading) return
+				if (isReading > 0) return
 				check(!isRemote) { "This map is not owned by this side" }
 				backlog.add(currentVersion() to Entry(Action.REMOVE, KOptional(nativeKey.copy(key)), KOptional()))
 				purgeBacklog()
@@ -168,9 +168,14 @@ class NetworkedMap<K, V>(
 
 	private val clearEntry = Entry<K, V>(Action.CLEAR, KOptional(), KOptional())
 	private val backlog = ArrayDeque<Pair<Long, Entry<K, V>>>()
-	private val delayed = ArrayDeque<Pair<Double, Entry<K, V>>>()
-	private var currentTime = 0.0
-	private var isReading = false
+
+	private val queue = InterpolationQueue<Entry<K, V>> {
+		isReading++
+		it.apply(this)
+		isReading--
+	}
+
+	private var isReading = 0
 	private var isRemote = false
 	private var isInterpolating = false
 
@@ -192,10 +197,10 @@ class NetworkedMap<K, V>(
 	override fun readInitial(data: DataInputStream, isLegacy: Boolean) {
 		try {
 			isRemote = true
-			isReading = true
+			isReading++
 
 			backlog.clear()
-			delayed.clear()
+			queue.clear()
 			clear()
 
 			backlog.add(currentVersion() to clearEntry)
@@ -228,31 +233,31 @@ class NetworkedMap<K, V>(
 
 			purgeBacklog()
 		} finally {
-			isReading = false
+			isReading--
 		}
 	}
 
 	override fun writeInitial(data: DataOutputStream, isLegacy: Boolean) {
 		if (isDumb && isLegacy) {
-			val construct = HashMap<K, V>(size + delayed.size)
+			val construct = HashMap<K, V>(size + queue.size)
 
 			for ((k, v) in entries) {
 				construct[k] = v
 			}
 
-			for ((_, v) in delayed) {
+			for (v in queue) {
 				v.apply(construct)
 			}
 
 			dumbCodec.write(data, construct)
 		} else {
-			data.writeVarInt(size + delayed.size)
+			data.writeVarInt(size + queue.size)
 
 			for ((k, v) in entries) {
 				Entry(Action.ADD, KOptional(k), KOptional(v)).write(data, isLegacy, this)
 			}
 
-			for ((_, v) in delayed) {
+			for (v in queue) {
 				v.write(data, isLegacy, this)
 			}
 		}
@@ -265,31 +270,18 @@ class NetworkedMap<K, V>(
 		}
 
 		try {
-			isReading = true
+			isReading++
 
 			while (true) {
 				when (val action = data.readUnsignedByte()) {
 					0 -> break
-					1 -> {
-						readInitial(data, isLegacy)
-						isReading = true
-					}
+					1 -> readInitial(data, isLegacy)
 					2 -> {
 						val change = if (isLegacy) readLegacyEntry(data) else readNativeEntry(data)
 						backlog.add(currentVersion() to change)
 
 						if (isInterpolating) {
-							val actualDelay = interpolationDelay + currentTime
-
-							if (delayed.isNotEmpty() && delayed.last().first > actualDelay) {
-								delayed.forEach { it.second.apply(this) }
-								delayed.clear()
-							}
-
-							if (interpolationDelay > 0.0)
-								delayed.add(actualDelay to change)
-							else
-								change.apply(this)
+							queue.push(change, interpolationDelay)
 						} else {
 							change.apply(this)
 						}
@@ -301,7 +293,7 @@ class NetworkedMap<K, V>(
 
 			purgeBacklog()
 		} finally {
-			isReading = false
+			isReading--
 		}
 	}
 
@@ -341,30 +333,12 @@ class NetworkedMap<K, V>(
 	override fun disableInterpolation() {
 		if (isInterpolating) {
 			isInterpolating = false
-
-			try {
-				isReading = true
-				delayed.forEach { it.second.apply(this) }
-				delayed.clear()
-			} finally {
-				isReading = false
-			}
-
+			queue.clear()
 			purgeBacklog()
 		}
 	}
 
 	override fun tickInterpolation(delta: Double) {
-		currentTime += delta
-
-		try {
-			isReading = true
-
-			while (delayed.isNotEmpty() && delayed.first().first <= currentTime) {
-				delayed.removeFirst().second.apply(this)
-			}
-		} finally {
-			isReading = false
-		}
+		queue.tick(delta)
 	}
 }
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/syncher/NetworkedSignal.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/syncher/NetworkedSignal.kt
index 80d52e80..904dbff0 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/network/syncher/NetworkedSignal.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/syncher/NetworkedSignal.kt
@@ -17,10 +17,9 @@ import java.io.DataOutputStream
 class NetworkedSignal<S>(private val codec: StreamCodec<S>, private val maxSize: Int = 100) : NetworkedElement(), Iterable<S>, Iterator<S> {
 	private data class Signal<S>(val version: Long, val signal: S)
 
-	private var currentTime = 0.0
 	private val visibleSignals = ArrayDeque<Signal<S>>()
 	private val internalSignals = ArrayDeque<Signal<S>>()
-	private val delayedSignals = ArrayDeque<Pair<Double, S>>()
+	private val delayedSignals = InterpolationQueue<S> { push(it) }
 	private var isInterpolating = false
 
 	override fun readInitial(data: DataInputStream, isLegacy: Boolean) {}
@@ -34,14 +33,7 @@ class NetworkedSignal<S>(private val codec: StreamCodec<S>, private val maxSize:
 			val signal = codec.read(data)
 
 			if (isInterpolating && interpolationDelay > 0.0) {
-				val actualDelay = interpolationDelay + currentTime
-
-				if (delayedSignals.isNotEmpty() && delayedSignals.last().first > actualDelay) {
-					delayedSignals.forEach { push(it.second) }
-					delayedSignals.clear()
-				}
-
-				delayedSignals.add(actualDelay to signal)
+				delayedSignals.push(signal, interpolationDelay)
 			} else {
 				push(signal)
 			}
@@ -68,20 +60,11 @@ class NetworkedSignal<S>(private val codec: StreamCodec<S>, private val maxSize:
 
 	override fun disableInterpolation() {
 		isInterpolating = false
-
-		for ((_, v) in delayedSignals) {
-			push(v)
-		}
-
 		delayedSignals.clear()
 	}
 
 	override fun tickInterpolation(delta: Double) {
-		currentTime += delta
-
-		while (delayedSignals.isNotEmpty() && delayedSignals.first().first <= currentTime) {
-			push(delayedSignals.removeFirst().second)
-		}
+		delayedSignals.tick(delta)
 	}
 
 	val isEmpty: Boolean
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/IntegratedStarboundServer.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/IntegratedStarboundServer.kt
index 6ebd3623..dbdc855e 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/IntegratedStarboundServer.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/IntegratedStarboundServer.kt
@@ -9,7 +9,7 @@ class IntegratedStarboundServer(val client: StarboundClient, root: File) : Starb
 		channels.createLocalChannel()
 	}
 
-	override fun tick0() {
+	override fun tick0(delta: Double) {
 		if (client.isShutdown) {
 			shutdown()
 		}
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/ServerConnection.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/ServerConnection.kt
index 1f3af7f3..80ec7a64 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/ServerConnection.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/ServerConnection.kt
@@ -423,7 +423,7 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn
 		warpQueue.trySend(destination to deploy)
 	}
 
-	fun tick() {
+	fun tick(delta: Double) {
 		if (!isConnected || !channel.isOpen)
 			return
 
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/StarboundServer.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/StarboundServer.kt
index bafb23a0..b14efce4 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/StarboundServer.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/StarboundServer.kt
@@ -187,11 +187,11 @@ sealed class StarboundServer(val root: File) : BlockableEventLoop("Server thread
 
 	init {
 		scheduleAtFixedRate(Runnable {
-			channels.broadcast(UniverseTimeUpdatePacket(universeClock.seconds))
+			channels.broadcast(UniverseTimeUpdatePacket(universeClock.time))
 		}, Globals.universeServer.clockUpdatePacketInterval, Globals.universeServer.clockUpdatePacketInterval, TimeUnit.MILLISECONDS)
 
 		scheduleAtFixedRate(Runnable {
-			tickNormal()
+			tickNormal(Starbound.TIMESTEP)
 		}, Starbound.TIMESTEP_NANOS, Starbound.TIMESTEP_NANOS, TimeUnit.NANOSECONDS)
 
 		scheduleAtFixedRate(Runnable {
@@ -232,7 +232,7 @@ sealed class StarboundServer(val root: File) : BlockableEventLoop("Server thread
 	}
 
 	protected abstract fun close0()
-	protected abstract fun tick0()
+	protected abstract fun tick0(delta: Double)
 
 	private fun tickSystemWorlds() {
 		systemWorlds.values.removeIf {
@@ -246,7 +246,7 @@ sealed class StarboundServer(val root: File) : BlockableEventLoop("Server thread
 
 			scope.launch {
 				try {
-					it.get().tick()
+					it.get().tick(Starbound.SYSTEM_WORLD_TIMESTEP)
 				} catch (err: Throwable) {
 					LOGGER.fatal("Exception in system world $it event loop", err)
 				}
@@ -261,20 +261,20 @@ sealed class StarboundServer(val root: File) : BlockableEventLoop("Server thread
 		}
 	}
 
-	private fun tickNormal() {
+	private fun tickNormal(delta: Double) {
 		try {
 			// universeClock.nanos += Starbound.TIMESTEP_NANOS
 
 			channels.connections.forEach {
 				try {
-					it.tick()
+					it.tick(delta)
 				} catch (err: Throwable) {
 					LOGGER.error("Exception while ticking client connection", err)
 					it.disconnect("Exception while ticking client connection: $err")
 				}
 			}
 
-			tick0()
+			tick0(delta)
 		} catch (err: Throwable) {
 			LOGGER.fatal("Exception in main server event loop", err)
 			shutdown()
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerChunk.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerChunk.kt
index 9498e83b..6881a2ef 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerChunk.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerChunk.kt
@@ -490,7 +490,7 @@ class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk<ServerWorld, Server
 		return result
 	}
 
-	override fun tick() {
+	override fun tick(delta: Double) {
 		ticks++
 
 		ticketsLock.withLock {
@@ -521,18 +521,18 @@ class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk<ServerWorld, Server
 		if (state != ChunkState.FULL)
 			return
 
-		super.tick()
+		super.tick(delta)
 
 		foregroundHealth.entries.removeIf { (pos, health) ->
 			val (x, y) = pos
-			val remove = !health.tick(cells.value[x, y].foreground.material.value.actualDamageTable)
+			val remove = !health.tick(cells.value[x, y].foreground.material.value.actualDamageTable, delta)
 			onTileHealthUpdate(x, y, false, health)
 			remove
 		}
 
 		backgroundHealth.entries.removeIf { (pos, health) ->
 			val (x, y) = pos
-			val remove = !health.tick(cells.value[x, y].background.material.value.actualDamageTable)
+			val remove = !health.tick(cells.value[x, y].background.material.value.actualDamageTable, delta)
 			onTileHealthUpdate(x, y, true, health)
 			remove
 		}
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerSystemWorld.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerSystemWorld.kt
index 50e7f0fa..1a49690a 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerSystemWorld.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerSystemWorld.kt
@@ -150,7 +150,7 @@ class ServerSystemWorld : SystemWorld {
 
 	private constructor(server: StarboundServer, location: Vector3i) : super(location, server.universeClock, server.universe) {
 		this.server = server
-		this.lastSpawn = clock.seconds - Globals.systemWorld.objectSpawnCycle
+		this.lastSpawn = clock.time - Globals.systemWorld.objectSpawnCycle
 		objectSpawnTime = random.nextRange(Globals.systemWorld.objectSpawnInterval)
 	}
 
@@ -173,27 +173,27 @@ class ServerSystemWorld : SystemWorld {
 				val name = it.second.sample(random).orNull() ?: return@ifPresent
 				val uuid = UUID(random.nextLong(), random.nextLong())
 				val prototype = Globals.systemObjects[name] ?: throw NullPointerException("Tried to create $name system world object, but there is no such object in /system_objects.config!")
-				val create = ServerEntity(prototype.create(uuid, name), uuid, randomObjectSpawnPosition(), clock.seconds)
-				create.enterOrbit(UniversePos(location), Vector2d.ZERO, clock.seconds) // orbit center of system
+				val create = ServerEntity(prototype.create(uuid, name), uuid, randomObjectSpawnPosition(), clock.time)
+				create.enterOrbit(UniversePos(location), Vector2d.ZERO, clock.time) // orbit center of system
 			}
 		}
 	}
 
 	private suspend fun spawnObjects() {
-		var diff = Globals.systemWorld.objectSpawnCycle.coerceAtMost(clock.seconds - lastSpawn)
-		lastSpawn = clock.seconds - diff
+		var diff = Globals.systemWorld.objectSpawnCycle.coerceAtMost(clock.time - lastSpawn)
+		lastSpawn = clock.time - diff
 
 		while (diff > objectSpawnTime) {
 			lastSpawn += objectSpawnTime
 			objectSpawnTime = random.nextRange(Globals.systemWorld.objectSpawnInterval)
-			diff = clock.seconds - lastSpawn
+			diff = clock.time - lastSpawn
 
 			Globals.systemWorld.objectSpawnPool.sample(random).ifPresent {
 				val uuid = UUID(random.nextLong(), random.nextLong())
 				val config = Globals.systemObjects[it]?.create(uuid, it) ?: throw NullPointerException("Tried to create $it system world object, but there is no such object in /system_objects.config!")
 				val pos = randomObjectSpawnPosition()
 
-				if (clock.seconds > lastSpawn + objectSpawnTime && config.moving) {
+				if (clock.time > lastSpawn + objectSpawnTime && config.moving) {
 					// if this is not the last object we're spawning, and it's moving, immediately put it in orbit around a planet
 					val targets = universe.children(systemLocation).filter { child ->
 						entities.values.none { it.orbit.map { it.target == child }.orElse(false) }
@@ -256,7 +256,7 @@ class ServerSystemWorld : SystemWorld {
 
 	// in original engine, ticking happens at 20 updates per second
 	// Since there is no Lua driven code, we can tick as fast as we want
-	suspend fun tick(delta: Double = Starbound.SYSTEM_WORLD_TIMESTEP) {
+	suspend fun tick(delta: Double) {
 		var next = tasks.poll()
 
 		while (next != null) {
@@ -299,17 +299,17 @@ class ServerSystemWorld : SystemWorld {
 
 		private val netVersions = Object2LongOpenHashMap<UUID>()
 
-		suspend fun tick(delta: Double = Starbound.SYSTEM_WORLD_TIMESTEP) {
+		suspend fun tick(delta: Double) {
 			val orbit = destination as? SystemWorldLocation.Orbit
 
 			// if destination is an orbit we haven't started orbiting yet, update the time
 			if (orbit != null)
-				destination = SystemWorldLocation.Orbit(orbit.position.copy(enterTime = clock.seconds))
+				destination = SystemWorldLocation.Orbit(orbit.position.copy(enterTime = clock.time))
 
 			suspend fun nearPlanetOrbit(planet: UniversePos): Orbit {
 				val toShip = planetPosition(planet) - position
 				val angle = toShip.toAngle()
-				return Orbit(planet, 1, clock.seconds, Vector2d(cos(angle), sin(angle)) * (planetSize(planet) / 2.0 + Globals.systemWorld.clientShip.orbitDistance))
+				return Orbit(planet, 1, clock.time, Vector2d(cos(angle), sin(angle)) * (planetSize(planet) / 2.0 + Globals.systemWorld.clientShip.orbitDistance))
 			}
 
 			if (location is SystemWorldLocation.Celestial) {
@@ -331,7 +331,7 @@ class ServerSystemWorld : SystemWorld {
 				val destination: Vector2d
 
 				if (this.orbit != null) {
-					this.orbit = this.orbit!!.copy(enterTime = clock.seconds)
+					this.orbit = this.orbit!!.copy(enterTime = clock.time)
 					destination = orbitPosition(this.orbit!!)
 				} else {
 					destination = this.destination.resolve(this@ServerSystemWorld) ?: position
@@ -446,8 +446,8 @@ class ServerSystemWorld : SystemWorld {
 		var hasExpired = false
 			private set
 
-		suspend fun tick(delta: Double = Starbound.SYSTEM_WORLD_TIMESTEP) {
-			if (!data.permanent && spawnTime > 0.0 && clock.seconds > spawnTime + data.lifeTime)
+		suspend fun tick(delta: Double) {
+			if (!data.permanent && spawnTime > 0.0 && clock.time > spawnTime + data.lifeTime)
 				hasExpired = true
 
 			val orbit = orbit.orNull()
@@ -456,7 +456,7 @@ class ServerSystemWorld : SystemWorld {
 				position = orbitPosition(orbit)
 			} else if (data.permanent || !data.moving) {
 				// permanent locations always have a solar orbit
-				enterOrbit(systemLocation, Vector2d.ZERO, clock.seconds)
+				enterOrbit(systemLocation, Vector2d.ZERO, clock.time)
 			} else if (approach != null) {
 				if (ships.values.any { it.location == location })
 					return
@@ -468,9 +468,9 @@ class ServerSystemWorld : SystemWorld {
 					position += toApproach.unitVector * data.speed * delta
 
 					if ((approach - position).length < planetSize(this.approach!!) + data.orbitDistance)
-						enterOrbit(this.approach!!, approach, clock.seconds)
+						enterOrbit(this.approach!!, approach, clock.time)
 				} else {
-					enterOrbit(approach!!, Vector2d.ZERO, clock.seconds)
+					enterOrbit(approach!!, Vector2d.ZERO, clock.time)
 				}
 			} else {
 				val planets = universe.children(systemLocation).filter { child ->
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorld.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorld.kt
index a12c2177..1851b292 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorld.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorld.kt
@@ -141,7 +141,9 @@ class ServerWorld private constructor(
 	}
 
 	init {
-		eventLoop.scheduleAtFixedRate(::tick, 0L, Starbound.TIMESTEP_NANOS, TimeUnit.NANOSECONDS)
+		eventLoop.scheduleAtFixedRate(Runnable {
+			tick(Starbound.TIMESTEP)
+		}, 0L, Starbound.TIMESTEP_NANOS, TimeUnit.NANOSECONDS)
 	}
 
 	override fun toString(): String {
@@ -243,7 +245,7 @@ class ServerWorld private constructor(
 		return unapplied
 	}
 
-	override fun tick() {
+	override fun tick(delta: Double) {
 		try {
 			if (clients.isEmpty() && isBusy <= 0) {
 				idleTicks++
@@ -259,7 +261,7 @@ class ServerWorld private constructor(
 				return
 			}
 
-			super.tick()
+			super.tick(delta)
 
 			val packet = StepUpdatePacket(ticks)
 
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/util/Clocks.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/util/Clocks.kt
index e11f2611..89e28006 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/util/Clocks.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/util/Clocks.kt
@@ -1,17 +1,13 @@
 package ru.dbotthepony.kstarbound.util
 
-import ru.dbotthepony.kstarbound.Starbound
 import ru.dbotthepony.kstarbound.io.readDouble
 import ru.dbotthepony.kstarbound.io.writeDouble
-import ru.dbotthepony.kstarbound.network.syncher.NetworkedElement
 import java.io.DataInputStream
 import java.io.DataOutputStream
 
 interface IClock {
-	val nanos: Long
-	val micros: Long get() = nanos / 1_000L
-	val millis: Long get() = nanos / 1_000_000L
-	val seconds: Double get() = (nanos / 1_000L) / 1_000_000.0
+	val time: Double
+	val nanos: Long get() = (time * 1_000_000_000.0).toLong()
 }
 
 // this is stupid, but legacy protocol requires it
@@ -28,62 +24,44 @@ class RelativeClock() : IClock {
 		read(stream, isLegacy)
 	}
 
-	private var pointOfReference = 0L
+	private var pointOfReference = 0.0
 	private var pointOfReferenceSet = false
 
-	override var nanos: Long = 0L
+	override var time: Double = 0.0
 		private set
 
-	fun set(age: Long) {
+	fun set(age: Double) {
 		pointOfReferenceSet = false
-		nanos = age
+		time = age
 	}
 
-	fun update(newPointOfReference: Long) {
+	fun update(newPointOfReference: Double) {
 		if (pointOfReferenceSet) {
 			val diff = newPointOfReference - pointOfReference
 
 			if (diff > 0L)
-				nanos += diff
+				time += diff
 		}
 
 		pointOfReference = newPointOfReference
 	}
 
-	fun update(newPointOfReference: Double) {
-		return update((newPointOfReference * 1_000_000_000.0).toLong())
-	}
-
 	fun write(stream: DataOutputStream, isLegacy: Boolean) {
 		stream.writeBoolean(pointOfReferenceSet)
 
-		if (isLegacy) {
-			if (pointOfReferenceSet)
-				stream.writeDouble(pointOfReference / 1_000_000_000.0)
+		if (pointOfReferenceSet)
+			stream.writeDouble(pointOfReference)
 
-			stream.writeDouble(nanos / 1_000_000_000.0)
-		} else {
-			if (pointOfReferenceSet)
-				stream.writeLong(pointOfReference)
-
-			stream.writeLong(nanos)
-		}
+		stream.writeDouble(time)
 	}
 
 	fun read(stream: DataInputStream, isLegacy: Boolean) {
 		pointOfReferenceSet = stream.readBoolean()
 
-		if (isLegacy) {
-			if (pointOfReferenceSet)
-				pointOfReference = (stream.readDouble() * 1_000_000_000.0).toLong()
+		if (pointOfReferenceSet)
+			pointOfReference = stream.readDouble()
 
-			nanos = (stream.readDouble() * 1_000_000_000.0).toLong()
-		} else {
-			if (pointOfReferenceSet)
-				pointOfReference = stream.readLong()
-
-			nanos = stream.readLong()
-		}
+		time = stream.readDouble()
 	}
 }
 
@@ -116,8 +94,8 @@ class JVMClock : IClock {
 		}
 	}
 
-	override val nanos: Long
-		get() = if (isPaused) baseline else (System.nanoTime() - origin) + baseline
+	override val time: Double
+		get() = if (isPaused) (baseline / 1_000_000_000.0) else ((System.nanoTime() - origin) + baseline) / 1_000_000_000.0
 }
 
 class GameTimer(val time: Double = 0.0) {
@@ -153,12 +131,12 @@ class GameTimer(val time: Double = 0.0) {
 		timer = time - timer
 	}
 
-	fun tick(delta: Double = Starbound.TIMESTEP): Boolean {
+	fun tick(delta: Double): Boolean {
 		timer = (timer - delta).coerceAtLeast(0.0)
 		return timer == 0.0
 	}
 
-	fun wrapTick(delta: Double = Starbound.TIMESTEP): Boolean {
+	fun wrapTick(delta: Double): Boolean {
 		val result = tick(delta)
 		if (result) reset()
 		return result
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/Chunk.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/Chunk.kt
index c963ed55..e9d9e49d 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/Chunk.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/Chunk.kt
@@ -270,7 +270,7 @@ abstract class Chunk<WorldType : World<WorldType, This>, This : Chunk<WorldType,
 
 	}
 
-	open fun tick() {
+	open fun tick(delta: Double) {
 
 	}
 }
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/Sky.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/Sky.kt
index 0848e1fb..fd72eab2 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/Sky.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/Sky.kt
@@ -7,14 +7,13 @@ import ru.dbotthepony.kommons.util.getValue
 import ru.dbotthepony.kommons.util.setValue
 import ru.dbotthepony.kommons.vector.Vector2d
 import ru.dbotthepony.kstarbound.Globals
-import ru.dbotthepony.kstarbound.Starbound
 import ru.dbotthepony.kstarbound.defs.world.FlyingType
 import ru.dbotthepony.kstarbound.defs.world.SkyGlobalConfig
 import ru.dbotthepony.kstarbound.defs.world.SkyParameters
 import ru.dbotthepony.kstarbound.defs.world.SkyType
 import ru.dbotthepony.kstarbound.defs.world.WarpPhase
-import ru.dbotthepony.kstarbound.network.syncher.NetworkedGroup
 import ru.dbotthepony.kstarbound.network.syncher.MasterElement
+import ru.dbotthepony.kstarbound.network.syncher.NetworkedGroup
 import ru.dbotthepony.kstarbound.network.syncher.networkedBoolean
 import ru.dbotthepony.kstarbound.network.syncher.networkedData
 import ru.dbotthepony.kstarbound.network.syncher.networkedDouble
@@ -74,7 +73,7 @@ class Sky() {
 	var referenceClock: IClock? = null
 		set(value) {
 			field = value
-			time = value?.seconds ?: time
+			time = value?.time ?: time
 		}
 
 	val speedupTime: Double get() {
@@ -192,8 +191,8 @@ class Sky() {
 	}
 
 
-	fun tick(delta: Double = Starbound.TIMESTEP) {
-		time = referenceClock?.seconds ?: (time + delta)
+	fun tick(delta: Double) {
+		time = referenceClock?.time ?: (time + delta)
 		flashTimer = (flashTimer - delta).coerceAtLeast(0.0)
 
 		if (flyingType != FlyingType.NONE) {
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/SystemWorld.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/SystemWorld.kt
index 4d8c89af..cd8267a0 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/SystemWorld.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/SystemWorld.kt
@@ -149,7 +149,7 @@ abstract class SystemWorld(val location: Vector3i, val clock: JVMClock, val univ
 		val interval = orbitInterval(distance, coordinate.isSatellite)
 
 		val start = random.nextFloat().toDouble()
-		val offset = (clock.seconds % interval) / interval
+		val offset = (clock.time % interval) / interval
 		val direction = if (random.nextFloat() > 0.5f) 1 else -1
 		val angle = (start + direction * offset) * PI * 2.0
 		return parentPosition + Vector2d(cos(angle) * distance, sin(angle) * distance)
@@ -159,7 +159,7 @@ abstract class SystemWorld(val location: Vector3i, val clock: JVMClock, val univ
 		val targetPosition = if (orbit.target.isPlanet || orbit.target.isSatellite) planetPosition(orbit.target) else Vector2d.ZERO
 		val distance = orbit.enterPosition.length
 		val interval = orbitInterval(distance, false)
-		val timeOffset = ((clock.seconds - orbit.enterTime) % interval) / interval
+		val timeOffset = ((clock.time - orbit.enterTime) % interval) / interval
 		val angle = (orbit.enterPosition * -1).toAngle() + orbit.direction * timeOffset * PI * 2.0
 		return targetPosition + Vector2d(cos(angle) * distance, sin(angle) * distance)
 	}
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/TileHealth.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/TileHealth.kt
index 0d2bf721..be3bb152 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/TileHealth.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/TileHealth.kt
@@ -7,7 +7,6 @@ import ru.dbotthepony.kommons.io.writeStruct2f
 import ru.dbotthepony.kommons.util.getValue
 import ru.dbotthepony.kommons.util.setValue
 import ru.dbotthepony.kommons.vector.Vector2d
-import ru.dbotthepony.kstarbound.Starbound
 import ru.dbotthepony.kstarbound.defs.tile.TileDamage
 import ru.dbotthepony.kstarbound.defs.tile.TileDamageConfig
 import ru.dbotthepony.kstarbound.defs.tile.TileDamageType
@@ -98,7 +97,7 @@ sealed class TileHealth() {
 	val isTicking: Boolean
 		get() = !isHealthy && !isDead
 
-	fun tick(config: TileDamageConfig, delta: Double = Starbound.TIMESTEP): Boolean {
+	fun tick(config: TileDamageConfig, delta: Double): Boolean {
 		if (isDead || isHealthy)
 			return false
 
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt
index 069bb8c6..e0cc2fd0 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt
@@ -275,19 +275,19 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
 		broadcast(SetPlayerStartPacket(position, respawnInWorld))
 	}
 
-	open fun tick() {
+	open fun tick(delta: Double) {
 		ticks++
 
 		Starbound.EXECUTOR.submit(ParallelPerform(dynamicEntities.spliterator(), {
 			if (!it.isRemote) {
-				it.movement.move()
+				it.movement.move(delta)
 			}
 		})).join()
 
 		entityList.forEach {
 			try {
 				if (it.isInWorld) // entities might remove other entities during tick
-					it.tick()
+					it.tick(delta)
 			} catch (err: Throwable) {
 				if (it.isRemote && isServer) {
 					LOGGER.error("Exception ticking client spawned entity $it, removing", err)
@@ -299,9 +299,9 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
 		}
 
 		for (chunk in chunkMap.chunks())
-			chunk.tick()
+			chunk.tick(delta)
 
-		sky.tick()
+		sky.tick(delta)
 	}
 
 	protected abstract fun chunkFactory(pos: ChunkPos): ChunkType
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/AbstractEntity.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/AbstractEntity.kt
index d1c002ef..0aa74bff 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/AbstractEntity.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/AbstractEntity.kt
@@ -172,11 +172,11 @@ abstract class AbstractEntity(path: String) : JsonDriven(path), Comparable<Abstr
 
 	var isRemote: Boolean = false
 
-	open fun tick() {
+	open fun tick(delta: Double) {
 		mailbox.executeQueuedTasks()
 
 		if (networkGroup.upstream.isInterpolating) {
-			networkGroup.upstream.tickInterpolation(Starbound.TIMESTEP)
+			networkGroup.upstream.tickInterpolation(delta)
 		}
 	}
 
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/ActorMovementController.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/ActorMovementController.kt
index c8d5ecc2..3cc1ce83 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/ActorMovementController.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/ActorMovementController.kt
@@ -5,7 +5,6 @@ import ru.dbotthepony.kommons.util.KOptional
 import ru.dbotthepony.kommons.util.getValue
 import ru.dbotthepony.kommons.util.setValue
 import ru.dbotthepony.kommons.vector.Vector2d
-import ru.dbotthepony.kstarbound.Starbound
 import ru.dbotthepony.kstarbound.defs.ActorMovementParameters
 import ru.dbotthepony.kstarbound.defs.JumpProfile
 import ru.dbotthepony.kstarbound.defs.MovementParameters
@@ -204,7 +203,7 @@ class ActorMovementController() : MovementController() {
 		controlMovementModifiers = ActorMovementModifiers.EMPTY
 	}
 
-	override fun move() {
+	override fun move(delta: Double) {
 		// TODO: anchor entity
 
 		if (anchorEntity?.isInWorld != true)
@@ -222,8 +221,8 @@ class ActorMovementController() : MovementController() {
 			isGroundMovement = false
 			isLiquidMovement = false
 
-			velocity = (anchorEntity.position - position) / Starbound.TIMESTEP
-			super.move()
+			velocity = (anchorEntity.position - position) / delta
+			super.move(delta)
 			position = anchorEntity.position
 		} else {
 			val movementParameters = actorMovementParameters.merge(controlActorMovementParameters)
@@ -267,15 +266,15 @@ class ActorMovementController() : MovementController() {
 
 			targetHorizontalAmbulatingVelocity = 0.0
 
-			rotation = (rotation + controlRotationRate * Starbound.TIMESTEP) % (PI * 2.0)
-			velocity += controlAcceleration * Starbound.TIMESTEP + controlForce / mass * Starbound.TIMESTEP
+			rotation = (rotation + controlRotationRate * delta) % (PI * 2.0)
+			velocity += controlAcceleration * delta + controlForce / mass * delta
 
 			approachVelocities.forEach {
-				approachVelocity(it.target, it.maxControlForce)
+				approachVelocity(it.target, it.maxControlForce, delta)
 			}
 
 			approachVelocityAngles.forEach {
-				approachVelocityAlongAngle(it.alongAngle, it.targetVelocity, it.maxControlForce, it.positiveOnly)
+				approachVelocityAlongAngle(it.alongAngle, it.targetVelocity, it.maxControlForce, delta, it.positiveOnly)
 			}
 
 			isLiquidMovement = liquidPercentage >= (actorMovementParameters.minimumLiquidPercentage ?: 0.0)
@@ -291,9 +290,9 @@ class ActorMovementController() : MovementController() {
 					flyVelocity = flyVelocity.unitVector * (actorMovementParameters.flySpeed ?: 0.0)
 
 				if (isLiquidMovement)
-					approachVelocity(flyVelocity * (1.0 - liquidImpedance) * movementModifiers.speedModifier, (movementParameters.liquidForce ?: 0.0) * movementModifiers.liquidMovementModifier)
+					approachVelocity(flyVelocity * (1.0 - liquidImpedance) * movementModifiers.speedModifier, (movementParameters.liquidForce ?: 0.0) * movementModifiers.liquidMovementModifier, delta)
 				else
-					approachVelocity(flyVelocity * movementModifiers.speedModifier, movementParameters.airForce ?: 0.0)
+					approachVelocity(flyVelocity * movementModifiers.speedModifier, movementParameters.airForce ?: 0.0, delta)
 
 				if (flyVelocity.x > 0.0)
 					updatedMovingDirection = Direction.RIGHT
@@ -325,7 +324,7 @@ class ActorMovementController() : MovementController() {
 				val maxGroundSustain = movementParameters.groundMovementMaximumSustain ?: 0.0
 				val groundCheckDistance = movementParameters.groundMovementCheckDistance ?: 0.0
 
-				groundMovementSustainTimer.tick()
+				groundMovementSustainTimer.tick(delta)
 
 				if (isOnGround) {
 					groundMovementSustainTimer = GameTimer(maxGroundSustain)
@@ -365,13 +364,13 @@ class ActorMovementController() : MovementController() {
 					velocity = velocity.copy(y = velocity.y + (jumpProfile.jumpSpeed ?: 0.0) * (jumpProfile.jumpInitialPercentage ?: 0.0) * jumpModifier)
 					groundMovementSustainTimer = GameTimer(0.0)
 				} else if (holdJump) {
-					reJumpTimer.tick()
-					jumpHoldTimer?.tick()
+					reJumpTimer.tick(delta)
+					jumpHoldTimer?.tick(delta)
 
-					approachYVelocity((jumpProfile.jumpSpeed ?: 0.0) * jumpModifier, (jumpProfile.jumpControlForce ?: 0.0) * jumpModifier)
+					approachYVelocity((jumpProfile.jumpSpeed ?: 0.0) * jumpModifier, (jumpProfile.jumpControlForce ?: 0.0) * jumpModifier, delta)
 				} else {
 					isJumping = false
-					reJumpTimer.tick()
+					reJumpTimer.tick(delta)
 				}
 
 				if (controlMove == Direction.LEFT) {
@@ -397,7 +396,7 @@ class ActorMovementController() : MovementController() {
 					else
 						movementParameters.airForce ?: 0.0
 
-					approachXVelocity(targetHorizontalAmbulatingVelocity + surfaceVelocity.x, ambulatingAccelerate)
+					approachXVelocity(targetHorizontalAmbulatingVelocity + surfaceVelocity.x, ambulatingAccelerate, delta)
 				}
 			}
 
@@ -424,7 +423,7 @@ class ActorMovementController() : MovementController() {
 			isFlying = controlFly != null
 			isFalling = (velocity.y < (movementParameters.fallStatusSpeedMin ?: 0.0)) && !isGroundMovement
 
-			super.move()
+			super.move(delta)
 
 			lastControlDown = controlDown
 			lastControlJump = controlJump
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/Animator.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/Animator.kt
index d8a63c59..7f4ad739 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/Animator.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/Animator.kt
@@ -23,8 +23,8 @@ import ru.dbotthepony.kstarbound.math.Interpolator
 import ru.dbotthepony.kstarbound.math.PeriodicFunction
 import ru.dbotthepony.kstarbound.math.approachAngle
 import ru.dbotthepony.kstarbound.network.syncher.InternedStringCodec
-import ru.dbotthepony.kstarbound.network.syncher.NetworkedGroup
 import ru.dbotthepony.kstarbound.network.syncher.NetworkedElement
+import ru.dbotthepony.kstarbound.network.syncher.NetworkedGroup
 import ru.dbotthepony.kstarbound.network.syncher.NetworkedList
 import ru.dbotthepony.kstarbound.network.syncher.NetworkedMap
 import ru.dbotthepony.kstarbound.network.syncher.NetworkedSignal
@@ -34,18 +34,15 @@ import ru.dbotthepony.kstarbound.network.syncher.networkedColor
 import ru.dbotthepony.kstarbound.network.syncher.networkedEventCounter
 import ru.dbotthepony.kstarbound.network.syncher.networkedFixedPoint
 import ru.dbotthepony.kstarbound.network.syncher.networkedFloat
-import ru.dbotthepony.kstarbound.network.syncher.networkedList
 import ru.dbotthepony.kstarbound.network.syncher.networkedPointer
 import ru.dbotthepony.kstarbound.network.syncher.networkedSignedInt
 import ru.dbotthepony.kstarbound.network.syncher.networkedString
 import ru.dbotthepony.kstarbound.network.syncher.networkedUnsignedInt
 import ru.dbotthepony.kstarbound.util.random.random
-import ru.dbotthepony.kstarbound.world.positiveModulo
-import java.util.Collections
+import java.util.*
 import java.util.function.Consumer
 import kotlin.math.atan2
 import kotlin.math.cos
-import kotlin.math.roundToInt
 import kotlin.math.sin
 import kotlin.math.sqrt
 
@@ -590,7 +587,7 @@ class Animator() {
 
 	// TODO: Dynamic target
 	@Suppress("Name_Shadowing")
-	fun tick(delta: Double = Starbound.TIMESTEP) {
+	fun tick(delta: Double) {
 		val delta = delta * animationRate
 
 		for (state in stateTypes.values) {
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/DynamicEntity.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/DynamicEntity.kt
index 53f4a0f0..e6426317 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/DynamicEntity.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/DynamicEntity.kt
@@ -30,8 +30,8 @@ abstract class DynamicEntity(path: String) : AbstractEntity(path) {
 
 	private var fixturesChangeset = -1
 
-	override fun tick() {
-		super.tick()
+	override fun tick(delta: Double) {
+		super.tick(delta)
 
 		if (isRemote && networkGroup.upstream.isInterpolating) {
 			movement.updateFixtures()
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/ItemDropEntity.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/ItemDropEntity.kt
index 6888071c..0f42558a 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/ItemDropEntity.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/ItemDropEntity.kt
@@ -111,7 +111,7 @@ class ItemDropEntity() : DynamicEntity("/") {
 	fun take(by: AbstractEntity): ItemStack {
 		if (canTake) {
 			state = State.TAKEN
-			age.set(0L)
+			age.set(0.0)
 			owningEntity = by.entityID
 			return item.copy()
 		}
@@ -124,8 +124,8 @@ class ItemDropEntity() : DynamicEntity("/") {
 	override val metaBoundingBox: AABB
 		get() = AABB(position - Vector2d(0.5, 0.5), position + Vector2d(0.5, 0.5))
 
-	override fun tick() {
-		super.tick()
+	override fun tick(delta: Double) {
+		super.tick(delta)
 
 		if (!isRemote) {
 			if (item.isEmpty) {
@@ -138,7 +138,7 @@ class ItemDropEntity() : DynamicEntity("/") {
 			if (state != State.TAKEN)
 				age.update(world.sky.time)
 			else if (stayAliveFor > 0.0) {
-				stayAliveFor -= Starbound.TIMESTEP
+				stayAliveFor -= delta
 
 				if (stayAliveFor <= 0.0) {
 					state = State.DEAD
@@ -157,10 +157,10 @@ class ItemDropEntity() : DynamicEntity("/") {
 					remove(RemovalReason.REMOVED)
 				} else if (stayAliveFor == -1.0) {
 					val diff = world.geometry.diff(entity.position, position)
-					movement.approachVelocity(diff.unitVector * Globals.itemDrop.velocity, Globals.itemDrop.velocityApproach)
+					movement.approachVelocity(diff.unitVector * Globals.itemDrop.velocity, Globals.itemDrop.velocityApproach, delta)
 
 					if (diff.length < Globals.itemDrop.pickupDistance) {
-						stayAliveFor = 0.05 // stay alive a little longer so pickup "animation" doesn't get cut off early
+						stayAliveFor = Starbound.TIMESTEP * 4.0 // stay alive a little longer so pickup "animation" doesn't get cut off early
 					}
 				}
 
@@ -190,7 +190,7 @@ class ItemDropEntity() : DynamicEntity("/") {
 						}
 
 						entity.item.size -= diff
-						age.set(min(age.nanos, entity.age.nanos))
+						age.set(min(age.time, entity.age.time))
 
 						// Average the position and velocity of the drop we merged with
 						//movement.position += world.geometry.diff(movement.position, entity.movement.position) / 2.0
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/MovementController.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/MovementController.kt
index bb1766e0..67fdf788 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/MovementController.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/MovementController.kt
@@ -15,7 +15,6 @@ import ru.dbotthepony.kommons.util.setValue
 import ru.dbotthepony.kommons.vector.Vector2d
 import ru.dbotthepony.kommons.vector.times
 import ru.dbotthepony.kstarbound.Globals
-import ru.dbotthepony.kstarbound.Starbound
 import ru.dbotthepony.kstarbound.defs.MovementParameters
 import ru.dbotthepony.kstarbound.math.Interpolator
 import ru.dbotthepony.kstarbound.network.syncher.NetworkedGroup
@@ -29,7 +28,6 @@ import ru.dbotthepony.kstarbound.world.World
 import ru.dbotthepony.kstarbound.world.physics.CollisionPoly
 import ru.dbotthepony.kstarbound.world.physics.CollisionType
 import ru.dbotthepony.kstarbound.world.physics.Poly
-import java.util.stream.Stream
 import kotlin.math.PI
 import kotlin.math.absoluteValue
 import kotlin.math.acos
@@ -219,7 +217,7 @@ open class MovementController() {
 
 	}
 
-	fun approachVelocity(targetVelocity: Vector2d, maxControlForce: Double) {
+	fun approachVelocity(targetVelocity: Vector2d, maxControlForce: Double, delta: Double) {
 		// Instead of applying the force directly, work backwards and figure out the
 		// maximum acceleration that could be achieved by the current control force,
 		// and maximize the change in velocity based on that.
@@ -229,13 +227,13 @@ open class MovementController() {
 
 		if (mag == 0.0) return
 
-		val maximumAcceleration = maxControlForce / mass * Starbound.TIMESTEP
+		val maximumAcceleration = maxControlForce / mass * delta
 		val clampedMag = mag.coerceIn(0.0, maximumAcceleration)
 
 		velocity += diff * (clampedMag / mag)
 	}
 
-	fun approachVelocityAlongAngle(angle: Double, targetVelocity: Double, maxControlForce: Double, positiveOnly: Boolean = false) {
+	fun approachVelocityAlongAngle(angle: Double, targetVelocity: Double, maxControlForce: Double, delta: Double, positiveOnly: Boolean = false) {
 		// Same strategy as approachVelocity, work backwards to figure out the
 		// maximum acceleration and apply that.
 
@@ -249,26 +247,26 @@ open class MovementController() {
 
 		if (diff == 0.0 || positiveOnly && diff < 0.0) return
 
-		val maximumAcceleration = maxControlForce / mass * Starbound.TIMESTEP
+		val maximumAcceleration = maxControlForce / mass * delta
 		val diffMag = diff.absoluteValue
 		val clampedMag = diffMag.coerceIn(0.0, maximumAcceleration)
 
 		velocity += axis * diff * (clampedMag / diffMag)
 	}
 
-	fun approachXVelocity(velocity: Double, maxControlForce: Double) {
-		approachVelocityAlongAngle(0.0, velocity, maxControlForce)
+	fun approachXVelocity(velocity: Double, maxControlForce: Double, delta: Double) {
+		approachVelocityAlongAngle(0.0, velocity, maxControlForce, delta)
 	}
 
-	fun approachYVelocity(velocity: Double, maxControlForce: Double) {
-		approachVelocityAlongAngle(PI / 2.0, velocity, maxControlForce)
+	fun approachYVelocity(velocity: Double, maxControlForce: Double, delta: Double) {
+		approachVelocityAlongAngle(PI / 2.0, velocity, maxControlForce, delta)
 	}
 
 	/**
 	 * this function is executed in parallel
 	 */
 	// TODO: Ghost collisions occur, where objects trip on edges
-	open fun move() {
+	open fun move(delta: Double) {
 		isZeroGravity = isGravityDisabled || gravityMultiplier == 0.0 || determineGravity().lengthSquared == 0.0
 
 		val movementParameters = movementParameters
@@ -281,7 +279,7 @@ open class MovementController() {
 		// TODO: Here: moving platforms sticky code
 
 		if (movementParameters.collisionPoly == null || !movementParameters.collisionPoly.map({ true }, { it.isNotEmpty() }) || movementParameters.collisionEnabled != true) {
-			position += velocity * Starbound.TIMESTEP
+			position += velocity * delta
 			surfaceSlope = Vector2d.POSITIVE_Y
 			surfaceVelocity = Vector2d.ZERO
 			isOnGround = false
@@ -295,14 +293,14 @@ open class MovementController() {
 		var steps = 1
 
 		movementParameters.maxMovementPerStep?.let {
-			steps = (velocity.length * Starbound.TIMESTEP / it).toInt() + 1
+			steps = (velocity.length * delta / it).toInt() + 1
 		}
 
 		var relativeVelocity = velocity
 		surfaceSlope = Vector2d.POSITIVE_Y
 		// TODO: Here: moving platforms sticky code
 
-		val dt = Starbound.TIMESTEP / steps
+		val dt = delta / steps
 
 		for (step in 0 until steps) {
 			val velocityMagnitude = relativeVelocity.length
@@ -384,14 +382,14 @@ open class MovementController() {
 						// independently).
 
 						if (relativeVelocity.x < 0.0 && correction.x > 0.0)
-							relativeVelocity = relativeVelocity.copy(x = (relativeVelocity.x + correction.x / Starbound.TIMESTEP).coerceAtMost(0.0))
+							relativeVelocity = relativeVelocity.copy(x = (relativeVelocity.x + correction.x / dt).coerceAtMost(0.0))
 						else if (relativeVelocity.x > 0.0 && correction.x < 0.0)
-							relativeVelocity = relativeVelocity.copy(x = (relativeVelocity.x + correction.x / Starbound.TIMESTEP).coerceAtLeast(0.0))
+							relativeVelocity = relativeVelocity.copy(x = (relativeVelocity.x + correction.x / dt).coerceAtLeast(0.0))
 
 						if (relativeVelocity.y < 0.0 && correction.y > 0.0)
-							relativeVelocity = relativeVelocity.copy(y = (relativeVelocity.y + correction.y / Starbound.TIMESTEP).coerceAtMost(0.0))
+							relativeVelocity = relativeVelocity.copy(y = (relativeVelocity.y + correction.y / dt).coerceAtMost(0.0))
 						else if (relativeVelocity.y > 0.0 && correction.y < 0.0)
-							relativeVelocity = relativeVelocity.copy(y = (relativeVelocity.y + correction.y / Starbound.TIMESTEP).coerceAtLeast(0.0))
+							relativeVelocity = relativeVelocity.copy(y = (relativeVelocity.y + correction.y / dt).coerceAtLeast(0.0))
 					}
 				}
 			}
@@ -410,7 +408,7 @@ open class MovementController() {
 		if (!isZeroGravity && stickingDirection == null) {
 			val buoyancy = (movementParameters.liquidBuoyancy ?: 0.0).coerceIn(0.0, 1.0) + liquidPercentage + (movementParameters.airBuoyancy ?: 0.0).coerceIn(0.0, 1.0) * (1.0 - liquidPercentage)
 			val gravity = determineGravity() * (movementParameters.gravityMultiplier ?: 1.0) * (1.0 - buoyancy)
-			var environmentVelocity = gravity * Starbound.TIMESTEP
+			var environmentVelocity = gravity * delta
 
 			if (isOnGround && (movementParameters.slopeSlidingFactor ?: 0.0) != 0.0 && surfaceSlope != Vector2d.ZERO)
 				environmentVelocity += -surfaceSlope * (surfaceSlope.x * surfaceSlope.y) * (movementParameters.slopeSlidingFactor ?: 0.0)
@@ -432,7 +430,7 @@ open class MovementController() {
 			// but it is applied here as a multiplicative factor from [0, 1] so it does
 			// not induce oscillation at very high friction and so it cannot be
 			// negative.
-			val frictionFactor = (friction / mass * Starbound.TIMESTEP).coerceIn(0.0, 1.0)
+			val frictionFactor = (friction / mass * delta).coerceIn(0.0, 1.0)
 			newVelocity = linearInterpolation(frictionFactor, newVelocity, refVel)
 		}
 
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/TileEntity.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/TileEntity.kt
index cfadbe8d..c4d6e97f 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/TileEntity.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/TileEntity.kt
@@ -216,8 +216,8 @@ abstract class TileEntity(path: String) : AbstractEntity(path) {
 		}
 	}
 
-	override fun tick() {
-		super.tick()
+	override fun tick(delta: Double) {
+		super.tick(delta)
 
 		if (needToUpdateSpaces) {
 			updateMaterialSpacesNow()
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/WorldObject.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/WorldObject.kt
index ca3a0911..950e0b62 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/WorldObject.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/WorldObject.kt
@@ -394,19 +394,19 @@ open class WorldObject(val config: Registry.Entry<ObjectDefinition>) : TileEntit
 		drawablesCache.invalidate()
 	}
 
-	override fun tick() {
-		super.tick()
+	override fun tick(delta: Double) {
+		super.tick(delta)
 
-		flickerPeriod?.update(Starbound.TIMESTEP, world.random)
+		flickerPeriod?.update(delta, world.random)
 
 		if (!isRemote) {
-			tileHealth.tick(config.value.damageConfig)
-			animator.tick()
+			tileHealth.tick(config.value.damageConfig, delta)
+			animator.tick(delta)
 
 			val orientation = orientation
 
 			if (orientation != null) {
-				frameTimer = (frameTimer + Starbound.TIMESTEP) % orientation.animationCycle
+				frameTimer = (frameTimer + delta) % orientation.animationCycle
 				val oldFrame = frame
 				frame = (frameTimer / orientation.animationCycle * orientation.frames).toInt().coerceIn(0, orientation.frames)