- tidy up player layer
 - no need for smaller web tiles, make the large ones render as pixelated
 - make tile server and websocket server URL not-hardcoded
 - tidy up dynamic tile refreshing
This commit is contained in:
Adam 2022-06-19 10:16:37 +02:00
parent e9a6f99fb8
commit f151027464
9 changed files with 312 additions and 135 deletions

View file

@ -1,10 +1,13 @@
import dev.adamko.kafkatorio.gradle.taskProvider
import org.jetbrains.kotlin.gradle.plugin.KotlinDependencyHandler
import org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsRootExtension
import org.jetbrains.kotlin.gradle.targets.js.webpack.KotlinWebpackConfig
import org.jetbrains.kotlin.gradle.targets.js.yarn.YarnRootExtension
plugins {
// dev.adamko.kafkatorio.lang.`kotlin-js`
dev.adamko.kafkatorio.lang.`kotlin-multiplatform`
dev.adamko.kafkatorio.lang.`kotlin-js`
// dev.adamko.kafkatorio.lang.`kotlin-multiplatform`
kotlin("plugin.serialization")
id("io.kvision")
}
@ -19,13 +22,42 @@ project.ext.set("io.kvision.plugin.enableBackendTasks", false)
kotlin {
js(IR) {
browser()
binaries.executable()
rootProject.extensions.configure<NodeJsRootExtension> {
// https://github.com/rjaros/kvision/issues/410
versions.webpackCli.version = "4.10.0"
versions.webpackDevServer.version = "4.9.2"
}
rootProject.extensions.configure<YarnRootExtension> {
resolution("http-proxy-middleware", "^2.0.6")
}
//rootProject.afterEvaluate {
// extensions.configure<NodeJsRootExtension> {
// // https://github.com/rjaros/kvision/issues/410
// versions.webpackCli.version = "4.10.0"
// }
//}
afterEvaluate {
rootProject.extensions.configure<NodeJsRootExtension> {
// https://github.com/rjaros/kvision/issues/410
versions.webpackCli.version = "4.10.0"
versions.webpackDevServer.version = "4.9.2"
}
js("frontend", IR) {
rootProject.extensions.configure<YarnRootExtension> {
resolution("http-proxy-middleware", "^2.0.6")
}
}
kotlin {
js(IR) {
// browser()
// binaries.executable()
// }
//
// js("frontend", IR) {
// nodejs()
browser {
runTask {
@ -35,13 +67,20 @@ kotlin {
open = false,
port = 3000,
proxy = mutableMapOf(
"/kv/*" to "http://localhost:8080",
"/kvws/*" to mapOf(
"target" to "ws://localhost:8080",
"/tiles" to mapOf(
"target" to "http://localhost:12080",
"secure" to false,
"changeOrigin" to true,
),
"/ws/foo" to mapOf(
"target" to "ws://localhost:12080",
// "secure" to false,
"ws" to true,
)
// "changeOrigin" to true,
),
),
static = mutableListOf("$buildDir/processedResources/frontend/main")
// static = mutableListOf("$buildDir/processedResources/frontend/main")
static = mutableListOf("$buildDir/processedResources/js/main")
)
}
webpackTask {
@ -54,11 +93,14 @@ kotlin {
}
}
binaries.executable()
// compilations["main"].packageJson {
// this.devDependencies["webpack-dev-server"] = "4.9.1"
// }
}
jvm("backend") {
}
// jvm("backend") {
//
// }
sourceSets {
@ -69,7 +111,8 @@ kotlin {
}
}
val commonMain by getting {
// val commonMain by getting {
main {
dependencies {
implementation(dependencies.platform(libs.kotlinx.coroutines.bom))
implementation(dependencies.platform(libs.kotlin.jsWrappers.bom))
@ -79,13 +122,13 @@ kotlin {
implementation(projects.modules.eventsSchema)
implementation(libs.kotlinx.html)
}
}
// }
// }
val jsMain by getting {
dependsOn(commonMain)
dependencies {
// val jsMain by getting {
// dependsOn(commonMain)
//
// dependencies {
implementation(projects.modules.eventsSchema)
@ -108,36 +151,52 @@ kotlin {
implementation(libs.kotlinx.coroutines.core)
implementation(devNpm("http-proxy-middleware", "^2.0.6"))
implementation(dependencies.platform(devNpm("http-proxy-middleware", "^2.0.6")))
// implementation(dependencies.platform(npm("follow-redirects", "^1.14.8")))
// implementation(dependencies.platform(npm("nanoid", "^3.1.31")))
// implementation(dependencies.platform(npm("minimist", "^1.2.6")))
// implementation(dependencies.platform(npm("async", "^2.6.4")))
// implementation(dependencies.platform(npm("node-forge", "^1.3.0")))
// implementation(dependencies.platform(npm("socket.io", "^4.5.1")))
// implementation(dependencies.platform(devNpm("http-proxy-middleware", "^3.0.0-beta.0")))
// implementation(dependencies.platform(devNpm("webpack-dev-server", "4.9.1")))
}
// val webDir = file("src/main/web")
resources.srcDir("src/main/web")
resources.srcDir("src/main/resources")
kotlin.srcDir("src/main/kotlin")
}
val frontendMain by getting {
dependsOn(jsMain)
// resources.srcDir("src/main/web")
// resources.srcDir("src/main/resources")
// kotlin.srcDir("src/main/kotlin")
}
val jsTest by getting {
// val frontendMain by getting {
// dependsOn(jsMain)
//// resources.srcDir("src/main/web")
//// resources.srcDir("src/main/resources")
// }
//
// val jsTest by getting {
// dependencies {
// implementation(kotlin("test-js"))
//
// kvision("kvision-testutils")
//
//// implementation(npm("karma", "^6.3.16"))
// }
//
// kotlin.srcDir("src/test/kotlin")
// }
test {
dependencies {
implementation(kotlin("test-js"))
kvision("kvision-testutils")
// implementation(npm("karma", "^6.3.16"))
// implementation(npm("karma", "^6.3.16"))
}
kotlin.srcDir("src/test/kotlin")
}
}
}
@ -152,17 +211,42 @@ fun KotlinDependencyHandler.kvision(
implementation("io.kvision:$module:${version.get()}", configure)
}
// https://youtrack.jetbrains.com/issue/KT-42420
////// https://youtrack.jetbrains.com/issue/KT-42420
//afterEvaluate {
// yarn {
// resolution("mocha", "9.2.2")
// resolution("follow-redirects", "1.14.8")
// resolution("nanoid", "3.1.31")
// resolution("minimist", "1.2.6")
// resolution("async", "2.6.4")
// resolution("node-forge", "1.3.0")
//// resolution("mocha", "9.2.2")
//// resolution("follow-redirects", "1.14.8")
//// resolution("nanoid", "3.1.31")
//// resolution("minimist", "1.2.6")
//// resolution("async", "2.6.4")
//// resolution("node-forge", "1.3.0")
//// resolution("socket.io", "^4.5.1")
//// resolution("http-proxy-middleware", "^3.0.0-beta.0")
// resolution("webpack-dev-server", "4.9.1")
// }
//}
evaluationDependsOn(rootProject.path)
evaluationDependsOn(projects.modules.eventsSchema.dependencyProject.path)
//evaluationDependsOn(rootProject.path)
//evaluationDependsOn(projects.modules.eventsSchema.dependencyProject.path)
// stop warnings when building
tasks.configureEach {
when (name) {
"productionExecutableCompileSync" ->
dependsOn(taskProvider("compileProductionExecutableKotlinJs"))
"testTestDevelopmentExecutableCompileSync" ->
dependsOn(taskProvider("compileTestDevelopmentExecutableKotlinJs"))
// "jsTestTestDevelopmentExecutableCompileSync" ->
// dependsOn(taskProvider("compileTestDevelopmentExecutableKotlinFrontend"))
// "jsProductionExecutableCompileSync" ->
// dependsOn(taskProvider("compileProductionExecutableKotlinFrontend"))
"browserDevelopmentRun" ->
dependsOn(taskProvider("browserDevelopmentWebpack"))
"browserDevelopmentWebpack" ->
dependsOn(taskProvider("assemble"))
"browserProductionWebpack" ->
dependsOn(taskProvider("developmentExecutableCompileSync"))
}
}

View file

@ -38,8 +38,7 @@ class App(
wsService: WebsocketService,
) : Application(), CoroutineScope {
override val coroutineContext: CoroutineContext =
CoroutineName("WebMapApp") + Job(rootJob)
override val coroutineContext: CoroutineContext = CoroutineName("WebMapApp")
private var appState: MutableMap<String, Any> = mutableMapOf()
@ -62,7 +61,7 @@ class App(
override fun start(state: Map<String, Any>) {
this.appState = state.toMutableMap()
val root = root("kvapp") {
root("kvapp") {
div("Kafkatorio Web Map")
add(kvMaps)
}
@ -74,18 +73,16 @@ class App(
it.redraw()
}
})
// this.asDynamic()._fadeAnimated = false
}
val doc = root.getElement()?.ownerDocument
val window = doc!!.defaultView!!
wsService.packetsFlow
.filterIsInstance<EventServerPacket.ChunkTileSaved>()
.onEach {
println("[packetsFlow] triggering tile refresh ${it.filename.value}")
gameState.map.refreshUpdatedTilePng(doc, window, it.filename)
gameState.map.refreshUpdatedTilePng(
// doc, window,
it.filename
)
}.launchIn(this)
}
@ -104,8 +101,8 @@ fun main() {
{
val reduxStore = createFactorioReduxStore()
val wsService = WebsocketService(
"ws://localhost:9073/ws",
reduxStore,
// "ws://localhost:9073/ws", // TODO load from config
reduxStore = reduxStore,
)
App(
reduxStore = reduxStore,

View file

@ -0,0 +1,12 @@
package dev.adamko.kafkatorio.webmap
import kotlinx.browser.window
object Props {
@Suppress("HttpUrlsUsage")
val websocketServerUrl: String
get() = window.location.href
.replace("http://", "ws://")
.replace("https://", "wss://") +
"ws/foo"
}

View file

@ -0,0 +1,33 @@
package dev.adamko.kafkatorio.webmap.externals
import io.kvision.maps.externals.leaflet.DoneCallback
import io.kvision.maps.externals.leaflet.geo.LatLng
import io.kvision.maps.externals.leaflet.layer.tile.TileLayer
import io.kvision.maps.externals.leaflet.layer.vector.Polyline
import org.w3c.dom.HTMLElement
inline var TileLayer<TileLayer.TileLayerOptions>.tileOnLoad: TileOnLoadFn?
get() = asDynamic()._tileOnLoad as TileOnLoadFn
set(value) {
asDynamic()._tileOnLoad = value
}
typealias TileOnLoadFn = (done: DoneCallback, tile: HTMLElement) -> Unit
sealed interface PolylinePathPoints {
value class D1(val points: Array<LatLng>) : PolylinePathPoints
value class D2(val points: Array<Array<LatLng>>) : PolylinePathPoints
value class D3(val points: Array<Array<Array<LatLng>>>) : PolylinePathPoints
}
inline fun Polyline<*>.getLatLngs2(): PolylinePathPoints? {
return when (val points = this.getLatLngs()) {
is Array<LatLng> -> PolylinePathPoints.D1(points as Array<LatLng>)
is Array<Array<LatLng>> -> PolylinePathPoints.D2(points as Array<Array<LatLng>>)
is Array<Array<Array<LatLng>>> -> PolylinePathPoints.D3(points as Array<Array<Array<LatLng>>>)
else -> null
}
}

View file

@ -1,6 +1,8 @@
package dev.adamko.kafkatorio.webmap
import dev.adamko.kafkatorio.schema.common.TilePngFilename
import dev.adamko.kafkatorio.webmap.externals.TileOnLoadFn
import dev.adamko.kafkatorio.webmap.externals.tileOnLoad
import io.kvision.maps.Maps
import io.kvision.maps.Maps.Companion.L
import io.kvision.maps.externals.leaflet.DoneCallback
@ -11,15 +13,15 @@ import io.kvision.maps.externals.leaflet.geo.CRS
import io.kvision.maps.externals.leaflet.geo.LatLng
import io.kvision.maps.externals.leaflet.layer.LayerGroup
import io.kvision.maps.externals.leaflet.layer.tile.TileLayer
import io.kvision.utils.obj
import io.kvision.utils.px
import kotlinx.browser.document
import kotlinx.browser.window
import kotlinx.coroutines.await
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
import org.w3c.dom.Document
import kotlinx.js.jso
import org.w3c.dom.HTMLElement
import org.w3c.dom.Image
import org.w3c.dom.Window
import org.w3c.dom.asList
import org.w3c.fetch.NO_CORS
import org.w3c.fetch.RELOAD
@ -29,7 +31,7 @@ import org.w3c.fetch.RequestMode
class FactorioMap(
private val tileUrlTemplate: String = """http://localhost:9073/tiles/s1/z{z}/x{x}/y{y}.png""",
private val tileUrlTemplate: String = """/tiles/s1/z{z}/x{x}/y{y}.png""",
) {
val kvMap: Maps = Maps {
@ -38,21 +40,24 @@ class FactorioMap(
margin = 10.px
}
val factorioTerrainLayer: TileLayer<*> = buildFactorioTerrainLayer()
private val factorioTerrainLayer: TileLayer<*> = buildFactorioTerrainLayer()
val playerIconsLayer: LayerGroup = LayerGroup()
init {
val baseLayers: Control.LayersObject = obj<Control.LayersObject> {}
baseLayers["Terrain"] = factorioTerrainLayer
val overlays: Control.LayersObject = obj<Control.LayersObject> {}
overlays["Players"] = playerIconsLayer
val baseLayers: Control.LayersObject = jso {
set("Terrain", factorioTerrainLayer)
}
val overlays: Control.LayersObject = jso {
set("Players", playerIconsLayer)
}
val layersControl = Layers(
baseLayers = baseLayers,
overlays = overlays,
options = obj<Layers.LayersOptions> {
options = jso {
sortLayers = true
}
)
@ -78,21 +83,19 @@ class FactorioMap(
suspend fun refreshUpdatedTilePng(
document: Document,
window: Window,
tilePngFilename: TilePngFilename,
): Unit = coroutineScope {
launch {
document
.querySelectorAll("img.leaflet-tile-loaded")
.asList()
.filterIsInstance<Image>()
.filter { img ->
tilePngFilename.value in img.src
}
.forEach { img ->
val imgSrc = img.src //.substringBeforeLast('?')
document
.querySelectorAll("img.leaflet-tile-loaded")
.asList()
.filterIsInstance<Image>()
.filter { img ->
tilePngFilename.value in img.src
}
.forEach { img ->
val imgSrc = img.src
launch {
window.fetch(
imgSrc,
RequestInit(
@ -100,24 +103,14 @@ class FactorioMap(
mode = RequestMode.NO_CORS,
)
).then {
println("[refreshUpdatedTilePng] updating $imgSrc")
img.setAttribute(DYNAMIC_RELOAD_ATT, "true")
img.src = imgSrc
window.requestAnimationFrame {
println("[refreshUpdatedTilePng] updating $imgSrc")
img.setAttribute(DYNAMIC_RELOAD_ATT, "true")
img.src = imgSrc
}
}.await()
}
}
//// println("fetching $imgSrc")
// val newImg = Image()
// newImg.onload = {
//// window.requestAnimationFrame {
//// println("image loaded ${newImg!!.src}")
// img.setAttribute(DYNAMIC_RELOAD_ATT, "true")
// img.src = newImg.src
// Unit
//// }
// }
// newImg.src = imgSrc //+ "?t=${currentTimeMillis()}"
}
}
private fun buildFactorioTerrainLayer(): TileLayer<TileLayer.TileLayerOptions> {
@ -132,7 +125,7 @@ class FactorioMap(
minZoom = -2
maxZoom = 6
maxNativeZoom = 3
maxNativeZoom = -1
minNativeZoom = -1
noWrap = true
@ -140,13 +133,14 @@ class FactorioMap(
updateWhenZooming = true
}
val currentTileOnLoad: (DoneCallback, HTMLElement) -> Unit =
baseTileLayer.asDynamic()._tileOnLoad as (DoneCallback, HTMLElement) -> Unit
baseTileLayer.asDynamic()._tileOnLoad = { done: DoneCallback, tile: HTMLElement ->
if (tile.hasAttribute(DYNAMIC_RELOAD_ATT)) {
val currentTileOnLoad: TileOnLoadFn? = baseTileLayer.tileOnLoad
if (currentTileOnLoad != null) {
baseTileLayer.tileOnLoad = { done: DoneCallback, tile: HTMLElement ->
if (!tile.hasAttribute(DYNAMIC_RELOAD_ATT)) {
currentTileOnLoad(done, tile)
}
tile.removeAttribute(DYNAMIC_RELOAD_ATT)
} else {
currentTileOnLoad(done, tile)
}
}

View file

@ -10,9 +10,8 @@ import dev.adamko.kafkatorio.schema.packets.PlayerUpdate
import dev.adamko.kafkatorio.webmap.latLng
import io.kvision.maps.Maps
import io.kvision.maps.externals.leaflet.geo.LatLng
import io.kvision.maps.externals.leaflet.layer.overlay.Tooltip
import io.kvision.maps.externals.leaflet.layer.vector.CircleMarker
import io.kvision.utils.obj
import kotlinx.js.jso
fun handlePlayerUpdate(
@ -100,7 +99,7 @@ data class PlayerState(
className = "player-marker"
// interactive = false
}.also { circle ->
circle.bindTooltip("", obj<Tooltip.TooltipOptions> {
circle.bindTooltip("", jso {
// interactive = false
})
}

View file

@ -22,10 +22,11 @@ import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.launch
import org.w3c.dom.MessageEvent
import org.w3c.dom.WebSocket
import org.w3c.dom.events.Event
class WebsocketService(
wsUrl: String,
wsUrl: String = Props.websocketServerUrl,
private val reduxStore: ReduxStore<FactorioGameState, FactorioUpdate>,
) : CoroutineScope {
@ -33,6 +34,7 @@ class WebsocketService(
CoroutineName("WebsocketService") + Job(rootJob)
private val ws = WebSocket(wsUrl)
// private val ws = WebSocket("ws://localhost:12080/ws/foo")
private val _packetsFlow = MutableSharedFlow<EventServerPacket>()
val packetsFlow: SharedFlow<EventServerPacket>
@ -40,6 +42,9 @@ class WebsocketService(
init {
ws.onmessage = ::handleMessageEvent
ws.onerror = ::handleError
println("[WebsocketService] init ${ws.url}, ${ws.protocol}")
}
private fun handleMessageEvent(msg: MessageEvent) {
@ -52,35 +57,46 @@ class WebsocketService(
&& data.endsWith("}")
) {
println(data.replace('\n', ' '))
println("[WebsocketService.handleMessageEvent] ${data.replace('\n', ' ')}")
val packet = jsonMapper.decodeFromString(EventServerPacket.serializer(), data)
launch {
_packetsFlow.emit(packet)
val packet: EventServerPacket? = runCatching {
jsonMapper.decodeFromString(EventServerPacket.serializer(), data)
}.getOrElse {
null
}
when (packet) {
is EventServerPacket.ChunkTileSaved -> {}
if (packet != null) {
is EventServerPacket.Kafkatorio ->
when (val packetData = packet.packet.data) {
is PlayerUpdate -> reduxStore.dispatch(
FactorioUpdate.Player(packet.packet.tick, packetData)
)
is ConfigurationUpdate,
is ConsoleChatUpdate,
is ConsoleCommandUpdate,
is PrototypesUpdate,
is SurfaceUpdate,
is EntityUpdate,
is MapChunkUpdate -> {
// to be continued...
launch { _packetsFlow.emit(packet) }
when (packet) {
is EventServerPacket.ChunkTileSaved -> {}
is EventServerPacket.Kafkatorio ->
when (val packetData = packet.packet.data) {
is PlayerUpdate -> reduxStore.dispatch(
FactorioUpdate.Player(packet.packet.tick, packetData)
)
is ConfigurationUpdate,
is ConsoleChatUpdate,
is ConsoleCommandUpdate,
is PrototypesUpdate,
is SurfaceUpdate,
is EntityUpdate,
is MapChunkUpdate -> {
// to be continued...
}
}
}
}
} else {
println("[WebsocketService.handleMessageEvent] unknown json message ${msg.data}")
}
} else {
println(data)
println("[WebsocketService.handleMessageEvent] non-json message ${msg.data}")
}
}
private fun handleError(error: Event) {
println("ws error: ${JSON.stringify(error)}")
}
}

View file

@ -1,8 +1,10 @@
/* https://github.com/Leaflet/Leaflet/issues/3575 */
.leaflet-tile {
width: 257px !important;
height: 257px !important;
box-shadow: 0 0 1px rgba(0, 0, 0, 0.05);
/* https://github.com/Leaflet/Leaflet/issues/3575 */
/*width: 257px !important;*/
/*height: 257px !important;*/
/*box-shadow: 0 0 1px rgba(0, 0, 0, 0.05);*/
image-rendering: pixelated;
}
html {

View file

@ -1,15 +1,55 @@
config.resolve.modules.push("../../processedResources/js/main");
if (config.devServer) {
config.devServer.hot = true;
config.devtool = 'eval-cheap-source-map';
config.devServer.hot = true;
config.devtool = 'eval-cheap-source-map';
config.devServer.host = "0.0.0.0"
} else {
config.devtool = undefined;
config.devtool = undefined;
}
// disable bundle size warning
config.performance = {
assetFilter: function (assetFilename) {
return !assetFilename.endsWith('.js');
},
assetFilter: function (assetFilename) {
return !assetFilename.endsWith('.js');
},
};
// // dev server
// config.devServer = {
// open: false,
// // devMiddleware: {
// // index: false, // specify to enable root proxying
// // },
// port: 3000,
// proxy: {
// // context: () => true,
// // target: "http://localhost:12080",
// // secure: false,
// // ws: true,
// // changeOrigin: true,
//
// // "/tiles": {
// // target: "http://localhost:12080",
// // // "pathRewrite": { "^/tiles": "" },
// // // "logger": "console",
// // // "logLevel": "debug",
// // secure: false,
// // changeOrigin: true,
// // },
// // "/ws": {
// // target: "http://localhost:12080",
// // // pathRewrite: { "^/ws": "" },
// // // "logLevel": "debug",
// // // "logger": "console",
// // secure: false,
// // ws: true,
// // changeOrigin: true,
// // },
// },
// "static": [
// // "../../processedResources/js/main",
// "D:\\Users\\Adam\\Projects\\games\\kafkatorio\\modules\\web-map\\build\\processedResources\\frontend\\main",
// ],
// };