From 7a19a973aa2474ef5fcdec31a1186b60c6f9299f Mon Sep 17 00:00:00 2001 From: JustOneSummer Date: Wed, 26 Jul 2023 18:41:12 +0800 Subject: [PATCH] init --- .gitignore | 42 +++ config/Damage.json | 134 ++++++++ config/Nation.json | 54 ++++ config/Pr.json | 65 ++++ config/ShipType.json | 26 ++ config/Wins.json | 32 ++ config/config.json | 3 + config/proxy.json | 31 ++ dependency-reduced-pom.xml | 84 +++++ pom.xml | 138 ++++++++ .../java/com/shinoaki/wows/real/Main.java | 73 +++++ .../wows/real/cache/SubUserCache.java | 54 ++++ .../com/shinoaki/wows/real/config/Config.java | 37 +++ .../wows/real/config/ProxyConfig.java | 37 +++ .../wows/real/config/type/DataType.java | 9 + .../shinoaki/wows/real/mqtt/MqttConfig.java | 59 ++++ .../shinoaki/wows/real/mqtt/MqttService.java | 127 ++++++++ .../wows/real/service/WowsUserService.java | 260 +++++++++++++++ .../shinoaki/wows/real/service/WsService.java | 132 ++++++++ .../shinoaki/wows/real/timer/ServerTimer.java | 107 +++++++ .../shinoaki/wows/real/wows/AccountInfo.java | 10 + .../wows/real/wows/PlayerBattleInfo.java | 36 +++ .../wows/real/wows/PlayerPrUtils.java | 11 + .../shinoaki/wows/real/wows/WowsCache.java | 300 ++++++++++++++++++ .../shinoaki/wows/real/wows/WowsShipInfo.java | 38 +++ .../wows/real/wows/game/BattleInfoData.java | 54 ++++ .../wows/real/wows/game/info/AvgInfo.java | 60 ++++ .../wows/real/wows/game/info/BattleInfo.java | 29 ++ .../ControlCapturedAndDroppedPointsInfo.java | 24 ++ .../wows/real/wows/game/info/FragsInfo.java | 38 +++ .../real/wows/game/info/HitRatioInfo.java | 31 ++ .../wows/real/wows/game/info/MaxInfo.java | 70 ++++ .../wows/real/wows/global/KeyValue.java | 8 + .../wows/real/wows/global/NationData.java | 9 + .../wows/real/wows/global/PrInfo.java | 28 ++ .../wows/real/wows/global/ShipType.java | 8 + .../wows/real/wows/service/CacheShipMap.java | 14 + .../wows/real/wows/service/WowsHttpData.java | 51 +++ .../wows/real/wows/source/DamageData.java | 20 ++ .../wows/real/wows/source/WinsData.java | 16 + .../wows/real/wows/user/WowsClanInfo.java | 29 ++ .../wows/real/wows/user/WowsUserInfo.java | 30 ++ .../wows/real/wows/view/UserShipInfoView.java | 14 + .../wows/real/ws/data/BaseWsPackage.java | 30 ++ .../wows/real/ws/data/WsPathType.java | 16 + .../WowsWebSocketServerInitializer.java | 50 +++ .../wows/real/ws/server/WowsWebsocket.java | 31 ++ .../server/handler/ChannelEventsHandler.java | 78 +++++ .../server/handler/WebSocketFrameHandler.java | 34 ++ .../handler/WebSocketIndexPageHandler.java | 54 ++++ src/main/resources/logback.xml | 35 ++ 51 files changed, 2760 insertions(+) create mode 100644 .gitignore create mode 100644 config/Damage.json create mode 100644 config/Nation.json create mode 100644 config/Pr.json create mode 100644 config/ShipType.json create mode 100644 config/Wins.json create mode 100644 config/config.json create mode 100644 config/proxy.json create mode 100644 dependency-reduced-pom.xml create mode 100644 pom.xml create mode 100644 src/main/java/com/shinoaki/wows/real/Main.java create mode 100644 src/main/java/com/shinoaki/wows/real/cache/SubUserCache.java create mode 100644 src/main/java/com/shinoaki/wows/real/config/Config.java create mode 100644 src/main/java/com/shinoaki/wows/real/config/ProxyConfig.java create mode 100644 src/main/java/com/shinoaki/wows/real/config/type/DataType.java create mode 100644 src/main/java/com/shinoaki/wows/real/mqtt/MqttConfig.java create mode 100644 src/main/java/com/shinoaki/wows/real/mqtt/MqttService.java create mode 100644 src/main/java/com/shinoaki/wows/real/service/WowsUserService.java create mode 100644 src/main/java/com/shinoaki/wows/real/service/WsService.java create mode 100644 src/main/java/com/shinoaki/wows/real/timer/ServerTimer.java create mode 100644 src/main/java/com/shinoaki/wows/real/wows/AccountInfo.java create mode 100644 src/main/java/com/shinoaki/wows/real/wows/PlayerBattleInfo.java create mode 100644 src/main/java/com/shinoaki/wows/real/wows/PlayerPrUtils.java create mode 100644 src/main/java/com/shinoaki/wows/real/wows/WowsCache.java create mode 100644 src/main/java/com/shinoaki/wows/real/wows/WowsShipInfo.java create mode 100644 src/main/java/com/shinoaki/wows/real/wows/game/BattleInfoData.java create mode 100644 src/main/java/com/shinoaki/wows/real/wows/game/info/AvgInfo.java create mode 100644 src/main/java/com/shinoaki/wows/real/wows/game/info/BattleInfo.java create mode 100644 src/main/java/com/shinoaki/wows/real/wows/game/info/ControlCapturedAndDroppedPointsInfo.java create mode 100644 src/main/java/com/shinoaki/wows/real/wows/game/info/FragsInfo.java create mode 100644 src/main/java/com/shinoaki/wows/real/wows/game/info/HitRatioInfo.java create mode 100644 src/main/java/com/shinoaki/wows/real/wows/game/info/MaxInfo.java create mode 100644 src/main/java/com/shinoaki/wows/real/wows/global/KeyValue.java create mode 100644 src/main/java/com/shinoaki/wows/real/wows/global/NationData.java create mode 100644 src/main/java/com/shinoaki/wows/real/wows/global/PrInfo.java create mode 100644 src/main/java/com/shinoaki/wows/real/wows/global/ShipType.java create mode 100644 src/main/java/com/shinoaki/wows/real/wows/service/CacheShipMap.java create mode 100644 src/main/java/com/shinoaki/wows/real/wows/service/WowsHttpData.java create mode 100644 src/main/java/com/shinoaki/wows/real/wows/source/DamageData.java create mode 100644 src/main/java/com/shinoaki/wows/real/wows/source/WinsData.java create mode 100644 src/main/java/com/shinoaki/wows/real/wows/user/WowsClanInfo.java create mode 100644 src/main/java/com/shinoaki/wows/real/wows/user/WowsUserInfo.java create mode 100644 src/main/java/com/shinoaki/wows/real/wows/view/UserShipInfoView.java create mode 100644 src/main/java/com/shinoaki/wows/real/ws/data/BaseWsPackage.java create mode 100644 src/main/java/com/shinoaki/wows/real/ws/data/WsPathType.java create mode 100644 src/main/java/com/shinoaki/wows/real/ws/server/WowsWebSocketServerInitializer.java create mode 100644 src/main/java/com/shinoaki/wows/real/ws/server/WowsWebsocket.java create mode 100644 src/main/java/com/shinoaki/wows/real/ws/server/handler/ChannelEventsHandler.java create mode 100644 src/main/java/com/shinoaki/wows/real/ws/server/handler/WebSocketFrameHandler.java create mode 100644 src/main/java/com/shinoaki/wows/real/ws/server/handler/WebSocketIndexPageHandler.java create mode 100644 src/main/resources/logback.xml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d25e44b --- /dev/null +++ b/.gitignore @@ -0,0 +1,42 @@ +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### IntelliJ IDEA ### +.idea/modules.xml +.idea/jarRepositories.xml +.idea/compiler.xml +.idea/libraries/ +*.iws +*.iml +*.ipr +mqtt.json +/cache +/logs +/.idea + +### Eclipse ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store diff --git a/config/Damage.json b/config/Damage.json new file mode 100644 index 0000000..863c44e --- /dev/null +++ b/config/Damage.json @@ -0,0 +1,134 @@ +{ + "AirCarrier": [ + { + "code": 1, + "value": 60000, + "color": "#FE7903" + }, + { + "code": 2, + "value": 71000, + "color": "#FE7903" + }, + { + "code": 3, + "value": 84000, + "color": "#02C9B3" + }, + { + "code": 4, + "value": 113000, + "color": "#A00DC5" + } + ], + "Battleship": [ + { + "code": 1, + "value": 64000, + "color": "#FE7903" + }, + { + "code": 2, + "value": 72000, + "color": "#44B300" + }, + { + "code": 3, + "value": 97000, + "color": "#02C9B3" + }, + { + "code": 4, + "value": 108000, + "color": "#A00DC5" + } + ], + "Destroyer": [ + { + "code": 1, + "value": 33000, + "color": "#FE7903" + }, + { + "code": 2, + "value": 40000, + "color": "#44B300" + }, + { + "code": 3, + "value": 55000, + "color": "#02C9B3" + }, + { + "code": 4, + "value": 64000, + "color": "#A00DC5" + } + ], + "Cruiser": [ + { + "code": 1, + "value": 47000, + "color": "#FE7903" + }, + { + "code": 2, + "value": 55000, + "color": "#44B300" + }, + { + "code": 3, + "value": 83000, + "color": "#02C9B3" + }, + { + "code": 4, + "value": 95000, + "color": "#A00DC5" + } + ], + "Submarine": [ + { + "code": 1, + "value": 70000, + "color": "#FE7903" + }, + { + "code": 2, + "value": 70000, + "color": "#44B300" + }, + { + "code": 3, + "value": 70000, + "color": "#02C9B3" + }, + { + "code": 4, + "value": 70000, + "color": "#A00DC5" + } + ], + "Auxiliary": [ + { + "code": 1, + "value": 60000, + "color": "#FE7903" + }, + { + "code": 2, + "value": 71000, + "color": "#FE7903" + }, + { + "code": 3, + "value": 84000, + "color": "#02C9B3" + }, + { + "code": 4, + "value": 113000, + "color": "#A00DC5" + } + ] +} diff --git a/config/Nation.json b/config/Nation.json new file mode 100644 index 0000000..cb3dec4 --- /dev/null +++ b/config/Nation.json @@ -0,0 +1,54 @@ +[ + { + "nation": "Commonwealth", + "cn": "英联邦" + }, + { + "nation": "Europe", + "cn": "欧洲" + }, + { + "nation": "France", + "cn": "法国" + }, + { + "nation": "Germany", + "cn": "德国" + }, + { + "nation": "Italy", + "cn": "意大利" + }, + { + "nation": "Japan", + "cn": "日本" + }, + { + "nation": "Pan_America", + "cn": "泛美" + }, + { + "nation": "Pan_Asia", + "cn": "泛亚" + }, + { + "nation": "United_Kingdom", + "cn": "英国" + }, + { + "nation": "USA", + "cn": "美国" + }, + { + "nation": "Russia", + "cn": "苏联" + }, + { + "nation": "Netherlands", + "cn": "荷兰" + }, + { + "nation": "Spain", + "cn": "西班牙" + } +] diff --git a/config/Pr.json b/config/Pr.json new file mode 100644 index 0000000..d3690e6 --- /dev/null +++ b/config/Pr.json @@ -0,0 +1,65 @@ +[ + { + "code": 0, + "value": 0, + "name": "暂无数据", + "englishName": "No Rating", + "color": "#828282" + }, + { + "code": 1, + "value": 750, + "name": "还需努力", + "englishName": "Bad", + "color": "#FE0E00" + }, + { + "code": 2, + "value": 1100, + "name": "低于平均", + "englishName": "Below Average", + "color": "#FE7903" + }, + { + "code": 3, + "value": 1350, + "name": "平均水平", + "englishName": "Average", + "color": "#FFC71F" + }, + { + "code": 4, + "value": 1550, + "name": "好", + "englishName": "Good", + "color": "#44B300" + }, + { + "code": 5, + "value": 1750, + "name": "很好", + "englishName": "Very Good", + "color": "#318000" + }, + { + "code": 6, + "value": 2100, + "name": "非常好", + "englishName": "Great", + "color": "#02C9B3" + }, + { + "code": 7, + "value": 2450, + "name": "大佬水平", + "englishName": "Unicum", + "color": "#D042F3" + }, + { + "code": 8, + "value": 2450, + "name": "神佬水平", + "englishName": "Super Unicum", + "color": "#A00DC5" + } +] diff --git a/config/ShipType.json b/config/ShipType.json new file mode 100644 index 0000000..067e96f --- /dev/null +++ b/config/ShipType.json @@ -0,0 +1,26 @@ +[ + { + "shipType": "AirCarrier", + "typeName": "航空母舰" + }, + { + "shipType": "Battleship", + "typeName": "战列舰" + }, + { + "shipType": "Destroyer", + "typeName": "驱逐舰" + }, + { + "shipType": "Cruiser", + "typeName": "巡洋舰" + }, + { + "shipType": "Submarine", + "typeName": "潜艇" + }, + { + "shipType": "Auxiliary", + "typeName": "辅助航母" + } +] diff --git a/config/Wins.json b/config/Wins.json new file mode 100644 index 0000000..67e22d4 --- /dev/null +++ b/config/Wins.json @@ -0,0 +1,32 @@ +[ + { + "code": 1, + "value": 40, + "color": "#f44336" + }, + { + "code": 2, + "value": 45, + "color": "#ff9800" + }, + { + "code": 3, + "value": 50, + "color": "#8bc34a" + }, + { + "code": 4, + "value": 55, + "color": "#00bcd4" + }, + { + "code": 5, + "value": 60, + "color": "#9c27b0" + }, + { + "code": 6, + "value": 65, + "color": "#673ab7" + } +] diff --git a/config/config.json b/config/config.json new file mode 100644 index 0000000..c90f155 --- /dev/null +++ b/config/config.json @@ -0,0 +1,3 @@ +{ + "wsPort": 6677 +} diff --git a/config/proxy.json b/config/proxy.json new file mode 100644 index 0000000..70fcaa4 --- /dev/null +++ b/config/proxy.json @@ -0,0 +1,31 @@ +{ + "host": "127.0.0.1", + "port": 7890, + "server": [ + { + "enableProxy": false, + "server": "asia", + "type": "api" + }, + { + "enableProxy": false, + "server": "eu", + "type": "api" + }, + { + "enableProxy": false, + "server": "ru", + "type": "vortex" + }, + { + "enableProxy": false, + "server": "na", + "type": "api" + }, + { + "enableProxy": false, + "server": "cn", + "type": "vortex" + } + ] +} diff --git a/dependency-reduced-pom.xml b/dependency-reduced-pom.xml new file mode 100644 index 0000000..0e9d634 --- /dev/null +++ b/dependency-reduced-pom.xml @@ -0,0 +1,84 @@ + + + 4.0.0 + com.shinoaki.wows.real + wows-real-web + 1.0-SNAPSHOT + + + + maven-plugin-plugin + 3.2 + + wows-real-web + true + + + + maven-compiler-plugin + 3.8.1 + + 20 + 20 + --enable-preview + + + + maven-shade-plugin + 3.2.4 + + + package + + shade + + + + + com.shinoaki.wows.real.Main + + + + + + + + + + + jakarta.websocket + jakarta.websocket-api + 2.1.1 + provided + + + jakarta.websocket + jakarta.websocket-client-api + 2.1.1 + provided + + + junit + junit + 4.13.2 + test + + + hamcrest-core + org.hamcrest + + + + + + 20 + 20 + 1.2.5 + 20 + 2.14.2 + UTF-8 + 2.0.7 + 0.3.4 + 1.4.8 + + diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..05c20db --- /dev/null +++ b/pom.xml @@ -0,0 +1,138 @@ + + 4.0.0 + + com.shinoaki.wows.real + wows-real-web + 1.0-SNAPSHOT + jar + + + UTF-8 + 20 + 20 + 20 + 0.3.4 + 2.14.2 + 1.2.5 + 2.0.7 + 1.4.8 + + + + + com.shinoaki.wows.api + wows-api + ${wows.api.version} + + + + jakarta.websocket + jakarta.websocket-api + 2.1.1 + provided + + + + jakarta.websocket + jakarta.websocket-client-api + 2.1.1 + provided + + + + io.netty + netty-all + 4.1.92.Final + + + + org.eclipse.paho + org.eclipse.paho.mqttv5.client + ${mqtt.version} + + + + com.fasterxml.jackson.core + jackson-core + ${json.version} + + + + com.fasterxml.jackson.core + jackson-databind + ${json.version} + + + + com.fasterxml.jackson.datatype + jackson-datatype-jdk8 + ${json.version} + + + ch.qos.logback + logback-core + ${logback.version} + + + ch.qos.logback + logback-classic + ${logback.version} + + + org.slf4j + slf4j-api + ${slf4j.api.version} + + + junit + junit + 4.13.2 + test + + + + + + + org.apache.maven.plugins + maven-plugin-plugin + 3.2 + + wows-real-web + true + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + 20 + 20 + --enable-preview + + + + org.apache.maven.plugins + maven-shade-plugin + 3.2.4 + + + package + + shade + + + + + com.shinoaki.wows.real.Main + + + + + + + + + diff --git a/src/main/java/com/shinoaki/wows/real/Main.java b/src/main/java/com/shinoaki/wows/real/Main.java new file mode 100644 index 0000000..ca80e30 --- /dev/null +++ b/src/main/java/com/shinoaki/wows/real/Main.java @@ -0,0 +1,73 @@ +package com.shinoaki.wows.real; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.shinoaki.wows.api.utils.JsonUtils; +import com.shinoaki.wows.real.config.Config; +import com.shinoaki.wows.real.mqtt.MqttService; +import com.shinoaki.wows.real.timer.ServerTimer; +import com.shinoaki.wows.real.ws.server.WowsWebsocket; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.lang.reflect.Type; + +/** + * @author Xun + * create or update time = 2023/7/20 16:46 星期四 + */ +public class Main { + public static final Logger log = LoggerFactory.getLogger(Main.class); + + public static void main(String[] args) { + System.setProperty("logback.configurationFile", "config/logback.xml"); + try { + JsonUtils json = new JsonUtils(); + log.info("加载配置信息... "); + var config = json.parse(readFileData(fileConfig("config.json")), new TypeReference() { + @Override + public Type getType() { + return super.getType(); + } + }); + MqttService service = new MqttService(Config.mqttConfig(json)); + service.connect(); + WowsWebsocket websocket = new WowsWebsocket(); + websocket.start(config.wsPort(), "/yuyuko"); + log.info("初始化成功..."); + //服务配置 + ServerTimer timer = new ServerTimer(); + timer.updateConfig(); + timer.timerQueryAccountId(); + timer.timerSubTopic(service); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + log.error("线程异常!", e); + } catch (Exception e) { + log.error("服务运行异常!", e); + } + } + + + public static File fileConfig(String fileName) { + return new File(pathHome() + "config" + File.separator + fileName); + } + + public static String readFileData(File file) throws IOException { + try (FileInputStream in = new FileInputStream(file)) { + return new String(in.readAllBytes()); + } + } + + /** + * 项目根目录 File.separator 结尾 + * + * @return + */ + public static String pathHome() { + return System.getProperty("user.dir") + File.separator; + } + +} diff --git a/src/main/java/com/shinoaki/wows/real/cache/SubUserCache.java b/src/main/java/com/shinoaki/wows/real/cache/SubUserCache.java new file mode 100644 index 0000000..907b47d --- /dev/null +++ b/src/main/java/com/shinoaki/wows/real/cache/SubUserCache.java @@ -0,0 +1,54 @@ +package com.shinoaki.wows.real.cache; + +import com.shinoaki.wows.api.type.WowsServer; +import com.shinoaki.wows.real.wows.AccountInfo; +import io.netty.channel.ChannelId; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.stream.Collectors; + +/** + * @author Xun + * create or update time = 2023/7/25 16:51 星期二 + */ +public class SubUserCache { + private static final Logger log = LoggerFactory.getLogger(SubUserCache.class); + + private SubUserCache() { + } + + private static final ConcurrentMap> USER_CACHE = new ConcurrentHashMap<>(); + + public static void put(ChannelId id, Set accountId) { + log.info("{} 添加账号信息={}", id, accountId); + USER_CACHE.put(id, accountId); + } + + + public static Map check(long accountId) { + Map map = new HashMap<>(); + for (var kv : USER_CACHE.entrySet()) { + Optional first = kv.getValue().stream().filter(x -> x.accountId() == accountId).findFirst(); + first.ifPresent(aLong -> map.put(kv.getKey(), aLong)); + } + return map; + } + + public static Map> all() { + Set set = new HashSet<>(); + USER_CACHE.values().forEach(set::addAll); + return set.stream().collect(Collectors.groupingBy(AccountInfo::server)); + } + + public static List allAccountId() { + Set set = new HashSet<>(); + USER_CACHE.values().forEach(set::addAll); + return set.stream().map(AccountInfo::accountId).toList(); + } + + +} diff --git a/src/main/java/com/shinoaki/wows/real/config/Config.java b/src/main/java/com/shinoaki/wows/real/config/Config.java new file mode 100644 index 0000000..3edef69 --- /dev/null +++ b/src/main/java/com/shinoaki/wows/real/config/Config.java @@ -0,0 +1,37 @@ +package com.shinoaki.wows.real.config; + +import com.shinoaki.wows.api.utils.JsonUtils; +import com.shinoaki.wows.real.Main; +import com.shinoaki.wows.real.mqtt.MqttConfig; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.UUID; + +/** + * @author Xun + * create or update time = 2023/7/26 18:21 星期三 + */ +public record Config(int wsPort) { + + + public static MqttConfig mqttConfig(JsonUtils json) throws IOException { + File file = Main.fileConfig("mqtt.json"); + if (file.exists()) { + return json.parse(Main.readFileData(file), MqttConfig.class); + } else { + MqttConfig c = new MqttConfig(); + c.setBroker("tcp://124.222.76.59:1883"); + c.setClientId("yuyuko-" + UUID.randomUUID()); + c.setUserName("yuyuko-user"); + c.setPassword("yuyuko"); + c.setTopic("yuyuko/wows/real"); + try (FileOutputStream out = new FileOutputStream(file)) { + out.write(json.toJson(c).getBytes(StandardCharsets.UTF_8)); + } + return c; + } + } +} diff --git a/src/main/java/com/shinoaki/wows/real/config/ProxyConfig.java b/src/main/java/com/shinoaki/wows/real/config/ProxyConfig.java new file mode 100644 index 0000000..6168cc4 --- /dev/null +++ b/src/main/java/com/shinoaki/wows/real/config/ProxyConfig.java @@ -0,0 +1,37 @@ +package com.shinoaki.wows.real.config; + +import com.shinoaki.wows.api.type.WowsServer; +import com.shinoaki.wows.real.config.type.DataType; + +/** + * @author Xun + * create or update time = 2023/7/20 11:53 星期四 + */ +public record ProxyConfig(String host, int port, ProxyServer[] server) { + + public ProxyServer findServer(WowsServer server) { + for (var s : server()) { + if (s.wowsServer() == server) { + return s; + } + } + return null; + } + + public record ProxyServer(boolean enableProxy, String server, String type) { + + public WowsServer wowsServer() { + return WowsServer.findCodeByNull(server); + } + + public DataType dataType() { + if (DataType.API.name().equalsIgnoreCase(type)) { + return DataType.API; + } else if (DataType.VORTEX.name().equalsIgnoreCase(type)) { + return DataType.VORTEX; + } else { + return null; + } + } + } +} diff --git a/src/main/java/com/shinoaki/wows/real/config/type/DataType.java b/src/main/java/com/shinoaki/wows/real/config/type/DataType.java new file mode 100644 index 0000000..e2ac71d --- /dev/null +++ b/src/main/java/com/shinoaki/wows/real/config/type/DataType.java @@ -0,0 +1,9 @@ +package com.shinoaki.wows.real.config.type; + +/** + * @author Xun + * create or update time = 2023/7/20 12:07 星期四 + */ +public enum DataType { + API, VORTEX +} diff --git a/src/main/java/com/shinoaki/wows/real/mqtt/MqttConfig.java b/src/main/java/com/shinoaki/wows/real/mqtt/MqttConfig.java new file mode 100644 index 0000000..e651ac0 --- /dev/null +++ b/src/main/java/com/shinoaki/wows/real/mqtt/MqttConfig.java @@ -0,0 +1,59 @@ +package com.shinoaki.wows.real.mqtt; + +/** + * @author Xun + * create or update time = 2023/7/25 15:20 星期二 + */ + +public class MqttConfig { + + private String broker; + + private String clientId; + + private String userName; + + private String password; + + private String topic; + + public String getBroker() { + return broker; + } + + public void setBroker(String broker) { + this.broker = broker; + } + + public String getClientId() { + return clientId; + } + + public void setClientId(String clientId) { + this.clientId = clientId; + } + + public String getUserName() { + return userName; + } + + public void setUserName(String userName) { + this.userName = userName; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public String getTopic() { + return topic; + } + + public void setTopic(String topic) { + this.topic = topic; + } +} diff --git a/src/main/java/com/shinoaki/wows/real/mqtt/MqttService.java b/src/main/java/com/shinoaki/wows/real/mqtt/MqttService.java new file mode 100644 index 0000000..1c81c62 --- /dev/null +++ b/src/main/java/com/shinoaki/wows/real/mqtt/MqttService.java @@ -0,0 +1,127 @@ +package com.shinoaki.wows.real.mqtt; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.shinoaki.wows.api.utils.JsonUtils; +import com.shinoaki.wows.real.cache.SubUserCache; +import com.shinoaki.wows.real.service.WsService; +import com.shinoaki.wows.real.wows.PlayerBattleInfo; +import org.eclipse.paho.mqttv5.client.*; +import org.eclipse.paho.mqttv5.client.persist.MemoryPersistence; +import org.eclipse.paho.mqttv5.common.MqttException; +import org.eclipse.paho.mqttv5.common.MqttMessage; +import org.eclipse.paho.mqttv5.common.MqttSubscription; +import org.eclipse.paho.mqttv5.common.packet.MqttProperties; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.nio.charset.StandardCharsets; +import java.util.List; + +/** + * @author Xun + * create or update time = 2023/7/25 16:05 星期二 + */ +public class MqttService { + public static final Logger log = LoggerFactory.getLogger(MqttService.class); + private static final JsonUtils json = new JsonUtils(); + private static final MemoryPersistence persistence = new MemoryPersistence(); + private static final MqttConnectionOptions connOpts = new MqttConnectionOptions(); + private MqttClient mqttClient = null; + private final MqttConfig config; + private final MyMqttMessageListener myMqttMessageListener; + + static { + connOpts.setCleanStart(true); + connOpts.setAutomaticReconnect(true); + } + + public MqttService(MqttConfig config) { + this.config = config; + this.myMqttMessageListener = new MyMqttMessageListener(); + } + + + public void connect() { + if (mqttClient != null) { + return; + } + connOpts.setUserName(config.getUserName()); + connOpts.setPassword(config.getPassword().getBytes(StandardCharsets.UTF_8)); + try { + mqttClient = new MqttClient(config.getBroker(), config.getClientId(), persistence); + mqttClient.setCallback(new MqttCallback() { + @Override + public void disconnected(MqttDisconnectResponse disconnectResponse) { + log.error("mqtt 连接断开..."); + } + + @Override + public void mqttErrorOccurred(MqttException exception) { + log.error("mqtt error ", exception); + } + + @Override + public void messageArrived(String topic, MqttMessage message) throws Exception { + + } + + @Override + public void deliveryComplete(IMqttToken token) { + + } + + @Override + public void connectComplete(boolean reconnect, String serverURI) { + log.info("mqtt 建立连接!"); + onMessage(); + } + + @Override + public void authPacketArrived(int reasonCode, MqttProperties properties) { + + } + }); + mqttClient.connect(connOpts); + } catch (MqttException e) { + log.error("mqtt 服务器连接异常!"); + } + log.info("mqtt 服务器连接成功!"); + } + + public void onMessage() { + List longs = SubUserCache.allAccountId(); + if (longs.isEmpty()) { + return; + } + MqttSubscription[] subscriptions = new MqttSubscription[longs.size()]; + for (int i = 0; i < longs.size(); i++) { + subscriptions[i] = new MqttSubscription(config.getTopic() + "/" + longs.get(i), 0); + } + IMqttMessageListener[] messageListeners = new IMqttMessageListener[]{ + myMqttMessageListener + }; + try { + mqttClient.subscribe(subscriptions, messageListeners); + } catch (MqttException e) { + log.error("订阅消息异常!", e); + } + } + + public static class MyMqttMessageListener implements IMqttMessageListener { + + @Override + public void messageArrived(String topic, MqttMessage message) throws Exception { + try { + PlayerBattleInfo battleInfo = json.parse(new String(message.getPayload()), PlayerBattleInfo.class); + for (var info : battleInfo.infoList()) { + var map = SubUserCache.check(info.accountId()); + if (!map.isEmpty()) { + WsService.playerGame(battleInfo, map); + } + } + } catch (JsonProcessingException e) { + log.error("序列化消息异常!", e); + } + } + } +} diff --git a/src/main/java/com/shinoaki/wows/real/service/WowsUserService.java b/src/main/java/com/shinoaki/wows/real/service/WowsUserService.java new file mode 100644 index 0000000..9adffd2 --- /dev/null +++ b/src/main/java/com/shinoaki/wows/real/service/WowsUserService.java @@ -0,0 +1,260 @@ +package com.shinoaki.wows.real.service; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.shinoaki.wows.api.codec.http.WowsHttpClanTools; +import com.shinoaki.wows.api.codec.http.WowsHttpShipTools; +import com.shinoaki.wows.api.codec.http.WowsHttpUserTools; +import com.shinoaki.wows.api.data.ShipInfo; +import com.shinoaki.wows.api.developers.account.DevelopersUserInfo; +import com.shinoaki.wows.api.developers.clan.DevelopersSearchUserClan; +import com.shinoaki.wows.api.error.HttpStatusException; +import com.shinoaki.wows.api.error.StatusException; +import com.shinoaki.wows.api.type.WowsBattlesType; +import com.shinoaki.wows.api.type.WowsServer; +import com.shinoaki.wows.api.utils.JsonUtils; +import com.shinoaki.wows.api.vortex.clan.account.VortexSearchClanUser; +import com.shinoaki.wows.real.Main; +import com.shinoaki.wows.real.config.ProxyConfig; +import com.shinoaki.wows.real.config.type.DataType; +import com.shinoaki.wows.real.wows.service.CacheShipMap; +import com.shinoaki.wows.real.wows.service.WowsHttpData; +import com.shinoaki.wows.real.wows.user.WowsClanInfo; +import com.shinoaki.wows.real.wows.user.WowsUserInfo; + +import java.io.*; +import java.lang.reflect.Type; +import java.net.http.HttpClient; +import java.util.ArrayList; +import java.util.EnumMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import static com.shinoaki.wows.real.Main.log; + +/** + * @author Xun + * create or update time = 2023/7/25 21:06 星期二 + */ +public class WowsUserService { + public static final String TOKEN = "907d9c6bfc0d896a2c156e57194a97cf"; + public static final String v1 = "ship-map-v1-cache"; + public static final String v2 = "ship-map-v2-cache"; + + private WowsUserService() { + + } + + public static WowsUserInfo userInfo(WowsHttpData wowsHttpData, long accountId) { + JsonUtils json = new JsonUtils(); + try { + //检查缓存 + HttpClient httpClient = wowsHttpData.httpClient(); + WowsUserInfo wowsUserInfo; + WowsHttpUserTools user = new WowsHttpUserTools(json, httpClient, wowsHttpData.server()); + WowsHttpClanTools clan = new WowsHttpClanTools(json, httpClient, wowsHttpData.server()); + if (wowsHttpData.dataType() == DataType.API) { + DevelopersUserInfo userInfo = user.userInfoDevelopers(TOKEN, accountId); + DevelopersSearchUserClan userClan = clan.developers(TOKEN).userSearchClanDevelopers(accountId); + WowsClanInfo clanInfo; + if (userClan.clan_id() <= 0) { + clanInfo = WowsClanInfo.empty(); + } else { + clanInfo = WowsClanInfo.clanNoColor(userClan.clan_id(), userClan.clan().tag(), userClan.clan().name(), ""); + } + wowsUserInfo = WowsUserInfo.user(accountId, userInfo.nickname(), wowsHttpData.server(), clanInfo, userInfo.created_at()); + } else { + VortexSearchClanUser clanUser = clan.vortex().userSearchClanVortex(accountId); + var clanInfo = new WowsClanInfo(clanUser.clan_id(), clanUser.clan().tag(), clanUser.clan().name(), "", clanUser.clan().color(), -1); + List userName = fileUserName(accountId); + wowsUserInfo = WowsUserInfo.user(accountId, userName.get(0), wowsHttpData.server(), clanInfo, Long.parseLong(userName.get(1))); + } + return wowsUserInfo; + } catch (StatusException | HttpStatusException | IOException e) { + log.error("{}-{} 解析用户信息异常!", wowsHttpData.server().getCode(), accountId, e); + return WowsUserInfo.empty(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + log.error("{}-{} 解析用户信息异常! InterruptedException ", wowsHttpData.server().getCode(), accountId, e); + return WowsUserInfo.empty(); + } + } + + public static WowsHttpData http(WowsServer server) { + File file = Main.fileConfig("proxy.json"); + JsonUtils json = new JsonUtils(); + try (FileInputStream in = new FileInputStream(file)) { + String data = new String(in.readAllBytes()); + var proxyConfig = json.parse(data, new TypeReference() { + @Override + public Type getType() { + return super.getType(); + } + }); + return new WowsHttpData(proxyConfig, server); + } catch (IOException e) { + log.error("读取配置文件异常!", e); + } + return null; + } + + public static CacheShipMap dataV1(WowsHttpData wowsHttpData, long accountId) throws IOException { + return ship(v1, accountId, shipMapByPvpByRank(wowsHttpData, accountId)); + } + + public static CacheShipMap dataV2(WowsHttpData wowsHttpData, long accountId) throws IOException { + return ship(v2, accountId, shipMap(wowsHttpData, accountId)); + } + + private static CacheShipMap ship(String versionCache, long accountId, Map> shipMap) throws IOException { + JsonUtils json = new JsonUtils(); + File file = fileShip(versionCache, accountId); + boolean filedCreateCheck = fileCreateCheck(file); + Map> ship; + if (file.exists()) { + try (FileInputStream in = new FileInputStream(file)) { + String data = new String(in.readAllBytes()); + ship = json.parse(data, new TypeReference>>() { + @Override + public Type getType() { + return super.getType(); + } + }); + } + } else { + ship = Map.of(); + } + try (FileWriter writer = new FileWriter(file)) { + writer.write(json.toJson(shipMap)); + writer.flush(); + } + if (ship.isEmpty()) { + return new CacheShipMap(filedCreateCheck, ship); + } + return new CacheShipMap(filedCreateCheck, recent(shipMap, ship)); + } + + + private static Map> shipMap(WowsHttpData wowsHttpData, long accountId) { + JsonUtils json = new JsonUtils(); + WowsHttpShipTools tools = new WowsHttpShipTools(json, wowsHttpData.httpClient(), wowsHttpData.server(), accountId); + try { + Map> shipMap; + if (wowsHttpData.dataType() == DataType.VORTEX) { + shipMap = new EnumMap<>(WowsBattlesType.class); + for (var ws : WowsBattlesType.values()) { + if (ws == WowsBattlesType.PVP) { + continue; + } + shipMap.put(ws, tools.vortex().shipList(ws).toShipInfoList()); + } + } else { + shipMap = tools.developers(TOKEN).shipList().toShipInfoMap(); + } + return shipMap; + } catch (StatusException | IOException | HttpStatusException e) { + log.error("server={}.accountid={},请求失败!", wowsHttpData.server().getCode(), accountId, e); + } catch (InterruptedException e) { + log.error("server={}.accountid={},请求失败!", wowsHttpData.server().getCode(), accountId, e); + Thread.currentThread().interrupt(); + } + return Map.of(); + } + + /** + * 判断用户是否有战斗记录使用,vortex模式下请求次数减少到两次,但是详细的数据计算需要走一次完整的计算(仅限vortex) + */ + private static Map> shipMapByPvpByRank(WowsHttpData wowsHttpData, long accountId) { + JsonUtils json = new JsonUtils(); + WowsHttpShipTools tools = new WowsHttpShipTools(json, wowsHttpData.httpClient(), wowsHttpData.server(), accountId); + try { + Map> shipMap; + if (wowsHttpData.dataType() == DataType.VORTEX) { + shipMap = new EnumMap<>(WowsBattlesType.class); + String name = null; + long createTime = 0; + for (var ws : WowsBattlesType.values()) { + if (ws == WowsBattlesType.PVP || ws == WowsBattlesType.RANK_SOLO) { + var ship = tools.vortex().shipList(ws); + name = ship.name(); + createTime = (long) ship.created_at(); + shipMap.put(ws, ship.toShipInfoList()); + } + } + fileUserName(accountId, name, createTime); + } else { + shipMap = tools.developers(TOKEN).shipList().toShipInfoMap(); + } + return shipMap; + } catch (StatusException | IOException | HttpStatusException e) { + log.error("server={}.accountid={},请求失败!", wowsHttpData.server().getCode(), accountId, e); + } catch (InterruptedException e) { + log.error("server={}.accountid={},请求失败!", wowsHttpData.server().getCode(), accountId, e); + Thread.currentThread().interrupt(); + } + return Map.of(); + } + private static Map> recent(Map> newData, Map> historyData) { + Map> ship = new EnumMap<>(WowsBattlesType.class); + newData.forEach((k, v) -> { + List infoList = new ArrayList<>(); + var historyMap = historyData.getOrDefault(k, List.of()).stream().collect(Collectors.toMap(ShipInfo::shipId, value -> value)); + v.forEach(shipInfo -> { + ShipInfo info = historyMap.get(shipInfo.shipId()); + if (info != null) { + ShipInfo temp = shipInfo.subtraction(info); + if (temp.battle().battle() > 0) { + infoList.add(temp); + } + } else { + if (shipInfo.battle().battle() > 0) { + infoList.add(shipInfo); + } + } + }); + ship.put(k, infoList); + }); + return ship; + } + + private static List fileUserName(long accountId) throws IOException { + try (BufferedReader bufferedReader = new BufferedReader(new FileReader(fileUserNameTxt(accountId)))) { + return bufferedReader.lines().toList(); + } + } + + private static void fileUserName(long accountId, String userName, long createTime) throws IOException { + var f = fileUserNameTxt(accountId); + fileCreateCheck(f); + try (BufferedWriter buffered = new BufferedWriter(new FileWriter(f))) { + buffered.write(userName); + buffered.newLine(); + buffered.write(String.valueOf(createTime)); + buffered.flush(); + } + } + + private static File fileShip(String v, long accountId) { + return new File(Main.pathHome() + "cache" + File.separator + v + File.separator + accountId + ".json"); + } + + private static File fileUserNameTxt(long accountId) { + return new File(Main.pathHome() + "cache" + File.separator + "name" + File.separator + accountId + ".txt"); + } + + /** + * 是否创建了文件 + * + * @param file + * @return + */ + private static boolean fileCreateCheck(File file) { + int i = file.getPath().lastIndexOf(File.separator); + String substring = file.getPath().substring(0, i); + var f = new File(substring); + if (!f.exists()) { + return f.mkdirs(); + } + return false; + } +} diff --git a/src/main/java/com/shinoaki/wows/real/service/WsService.java b/src/main/java/com/shinoaki/wows/real/service/WsService.java new file mode 100644 index 0000000..374c7da --- /dev/null +++ b/src/main/java/com/shinoaki/wows/real/service/WsService.java @@ -0,0 +1,132 @@ +package com.shinoaki.wows.real.service; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.shinoaki.wows.api.data.ShipInfo; +import com.shinoaki.wows.api.pr.PrUtils; +import com.shinoaki.wows.api.type.WowsBattlesType; +import com.shinoaki.wows.api.utils.JsonUtils; +import com.shinoaki.wows.real.cache.SubUserCache; +import com.shinoaki.wows.real.config.type.DataType; +import com.shinoaki.wows.real.wows.AccountInfo; +import com.shinoaki.wows.real.wows.PlayerBattleInfo; +import com.shinoaki.wows.real.wows.WowsCache; +import com.shinoaki.wows.real.wows.game.BattleInfoData; +import com.shinoaki.wows.real.wows.global.PrInfo; +import com.shinoaki.wows.real.wows.service.CacheShipMap; +import com.shinoaki.wows.real.wows.service.WowsHttpData; +import com.shinoaki.wows.real.wows.user.WowsUserInfo; +import com.shinoaki.wows.real.wows.view.UserShipInfoView; +import com.shinoaki.wows.real.ws.data.BaseWsPackage; +import com.shinoaki.wows.real.ws.data.WsPathType; +import com.shinoaki.wows.real.ws.server.handler.ChannelEventsHandler; +import io.netty.channel.ChannelId; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.lang.reflect.Type; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +/** + * @author Xun + * create or update time = 2023/7/25 18:52 星期二 + */ +public class WsService { + private WsService() { + } + + private static final Logger log = LoggerFactory.getLogger(WsService.class); + public static final ExecutorService EXECUTOR_SERVICE = Executors.newVirtualThreadPerTaskExecutor(); + + /** + * user_accountId + * + * @param path + * @param dataJson + */ + public static void select(ChannelId channelId, String path, String dataJson) { + JsonUtils json = new JsonUtils(); + try { + switch (path) { + case "user_accountId" -> SubUserCache.put(channelId, json.parse(dataJson, new TypeReference>() { + @Override + public Type getType() { + return super.getType(); + } + })); + } + } catch (Exception e) { + log.error("ws 路由处理服务异常!", e); + } + } + + /** + * 推送对局战绩 + * + * @param battleInfo 对局战绩信息 + * @param map 待推送用户信息 + */ + public static void playerGame(PlayerBattleInfo battleInfo, Map map) { + EXECUTOR_SERVICE.submit(() -> { + try { + //处理与计算 + JsonUtils json = new JsonUtils(); + for (var kv : map.entrySet()) { + sendMsg(kv.getKey(), json.toJson(battleInfo)); + } + } catch (Exception e) { + log.error("计算战绩信息服务异常!", e); + } + }); + } + + public static void userPlayerOnShip(WowsHttpData wowsHttpData, long accountId) { + //先判断v1版本数据,如果v1版本数据有变动则进入v2版本判断,仅限vortex模式时 + EXECUTOR_SERVICE.submit(() -> { + try { + CacheShipMap dataV1 = WowsUserService.dataV1(wowsHttpData, accountId); + if (wowsHttpData.dataType() == DataType.VORTEX) { + if ((!dataV1.emptyFile()) && dataV1.shipMap().isEmpty()) { + return; + } + dataV1 = WowsUserService.dataV2(wowsHttpData, accountId); + } + if (dataV1.shipMap().isEmpty()) { + return; + } + WowsUserInfo userInfo = WowsUserService.userInfo(wowsHttpData, accountId); + for (Map.Entry> entry : dataV1.shipMap().entrySet()) { + WowsBattlesType k = entry.getKey(); + if (k == WowsBattlesType.PVP) { + continue; + } + List v = entry.getValue(); + for (ShipInfo ship : v) { + var pr = PrInfo.pr(PrUtils.prShip(ship, WowsCache.getPr(ship.shipId()))); + UserShipInfoView view = new UserShipInfoView(userInfo, WowsCache.getShipMap(ship.shipId()), k, pr, BattleInfoData.to(ship)); + sendMsg(accountId, new BaseWsPackage<>(WsPathType.user_real_info.name(), view)); + } + } + } catch (IOException e) { + log.error("{} 推送用户个人战绩信息异常!", accountId, e); + } + }); + } + + private static void sendMsg(long accountId, BaseWsPackage baseWsPackage) throws JsonProcessingException { + JsonUtils json = new JsonUtils(); + log.info("{} 推送消息... {}", accountId, baseWsPackage.getPath()); + for (var key : SubUserCache.check(accountId).keySet()) { + sendMsg(key, json.toJson(baseWsPackage)); + } + } + + private static void sendMsg(ChannelId id, String msg) { + ChannelEventsHandler.sendMsg(id, msg); + } +} diff --git a/src/main/java/com/shinoaki/wows/real/timer/ServerTimer.java b/src/main/java/com/shinoaki/wows/real/timer/ServerTimer.java new file mode 100644 index 0000000..da06b09 --- /dev/null +++ b/src/main/java/com/shinoaki/wows/real/timer/ServerTimer.java @@ -0,0 +1,107 @@ +package com.shinoaki.wows.real.timer; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonNode; +import com.shinoaki.wows.api.pr.PrData; +import com.shinoaki.wows.api.utils.JsonUtils; +import com.shinoaki.wows.real.Main; +import com.shinoaki.wows.real.cache.SubUserCache; +import com.shinoaki.wows.real.mqtt.MqttService; +import com.shinoaki.wows.real.service.WowsUserService; +import com.shinoaki.wows.real.service.WsService; +import com.shinoaki.wows.real.wows.WowsCache; +import com.shinoaki.wows.real.wows.WowsShipInfo; +import com.shinoaki.wows.real.wows.service.WowsHttpData; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.IOException; +import java.lang.reflect.Type; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +/** + * @author Xun + * create or update time = 2023/7/25 20:56 星期二 + */ +public class ServerTimer { + public static final Logger log = LoggerFactory.getLogger(ServerTimer.class); + public static final ScheduledExecutorService EXECUTOR_SERVICE = Executors.newScheduledThreadPool(1); + + public void updateConfig() { + EXECUTOR_SERVICE.scheduleWithFixedDelay(() -> { + try { + JsonUtils json = new JsonUtils(); + WowsCache.init(shipList(json), shipPr(json), Main.pathHome() + "config" + File.separator); + } catch (IOException e) { + log.error("加载wows cache缓存异常!", e); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + log.error("加载wows cache缓存异常!", e); + } + }, 0, 6, TimeUnit.HOURS); + } + + public void timerQueryAccountId() { + EXECUTOR_SERVICE.scheduleWithFixedDelay(() -> { + for (var kv : SubUserCache.all().entrySet()) { + WowsHttpData wowsHttpData = WowsUserService.http(kv.getKey()); + kv.getValue().forEach(x -> WsService.userPlayerOnShip(wowsHttpData, x.accountId())); + } + }, 1, 5, TimeUnit.MINUTES); + } + + public void timerSubTopic(MqttService service) { + List longs = SubUserCache.allAccountId(); + if (longs.isEmpty()) { + EXECUTOR_SERVICE.scheduleWithFixedDelay(service::onMessage, 10, 60, TimeUnit.SECONDS); + } else { + EXECUTOR_SERVICE.scheduleWithFixedDelay(service::onMessage, 1, 5, TimeUnit.MINUTES); + } + } + + + private static List shipList(JsonUtils json) throws IOException, InterruptedException { + final URI uri = URI.create("https://v3-api.wows.shinoaki.com/public/wows/encyclopedia/ship/search"); + HttpClient client = HttpClient.newBuilder().build(); + HttpResponse response = client.send(HttpRequest.newBuilder().GET().uri(uri).setHeader("Yuyuko-Client-Type", "SWAGGER;test").build(), + HttpResponse.BodyHandlers.ofByteArray()); + if (response.statusCode() == 200) { + JsonNode node = json.parse(new String(response.body())); + return json.parse(node.get("data").toString(), new TypeReference>() { + @Override + public Type getType() { + return super.getType(); + } + }); + } + log.error("请求战舰信息数据异常!"); + return List.of(); + } + + private static Map shipPr(JsonUtils json) throws IOException, InterruptedException { + final URI uri = URI.create("https://v3-api.wows.shinoaki.com/public/wows/encyclopedia/ship/server/data"); + HttpClient client = HttpClient.newBuilder().build(); + HttpResponse response = client.send(HttpRequest.newBuilder().GET().uri(uri).setHeader("Yuyuko-Client-Type", "SWAGGER;test").build(), + HttpResponse.BodyHandlers.ofByteArray()); + if (response.statusCode() == 200) { + JsonNode node = json.parse(new String(response.body())); + return json.parse(node.get("data").toString(), new TypeReference>() { + @Override + public Type getType() { + return super.getType(); + } + }); + } + log.error("请求战舰pr信息数据异常!"); + return Map.of(); + } +} diff --git a/src/main/java/com/shinoaki/wows/real/wows/AccountInfo.java b/src/main/java/com/shinoaki/wows/real/wows/AccountInfo.java new file mode 100644 index 0000000..99fd64f --- /dev/null +++ b/src/main/java/com/shinoaki/wows/real/wows/AccountInfo.java @@ -0,0 +1,10 @@ +package com.shinoaki.wows.real.wows; + +import com.shinoaki.wows.api.type.WowsServer; + +/** + * @author Xun + * create or update time = 2023/7/25 21:24 星期二 + */ +public record AccountInfo(WowsServer server,long accountId) { +} diff --git a/src/main/java/com/shinoaki/wows/real/wows/PlayerBattleInfo.java b/src/main/java/com/shinoaki/wows/real/wows/PlayerBattleInfo.java new file mode 100644 index 0000000..f7d5c9c --- /dev/null +++ b/src/main/java/com/shinoaki/wows/real/wows/PlayerBattleInfo.java @@ -0,0 +1,36 @@ +package com.shinoaki.wows.real.wows; + +import java.util.List; + +/** + * @author Xun + * create or update time = 2023/7/25 17:12 星期二 + */ +public record PlayerBattleInfo(String battleType, + + long time, + + List infoList) { + + public record BattleInfo( + + String server, + /** + * 机器人是0 + */ + long accountId, + + String userName, + + long shipId, + + boolean hidden, + + long clanId, + + String tag, + + int relation) { + + } +} diff --git a/src/main/java/com/shinoaki/wows/real/wows/PlayerPrUtils.java b/src/main/java/com/shinoaki/wows/real/wows/PlayerPrUtils.java new file mode 100644 index 0000000..58530be --- /dev/null +++ b/src/main/java/com/shinoaki/wows/real/wows/PlayerPrUtils.java @@ -0,0 +1,11 @@ +package com.shinoaki.wows.real.wows; + +import java.net.URI; + +/** + * @author Xun + * create or update time = 2023/7/25 18:28 星期二 + */ +public class PlayerPrUtils { + private static final URI uri = URI.create(""); +} diff --git a/src/main/java/com/shinoaki/wows/real/wows/WowsCache.java b/src/main/java/com/shinoaki/wows/real/wows/WowsCache.java new file mode 100644 index 0000000..bbebb09 --- /dev/null +++ b/src/main/java/com/shinoaki/wows/real/wows/WowsCache.java @@ -0,0 +1,300 @@ +package com.shinoaki.wows.real.wows; + + +import com.fasterxml.jackson.core.type.TypeReference; +import com.shinoaki.wows.api.data.ShipInfo; +import com.shinoaki.wows.api.pr.PrData; +import com.shinoaki.wows.api.type.WowsBattlesType; +import com.shinoaki.wows.api.utils.JsonUtils; +import com.shinoaki.wows.real.wows.global.NationData; +import com.shinoaki.wows.real.wows.global.PrInfo; +import com.shinoaki.wows.real.wows.global.ShipType; +import com.shinoaki.wows.real.wows.source.DamageData; +import com.shinoaki.wows.real.wows.source.WinsData; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.lang.reflect.Type; +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.*; + +/** + * @author Xun + * @date 2023/5/11 23:54 星期四 + */ +public class WowsCache { + private WowsCache() { + + } + + private static final List PR_INFO_LIST = new ArrayList<>(); + private static final List NATION_DATA_LIST = new ArrayList<>(); + private static final List SHIP_TYPE_LIST = new ArrayList<>(); + private static final Map> COLOR_DAMAGE_LIST = new HashMap<>(); + private static final List COLOR_WINS_LIST = new ArrayList<>(); + private static final Map SHIP_MAP = new TreeMap<>(); + private static final Map SHIP_PR_MAP = new TreeMap<>(); + private static final Set LEVEL = new TreeSet<>(); + private static final Set GROUP_TYPE = new HashSet<>(); + private static final Set SHIP_TYPE = new HashSet<>(); + private static final Set COUNTRY = new HashSet<>(); + + public static List getPrInfoList() { + return PR_INFO_LIST; + } + + public static List getShipTypeList() { + return SHIP_TYPE_LIST; + } + + public static List getNationDataList() { + return NATION_DATA_LIST; + } + + public static WowsShipInfo getShipMap(long shipId) { + return SHIP_MAP.getOrDefault(shipId, WowsShipInfo.empty(shipId)); + } + + public static Map getShipMap() { + return SHIP_MAP; + } + + public static List groupType() { + return GROUP_TYPE.stream().toList(); + } + + public static List shipType() { + return SHIP_TYPE.stream().toList(); + } + + public static List level() { + return LEVEL.stream().toList(); + } + + public static List country() { + return COUNTRY.stream().toList(); + } + + + public static void init(List list, Map prDataMap, String path) throws IOException { + JsonUtils utils = new JsonUtils(); + loadPr(new File(path + "Pr.json"), utils); + loadShipType(new File(path + "ShipType.json"), utils); + loadNation(new File(path + "Nation.json"), utils); + loadDamage(new File(path + "Damage.json"), utils); + loadWins(new File(path + "Wins.json"), utils); + if (!list.isEmpty()) { + list.forEach(x -> { + SHIP_MAP.put(x.shipId(), x); + LEVEL.add(x.level()); + GROUP_TYPE.add(x.groupType()); + SHIP_TYPE.add(x.shipType()); + COUNTRY.add(x.country()); + }); + } + if (!prDataMap.isEmpty()) { + SHIP_PR_MAP.clear(); + SHIP_PR_MAP.putAll(prDataMap); + } + } + + public static PrData getPr(long pr) { + return SHIP_PR_MAP.getOrDefault(pr, PrData.empty()); + } + + public static PrInfo getPr(int pr) { + if (pr <= 0) { + return PR_INFO_LIST.get(0); + } + for (var x : PR_INFO_LIST) { + if (pr < x.value()) { + return x; + } + } + return PR_INFO_LIST.get(PR_INFO_LIST.size() - 1); + } + + public static DamageData getDamage(long shipId, double value) { + List list = COLOR_DAMAGE_LIST.getOrDefault(WowsCache.getShipMap(shipId).shipType(), List.of()); + if (list.isEmpty()) { + return DamageData.empty(); + } + if (value <= 0) { + return list.get(0); + } + for (var x : list) { + if (value < x.value()) { + return x; + } + } + return list.get(list.size() - 1); + } + + public static WinsData getWins(double value) { + if (value <= 0.0) { + return COLOR_WINS_LIST.get(0); + } + for (var x : COLOR_WINS_LIST) { + if (value < x.value()) { + return x; + } + } + return COLOR_WINS_LIST.get(COLOR_WINS_LIST.size() - 1); + } + + /** + * 双精度溢出验证-外加小数点后两位 + * + * @param data 双精度数值 + * @return NaN等全部返回0 + */ + public static double doubleCheckAnd_HALF_UP(double data) { + if (Double.isInfinite(data)) { + return 0.0; + } else if (Double.isNaN(data)) { + return 0.0; + } else if (data <= 0.0) { + return 0.0; + } else { + return BigDecimal.valueOf(data).setScale(2, RoundingMode.HALF_UP).doubleValue(); + } + } + + public static int doubleCheckAnd_HALF_UP_Int(double data) { + if (Double.isInfinite(data)) { + return 0; + } else if (Double.isNaN(data)) { + return 0; + } else if (data <= 0.0) { + return 0; + } else { + return BigDecimal.valueOf(data).setScale(0, RoundingMode.HALF_UP).intValue(); + } + } + + public static Map> process(Map> info, long shipId) { + Map> shipMap = new TreeMap<>(); + if (shipId == 0) { + for (var entry : info.entrySet()) { + for (var value : entry.getValue()) { + Map shipInfoMap = shipMap.getOrDefault(value.shipId(), new EnumMap<>(WowsBattlesType.class)); + shipInfoMap.put(entry.getKey(), value); + shipMap.put(value.shipId(), shipInfoMap); + } + } + } else { + for (var entry : info.entrySet()) { + for (var value : entry.getValue()) { + if (value.shipId() == shipId) { + Map shipInfoMap = shipMap.getOrDefault(value.shipId(), new EnumMap<>(WowsBattlesType.class)); + shipInfoMap.put(entry.getKey(), value); + shipMap.put(value.shipId(), shipInfoMap); + } + } + } + } + return shipMap; + } + + public static int zero(int data) { + return Math.max(data, -1); + } + + public static long zero(long data) { + return Math.max(data, -1); + } + + private static void loadPr(File file, JsonUtils utils) throws IOException { + try (FileInputStream in = new FileInputStream(file)) { + loadPrInfo(utils.parse(new String(in.readAllBytes()), new TypeReference>() { + @Override + public Type getType() { + return super.getType(); + } + })); + } + } + + private static void loadDamage(File file, JsonUtils utils) throws IOException { + try (FileInputStream in = new FileInputStream(file)) { + loadDamage(utils.parse(new String(in.readAllBytes()), new TypeReference>>() { + @Override + public Type getType() { + return super.getType(); + } + })); + } + } + + private static void loadWins(File file, JsonUtils utils) throws IOException { + try (FileInputStream in = new FileInputStream(file)) { + loadWins(utils.parse(new String(in.readAllBytes()), new TypeReference>() { + @Override + public Type getType() { + return super.getType(); + } + })); + } + } + + private static void loadNation(File file, JsonUtils utils) throws IOException { + try (FileInputStream in = new FileInputStream(file)) { + loadNation(utils.parse(new String(in.readAllBytes()), new TypeReference>() { + @Override + public Type getType() { + return super.getType(); + } + })); + } + } + + private static void loadShipType(File file, JsonUtils utils) throws IOException { + try (FileInputStream in = new FileInputStream(file)) { + loadShipType(utils.parse(new String(in.readAllBytes()), new TypeReference>() { + @Override + public Type getType() { + return super.getType(); + } + })); + } + } + + private static void loadPrInfo(List info) { + PR_INFO_LIST.clear(); + PR_INFO_LIST.addAll(info); + } + + private static void loadDamage(Map> info) { + COLOR_DAMAGE_LIST.clear(); + COLOR_DAMAGE_LIST.putAll(info); + } + + private static void loadWins(List info) { + COLOR_WINS_LIST.clear(); + COLOR_WINS_LIST.addAll(info); + } + + private static void loadNation(List info) { + NATION_DATA_LIST.clear(); + NATION_DATA_LIST.addAll(info); + } + + private static void loadShipType(List info) { + SHIP_TYPE_LIST.clear(); + SHIP_TYPE_LIST.addAll(info); + } + + private static boolean isEmptyOrNull(String data) { + return data == null || data.isBlank(); + } + + private static boolean matchName(String shipName, String matchName, String matchNameEnglish) { + if (isEmptyOrNull(shipName)) { + return true; + } + String u = shipName.toLowerCase(Locale.ROOT); + return matchName.toLowerCase(Locale.ROOT).contains(u) || matchNameEnglish.toLowerCase(Locale.ROOT).contains(u); + } +} diff --git a/src/main/java/com/shinoaki/wows/real/wows/WowsShipInfo.java b/src/main/java/com/shinoaki/wows/real/wows/WowsShipInfo.java new file mode 100644 index 0000000..b67317c --- /dev/null +++ b/src/main/java/com/shinoaki/wows/real/wows/WowsShipInfo.java @@ -0,0 +1,38 @@ +package com.shinoaki.wows.real.wows; + + +/** + * @author Xun + * @date 2023/5/15 15:20 星期一 + */ +public record WowsShipInfo( + + long shipId, + + String nameCn, + + String nameEnglish, + + String nameNumbers, + + int level, + + String shipType, + + String country, + + String imgSmall, + + String imgLarge, + + String imgMedium, + + String shipIndex, + + String groupType +) { + + public static WowsShipInfo empty(long shipId) { + return new WowsShipInfo(shipId, "未知战舰", "none", "none", 0, "none", "none", null, null, null, "none", "none"); + } +} diff --git a/src/main/java/com/shinoaki/wows/real/wows/game/BattleInfoData.java b/src/main/java/com/shinoaki/wows/real/wows/game/BattleInfoData.java new file mode 100644 index 0000000..af6c9d9 --- /dev/null +++ b/src/main/java/com/shinoaki/wows/real/wows/game/BattleInfoData.java @@ -0,0 +1,54 @@ +package com.shinoaki.wows.real.wows.game; + + +import com.shinoaki.wows.api.data.ShipInfo; +import com.shinoaki.wows.real.wows.WowsCache; +import com.shinoaki.wows.real.wows.game.info.*; +import com.shinoaki.wows.real.wows.source.DamageData; + +/** + * 战斗数据信息 + * + * @author Xun + * @date 2023/5/14 17:32 星期日 + */ +public record BattleInfoData( + + BattleInfo battleInfo, + + AvgInfo avgInfo, + + FragsInfo fragsInfo, + + MaxInfo maxInfo, + + ControlCapturedAndDroppedPointsInfo controlCapturedAndDroppedPointsInfo, + + HitRatioInfo hitRatioInfo, + + long lastBattleTime, + + long recordTime +) { + public static BattleInfoData to(ShipInfo info) { + return new BattleInfoData( + BattleInfo.to(info), + AvgInfo.to(info), + FragsInfo.to(info), + MaxInfo.to(info), + ControlCapturedAndDroppedPointsInfo.to(info), + HitRatioInfo.to(info), + info.lastBattleTime(), + info.recordTime() + ); + } + + public static BattleInfoData empty(long shipId) { + return new BattleInfoData(new BattleInfo(0, 0, 0, 0, 0), + new AvgInfo(0, DamageData.empty(), 0, 0, WowsCache.getWins(0), 0, 0, 0, 0, 0, 0, 0, 0), + new FragsInfo(0, 0, 0, 0, 0, 0, 0), + MaxInfo.empty(shipId), + new ControlCapturedAndDroppedPointsInfo(0, 0), + new HitRatioInfo(0, 0, 0, 0), 0, 0); + } +} diff --git a/src/main/java/com/shinoaki/wows/real/wows/game/info/AvgInfo.java b/src/main/java/com/shinoaki/wows/real/wows/game/info/AvgInfo.java new file mode 100644 index 0000000..b7ca2f9 --- /dev/null +++ b/src/main/java/com/shinoaki/wows/real/wows/game/info/AvgInfo.java @@ -0,0 +1,60 @@ +package com.shinoaki.wows.real.wows.game.info; + +import com.shinoaki.wows.api.data.ShipInfo; +import com.shinoaki.wows.real.wows.WowsCache; +import com.shinoaki.wows.real.wows.source.DamageData; +import com.shinoaki.wows.real.wows.source.WinsData; + + +/** + * @author Xun + * @date 2023/5/14 20:52 星期日 + */ +public record AvgInfo( + + int damage, + + DamageData damageData, + + int scoutingDamage, + + double win, + + WinsData winsData, + + double kd, + + double frags, + + int shipsSpotted, + + int planesKilled, + + int artAgro, + + int tpdAgro, + + int xp, + + int basicXp) { + + public static AvgInfo to(ShipInfo info) { + int damage = WowsCache.doubleCheckAnd_HALF_UP_Int(info.gameDamage()); + int wins = WowsCache.doubleCheckAnd_HALF_UP_Int(info.gameWins()); + return new AvgInfo( + damage, + WowsCache.getDamage(info.shipId(), damage), + WowsCache.doubleCheckAnd_HALF_UP_Int(info.gameScoutingDamage()), + wins, + WowsCache.getWins(wins), + WowsCache.doubleCheckAnd_HALF_UP(info.gameKd()), + WowsCache.doubleCheckAnd_HALF_UP(info.gameFrags()), + WowsCache.doubleCheckAnd_HALF_UP_Int(info.gameShipsSpotted()), + WowsCache.doubleCheckAnd_HALF_UP_Int(info.gamePlanesKilled()), + WowsCache.doubleCheckAnd_HALF_UP_Int(info.gameArtAgro()), + WowsCache.doubleCheckAnd_HALF_UP_Int(info.gameTpdAgro()), + WowsCache.doubleCheckAnd_HALF_UP_Int(info.gameXp()), + WowsCache.doubleCheckAnd_HALF_UP_Int(info.gameBasicXp()) + ); + } +} diff --git a/src/main/java/com/shinoaki/wows/real/wows/game/info/BattleInfo.java b/src/main/java/com/shinoaki/wows/real/wows/game/info/BattleInfo.java new file mode 100644 index 0000000..ad9aa75 --- /dev/null +++ b/src/main/java/com/shinoaki/wows/real/wows/game/info/BattleInfo.java @@ -0,0 +1,29 @@ +package com.shinoaki.wows.real.wows.game.info; + +import com.shinoaki.wows.api.data.ShipInfo; + +/** + * @author Xun + * @date 2023/5/14 20:56 星期日 + */ +public record BattleInfo( + + int battle, + + int wins, + + int losses, + + int survived, + + int winAndSurvived +) { + + public static BattleInfo to(ShipInfo info) { + return new BattleInfo(info.battle().battle(), + info.battle().wins(), + info.battle().losses(), + info.battle().survived(), + info.battle().winAndSurvived()); + } +} diff --git a/src/main/java/com/shinoaki/wows/real/wows/game/info/ControlCapturedAndDroppedPointsInfo.java b/src/main/java/com/shinoaki/wows/real/wows/game/info/ControlCapturedAndDroppedPointsInfo.java new file mode 100644 index 0000000..06f68f9 --- /dev/null +++ b/src/main/java/com/shinoaki/wows/real/wows/game/info/ControlCapturedAndDroppedPointsInfo.java @@ -0,0 +1,24 @@ +package com.shinoaki.wows.real.wows.game.info; + +import com.shinoaki.wows.api.data.ShipInfo; +import com.shinoaki.wows.real.wows.WowsCache; + + +/** + * 站点信息 + * + * @author Xun + * @date 2023/5/14 17:50 星期日 + */ +public record ControlCapturedAndDroppedPointsInfo( + + double gameContributionToCapture, + + double gameContributionToDefense) { + + public static ControlCapturedAndDroppedPointsInfo to(ShipInfo info) { + return new ControlCapturedAndDroppedPointsInfo( + WowsCache.doubleCheckAnd_HALF_UP(info.controlCapturedAndDroppedPoints().gameContributionToCapture()), + WowsCache.doubleCheckAnd_HALF_UP(info.controlCapturedAndDroppedPoints().gameContributionToDefense())); + } +} diff --git a/src/main/java/com/shinoaki/wows/real/wows/game/info/FragsInfo.java b/src/main/java/com/shinoaki/wows/real/wows/game/info/FragsInfo.java new file mode 100644 index 0000000..135d342 --- /dev/null +++ b/src/main/java/com/shinoaki/wows/real/wows/game/info/FragsInfo.java @@ -0,0 +1,38 @@ +package com.shinoaki.wows.real.wows.game.info; + +import com.shinoaki.wows.api.data.ShipInfo; + +/** + * 击杀信息 + * + * @author Xun + * @date 2023/5/14 17:32 星期日 + */ +public record FragsInfo( + + int frags, + + int fragsByMain, + + int fragsByAtba, + + int fragsByPlanes, + + int fragsByTpd, + + int fragsByRam, + + int fragsByDbomb +) { + public static FragsInfo to(ShipInfo info) { + return new FragsInfo( + info.fragsInfo().frags(), + info.fragsInfo().fragsByMain(), + info.fragsInfo().fragsByAtba(), + info.fragsInfo().fragsByPlanes(), + info.fragsInfo().fragsByTpd(), + info.fragsInfo().fragsByRam(), + info.fragsInfo().fragsByDbomb() + ); + } +} diff --git a/src/main/java/com/shinoaki/wows/real/wows/game/info/HitRatioInfo.java b/src/main/java/com/shinoaki/wows/real/wows/game/info/HitRatioInfo.java new file mode 100644 index 0000000..2e3bdf1 --- /dev/null +++ b/src/main/java/com/shinoaki/wows/real/wows/game/info/HitRatioInfo.java @@ -0,0 +1,31 @@ +package com.shinoaki.wows.real.wows.game.info; + +import com.shinoaki.wows.api.data.ShipInfo; +import com.shinoaki.wows.real.wows.WowsCache; + +/** + * 命中信息 + * + * @author Xun + * @date 2023/5/14 17:49 星期日 + */ +public record HitRatioInfo( + + double ratioMain, + + double ratioAtba, + + double ratioTpd, + + double ratioTbomb +) { + + public static HitRatioInfo to(ShipInfo info) { + return new HitRatioInfo( + WowsCache.doubleCheckAnd_HALF_UP(info.ratioMain().hitRatio()), + WowsCache.doubleCheckAnd_HALF_UP(info.ratioAtba().hitRatio()), + WowsCache.doubleCheckAnd_HALF_UP(info.ratioTpd().hitRatio()), + WowsCache.doubleCheckAnd_HALF_UP(info.ratioTbomb().hitRatio()) + ); + } +} diff --git a/src/main/java/com/shinoaki/wows/real/wows/game/info/MaxInfo.java b/src/main/java/com/shinoaki/wows/real/wows/game/info/MaxInfo.java new file mode 100644 index 0000000..095efa6 --- /dev/null +++ b/src/main/java/com/shinoaki/wows/real/wows/game/info/MaxInfo.java @@ -0,0 +1,70 @@ +package com.shinoaki.wows.real.wows.game.info; + +import com.shinoaki.wows.api.data.ShipInfo; + +/** + * 最高击杀信息 + * + * @author Xun + * @date 2023/5/14 17:34 星期日 + */ +public record MaxInfo(MaxShipInfo maxFrags, MaxShipInfo maxFragsByMain, MaxShipInfo maxFragsByTpd, MaxShipInfo maxFragsByDbomb, MaxShipInfo maxFragsByRam, + MaxShipInfo maxFragsByAtba, + MaxShipInfo maxDamageDealtToBuildings, MaxShipInfo maxFragsByPlanes, MaxShipInfo maxDamageDealt, MaxShipInfo maxScoutingDamage, + MaxShipInfo maxPlanesKilled, + MaxShipInfo maxShipsSpotted, MaxShipInfo maxTotalAgro, MaxShipInfo maxSuppressionsCount, MaxShipInfo maxXp, MaxShipInfo maxBasicXp) { + + public static MaxInfo to(ShipInfo info) { + return new MaxInfo( + MaxShipInfo.to(info.maxInfo().maxFrags()), + MaxShipInfo.to(info.maxInfo().maxFragsByMain()), + MaxShipInfo.to(info.maxInfo().maxFragsByTpd()), + MaxShipInfo.to(info.maxInfo().maxFragsByDbomb()), + MaxShipInfo.to(info.maxInfo().maxFragsByRam()), + MaxShipInfo.to(info.maxInfo().maxFragsByAtba()), + MaxShipInfo.to(info.maxInfo().maxDamageDealtToBuildings()), + MaxShipInfo.to(info.maxInfo().maxFragsByPlanes()), + MaxShipInfo.to(info.maxInfo().maxDamageDealt()), + MaxShipInfo.to(info.maxInfo().maxScoutingDamage()), + MaxShipInfo.to(info.maxInfo().maxPlanesKilled()), + MaxShipInfo.to(info.maxInfo().maxShipsSpotted()), + MaxShipInfo.to(info.maxInfo().maxTotalAgro()), + MaxShipInfo.to(info.maxInfo().maxSuppressionsCount()), + MaxShipInfo.to(info.maxInfo().maxXp()), + MaxShipInfo.to(info.maxInfo().maxBasicXp()) + ); + } + + public static MaxInfo empty(long shipId) { + return new MaxInfo(MaxShipInfo.empty(shipId), + MaxShipInfo.empty(shipId), + MaxShipInfo.empty(shipId), + MaxShipInfo.empty(shipId), + MaxShipInfo.empty(shipId), + MaxShipInfo.empty(shipId), + MaxShipInfo.empty(shipId), + MaxShipInfo.empty(shipId), + MaxShipInfo.empty(shipId), + MaxShipInfo.empty(shipId), + MaxShipInfo.empty(shipId), + MaxShipInfo.empty(shipId), + MaxShipInfo.empty(shipId), + MaxShipInfo.empty(shipId), + MaxShipInfo.empty(shipId), + MaxShipInfo.empty(shipId)); + } + + + public record MaxShipInfo( + long shipId, + + int value) { + public static MaxShipInfo to(com.shinoaki.wows.api.data.ship.MaxInfo.MaxShipInfo info) { + return new MaxShipInfo(info.shipId(), info.value()); + } + + private static MaxShipInfo empty(long shipId) { + return new MaxShipInfo(shipId, 0); + } + } +} diff --git a/src/main/java/com/shinoaki/wows/real/wows/global/KeyValue.java b/src/main/java/com/shinoaki/wows/real/wows/global/KeyValue.java new file mode 100644 index 0000000..c3db57b --- /dev/null +++ b/src/main/java/com/shinoaki/wows/real/wows/global/KeyValue.java @@ -0,0 +1,8 @@ +package com.shinoaki.wows.real.wows.global; + +/** + * @author Xun + * @date 2023/5/11 23:40 星期四 + */ +public record KeyValue(T key, String value) { +} diff --git a/src/main/java/com/shinoaki/wows/real/wows/global/NationData.java b/src/main/java/com/shinoaki/wows/real/wows/global/NationData.java new file mode 100644 index 0000000..4716aad --- /dev/null +++ b/src/main/java/com/shinoaki/wows/real/wows/global/NationData.java @@ -0,0 +1,9 @@ +package com.shinoaki.wows.real.wows.global; + +/** + * 地区信息 + * @author Xun + * @date 2023/5/11 23:39 星期四 + */ +public record NationData(String nation, String cn) { +} diff --git a/src/main/java/com/shinoaki/wows/real/wows/global/PrInfo.java b/src/main/java/com/shinoaki/wows/real/wows/global/PrInfo.java new file mode 100644 index 0000000..bd54508 --- /dev/null +++ b/src/main/java/com/shinoaki/wows/real/wows/global/PrInfo.java @@ -0,0 +1,28 @@ +package com.shinoaki.wows.real.wows.global; + + +import com.shinoaki.wows.real.wows.WowsCache; + +/** + * @author Xun + * @date 2023/5/11 23:38 星期四 + */ +public record PrInfo( + int code, + int value, + int nextValue, + String name, + String englishName, + String color +) implements Comparable { + + public static PrInfo pr(int pr) { + PrInfo info = WowsCache.getPr(pr); + return new PrInfo(info.code(), pr, pr > info.value() ? 0 : info.value() - pr, info.name, info.englishName, info.color); + } + + @Override + public int compareTo(PrInfo prJson) { + return this.value - prJson.value; + } +} diff --git a/src/main/java/com/shinoaki/wows/real/wows/global/ShipType.java b/src/main/java/com/shinoaki/wows/real/wows/global/ShipType.java new file mode 100644 index 0000000..15dc6d5 --- /dev/null +++ b/src/main/java/com/shinoaki/wows/real/wows/global/ShipType.java @@ -0,0 +1,8 @@ +package com.shinoaki.wows.real.wows.global; + +/** + * @author Xun + * @date 2023/5/12 0:16 星期五 + */ +public record ShipType(String shipType, String typeName) { +} diff --git a/src/main/java/com/shinoaki/wows/real/wows/service/CacheShipMap.java b/src/main/java/com/shinoaki/wows/real/wows/service/CacheShipMap.java new file mode 100644 index 0000000..9fffe72 --- /dev/null +++ b/src/main/java/com/shinoaki/wows/real/wows/service/CacheShipMap.java @@ -0,0 +1,14 @@ +package com.shinoaki.wows.real.wows.service; + +import com.shinoaki.wows.api.data.ShipInfo; +import com.shinoaki.wows.api.type.WowsBattlesType; + +import java.util.List; +import java.util.Map; + +/** + * @author Xun + * create or update time = 2023/7/26 18:19 星期三 + */ +public record CacheShipMap(boolean emptyFile, Map> shipMap) { +} diff --git a/src/main/java/com/shinoaki/wows/real/wows/service/WowsHttpData.java b/src/main/java/com/shinoaki/wows/real/wows/service/WowsHttpData.java new file mode 100644 index 0000000..e1994d8 --- /dev/null +++ b/src/main/java/com/shinoaki/wows/real/wows/service/WowsHttpData.java @@ -0,0 +1,51 @@ +package com.shinoaki.wows.real.wows.service; + +import com.shinoaki.wows.api.type.WowsServer; +import com.shinoaki.wows.real.config.ProxyConfig; +import com.shinoaki.wows.real.config.type.DataType; + +import java.net.InetSocketAddress; +import java.net.ProxySelector; +import java.net.http.HttpClient; +import java.util.Objects; + +/** + * @author Xun + * create or update time = 2023/7/26 18:18 星期三 + */ +public class WowsHttpData { + private final HttpClient httpClient; + private final ProxyConfig config; + private final WowsServer server; + private final DataType dataType; + + public WowsHttpData(ProxyConfig config, WowsServer server) { + HttpClient.Builder builder = HttpClient.newBuilder().followRedirects(HttpClient.Redirect.ALWAYS); + ProxyConfig.ProxyServer proxyServer = config.findServer(server); + if (proxyServer != null && proxyServer.enableProxy()) { + this.httpClient = builder.proxy(ProxySelector.of(new InetSocketAddress(config.host(), config.port()))).build(); + } else { + this.httpClient = builder.build(); + } + + this.dataType = Objects.requireNonNull(config.findServer(server)).dataType(); + this.config = config; + this.server = server; + } + + public WowsServer server() { + return server; + } + + public ProxyConfig config() { + return config; + } + + public HttpClient httpClient() { + return httpClient; + } + + public DataType dataType() { + return dataType; + } +} diff --git a/src/main/java/com/shinoaki/wows/real/wows/source/DamageData.java b/src/main/java/com/shinoaki/wows/real/wows/source/DamageData.java new file mode 100644 index 0000000..8fbff69 --- /dev/null +++ b/src/main/java/com/shinoaki/wows/real/wows/source/DamageData.java @@ -0,0 +1,20 @@ +package com.shinoaki.wows.real.wows.source; + +/** + * @author Xun + * @date 2023/5/29 7:35 星期一 + */ +public record DamageData( + int code, + int value, + String color +) implements Comparable { + @Override + public int compareTo(DamageData prJson) { + return Integer.compare(this.value, prJson.value); + } + + public static DamageData empty(){ + return new DamageData(0,0,"#FE7903"); + } +} diff --git a/src/main/java/com/shinoaki/wows/real/wows/source/WinsData.java b/src/main/java/com/shinoaki/wows/real/wows/source/WinsData.java new file mode 100644 index 0000000..5b11a66 --- /dev/null +++ b/src/main/java/com/shinoaki/wows/real/wows/source/WinsData.java @@ -0,0 +1,16 @@ +package com.shinoaki.wows.real.wows.source; + +/** + * @author Xun + * @date 2023/5/29 7:33 星期一 + */ +public record WinsData( + int code, + double value, + String color +) implements Comparable { + @Override + public int compareTo(WinsData prJson) { + return Double.compare(this.value, prJson.value); + } +} diff --git a/src/main/java/com/shinoaki/wows/real/wows/user/WowsClanInfo.java b/src/main/java/com/shinoaki/wows/real/wows/user/WowsClanInfo.java new file mode 100644 index 0000000..3ae97a5 --- /dev/null +++ b/src/main/java/com/shinoaki/wows/real/wows/user/WowsClanInfo.java @@ -0,0 +1,29 @@ +package com.shinoaki.wows.real.wows.user; + +/** + * @author Xun + * @date 2023/5/14 15:21 星期日 + */ +public record WowsClanInfo( + + long clanId, + + String tag, + + + String name, + + String description, + + String color, + + int activeLevel +) { + public static WowsClanInfo empty() { + return new WowsClanInfo(0, "", "", "", "#FFFAFA", -1); + } + + public static WowsClanInfo clanNoColor(long clanId, String tag, String name, String description) { + return new WowsClanInfo(clanId, tag, name, description, "#FFFAFA", -1); + } +} diff --git a/src/main/java/com/shinoaki/wows/real/wows/user/WowsUserInfo.java b/src/main/java/com/shinoaki/wows/real/wows/user/WowsUserInfo.java new file mode 100644 index 0000000..e9528d5 --- /dev/null +++ b/src/main/java/com/shinoaki/wows/real/wows/user/WowsUserInfo.java @@ -0,0 +1,30 @@ +package com.shinoaki.wows.real.wows.user; + +import com.shinoaki.wows.api.type.WowsServer; + +/** + * @author Xun + * @date 2023/5/14 15:21 星期日 + */ +public record WowsUserInfo( + + String server, + + String serverCn, + + WowsClanInfo clanInfo, + + long accountId, + + String userName, + + long accountCreateTime) { + + public static WowsUserInfo user(long accountId, String userName, WowsServer server, WowsClanInfo clanInfo, long createTime) { + return new WowsUserInfo(server.getCode(), server.getName(), clanInfo, accountId, userName, createTime); + } + + public static WowsUserInfo empty() { + return new WowsUserInfo("?", "?", WowsClanInfo.empty(), 0, "?", 0); + } +} diff --git a/src/main/java/com/shinoaki/wows/real/wows/view/UserShipInfoView.java b/src/main/java/com/shinoaki/wows/real/wows/view/UserShipInfoView.java new file mode 100644 index 0000000..1e6c76d --- /dev/null +++ b/src/main/java/com/shinoaki/wows/real/wows/view/UserShipInfoView.java @@ -0,0 +1,14 @@ +package com.shinoaki.wows.real.wows.view; + +import com.shinoaki.wows.api.type.WowsBattlesType; +import com.shinoaki.wows.real.wows.WowsShipInfo; +import com.shinoaki.wows.real.wows.game.BattleInfoData; +import com.shinoaki.wows.real.wows.global.PrInfo; +import com.shinoaki.wows.real.wows.user.WowsUserInfo; + +/** + * @author Xun + * create or update time = 2023/7/25 22:45 星期二 + */ +public record UserShipInfoView(WowsUserInfo userInfo, WowsShipInfo shipInfo, WowsBattlesType battlesType, PrInfo prInfo, BattleInfoData battleInfo) { +} diff --git a/src/main/java/com/shinoaki/wows/real/ws/data/BaseWsPackage.java b/src/main/java/com/shinoaki/wows/real/ws/data/BaseWsPackage.java new file mode 100644 index 0000000..9d32d1c --- /dev/null +++ b/src/main/java/com/shinoaki/wows/real/ws/data/BaseWsPackage.java @@ -0,0 +1,30 @@ +package com.shinoaki.wows.real.ws.data; + +/** + * @author Xun + * create or update time = 2023/7/25 18:38 星期二 + */ +public class BaseWsPackage { + private final String path; + private final long time; + + private final T data; + + public BaseWsPackage(String path, T data) { + this.path = path; + this.time = System.currentTimeMillis(); + this.data = data; + } + + public String getPath() { + return path; + } + + public long getTime() { + return time; + } + + public T getData() { + return data; + } +} diff --git a/src/main/java/com/shinoaki/wows/real/ws/data/WsPathType.java b/src/main/java/com/shinoaki/wows/real/ws/data/WsPathType.java new file mode 100644 index 0000000..9d47b01 --- /dev/null +++ b/src/main/java/com/shinoaki/wows/real/ws/data/WsPathType.java @@ -0,0 +1,16 @@ +package com.shinoaki.wows.real.ws.data; + +/** + * @author Xun + * create or update time = 2023/7/25 22:55 星期二 + */ +public enum WsPathType { + /** + * 个人战绩信息变动 + */ + user_real_info, + /** + * 监听数据变动 + */ + user_accountId +} diff --git a/src/main/java/com/shinoaki/wows/real/ws/server/WowsWebSocketServerInitializer.java b/src/main/java/com/shinoaki/wows/real/ws/server/WowsWebSocketServerInitializer.java new file mode 100644 index 0000000..23e41c9 --- /dev/null +++ b/src/main/java/com/shinoaki/wows/real/ws/server/WowsWebSocketServerInitializer.java @@ -0,0 +1,50 @@ +package com.shinoaki.wows.real.ws.server; + + +import com.shinoaki.wows.real.ws.server.handler.ChannelEventsHandler; +import com.shinoaki.wows.real.ws.server.handler.WebSocketFrameHandler; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.socket.SocketChannel; +import io.netty.handler.codec.http.HttpObjectAggregator; +import io.netty.handler.codec.http.HttpServerCodec; +import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolConfig; +import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler; +import io.netty.handler.ssl.SslContext; + +import java.util.List; + +/** + * @author Xun + * create or update time = 2023/7/25 16:41 星期二 + */ +public class WowsWebSocketServerInitializer extends ChannelInitializer { + private final SslContext sslContext; + private final int maxContentLength; + private final List websocketPathList; + + public WowsWebSocketServerInitializer(String websocketPath) { + this.sslContext = null; + this.maxContentLength = 65536; + if (websocketPath.startsWith("/")) { + this.websocketPathList = List.of(websocketPath); + } else { + this.websocketPathList = List.of("/" + websocketPath); + } + } + + + @Override + protected void initChannel(SocketChannel socketChannel) throws Exception { + //ssl处理 + ChannelPipeline pipeline = socketChannel.pipeline(); + if (sslContext != null) { + pipeline.addLast(sslContext.newHandler(socketChannel.alloc())); + } + pipeline.addLast(new ChannelEventsHandler(this.websocketPathList)); + pipeline.addLast(new HttpServerCodec()); + pipeline.addLast(new HttpObjectAggregator(maxContentLength)); + this.websocketPathList.forEach(path -> pipeline.addLast(new WebSocketServerProtocolHandler(WebSocketServerProtocolConfig.newBuilder().websocketPath(path).subprotocols(null).allowExtensions(true).build()))); + pipeline.addLast(new WebSocketFrameHandler()); + } +} diff --git a/src/main/java/com/shinoaki/wows/real/ws/server/WowsWebsocket.java b/src/main/java/com/shinoaki/wows/real/ws/server/WowsWebsocket.java new file mode 100644 index 0000000..8584867 --- /dev/null +++ b/src/main/java/com/shinoaki/wows/real/ws/server/WowsWebsocket.java @@ -0,0 +1,31 @@ +package com.shinoaki.wows.real.ws.server; + +import io.netty.bootstrap.ServerBootstrap; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.nio.NioServerSocketChannel; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * @author Xun + * create or update time = 2023/7/25 19:21 星期二 + */ +public class WowsWebsocket { + private final Logger log = LoggerFactory.getLogger(WowsWebsocket.class); + private final EventLoopGroup serverGroup = new NioEventLoopGroup(); + private final EventLoopGroup workerGroup = new NioEventLoopGroup(); + + public void start(int port, String path) throws InterruptedException { + ServerBootstrap bootstrap = new ServerBootstrap(); + bootstrap.group(serverGroup, workerGroup).channel(NioServerSocketChannel.class); + bootstrap.childHandler(new WowsWebSocketServerInitializer(path)); + bootstrap.bind(port).sync(); + log.info("websocket服务器启动! local port={} path={}", port, path); + } + + public void close() { + serverGroup.shutdownGracefully(); + workerGroup.shutdownGracefully(); + } +} diff --git a/src/main/java/com/shinoaki/wows/real/ws/server/handler/ChannelEventsHandler.java b/src/main/java/com/shinoaki/wows/real/ws/server/handler/ChannelEventsHandler.java new file mode 100644 index 0000000..eca62fe --- /dev/null +++ b/src/main/java/com/shinoaki/wows/real/ws/server/handler/ChannelEventsHandler.java @@ -0,0 +1,78 @@ +package com.shinoaki.wows.real.ws.server.handler; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelDuplexHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelId; +import io.netty.handler.codec.http.HttpObject; +import io.netty.handler.codec.http.HttpRequest; +import io.netty.handler.codec.http.websocketx.TextWebSocketFrame; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +/** + * @author Xun + * create or update time = 2023/7/25 16:55 星期二 + */ +public class ChannelEventsHandler extends ChannelDuplexHandler { + private final Logger log = LoggerFactory.getLogger(ChannelEventsHandler.class); + private static final ConcurrentMap CHANNEL_ID_CHANNEL_CONCURRENT_MAP = new ConcurrentHashMap<>(); + private static final Map BOOLEAN_MAP = new HashMap<>(); + + public ChannelEventsHandler(List path) { + path.forEach(x -> BOOLEAN_MAP.put(x, Boolean.TRUE)); + } + + private void put(Channel channel) { + CHANNEL_ID_CHANNEL_CONCURRENT_MAP.put(channel.id(), channel); + } + + private void remove(ChannelId id) { + CHANNEL_ID_CHANNEL_CONCURRENT_MAP.remove(id); + } + + public boolean check(String path) { + return BOOLEAN_MAP.getOrDefault(path, false); + } + + public static void sendMsg(ChannelId id, String msg) { + Channel channel = CHANNEL_ID_CHANNEL_CONCURRENT_MAP.getOrDefault(id, null); + if (channel != null) { + channel.writeAndFlush(new TextWebSocketFrame(msg)); + } + } + + @Override + public void channelRegistered(ChannelHandlerContext ctx) throws Exception { + log.info("channelRegistered 用户建立连接={}", ctx.channel().id()); + this.put(ctx.channel()); + super.channelRegistered(ctx); + } + + @Override + public void channelUnregistered(ChannelHandlerContext ctx) throws Exception { + log.info("channelUnregistered 用户断开连接={}", ctx.channel().id()); + this.remove(ctx.channel().id()); + super.channelUnregistered(ctx); + } + + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + if (msg instanceof HttpObject httpObject && httpObject instanceof HttpRequest request) { + String uri = request.uri(); + if (!check(uri)) { + log.info("未匹配的uri={}", uri); + ctx.channel().close(); + return; + } + } + ctx.fireChannelRead(msg); + } +} diff --git a/src/main/java/com/shinoaki/wows/real/ws/server/handler/WebSocketFrameHandler.java b/src/main/java/com/shinoaki/wows/real/ws/server/handler/WebSocketFrameHandler.java new file mode 100644 index 0000000..ec17e7e --- /dev/null +++ b/src/main/java/com/shinoaki/wows/real/ws/server/handler/WebSocketFrameHandler.java @@ -0,0 +1,34 @@ +package com.shinoaki.wows.real.ws.server.handler; + +import com.fasterxml.jackson.databind.JsonNode; +import com.shinoaki.wows.api.utils.JsonUtils; +import com.shinoaki.wows.real.service.WsService; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.SimpleChannelInboundHandler; +import io.netty.handler.codec.http.websocketx.TextWebSocketFrame; +import io.netty.handler.codec.http.websocketx.WebSocketFrame; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * @author Xun + * create or update time = 2023/7/25 16:32 星期二 + */ +public class WebSocketFrameHandler extends SimpleChannelInboundHandler { + private static final Logger log = LoggerFactory.getLogger(WebSocketFrameHandler.class); + + @Override + protected void channelRead0(ChannelHandlerContext ctx, WebSocketFrame frame) throws Exception { + if (frame instanceof TextWebSocketFrame msg) { + JsonUtils json = new JsonUtils(); + JsonNode node = json.parse(msg.text()); + WsService.select(ctx.channel().id(), node.get("path").asText(), node.get("data").toString()); + } + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { + log.info("websocket异常!", cause); + } + +} diff --git a/src/main/java/com/shinoaki/wows/real/ws/server/handler/WebSocketIndexPageHandler.java b/src/main/java/com/shinoaki/wows/real/ws/server/handler/WebSocketIndexPageHandler.java new file mode 100644 index 0000000..89490f3 --- /dev/null +++ b/src/main/java/com/shinoaki/wows/real/ws/server/handler/WebSocketIndexPageHandler.java @@ -0,0 +1,54 @@ +package com.shinoaki.wows.real.ws.server.handler; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelFutureListener; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.SimpleChannelInboundHandler; +import io.netty.handler.codec.http.DefaultFullHttpResponse; +import io.netty.handler.codec.http.FullHttpRequest; +import io.netty.handler.codec.http.FullHttpResponse; +import io.netty.handler.codec.http.HttpHeaders; +import io.netty.util.CharsetUtil; + +import static io.netty.handler.codec.http.HttpMethod.GET; +import static io.netty.handler.codec.http.HttpResponseStatus.*; +import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1; + +/** + * @author Xun + * create or update time = 2023/7/26 0:16 星期三 + */ +public class WebSocketIndexPageHandler extends SimpleChannelInboundHandler { + + + @Override + protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest req) throws Exception { + // Handle a bad request. + if (!req.getDecoderResult().isSuccess()) { + sendHttpResponse(ctx, req, new DefaultFullHttpResponse(HTTP_1_1, BAD_REQUEST)); + return; + } + // Allow only GET methods. + if (req.getMethod() != GET) { + sendHttpResponse(ctx, req, new DefaultFullHttpResponse(HTTP_1_1, FORBIDDEN)); + } + sendHttpResponse(ctx, req, new DefaultFullHttpResponse(HTTP_1_1, OK)); + } + + private static void sendHttpResponse(ChannelHandlerContext ctx, FullHttpRequest req, FullHttpResponse res) { + // Generate an error page if response getStatus code is not OK (200). + if (res.getStatus().code() != 200) { + ByteBuf buf = Unpooled.copiedBuffer(res.getStatus().toString(), CharsetUtil.UTF_8); + res.content().writeBytes(buf); + buf.release(); + HttpHeaders.setContentLength(res, res.content().readableBytes()); + } + // Send the response and close the connection if necessary. + ChannelFuture f = ctx.channel().writeAndFlush(res); + if (!HttpHeaders.isKeepAlive(req) || res.getStatus().code() != 200) { + f.addListener(ChannelFutureListener.CLOSE); + } + } +} diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml new file mode 100644 index 0000000..baa1114 --- /dev/null +++ b/src/main/resources/logback.xml @@ -0,0 +1,35 @@ + + + + + + + %red(%date{yyyy-MM-dd HH:mm:ss}) %highlight(%-5level) %red([%thread]) %boldMagenta(%logger{50}) %cyan(%msg%n) + + + + + true + + + ${log.path}/%d{yyyy-MM-dd}.log + + 30 + + + + %d{HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + + + + + +