Skip to content

Commit 30ef556

Browse files
committed
working websocket + hashes + other stuff
1 parent c2103b1 commit 30ef556

File tree

12 files changed

+319
-20
lines changed

12 files changed

+319
-20
lines changed

build.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,8 @@ dependencies {
7474
shade("io.ktor:ktor-client-content-negotiation:3.3.1")
7575
implementation("io.ktor:ktor-serialization-kotlinx-json:3.3.1")
7676
shade("io.ktor:ktor-serialization-kotlinx-json:3.3.1")
77+
implementation("io.ktor:ktor-server-websockets:3.3.1")
78+
shade("io.ktor:ktor-server-websockets:3.3.1")
7779
}
7880

7981
tasks {

src/main/java/org/polyfrost/polyplus/client/mixin/Mixin_ReplaceCapeTexture.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
import org.spongepowered.asm.mixin.injection.Inject;
1111
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
1212

13+
import static org.polyfrost.polyplus.network.plus.cache.CosmeticCache.getCosmetic;
14+
1315
@Mixin(NetworkPlayerInfo.class)
1416
public class Mixin_ReplaceCapeTexture {
1517
@Shadow
@@ -20,6 +22,12 @@ public class Mixin_ReplaceCapeTexture {
2022

2123
@Inject(method = "getLocationCape", at = @org.spongepowered.asm.mixin.injection.At("HEAD"))
2224
private void polyplus$onGetCape(CallbackInfoReturnable<ResourceLocation> cir) {
23-
this.locationCape = new ResourceLocation(PolyPlus.ID, "64px_poly.png");
25+
ResourceLocation cape = getCosmetic(gameProfile.getId(), "cape");
26+
if (cape != null) {
27+
System.out.println("Replacing cape for " + gameProfile.getName() + " with " + cape);
28+
this.locationCape = cape;
29+
} else {
30+
System.out.println("No cape found for " + gameProfile.getName());
31+
}
2432
}
2533
}

src/main/kotlin/org/polyfrost/polyplus/PolyPlus.kt

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,22 @@
11
package org.polyfrost.polyplus
22

3+
import com.sun.org.apache.xpath.internal.operations.Plus
4+
import dev.deftu.eventbus.EventBus
35
import io.ktor.client.HttpClient
46
import io.ktor.client.engine.cio.CIO
57
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
68
import io.ktor.client.plugins.defaultRequest
9+
import io.ktor.client.plugins.websocket.WebSockets
710
import io.ktor.http.userAgent
811
import io.ktor.serialization.kotlinx.json.json
912
import kotlinx.coroutines.CoroutineScope
1013
import kotlinx.coroutines.Dispatchers
1114
import kotlinx.coroutines.SupervisorJob
15+
import kotlinx.coroutines.delay
16+
import kotlinx.coroutines.launch
17+
import kotlinx.serialization.json.Json
18+
import kotlinx.serialization.modules.SerializersModule
19+
import kotlinx.serialization.modules.polymorphic
1220
import org.polyfrost.oneconfig.api.commands.v1.CommandManager
1321
import org.polyfrost.polyplus.client.ExampleCommand
1422
import org.polyfrost.polyplus.client.Config
@@ -31,6 +39,14 @@ import java.util.logging.Logger
3139
//#else
3240
import net.minecraftforge.fml.common.Mod
3341
import net.minecraftforge.fml.common.event.FMLInitializationEvent
42+
import org.polyfrost.oneconfig.api.event.v1.EventManager
43+
import org.polyfrost.polyplus.network.plus.cache.CosmeticCache
44+
import org.polyfrost.polyplus.network.plus.Cosmetics
45+
import org.polyfrost.polyplus.network.plus.Cosmetics.getOwned
46+
import org.polyfrost.polyplus.network.plus.PlusWebSocket
47+
import org.polyfrost.polyplus.network.plus.responses.WebSocketPacket
48+
import org.polyfrost.polyplus.utils.PlayerUtils
49+
3450
//#endif
3551
//#elseif NEOFORGE
3652
//$$ import net.neoforged.bus.api.IEventBus
@@ -101,7 +117,26 @@ class PolyPlus
101117

102118
launch = Instant.now()
103119
RPC.start()
120+
PlusWebSocket.start()
121+
PlusWebSocket.setOnMessage {
122+
val res: WebSocketPacket.ClientBound.FallibleResponse = json.decodeFromString(it)
123+
println("websocket message: ${json.encodeToString(res)}")
124+
}
125+
scope.launch {
126+
delay(3000)
127+
PlusWebSocket.getActiveCosmetics(PlayerUtils.uuid.toString())
128+
}
104129
Config.preload()
130+
scope.launch {
131+
val res = Cosmetics.getAll().await().getOrNull() ?: return@launch
132+
CosmeticCache.put(res.cosmetics)
133+
println("cosmetics: $res")
134+
getOwned().await()
135+
}
136+
137+
// listOf(
138+
// Cosmetics
139+
// ).forEach { EventManager.INSTANCE.register(it) }
105140
CommandManager.register(ExampleCommand)
106141
}
107142

@@ -113,16 +148,23 @@ class PolyPlus
113148
//#endif
114149

115150
companion object {
116-
val logger = Logger.getLogger(NAME)
151+
val json = Json {
152+
prettyPrint = true
153+
isLenient = true
154+
}
155+
156+
val logger: Logger = Logger.getLogger(NAME)
117157

118158
val client = HttpClient(CIO) {
119159
defaultRequest {
120160
userAgent("todo")
121161
}
122162

123163
install(ContentNegotiation) {
124-
json()
164+
json(json)
125165
}
166+
167+
install(WebSockets)
126168
}
127169
var launch: Instant? = null
128170

src/main/kotlin/org/polyfrost/polyplus/network/plus/Auth.kt

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,15 +33,14 @@ object Auth {
3333
var authJob: Deferred<AuthResponse>? = null
3434

3535
suspend fun getToken(): String {
36-
return authLock.withLock {
37-
authRes?.token ?: refreshToken()
38-
}
36+
val token = authLock.withLock { authRes?.token }
37+
return token ?: refreshToken()
3938
}
4039

4140
suspend fun refreshToken(): String {
4241
authJob?.let { return it.await().token }
4342

44-
return authLock.withLock {
43+
val lockedJob = authLock.withLock {
4544
authJob?.let { return it.await().token }
4645

4746
val job = PolyPlus.scope.async(start = CoroutineStart.LAZY) {
@@ -55,8 +54,10 @@ object Auth {
5554

5655
authJob = job
5756
job.start()
58-
job.await().token
57+
job
5958
}
59+
60+
return lockedJob.await().token
6061
}
6162

6263
suspend inline fun HttpClient.authRequest(method: HttpMethod, url: String, noinline builder: HttpRequestBuilder.() -> Unit): Result<HttpResponse> = runCatching {
@@ -76,7 +77,7 @@ object Auth {
7677
}
7778
}
7879

79-
return Result.success(response)
80+
return@runCatching response
8081
}
8182

8283
suspend fun runAuth() = withContext(Dispatchers.IO) {
@@ -88,6 +89,7 @@ object Auth {
8889
}
8990

9091
suspend inline fun <reified T> HttpClient.authRequest(method: HttpMethod, url: String) = runCatching {
92+
logger.info("Requesting: $url")
9193
val response = this.authRequest(method, url) {}
9294
response.getOrElse { return Result.failure<T>(it) }.body<T>()
9395
}

src/main/kotlin/org/polyfrost/polyplus/network/plus/Cosmetics.kt

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package org.polyfrost.polyplus.network.plus
22

33
import io.ktor.client.call.body
44
import io.ktor.client.request.get
5-
import io.ktor.client.request.request
65
import io.ktor.client.request.setBody
76
import io.ktor.client.statement.bodyAsText
87
import io.ktor.http.ContentType
@@ -11,23 +10,37 @@ import io.ktor.http.HttpStatusCode
1110
import io.ktor.http.contentType
1211
import kotlinx.coroutines.async
1312
import kotlinx.coroutines.launch
13+
import net.minecraft.client.renderer.texture.DynamicTexture
14+
import net.minecraft.util.ResourceLocation
15+
import org.polyfrost.oneconfig.api.event.v1.events.WorldEvent
16+
import org.polyfrost.oneconfig.api.event.v1.invoke.impl.Subscribe
17+
import org.polyfrost.oneconfig.utils.v1.dsl.mc
1418
import org.polyfrost.polyplus.PolyPlus
1519
import org.polyfrost.polyplus.PolyPlus.Companion.logger
1620
import org.polyfrost.polyplus.client.Config
1721
import org.polyfrost.polyplus.network.plus.Auth.authRequest
18-
import org.polyfrost.polyplus.network.plus.responses.Cosmetic
22+
import org.polyfrost.polyplus.network.plus.responses.CosmeticList
1923
import org.polyfrost.polyplus.network.plus.responses.PlayerCosmetics
2024
import org.polyfrost.polyplus.network.plus.responses.PutCosmetics
25+
import org.polyfrost.polyplus.utils.PlayerUtils
26+
import java.util.UUID
2127

2228
object Cosmetics {
29+
val players = HashMap<UUID, HashMap<String, Int>>()
30+
2331
fun getOwned() = PolyPlus.scope.async {
24-
val cosmetics: Result<PlayerCosmetics> = PolyPlus.client.authRequest(HttpMethod.Get, "${Config.apiUrl}/cosmetics/player")
25-
cosmetics.onFailure { logger.warning("Failed to fetch owned cosmetics: ${it.message}") }
26-
// put on player probably?
32+
val cosmetics: Result<PlayerCosmetics> = PolyPlus.client.authRequest(HttpMethod.Get, "${Config.apiUrl}cosmetics/player")
33+
val owned = cosmetics.onFailure { logger.warning("Failed to fetch owned cosmetics: ${it.message}") }.getOrElse { return@async }
34+
owned.owned.forEach { cosmetic ->
35+
players[PlayerUtils.uuid] = players.getOrPut(PlayerUtils.uuid) { HashMap() }.apply {
36+
this[cosmetic.type] = cosmetic.id
37+
println("set ${PlayerUtils.uuid} cosmetic ${cosmetic.type} to ${cosmetic.id}")
38+
}
39+
}
2740
}
2841

2942
fun setOwned(cosmetics: PutCosmetics) = PolyPlus.scope.launch {
30-
val response = PolyPlus.client.authRequest(HttpMethod.Put, "${Config.apiUrl}/cosmetics/player") {
43+
val response = PolyPlus.client.authRequest(HttpMethod.Put, "${Config.apiUrl}cosmetics/player") {
3144
contentType(ContentType.Application.Json)
3245
setBody(cosmetics)
3346
}.onFailure { logger.warning("Failed to set owned cosmetic: ${it.message}") }.getOrNull() ?: return@launch
@@ -36,8 +49,7 @@ object Cosmetics {
3649
}
3750

3851
fun getAll() = PolyPlus.scope.async {
39-
val cosmetics: Result<List<Cosmetic>> = runCatching { PolyPlus.client.get("${Config.apiUrl}/cosmetics") }.map { it.body() }
52+
val cosmetics: Result<CosmeticList> = runCatching { PolyPlus.client.get("${Config.apiUrl}cosmetics") }.map { it.body() }
4053
cosmetics.onFailure { logger.warning("Failed to fetch all cosmetics: ${it.message}") }
41-
// store somewhere/do something with them
4254
}
4355
}

src/main/kotlin/org/polyfrost/polyplus/network/plus/PlusWebSocket.kt

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,18 @@ package org.polyfrost.polyplus.network.plus
22

33
import io.ktor.client.plugins.websocket.DefaultClientWebSocketSession
44
import io.ktor.client.plugins.websocket.webSocket
5+
import io.ktor.client.request.url
56
import io.ktor.websocket.Frame
7+
import io.ktor.websocket.close
8+
import io.ktor.websocket.closeExceptionally
69
import io.ktor.websocket.readText
710
import kotlinx.coroutines.CoroutineScope
811
import kotlinx.coroutines.channels.Channel
912
import kotlinx.coroutines.launch
1013
import org.polyfrost.polyplus.PolyPlus
14+
import org.polyfrost.polyplus.client.Config
15+
import org.polyfrost.polyplus.network.plus.responses.WebSocketPacket
16+
import org.polyfrost.polyui.utils.mapToArray
1117

1218
object PlusWebSocket {
1319
private var session: DefaultClientWebSocketSession? = null
@@ -20,12 +26,19 @@ object PlusWebSocket {
2026

2127
fun start() = PolyPlus.scope.launch {
2228
try {
23-
PolyPlus.client.webSocket {
29+
PolyPlus.client.webSocket("${Config.apiUrl.replace("http", "ws")}websocket") {
2430
session = this
2531

32+
PolyPlus.logger.info("Connected to PolyPlus WebSocket")
33+
2634
val sender = launch {
2735
for (message in _outgoing) {
28-
send(Frame.Text(message))
36+
try {
37+
send(Frame.Text(message))
38+
} catch (e: Exception) {
39+
PolyPlus.logger.warning("Failed to send message over websocket: ${e.message}")
40+
return@launch
41+
}
2942
}
3043
}
3144

@@ -48,12 +61,24 @@ object PlusWebSocket {
4861
}
4962

5063
fun send(message: String): Result<Unit> {
64+
PolyPlus.logger.info("Tryna send ${message}")
5165
if (session == null) return Result.failure(IllegalStateException("WebSocket is not connected"))
5266

5367
PolyPlus.scope.launch {
54-
_outgoing.send(message)
68+
try {
69+
_outgoing.send(message)
70+
} catch (e: Exception) {
71+
PolyPlus.logger.warning("Failed to send message to the send channel: ${e.message}")
72+
session?.closeExceptionally(e)
73+
}
5574
}
5675

5776
return Result.success(Unit)
5877
}
78+
79+
fun getActiveCosmetics(vararg players: String): Result<Unit> {
80+
val message = WebSocketPacket.ServerBound.Packet.GetActiveCosmetics(players.toList())
81+
val jsonMessage = PolyPlus.json.encodeToString(WebSocketPacket.ServerBound.Packet.serializer(), message)
82+
return send(jsonMessage)
83+
}
5984
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package org.polyfrost.polyplus.network.plus.cache
2+
3+
import net.minecraft.client.renderer.texture.DynamicTexture
4+
import net.minecraft.util.ResourceLocation
5+
import org.polyfrost.oneconfig.utils.v1.dsl.mc
6+
import java.awt.image.BufferedImage
7+
8+
sealed class CachedCosmetic {
9+
class Cape(image: BufferedImage) : CachedCosmetic() {
10+
val resource: ResourceLocation? = mc.textureManager.getDynamicTextureLocation("polyplus/cape", DynamicTexture(image))
11+
}
12+
object InvalidType : CachedCosmetic()
13+
14+
fun asResource(): ResourceLocation? {
15+
return when (this) {
16+
is Cape -> resource
17+
is InvalidType -> null
18+
}
19+
}
20+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package org.polyfrost.polyplus.network.plus.cache
2+
3+
import io.ktor.client.request.get
4+
import io.ktor.client.statement.bodyAsBytes
5+
import kotlinx.coroutines.CompletableDeferred
6+
import kotlinx.coroutines.Deferred
7+
import kotlinx.coroutines.Dispatchers
8+
import kotlinx.coroutines.async
9+
import kotlinx.coroutines.launch
10+
import kotlinx.coroutines.withContext
11+
import net.minecraft.util.ResourceLocation
12+
import org.polyfrost.oneconfig.utils.v1.dsl.mc
13+
import org.polyfrost.polyplus.PolyPlus
14+
import org.polyfrost.polyplus.network.plus.Cosmetics.players
15+
import org.polyfrost.polyplus.network.plus.cache.CachedCosmetic
16+
import org.polyfrost.polyplus.network.plus.responses.Cosmetic
17+
import org.polyfrost.polyplus.utils.HashManager
18+
import java.io.File
19+
import java.io.FileOutputStream
20+
import java.util.UUID
21+
import javax.imageio.ImageIO
22+
import kotlin.uuid.Uuid
23+
24+
object CosmeticCache {
25+
val cache = HashMap<String, HashMap<Int, CachedCosmetic>>()
26+
val hashManager = HashManager("${DIRECTORY}hashes.txt")
27+
const val DIRECTORY = "./polyplus/cosmetics/"
28+
29+
suspend fun put(cosmetics: List<Cosmetic>) = withContext(Dispatchers.IO) {
30+
hashManager.awaitHashes()
31+
try {
32+
val directory = File(DIRECTORY)
33+
if (!directory.exists()) directory.mkdirs()
34+
for (cosmetic in cosmetics) {
35+
val cosmeticName = "${cosmetic.type}_${cosmetic.id}"
36+
val file = File("${DIRECTORY}$cosmeticName")
37+
38+
val cached = !hashManager.updateHash(cosmeticName, cosmetic.hash) && !file.createNewFile()
39+
40+
val cosmeticStream = if (cached) file.inputStream() else {
41+
val bytes = PolyPlus.client.get(cosmetic.url).bodyAsBytes()
42+
val outputStream = FileOutputStream(file)
43+
bytes.inputStream().use { input ->
44+
outputStream.use { output ->
45+
input.copyTo(output)
46+
}
47+
}
48+
49+
bytes.inputStream()
50+
}
51+
52+
mc.addScheduledTask { // we need a thread with opengl context to create textures
53+
cache.getOrPut(cosmetic.type) { HashMap() }[cosmetic.id] = when (cosmetic.type) {
54+
"cape" -> CachedCosmetic.Cape(ImageIO.read(cosmeticStream))
55+
else -> CachedCosmetic.InvalidType
56+
}
57+
}
58+
}
59+
hashManager.saveHashes()
60+
} catch (e: Exception) {
61+
PolyPlus.logger.warning("Failed to cache cosmetics: ${e.message}")
62+
}
63+
}
64+
65+
@JvmStatic
66+
fun getCosmetic(Uuid: UUID, type: String): ResourceLocation? {
67+
val id = players[Uuid]?.get(type) ?: return null
68+
return cache[type]?.get(id)?.asResource().also { if (it == null) println("no cached cosmetic with type $type for id $id") }
69+
}
70+
71+
}

0 commit comments

Comments
 (0)