forked from openrs2/openrs2
I'm still not particularly happy with this: if the JS5 download finishes before HTTP, it'll time out and kill the whole process. Similarly, because it takes so long to import the indexes and as we can't fetch groups in parallel with that, it can often time out early during the process. In the long term, I think I am going to try and move most of the logic outside of the Netty threads and communicate between threads with queues or channels. This would also allow us to run multiple JS5 clients in parallel. The code also needs some tidying up, particularly constants in the Js5ChannelHandler constructors. Signed-off-by: Graham <gpe@openrs2.org>
parent
0e1046d457
commit
6f02ab2f65
@ -0,0 +1,158 @@ |
||||
package org.openrs2.archive.cache |
||||
|
||||
import com.github.michaelbull.logging.InlineLogger |
||||
import io.netty.bootstrap.Bootstrap |
||||
import io.netty.channel.ChannelHandlerContext |
||||
import io.netty.channel.ChannelPipeline |
||||
import kotlinx.coroutines.CoroutineScope |
||||
import kotlinx.coroutines.asCoroutineDispatcher |
||||
import kotlinx.coroutines.cancel |
||||
import kotlinx.coroutines.launch |
||||
import org.bouncycastle.crypto.params.RSAKeyParameters |
||||
import org.openrs2.archive.cache.nxt.InitJs5RemoteConnection |
||||
import org.openrs2.archive.cache.nxt.Js5Request |
||||
import org.openrs2.archive.cache.nxt.Js5RequestEncoder |
||||
import org.openrs2.archive.cache.nxt.Js5Response |
||||
import org.openrs2.archive.cache.nxt.Js5ResponseDecoder |
||||
import org.openrs2.archive.cache.nxt.LoginResponse |
||||
import org.openrs2.archive.cache.nxt.MusicStreamClient |
||||
import org.openrs2.buffer.use |
||||
import org.openrs2.cache.MasterIndexFormat |
||||
import org.openrs2.protocol.Rs2Decoder |
||||
import org.openrs2.protocol.Rs2Encoder |
||||
import org.openrs2.protocol.js5.XorDecoder |
||||
import kotlin.coroutines.Continuation |
||||
|
||||
public class NxtJs5ChannelHandler( |
||||
bootstrap: Bootstrap, |
||||
gameId: Int, |
||||
hostname: String, |
||||
port: Int, |
||||
buildMajor: Int, |
||||
buildMinor: Int, |
||||
lastMasterIndexId: Int?, |
||||
continuation: Continuation<Unit>, |
||||
importer: CacheImporter, |
||||
key: RSAKeyParameters?, |
||||
private val token: String, |
||||
private val musicStreamClient: MusicStreamClient, |
||||
private val maxMinorBuildAttempts: Int = 5 |
||||
) : Js5ChannelHandler( |
||||
bootstrap, |
||||
gameId, |
||||
hostname, |
||||
port, |
||||
buildMajor, |
||||
buildMinor, |
||||
lastMasterIndexId, |
||||
continuation, |
||||
importer, |
||||
key, |
||||
MasterIndexFormat.LENGTHS, |
||||
maxInFlightRequests = 500 |
||||
) { |
||||
private data class MusicRequest(val archive: Int, val group: Int, val version: Int, val checksum: Int) |
||||
|
||||
private var inFlightRequests = 0 |
||||
private val pendingRequests = ArrayDeque<MusicRequest>() |
||||
private var scope: CoroutineScope? = null |
||||
private var minorBuildAttempts = 0 |
||||
|
||||
override fun createInitMessage(): Any { |
||||
return InitJs5RemoteConnection(buildMajor, buildMinor!!, token, 0) |
||||
} |
||||
|
||||
override fun createRequestMessage(prefetch: Boolean, archive: Int, group: Int): Any { |
||||
return Js5Request.Group(prefetch, archive, group, buildMajor) |
||||
} |
||||
|
||||
override fun createConnectedMessage(): Any? { |
||||
return Js5Request.Connected(buildMajor) |
||||
} |
||||
|
||||
override fun configurePipeline(pipeline: ChannelPipeline) { |
||||
pipeline.addBefore("handler", null, Js5RequestEncoder) |
||||
pipeline.addBefore("handler", null, XorDecoder()) |
||||
pipeline.addBefore("handler", null, Js5ResponseDecoder()) |
||||
|
||||
pipeline.remove(Rs2Encoder::class.java) |
||||
pipeline.remove(Rs2Decoder::class.java) |
||||
} |
||||
|
||||
override fun incrementVersion() { |
||||
buildMinor = buildMinor!! + 1 |
||||
|
||||
if (++minorBuildAttempts >= maxMinorBuildAttempts) { |
||||
buildMajor++ |
||||
buildMinor = 1 |
||||
} |
||||
} |
||||
|
||||
override fun channelActive(ctx: ChannelHandlerContext) { |
||||
super.channelActive(ctx) |
||||
scope = CoroutineScope(ctx.channel().eventLoop().asCoroutineDispatcher()) |
||||
} |
||||
|
||||
override fun channelInactive(ctx: ChannelHandlerContext) { |
||||
super.channelInactive(ctx) |
||||
scope!!.cancel() |
||||
} |
||||
|
||||
override fun channelRead0(ctx: ChannelHandlerContext, msg: Any) { |
||||
when (msg) { |
||||
is LoginResponse.Js5Ok -> handleOk(ctx) |
||||
is LoginResponse.ClientOutOfDate -> handleClientOutOfDate(ctx) |
||||
is LoginResponse -> throw Exception("Invalid response: $msg") |
||||
is Js5Response -> handleResponse(ctx, msg.prefetch, msg.archive, msg.group, msg.data) |
||||
else -> throw Exception("Unknown message type: ${msg.javaClass.name}") |
||||
} |
||||
} |
||||
|
||||
override fun channelReadComplete(ctx: ChannelHandlerContext) { |
||||
super.channelReadComplete(ctx) |
||||
|
||||
while (inFlightRequests < 6) { |
||||
val request = pendingRequests.removeFirstOrNull() ?: break |
||||
inFlightRequests++ |
||||
|
||||
logger.info { "Requesting archive ${request.archive} group ${request.group}" } |
||||
|
||||
scope!!.launch { |
||||
val archive = request.archive |
||||
val group = request.group |
||||
val version = request.version |
||||
val checksum = request.checksum |
||||
|
||||
musicStreamClient.request(archive, group, version, checksum, buildMajor).use { buf -> |
||||
inFlightRequests-- |
||||
|
||||
processResponse(ctx, archive, group, buf) |
||||
|
||||
/* |
||||
* Inject a fake channelReadComplete event to ensure we |
||||
* don't time out and to send any new music requests. |
||||
*/ |
||||
ctx.channel().pipeline().fireChannelReadComplete() |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
override fun isComplete(): Boolean { |
||||
return super.isComplete() && pendingRequests.isEmpty() && inFlightRequests == 0 |
||||
} |
||||
|
||||
override fun request(ctx: ChannelHandlerContext, archive: Int, group: Int, version: Int, checksum: Int) { |
||||
if (archive == MUSIC_ARCHIVE) { |
||||
pendingRequests += MusicRequest(archive, group, version, checksum) |
||||
} else { |
||||
super.request(ctx, archive, group, version, checksum) |
||||
} |
||||
} |
||||
|
||||
private companion object { |
||||
private val logger = InlineLogger() |
||||
|
||||
private const val MUSIC_ARCHIVE = 40 |
||||
} |
||||
} |
@ -0,0 +1,22 @@ |
||||
package org.openrs2.archive.cache |
||||
|
||||
import io.netty.channel.Channel |
||||
import io.netty.channel.ChannelInitializer |
||||
import io.netty.handler.timeout.ReadTimeoutHandler |
||||
import org.openrs2.archive.cache.nxt.ClientOutOfDateCodec |
||||
import org.openrs2.archive.cache.nxt.InitJs5RemoteConnectionCodec |
||||
import org.openrs2.archive.cache.nxt.Js5OkCodec |
||||
import org.openrs2.protocol.Protocol |
||||
import org.openrs2.protocol.Rs2Decoder |
||||
import org.openrs2.protocol.Rs2Encoder |
||||
|
||||
public class NxtJs5ChannelInitializer(private val handler: NxtJs5ChannelHandler) : ChannelInitializer<Channel>() { |
||||
override fun initChannel(ch: Channel) { |
||||
ch.pipeline().addLast( |
||||
ReadTimeoutHandler(30), |
||||
Rs2Encoder(Protocol(InitJs5RemoteConnectionCodec)), |
||||
Rs2Decoder(Protocol(Js5OkCodec, ClientOutOfDateCodec)) |
||||
) |
||||
ch.pipeline().addLast("handler", handler) |
||||
} |
||||
} |
@ -0,0 +1,77 @@ |
||||
package org.openrs2.archive.cache |
||||
|
||||
import io.netty.bootstrap.Bootstrap |
||||
import io.netty.channel.ChannelHandlerContext |
||||
import io.netty.channel.ChannelPipeline |
||||
import org.bouncycastle.crypto.params.RSAKeyParameters |
||||
import org.openrs2.cache.MasterIndexFormat |
||||
import org.openrs2.protocol.Rs2Decoder |
||||
import org.openrs2.protocol.Rs2Encoder |
||||
import org.openrs2.protocol.js5.Js5Request |
||||
import org.openrs2.protocol.js5.Js5RequestEncoder |
||||
import org.openrs2.protocol.js5.Js5Response |
||||
import org.openrs2.protocol.js5.Js5ResponseDecoder |
||||
import org.openrs2.protocol.js5.XorDecoder |
||||
import org.openrs2.protocol.login.LoginRequest |
||||
import org.openrs2.protocol.login.LoginResponse |
||||
import kotlin.coroutines.Continuation |
||||
|
||||
public class OsrsJs5ChannelHandler( |
||||
bootstrap: Bootstrap, |
||||
gameId: Int, |
||||
hostname: String, |
||||
port: Int, |
||||
build: Int, |
||||
lastMasterIndexId: Int?, |
||||
continuation: Continuation<Unit>, |
||||
importer: CacheImporter, |
||||
key: RSAKeyParameters? |
||||
) : Js5ChannelHandler( |
||||
bootstrap, |
||||
gameId, |
||||
hostname, |
||||
port, |
||||
build, |
||||
null, |
||||
lastMasterIndexId, |
||||
continuation, |
||||
importer, |
||||
key, |
||||
MasterIndexFormat.VERSIONED, |
||||
maxInFlightRequests = 200 |
||||
) { |
||||
override fun createInitMessage(): Any { |
||||
return LoginRequest.InitJs5RemoteConnection(buildMajor) |
||||
} |
||||
|
||||
override fun createRequestMessage(prefetch: Boolean, archive: Int, group: Int): Any { |
||||
return Js5Request.Group(prefetch, archive, group) |
||||
} |
||||
|
||||
override fun createConnectedMessage(): Any? { |
||||
return null |
||||
} |
||||
|
||||
override fun configurePipeline(pipeline: ChannelPipeline) { |
||||
pipeline.addBefore("handler", null, Js5RequestEncoder) |
||||
pipeline.addBefore("handler", null, XorDecoder()) |
||||
pipeline.addBefore("handler", null, Js5ResponseDecoder()) |
||||
|
||||
pipeline.remove(Rs2Encoder::class.java) |
||||
pipeline.remove(Rs2Decoder::class.java) |
||||
} |
||||
|
||||
override fun incrementVersion() { |
||||
buildMajor++ |
||||
} |
||||
|
||||
override fun channelRead0(ctx: ChannelHandlerContext, msg: Any) { |
||||
when (msg) { |
||||
is LoginResponse.Js5Ok -> handleOk(ctx) |
||||
is LoginResponse.ClientOutOfDate -> handleClientOutOfDate(ctx) |
||||
is LoginResponse -> throw Exception("Invalid response: $msg") |
||||
is Js5Response -> handleResponse(ctx, msg.prefetch, msg.archive, msg.group, msg.data) |
||||
else -> throw Exception("Unknown message type: ${msg.javaClass.name}") |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,8 @@ |
||||
package org.openrs2.archive.cache.nxt |
||||
|
||||
import org.openrs2.protocol.EmptyPacketCodec |
||||
|
||||
public object ClientOutOfDateCodec : EmptyPacketCodec<LoginResponse.ClientOutOfDate>( |
||||
opcode = 6, |
||||
packet = LoginResponse.ClientOutOfDate |
||||
) |
@ -0,0 +1,10 @@ |
||||
package org.openrs2.archive.cache.nxt |
||||
|
||||
import org.openrs2.protocol.Packet |
||||
|
||||
public data class InitJs5RemoteConnection( |
||||
public val buildMajor: Int, |
||||
public val buildMinor: Int, |
||||
public val token: String, |
||||
public val language: Int |
||||
) : Packet |
@ -0,0 +1,29 @@ |
||||
package org.openrs2.archive.cache.nxt |
||||
|
||||
import io.netty.buffer.ByteBuf |
||||
import org.openrs2.buffer.readString |
||||
import org.openrs2.buffer.writeString |
||||
import org.openrs2.crypto.StreamCipher |
||||
import org.openrs2.protocol.PacketCodec |
||||
import org.openrs2.protocol.PacketLength |
||||
|
||||
public object InitJs5RemoteConnectionCodec : PacketCodec<InitJs5RemoteConnection>( |
||||
length = PacketLength.VARIABLE_BYTE, |
||||
opcode = 15, |
||||
type = InitJs5RemoteConnection::class.java |
||||
) { |
||||
override fun decode(input: ByteBuf, cipher: StreamCipher): InitJs5RemoteConnection { |
||||
val buildMajor = input.readInt() |
||||
val buildMinor = input.readInt() |
||||
val token = input.readString() |
||||
val language = input.readUnsignedByte().toInt() |
||||
return InitJs5RemoteConnection(buildMajor, buildMinor, token, language) |
||||
} |
||||
|
||||
override fun encode(input: InitJs5RemoteConnection, output: ByteBuf, cipher: StreamCipher) { |
||||
output.writeInt(input.buildMajor) |
||||
output.writeInt(input.buildMinor) |
||||
output.writeString(input.token) |
||||
output.writeByte(input.language) |
||||
} |
||||
} |
@ -0,0 +1,25 @@ |
||||
package org.openrs2.archive.cache.nxt |
||||
|
||||
import io.netty.buffer.ByteBuf |
||||
import org.openrs2.crypto.StreamCipher |
||||
import org.openrs2.protocol.PacketCodec |
||||
|
||||
public object Js5OkCodec : PacketCodec<LoginResponse.Js5Ok>( |
||||
opcode = 0, |
||||
length = LoginResponse.Js5Ok.LOADING_REQUIREMENTS * 4, |
||||
type = LoginResponse.Js5Ok::class.java |
||||
) { |
||||
override fun decode(input: ByteBuf, cipher: StreamCipher): LoginResponse.Js5Ok { |
||||
val loadingRequirements = mutableListOf<Int>() |
||||
for (i in 0 until LoginResponse.Js5Ok.LOADING_REQUIREMENTS) { |
||||
loadingRequirements += input.readInt() |
||||
} |
||||
return LoginResponse.Js5Ok(loadingRequirements) |
||||
} |
||||
|
||||
override fun encode(input: LoginResponse.Js5Ok, output: ByteBuf, cipher: StreamCipher) { |
||||
for (requirement in input.loadingRequirements) { |
||||
output.writeInt(requirement) |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,14 @@ |
||||
package org.openrs2.archive.cache.nxt |
||||
|
||||
public sealed class Js5Request { |
||||
public data class Group( |
||||
public val prefetch: Boolean, |
||||
public val archive: Int, |
||||
public val group: Int, |
||||
public val build: Int |
||||
) : Js5Request() |
||||
|
||||
public data class Connected( |
||||
public val build: Int |
||||
) : Js5Request() |
||||
} |
@ -0,0 +1,36 @@ |
||||
package org.openrs2.archive.cache.nxt |
||||
|
||||
import io.netty.buffer.ByteBuf |
||||
import io.netty.channel.ChannelHandler |
||||
import io.netty.channel.ChannelHandlerContext |
||||
import io.netty.handler.codec.MessageToByteEncoder |
||||
|
||||
@ChannelHandler.Sharable |
||||
public object Js5RequestEncoder : MessageToByteEncoder<Js5Request>(Js5Request::class.java) { |
||||
override fun encode(ctx: ChannelHandlerContext, msg: Js5Request, out: ByteBuf) { |
||||
when (msg) { |
||||
is Js5Request.Group -> { |
||||
out.writeByte(if (msg.prefetch) 32 else 33) |
||||
out.writeByte(msg.archive) |
||||
out.writeInt(msg.group) |
||||
out.writeShort(msg.build) |
||||
out.writeShort(0) |
||||
} |
||||
is Js5Request.Connected -> { |
||||
out.writeByte(6) |
||||
out.writeMedium(5) |
||||
out.writeShort(0) |
||||
out.writeShort(msg.build) |
||||
out.writeShort(0) |
||||
} |
||||
} |
||||
} |
||||
|
||||
override fun allocateBuffer(ctx: ChannelHandlerContext, msg: Js5Request, preferDirect: Boolean): ByteBuf { |
||||
return if (preferDirect) { |
||||
ctx.alloc().ioBuffer(10, 10) |
||||
} else { |
||||
ctx.alloc().heapBuffer(10, 10) |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,11 @@ |
||||
package org.openrs2.archive.cache.nxt |
||||
|
||||
import io.netty.buffer.ByteBuf |
||||
import io.netty.buffer.DefaultByteBufHolder |
||||
|
||||
public data class Js5Response( |
||||
public val prefetch: Boolean, |
||||
public val archive: Int, |
||||
public val group: Int, |
||||
public val data: ByteBuf |
||||
) : DefaultByteBufHolder(data) |
@ -0,0 +1,121 @@ |
||||
package org.openrs2.archive.cache.nxt |
||||
|
||||
import io.netty.buffer.ByteBuf |
||||
import io.netty.channel.ChannelHandlerContext |
||||
import io.netty.handler.codec.ByteToMessageDecoder |
||||
import io.netty.handler.codec.DecoderException |
||||
import kotlin.math.min |
||||
|
||||
public class Js5ResponseDecoder : ByteToMessageDecoder() { |
||||
private data class Request(val prefetch: Boolean, val archive: Int, val group: Int) |
||||
|
||||
private enum class State { |
||||
READ_HEADER, |
||||
READ_LEN, |
||||
READ_DATA |
||||
} |
||||
|
||||
private var state = State.READ_HEADER |
||||
private val buffers = mutableMapOf<Request, ByteBuf>() |
||||
private var request: Request? = null |
||||
|
||||
override fun decode(ctx: ChannelHandlerContext, input: ByteBuf, out: MutableList<Any>) { |
||||
if (state == State.READ_HEADER) { |
||||
if (input.readableBytes() < 5) { |
||||
return |
||||
} |
||||
|
||||
val prefetch: Boolean |
||||
val archive = input.readUnsignedByte().toInt() |
||||
var group = input.readInt() |
||||
|
||||
if (group and 0x80000000.toInt() != 0) { |
||||
prefetch = true |
||||
group = group and 0x7FFFFFFF |
||||
} else { |
||||
prefetch = false |
||||
} |
||||
|
||||
request = Request(prefetch, archive, group) |
||||
|
||||
if (buffers.containsKey(request)) { |
||||
state = State.READ_DATA |
||||
} else { |
||||
state = State.READ_LEN |
||||
} |
||||
} |
||||
|
||||
if (state == State.READ_LEN) { |
||||
if (input.readableBytes() < 5) { |
||||
return |
||||
} |
||||
|
||||
val type = input.readUnsignedByte().toInt() |
||||
|
||||
val len = input.readInt() |
||||
if (len < 0) { |
||||
throw DecoderException("Length is negative: $len") |
||||
} |
||||
|
||||
val totalLen = if (type == 0) { |
||||
len + 5 |
||||
} else { |
||||
len + 9 |
||||
} |
||||
|
||||
if (totalLen < 0) { |
||||
throw DecoderException("Total length exceeds maximum ByteBuf size") |
||||
} |
||||
|
||||
val data = ctx.alloc().buffer(totalLen, totalLen) |
||||
data.writeByte(type) |
||||
data.writeInt(len) |
||||
|
||||
buffers[request!!] = data |
||||
|
||||
state = State.READ_DATA |
||||
} |
||||
|
||||
if (state == State.READ_DATA) { |
||||
val data = buffers[request!!]!! |
||||
|
||||
var blockLen = if (data.writerIndex() == 5) { |
||||
102400 - 10 |
||||
} else { |
||||
102400 - 5 |
||||
} |
||||
|
||||
blockLen = min(blockLen, data.writableBytes()) |
||||
|
||||
if (input.readableBytes() < blockLen) { |
||||
return |
||||
} |
||||
|
||||
data.writeBytes(input, blockLen) |
||||
|
||||
if (!data.isWritable) { |
||||
out += Js5Response(request!!.prefetch, request!!.archive, request!!.group, data) |
||||
buffers.remove(request!!) |
||||
request = null |
||||
} |
||||
|
||||
state = State.READ_HEADER |
||||
} |
||||
} |
||||
|
||||
override fun channelInactive(ctx: ChannelHandlerContext) { |
||||
super.channelInactive(ctx) |
||||
reset() |
||||
} |
||||
|
||||
override fun handlerRemoved0(ctx: ChannelHandlerContext?) { |
||||
reset() |
||||
} |
||||
|
||||
private fun reset() { |
||||
buffers.values.forEach(ByteBuf::release) |
||||
buffers.clear() |
||||
|
||||
state = State.READ_HEADER |
||||
} |
||||
} |
@ -0,0 +1,13 @@ |
||||
package org.openrs2.archive.cache.nxt |
||||
|
||||
import org.openrs2.protocol.Packet |
||||
|
||||
public sealed class LoginResponse : Packet { |
||||
public data class Js5Ok(val loadingRequirements: List<Int>) : LoginResponse() { |
||||
public companion object { |
||||
public const val LOADING_REQUIREMENTS: Int = 31 |
||||
} |
||||
} |
||||
|
||||
public object ClientOutOfDate : LoginResponse() |
||||
} |
@ -0,0 +1,30 @@ |
||||
package org.openrs2.archive.cache.nxt |
||||
|
||||
import io.netty.buffer.ByteBuf |
||||
import kotlinx.coroutines.future.await |
||||
import org.openrs2.buffer.ByteBufBodyHandler |
||||
import org.openrs2.buffer.use |
||||
import org.openrs2.http.checkStatusCode |
||||
import java.net.URI |
||||
import java.net.http.HttpClient |
||||
import java.net.http.HttpRequest |
||||
|
||||
public class MusicStreamClient( |
||||
private val client: HttpClient, |
||||
private val byteBufBodyHandler: ByteBufBodyHandler, |
||||
private val origin: String |
||||
) { |
||||
public suspend fun request(archive: Int, group: Int, version: Int, checksum: Int, build: Int): ByteBuf { |
||||
val uri = URI("$origin/ms?m=0&a=$archive&k=$build&g=$group&c=$checksum&v=$version") |
||||
|
||||
val request = HttpRequest.newBuilder(uri) |
||||
.GET() |
||||
.build() |
||||
|
||||
val response = client.sendAsync(request, byteBufBodyHandler).await() |
||||
response.body().use { buf -> |
||||
response.checkStatusCode() |
||||
return buf.retain() |
||||
} |
||||
} |
||||
} |
@ -1,8 +1,12 @@ |
||||
package org.openrs2.archive.game |
||||
|
||||
import org.bouncycastle.crypto.params.RSAKeyParameters |
||||
|
||||
public data class Game( |
||||
public val id: Int, |
||||
public val url: String?, |
||||
public val build: Int?, |
||||
public val lastMasterIndexId: Int? |
||||
public val buildMajor: Int?, |
||||
public val buildMinor: Int?, |
||||
public val lastMasterIndexId: Int?, |
||||
public val key: RSAKeyParameters? |
||||
) |
||||
|
@ -0,0 +1,49 @@ |
||||
-- @formatter:off |
||||
CREATE TYPE build AS ( |
||||
major INTEGER, |
||||
minor INTEGER |
||||
); |
||||
|
||||
ALTER TABLE games |
||||
ADD COLUMN key TEXT NULL, |
||||
ADD COLUMN build_minor INTEGER NULL; |
||||
|
||||
ALTER TABLE games |
||||
RENAME COLUMN build TO build_major; |
||||
|
||||
ALTER TABLE sources |
||||
ADD COLUMN build_minor INTEGER NULL; |
||||
|
||||
ALTER TABLE sources |
||||
RENAME COLUMN build TO build_major; |
||||
|
||||
DROP INDEX sources_master_index_id_game_id_build_idx; |
||||
|
||||
CREATE UNIQUE INDEX ON sources (master_index_id, game_id, build_major) |
||||
WHERE type = 'js5remote' AND build_minor IS NULL; |
||||
|
||||
CREATE UNIQUE INDEX ON sources (master_index_id, game_id, build_major, build_minor) |
||||
WHERE type = 'js5remote' AND build_minor IS NOT NULL; |
||||
|
||||
UPDATE games |
||||
SET |
||||
url = 'https://www.runescape.com/k=5/l=0/jav_config.ws?binaryType=2', |
||||
build_major = 919, |
||||
build_minor = 1, |
||||
key = $$-----BEGIN PUBLIC KEY----- |
||||
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAnnP2Sqv7uMM3rjmsLTQ3 |
||||
z4yt8/4j9MDS/2+/9KEkfnH2K/toJbyBUCMHvfS7SBvPiLXWaArNvIArEz/e5Cr3 |
||||
dk2mcSzmoVcsE1dJq/2eDIqRzhH9WB6zDz+5DO6ysRYap1VdMa4bKXMkM+e7V0c3 |
||||
9xiMEpjpeSs0cHGxTGlxLBGTFHYG1IZPLkDRJzhKD58Lu8bn2e3KCTuzzvZFf2AF |
||||
FZauENC6OswdfAZutlWdWkVOZsD9IB/ALNaY4W35PABZbsfT/ar85S/foXFwHJ+B |
||||
OHuF6BR5dYETUQ5Oasl0GUEaVUM9POv7KRv6cW7HWUQHYfQdApjdH+dORHtk4kMG |
||||
QAmk/VpTwWBkZWqDbglZBIkd5G7gs8JpluiUh11eRMC/xj99iZp4nt/FOoSNw2NO |
||||
GMTUPkHIySC4FQHNSxzbfCW5rQdSRw5+eyuo8MA6mg0LZH3jQuNnnYBg1hJTsdBp |
||||
0IrjOQWsfTiX+xZ6lUfRhFtGISuKchpGDZfmOtrZPJDvUgNy0z8w41V6NyiU/h7X |
||||
2TKYFQG1/c4Kr4BxT4tPl85nVbMulonfk/AD5l6BflEuHlChpkAhv14j6xRzGHWx |
||||
4pdpbHSzDkg/HBR5ka0D7Ua7W6uL3VFVCPAygPERZK1lpYE+m+k92H+i/K7gIV1M |
||||
1E07p8x5X9i0oDbZ0lxv8I8CAwEAAQ== |
||||
-----END PUBLIC KEY----- |
||||
$$ |
||||
WHERE name = 'runescape'; |
||||
-- @formatter:on |
Loading…
Reference in new issue