diff --git a/README.md b/README.md index 630ad0e..ab3ea55 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Sci Chicken -A PUBG pcap playback system that does not hog CPU/GPU, works with version 3.7.19. +A PUBG pcap playback system that does not hog CPU/GPU, works with version 3.7.28.9. ## Sniff diff --git a/backend/bunchstash.js b/backend/bunchstash.js index ead88e0..d599f2c 100644 --- a/backend/bunchstash.js +++ b/backend/bunchstash.js @@ -49,7 +49,16 @@ const bunchStash = { return [{ type: CONSTS.EventTypes.GAMESTOP, time: inputEvent.time }] } const messageType = bunch.uebuffer.readUInt8() - if (messageType === 1 /* NMT_Welcome, only care about this */) { + if (messageType === 0 /* NMT_Hello */) { + const IsLittleEndian = bunch.uebuffer.readUInt8() + const RemoteNetworkVersion = bunch.uebuffer.readUInt32() + const EncryptionToken = bunch.uebuffer.readString() + if (EncryptionToken.length === 24) { + return [{ type: CONSTS.EventTypes.ENCRYPTIONKEY, time: inputEvent.time, data: { EncryptionToken } }] + } else { + return null + } + } else if (messageType === 1 /* NMT_Welcome */) { const map = bunch.uebuffer.readString() const gameMode = bunch.uebuffer.readString() const unknown = bunch.uebuffer.readString() diff --git a/backend/constants.js b/backend/constants.js index 8882653..19a799f 100644 --- a/backend/constants.js +++ b/backend/constants.js @@ -25,6 +25,7 @@ module.exports = { // bunch process -> new event SELFLOCEX: Symbol('SelfLocEx'), CARSYNC: Symbol('CarSync'), + ENCRYPTIONKEY: Symbol('EncryptionKey'), GAMESTART: Symbol('GameStart'), GAMESTOP: Symbol('GameStop'), ACTOROPEN: Symbol('ActorOpen'), diff --git a/backend/packetparser.js b/backend/packetparser.js index 912572a..9e9167f 100644 --- a/backend/packetparser.js +++ b/backend/packetparser.js @@ -1,48 +1,93 @@ const CONSTS = require('./constants') const decoders = require('cap').decoders -const PROTOCOL = decoders.PROTOCOL const UEBuffer = require('./uebuffer') const logger = require('./logger') const Bunch = require('./bunch') +const crypto = require('crypto'); const allsame = input => { return input === 255 || input === 0 } const packetParserProto = { + EncryptionToken: '', + haveEncryptionToken: false, + + readRawBit (rawPacket, posBits) { + const SHIFTS = [1, 2, 4, 8, 16, 32, 64, 128] + const b = rawPacket[posBits >> 3] & SHIFTS[posBits & 0b0111] // x & 0111 means x % 8 -> 0..7 + return b != 0 + }, + // server 7000..7999 -> Game PC packet, UDP. Contains a set of bunches to a certain channel parseUEBunchPacket (rawPacket, payloadLength, pindex, isOutTraffic) { if (!payloadLength) { return } - const buf = UEBuffer(rawPacket) + let buf = UEBuffer(rawPacket) buf.skipBits(42 * 8) // skip to udp payload // The end of packet might still have udp padding, so use payloadLength buf.remainingBits = payloadLength * 8 buf._localRemainingBits = payloadLength * 8 - if (!isOutTraffic || isOutTraffic) { - // adjust size, according to NetConnection.cpp:701 - let lastByte = buf.buffer[41 + payloadLength] - if (lastByte > 0) { - // NOTE: UE4 code is remainingBitss - 1, I changed to 2, maybe PUBG modified this - let finalBitSize = buf.remainingBits - 2 - while ((lastByte & 0x80) === 0) { - lastByte = lastByte * 2 - finalBitSize-- - } - buf.remainingBits = finalBitSize - buf._localRemainingBits = finalBitSize - } else { - logger.fatal('Got 0 in last byte of udp payload, which looks wrong') - return + let IsHandshake = this.readRawBit(rawPacket, (42 * 8)) + if (IsHandshake) { + return null + } + let IsEncrypted = this.readRawBit(rawPacket, (42 * 8) + 1) + + // adjust size, according to NetConnection.cpp:701 + let lastByte = buf.buffer[41 + payloadLength] & 0xFF + if (lastByte > 0) { + // NOTE: UE4 code is remainingBitss - 1, I changed to 2, maybe PUBG modified this + let finalBitSize = (payloadLength * 8) - (IsEncrypted ? 1 : 2) + while ((lastByte & 0x80) === 0) { + lastByte = lastByte * 2 + finalBitSize-- } + buf.remainingBits = finalBitSize + buf._localRemainingBits = finalBitSize + } else { + logger.fatal('Got 0 in last byte of udp payload, which looks wrong') + return } - if (buf.readBit() || buf.readBit()) { - logger.warn({ pindex }, 'Got handshake or encrypted.') - return null // 1st 2nd bit -> handshake or encrypted + IsHandshake = buf.readBit() + IsEncrypted = buf.readBit() + + if (IsEncrypted && !this.haveEncryptionToken) { + //logger.warn('Got encrypted packet but don\'t have EncryptionToken') + return null + } else if (IsEncrypted && this.haveEncryptionToken) { + const nonce = new Buffer(buf.readBits(96)) + const tag = new Buffer(buf.readBits(128)) + const ciphertext = new Buffer(buf.readBits(buf.remainingBits)) + const cipher = crypto.createDecipheriv('aes-192-gcm', this.EncryptionToken, nonce); + cipher.setAuthTag(tag) + try { + let decryptedRaw = cipher.update(ciphertext) + decryptedRaw = Buffer.concat([decryptedRaw, cipher.final()]); + lastByte = decryptedRaw[decryptedRaw.length - 1] & 0xFF + if (lastByte > 0) { + // NOTE: UE4 code is remainingBitss - 1, I changed to 2, maybe PUBG modified this + finalBitSize = (decryptedRaw.length * 8) - 2 + while ((lastByte & 0x80) === 0) { + lastByte = lastByte * 2 + finalBitSize-- + } + buf = UEBuffer(decryptedRaw) + buf.remainingBits = finalBitSize + buf._localRemainingBits = finalBitSize + } else { + logger.fatal('Got 0 in last byte of udp payload, which looks wrong') + return + } + } catch (ex) { + logger.warn('Missed decrypting packet') + return null + } } + // this is Unreal packet id, 15 bit const packetId = buf.readInt(CONSTS.MAX_PACKETID) const event = { type: CONSTS.EventTypes.UEBUNCHES, valid: true, uepid: packetId, totalBunchesBits: buf.remainingBits, data: [] } @@ -99,6 +144,7 @@ const packetParserProto = { const chType = bReliable || bOpen ? buf.readInt(CONSTS.CHTYPE_MAX) : CONSTS.CHTYPE_NONE // when it is not Open or reliable, type = none + const bunchDataBits = buf.readInt(CONSTS.MAX_BUNCH_DATA_BITS) let preLeft = buf.remainingBits if (bunchDataBits > preLeft) { @@ -111,26 +157,27 @@ const packetParserProto = { // END OF getting bunch header, now get bunch data if (chType > 4) { logger.fatal({ pindex, chType }, 'Got unknown channel type') + return null } // ignore file and voice and unknown channel type - if (chType === CONSTS.CHTYPE_FILE || chType === CONSTS.CHTYPE_VOICE || chType > 4) { + if (chType === CONSTS.CHTYPE_FILE || chType === CONSTS.CHTYPE_VOICE) { buf.skipBits(bunchDataBits) continue } // assume -> control channel always got channel index = 0 if (chType === CONSTS.CHTYPE_CONTROL) { - if (isOutTraffic) { - // for outTraffic, we dont care control channel - buf.skipBits(bunchDataBits) - continue - } + //if (isOutTraffic) { + // // for outTraffic, we dont care control channel + // buf.skipBits(bunchDataBits) + // continue + //} if (chIndex !== 0 && !isOutTraffic) { logger.fatal({ chIndex, pindex }, 'Found non-zero Index CONTROL CHANNEL, not good!') buf.skipBits(bunchDataBits) continue } } - + // if we are here, we can assume the bunch is valid, bunch can be partial // since bunch might be stashed for a while, we need to copy bits from the current Buffer event.data.push(Bunch(bunchDataBits, @@ -149,7 +196,7 @@ const packetParserProto = { 14 /* ether header length offset */ ) let result = null - if (ipv4Info.info.protocol === PROTOCOL.IP.UDP) { + if (ipv4Info.info.protocol === decoders.PROTOCOL.IP.UDP) { const udpInfo = decoders.UDP(rawPacket, ipv4Info.offset) if (udpInfo.info.srcport >= 7000 && udpInfo.info.srcport <= 7999) { // the actor bunch packets, sent by channels @@ -162,6 +209,18 @@ const packetParserProto = { result.time = time } return result + }, + + setEncryptionToken (EncryptionToken) { + if (!this.haveEncryptionToken) { + this.EncryptionToken = EncryptionToken + this.haveEncryptionToken = true + } + }, + + clearEncryptionToken () { + this.haveEncryptionToken = false + this.EncryptionToken = '' } } diff --git a/backend/processor/game.js b/backend/processor/game.js index c313f8d..5950f88 100644 --- a/backend/processor/game.js +++ b/backend/processor/game.js @@ -25,90 +25,144 @@ function processGame (buf, actor, repObj, waitingHandle, dataOut, i) { case 23: dataOut.matchShortGuid = buf.readString() // matchShortGuid ! break + case 24: + buf.readBit() // bIsCustomGame + break case 25: buf.readBit() // bIsWinnerombieTeam break case 26: - buf.readUInt32() // RemainingTime + buf.readUInt32() // NumTeams break case 27: - buf.readUInt32() // MatchElapsedMinutes + buf.readUInt32() // RemainingTime break case 28: - buf.readUInt32() // bTimerPaused + buf.readUInt32() // MatchElapsedMinutes break case 29: - buf.readBit() // bShowLastCircleMark + buf.readFloat() // MatchElapsedTimeSec break case 30: - buf.readBit() // bCanShowLastCircleMark + buf.readBit() // bTimerPaused break case 31: - buf.readUInt32() // numAlivePlayers, never hit this + buf.readBit() // bShowLastCircleMark break case 32: - buf.readUInt32() // numJoinPlayers + buf.readBit() // bCanShowLastCircleMark break case 33: - buf.readUInt32() // NumAlivePlayers + buf.readBit() // bCanKillerSpectate break case 34: - buf.readUInt32() // NumAliveZombiePlayers + buf.readUInt32() // NumJoinPlayers break case 35: - buf.readUInt32() // NumAliveTeams + buf.readUInt32() // NumAlivePlayers break case 36: - buf.readUInt32() //NumStartPlayers + buf.readUInt32() // NumAliveZombiePlayers break case 37: - buf.readUInt32() // NumStartTeams + buf.readUInt32() // NumAliveTeams break case 38: - dataOut.poison = buf.readFloatVector() + buf.readUInt32() //NumStartPlayers break case 39: - dataOut.poisonRadius = buf.readFloat() // poison radius + buf.readUInt32() // NumStartTeams break case 40: - dataOut.safezone = buf.readFloatVector() + dataOut.safezone = buf.readFloatVector() // SafetyZonePosition break case 41: - dataOut.safezoneRadius = buf.readFloat() // safezone radius + dataOut.safezoneRadius = buf.readFloat() // SafetyZoneRadius break case 42: - buf.readFloatVector() // redzone ! + dataOut.poison = buf.readFloatVector() // PoisonGasWarningPosition break case 43: - buf.readFloat() // redzone radius ! + dataOut.poisonRadius = buf.readFloat() // PoisonGasWarningRadius break case 44: - buf.readFloat() // TotalReleaseDuration + buf.readFloatVector() // RedZonePosition break case 45: - buf.readFloat() // ElapsedReleaseDuration - break - case 46: - buf.readFloat() // TotalWarningDuration + buf.readFloat() // RedZoneRadius break + //case 46: + // // LastCirclePosition + // break case 47: - buf.readFloat() // ElapsedWarningDuration + buf.readFloat() // TotalReleaseDuration break case 48: - buf.readFloat() // not sure what, but it is float + buf.readFloat() // ElapsedReleaseDuration break case 49: - buf.readBit() // bIsTeamMatch + buf.readFloat() // TotalWarningDuration + break case 50: - buf.readBit() // bIsZombieMode + buf.readFloat() // ElapsedWarningDuration + break case 51: - buf.readFloatVector() // SafetyZoneBeginPosition + buf.readBit() // bIsGasRelease break case 52: - buf.readFloat() + buf.readBit() // bIsTeamMatch + break case 53: - buf.readByte() // MatchStartType + buf.readBit() // bIsZombieMode + break + case 54: + buf.readBit() // bUseXboxUnauthorizedDevice + break + case 55: + buf.readFloatVector() // SafetyZoneBeginPosition break + case 56: + buf.readFloat() // SafetyZoneBeginRadius + break + case 57: + buf.readBit() // MatchStartType + break + case 58: + buf.readBit() // bShowAircraftRoute + break + case 59: + buf.readBit() // bIsWarMode + break + case 60: + buf.readUInt32() // GoalScore + break + //case 61: + // // TeamScores + // break + //case 62: + // // NextRespawnTimeTick + // break + //case 63: + // // TimeLimitTick + // break + //case 64: + // // bIsTeamElimination + // break + //case 65: + // // bUseWarRoyaleBluezone + // break + //case 66: + // // bUsingSquadInTeam + // break + //case 67: + // // TeamIds + // break + //case 68: + // // TeamIndices + // break + //case 69: + // // TeamLeaderNames + // break default: return false } diff --git a/backend/processor/player.js b/backend/processor/player.js index 0d7336f..43c8208 100644 --- a/backend/processor/player.js +++ b/backend/processor/player.js @@ -170,194 +170,194 @@ function processPlayer (buf, actor, repObj, waitingHandle, dataOut, iteration) { break // ATslCharacter case 45: - buf.readInt(8) + buf.readInt(8) // Remote_CastAnim break case 46: - buf.readBit() + buf.readBit() // CurrentWeaponZoomLevel break case 47: - buf.readFloat() + buf.readFloat() // BuffFinalSpreadFactor break case 48: - buf.readObject() + buf.readObject() // InventoryFacade break case 49: - buf.readObject() + buf.readObject() // WeaponProcessor break case 50: - buf.readByte() + buf.readByte() // CharacterState break case 51: - buf.readBit() + buf.readBit() // bIsScopingRemote break case 52: - buf.readBit() + buf.readBit() // bIsAimingRemote break case 53: - buf.readBit() + buf.readBit() // bIsFirstPersonRemote break case 54: - buf.readBit() + buf.readBit() // bIsInVehicleRemote break case 55: - buf.readObject() + buf.readUInt32() // SpectatedCount break case 56: - buf.readFloat() + buf.readObject() // Team break - case 57: - buf.readObject() + case 57: // begin FTakeHitInfo + buf.readFloat() // ActualDamage break case 58: - buf.readObject() + buf.readObject() // DamageType break case 59: - buf.readVector(1, 20) + buf.readObject() // PlayerInstigator break case 60: - buf.readVector(1, 20) + buf.readVector(1, 20) // DamageOrigin break case 61: - buf.readName() + buf.readVector(1, 20) // RelHitLocation break case 62: - buf.readFloat() + buf.readName() // BoneName break case 63: - buf.readByte() + buf.readFloat() // DamageMaxRadius break case 64: - buf.readByte() + buf.readByte() // ShotDirPitch break case 65: - buf.readBit() + buf.readByte() // ShotDirYaw break case 66: - buf.readBit() + buf.readBit() // bPointDamage break case 67: - buf.readBit() + buf.readBit() // bRadialDamage break case 68: - buf.readByte() + buf.readBit() // bKilled break case 69: - buf.readName() + buf.readByte() // EnsureReplicationByte break case 70: - buf.readFloatVector() + buf.readName() // AttackerWeaponName break case 71: - buf.readInt(3) - break + buf.readFloatVector() // AttackerLocation + break // FTakeHitInfo end case 72: - buf.readFloat() + buf.readInt(3) // TargetingType break case 73: - buf.readBit() + buf.readFloat() // ReviveCastingTime break case 74: - buf.readBit() + buf.readBit() // bWantsToRun break case 75: - buf.readBit() + buf.readBit() // bWantsToSprint break case 76: - buf.readBit() + buf.readBit() // bWantsToSprintingAuto break case 77: - buf.readBit() + buf.readBit() // bWantsToRollingLeft break case 78: - buf.readBit() + buf.readBit() // bWantsToRollingRight break case 79: - buf.readBit() - break - case 80: - buf.readBit() + buf.readBit() // bIsPeekLeft break case 80: - buf.readBit() + buf.readBit() // bIsPeekRight break case 81: - buf.readBit() + buf.readBit() // IgnoreRotation break case 82: - buf.readBit() + buf.readBit() // bIsGroggying break case 83: - buf.readBit() + buf.readBit() // bIsThirdPerson break case 84: - buf.readBit() + buf.readBit() // bIsReviving break case 85: - buf.readBit() + buf.readBit() // bIsWeaponObstructed break case 86: - buf.readBit() + buf.readBit() // bIsCoatEquipped break case 87: - buf.readBit() + buf.readBit() // bIsZombie break case 88: - buf.readBit() + buf.readBit() // bIsThrowHigh break case 89: - buf.readRotationShort() + buf.readBit() // bUseRightShoulderAiming break case 90: - buf.readFixedVector(1, 16) + buf.readRotationShort() // GunDirectionSway break case 91: - buf.readObject() + buf.readFixedVector(1, 16) // AimOffsets break case 92: - buf.readBit() + buf.readObject() // NetOwnerController break case 93: - buf.readBit() + buf.readBit() // bAimStateActive break case 94: - const health = buf.readFloat() - dataOut.health = health + buf.readBit() // bIsHoldingBreath break case 95: - buf.readBit() + const health = buf.readFloat() // Health + dataOut.health = health break case 96: - buf.readFloat() + buf.readBit() // HealthMax break case 97: - buf.readFloat() + buf.readFloat() // GroggyHealth break case 98: - buf.readFloat() + buf.readFloat() // GroggyHealthMax break case 99: - buf.readFloat() + buf.readFloat() // BoostGauge break case 100: - buf.readInt(8) + buf.readFloat() // BoostGaugeMax break case 101: - buf.readObject() + buf.readInt(8) // ShoesSoundType break case 102: - buf.readBit() + buf.readObject() // VehicleRiderComponent break case 103: - buf.readInt(4) + buf.readBit() // bIsActiveRagdollActive break case 104: - buf.readBit() + buf.readInt(4) // PreReplicatedStanceMode break case 105: - buf.readBit() + buf.readBit() // bServerFinishedVault break case 106: - buf.readBit() + buf.readBit() // bWantsToCancelVault + break + case 107: + buf.readBit() // bIsDemoVaulting_CP break default: return false diff --git a/index.js b/index.js index e7086f2..d3f4f58 100644 --- a/index.js +++ b/index.js @@ -21,7 +21,7 @@ Example: node index.js playback '/SOME/COOL/DIR/pubg-game1.pcap' | pino function startWebServer () { const apiServerPort = 20086 backend.listen(apiServerPort, () => { - console.log('科学吃鸡 listening on http://localhost:' + apiServerPort) + console.log('Scientific Chicken Dinner listening on http://localhost:' + apiServerPort) }) } @@ -55,46 +55,44 @@ function main () { const rawPacketData = capBuffer.slice(0, nbytes) // one packet can generate 1 or 0 event const result = parser.parse(rawPacketData, new Date().getTime(), pIndex) - if (result) { + if (result != null) { // result can only be UEBunch event const l2Evts = bunchStash.feedEvent(result) if (l2Evts) { for (const l2Evt of l2Evts) { - gamestate.processPUBGEvent(l2Evt) + if (l2Evt.type === CONSTS.EventTypes.ENCRYPTIONKEY) { + console.log(`Got EncryptionToken ${l2Evt.data.EncryptionToken}`) + parser.setEncryptionToken(l2Evt.data.EncryptionToken) + } else { + if (l2Evt.type === CONSTS.EventTypes.GAMESTOP) { + parser.clearEncryptionToken() + } + gamestate.processPUBGEvent(l2Evt) + } } } } }) + startWebServer() } else if (args[0] === 'playback') { const parser = PacketParser() // read the file and get all the events out first const pbyp = PacketByPacket(args[1]) - const allEvents = [] + const finalEvents = [] pbyp.on('packet', packet => { - // packet contains { header, data } const result = parser.parse(packet.data, packet.header.timestamp) if (result != null) { - allEvents.push(result) - } - }) - pbyp.on('error', err => { - console.log('ERROR happened during parsing the pcap file', err) - }) - pbyp.on('end', result => { - console.log('pcap file processing finished.', result) - console.log(`Got ${allEvents.length} layer-1 events.`) - if (allEvents.length > 0) { - console.log( - `Time range: ${utils.toLocalISOString(allEvents[0].time)} -> ${utils.toLocalISOString(allEvents[allEvents.length - 1].time)}` - ) - // we have SelfLoc and UEBunches now, need to process the 2nd pass - const finalEvents = [] - for (const l1Evt of allEvents) { - const l2Evts = bunchStash.feedEvent(l1Evt) - if (l2Evts) { - for (const l2Evt of l2Evts) { - if (l2Evt.type === CONSTS.EventTypes.GAMESTART) { + const l2Evts = bunchStash.feedEvent(result) + if (l2Evts) { + for (const l2Evt of l2Evts) { + if (l2Evt.type === CONSTS.EventTypes.ENCRYPTIONKEY) { + console.log(`Got EncryptionToken ${l2Evt.data.EncryptionToken}`) + parser.setEncryptionToken(l2Evt.data.EncryptionToken) + } else { + if (l2Evt.type === CONSTS.EventTypes.GAMESTOP) { + parser.clearEncryptionToken() + } else if (l2Evt.type === CONSTS.EventTypes.GAMESTART) { // reset the state, so that stash can process further games gamestate.resetGameState() } @@ -102,11 +100,19 @@ function main () { } } } - console.log(`Got ${finalEvents.length} layer-2 events.`) + } + }) + pbyp.on('error', err => { + console.log('ERROR happened during parsing the pcap file', err) + }) + pbyp.on('end', result => { + console.log('pcap file processing finished.', result) + if (finalEvents.length > 0) { // now set the mode to playback and start the api server gamestate.setPlaybackMode(finalEvents) printStateOnConsole() startWebServer() + gamestate.startPlayback(1.0, finalEvents.length) } }) pbyp.resume() diff --git a/static/app.css b/static/app.css index 1791ae1..6019209 100644 --- a/static/app.css +++ b/static/app.css @@ -3,6 +3,7 @@ html, #app, body { height: 100% + color:#E53333 !important } html { overflow-y: hidden; } @@ -15,12 +16,13 @@ html { overflow-y: hidden; } top: 0; /* Stay at the top */ left: 0; /* background-color: #00695C; */ + background-color:transparent overflow-x: hidden; /* Disable horizontal scroll */ } /* Style page content */ #map { - margin-left: 225px; + margin-left: 0px; height: 100%; background-color: lightgray; } diff --git a/static/app.js b/static/app.js index 41ad4c2..07e31d5 100644 --- a/static/app.js +++ b/static/app.js @@ -1,6 +1,6 @@ const appData = { autoRefresh: true, - refreshInterval: 1000, + refreshInterval: 66, gsTime: 0, me: [-1, -1, 0, 0], meGuid: -1, @@ -31,7 +31,7 @@ vapp = new Vue({ showCar: true, showItemTop: true, - showItemDuoDuo: true, + showItemDuoDuo: false, showItemBasic: true, showItemAR: false, showItemSR: false, @@ -88,10 +88,10 @@ vapp = new Vue({ toggleRefresh () { if (appData.autoRefresh) { appData.autoRefresh = false - this.toggleButtonText = '开始刷新' + this.toggleButtonText = 'Start Refresh' } else { appData.autoRefresh = true - this.toggleButtonText = ' 停止刷新' + this.toggleButtonText = ' Stop Refresh' } }, setFPS (fps) { @@ -111,13 +111,13 @@ projection.setExtent([0, 0, 8192, 8192]) function getMapSource (mapType) { const mapPath = mapType === 'erangel' - ? 'erangel/v10' - : 'miramar/v3' - // if false, will use https://tiles3.pubgmap.net/maptiles/erangel/v10/{z}/{x}/{y}.png not sure if it is stable or not. But it will have more zoom, up to 5. Local only has up to 4 + ? 'erangel/v11' + : 'miramar/v5' + // if false, will use https://tiles2-v2.pubgmap.net/tiles/erangel/v11/{z}/{x}/{y}.png not sure if it is stable or not. But it will have more zoom, up to 5. Local only has up to 4 let useLocalResource = false const mapBase = useLocalResource ? '../maptiles' - : 'https://tiles3.pubgmap.net/maptiles' + : 'https://tiles2-v2.pubgmap.net/tiles' return new ol.source.XYZ({ url: `${mapBase}/${mapPath}/{z}/{x}/{y}.png`, diff --git a/static/index.html b/static/index.html index ef9a204..b2e3bfb 100644 --- a/static/index.html +++ b/static/index.html @@ -2,7 +2,7 @@
-胜者即是正义!
- @@ -31,9 +31,9 @@