diff --git a/.env.example b/.env.example index 3ffc8f5..8835300 100644 --- a/.env.example +++ b/.env.example @@ -8,3 +8,7 @@ POSTGRES_DB=game # MongoDB (required by the app) MONGO_ROOT_USERNAME=wingsemu MONGO_ROOT_PASSWORD=change_me_mongo_password + +# Optional (server secrets) +JWT_PRIVATE_KEY=change_me_jwt_secret +HEALTHCHECK_API_KEY=change_me_healthcheck_key diff --git a/README.md b/README.md index 2d1c43a..0ece974 100644 --- a/README.md +++ b/README.md @@ -134,13 +134,18 @@ cp .env.example .env docker compose up -d ``` -4. Check status: +4. Add game data resources for World server: + +- Create `./resources/dat/` +- Ensure at least `./resources/dat/Item.dat` exists (and other required game data files) + +5. Check status: ```bash docker compose ps ``` -5. Stop everything: +6. Stop everything: ```bash docker compose down diff --git a/config/family_achievements_configuration.yaml b/config/family_achievements_configuration.yaml new file mode 100644 index 0000000..a725170 --- /dev/null +++ b/config/family_achievements_configuration.yaml @@ -0,0 +1 @@ +counters: [] diff --git a/config/family_achievements_configuration.yml b/config/family_achievements_configuration.yml new file mode 100644 index 0000000..a725170 --- /dev/null +++ b/config/family_achievements_configuration.yml @@ -0,0 +1 @@ +counters: [] diff --git a/config/family_missions_configuration.yaml b/config/family_missions_configuration.yaml new file mode 100644 index 0000000..fe51488 --- /dev/null +++ b/config/family_missions_configuration.yaml @@ -0,0 +1 @@ +[] diff --git a/config/family_missions_configuration.yml b/config/family_missions_configuration.yml new file mode 100644 index 0000000..fe51488 --- /dev/null +++ b/config/family_missions_configuration.yml @@ -0,0 +1 @@ +[] diff --git a/docker-compose.yml b/docker-compose.yml index 4708810..44e645b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -62,7 +62,424 @@ services: timeout: 10s retries: 10 + master: + build: + context: . + dockerfile: docker/Dockerfile.service + args: + SERVICE_DIR: master + container_name: wingsemu-master + restart: unless-stopped + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_healthy + mongodb: + condition: service_healthy + mqtt: + condition: service_healthy + environment: + MASTER_PORT: 20500 + DATABASE_IP: postgres + DATABASE_PORT: 5432 + DATABASE_NAME: ${POSTGRES_DB:-game} + DATABASE_USER: ${POSTGRES_USER:-postgres} + DATABASE_PASSWORD: ${POSTGRES_PASSWORD} + REDIS_IP: redis + REDIS_PORT: 6379 + POSTGRES_DATABASE_IP: postgres + POSTGRES_DATABASE_PORT: 5432 + POSTGRES_DATABASE_NAME: ${POSTGRES_DB:-game} + POSTGRES_DATABASE_USERNAME: ${POSTGRES_USER:-postgres} + POSTGRES_DATABASE_PASSWORD: ${POSTGRES_PASSWORD} + WINGSEMU_MONGO_HOST: mongodb + WINGSEMU_MONGO_PORT: 27017 + WINGSEMU_MONGO_DB: wingsemu_logs + WINGSEMU_MONGO_USERNAME: ${MONGO_ROOT_USERNAME} + WINGSEMU_MONGO_PWD: ${MONGO_ROOT_PASSWORD} + MQTT_BROKER_ADDRESS: mqtt + MQTT_BROKER_PORT: 1883 + command: ["/app/Master.dll"] + ports: + - "20500:20500" + + login: + build: + context: . + dockerfile: docker/Dockerfile.service + args: + SERVICE_DIR: login-server + container_name: wingsemu-login + restart: unless-stopped + depends_on: + master: + condition: service_started + environment: + SERVER_PORT: 4004 + MASTER_IP: master + MASTER_PORT: 20500 + REDIS_IP: redis + REDIS_PORT: 6379 + MQTT_BROKER_ADDRESS: mqtt + MQTT_BROKER_PORT: 1883 + command: ["/app/LoginServer.dll"] + ports: + - "4004:4004" + + gamechannel: + build: + context: . + dockerfile: docker/Dockerfile.service + args: + SERVICE_DIR: game-server + container_name: wingsemu-gamechannel + restart: unless-stopped + depends_on: + master: + condition: service_started + database: + condition: service_started + bazaar: + condition: service_started + family: + condition: service_started + relation: + condition: service_started + mail: + condition: service_started + translation: + condition: service_started + environment: + MASTER_IP: master + MASTER_PORT: 20500 + DATABASE_IP: postgres + DATABASE_PORT: 5432 + DATABASE_NAME: ${POSTGRES_DB:-game} + DATABASE_USER: ${POSTGRES_USER:-postgres} + DATABASE_PASSWORD: ${POSTGRES_PASSWORD} + DB_SERVER_IP: database + DB_SERVER_PORT: 29999 + BAZAAR_SERVER_IP: bazaar + BAZAAR_SERVER_PORT: 25555 + FAMILY_SERVER_IP: family + FAMILY_SERVER_PORT: 26666 + RELATION_SERVER_IP: relation + RELATION_SERVER_PORT: 21111 + MAIL_SERVER_IP: mail + MAIL_SERVER_PORT: 27777 + TRANSLATIONS_SERVER_IP: translation + TRANSLATIONS_SERVER_PORT: 19999 + GAME_SERVER_IP: 0.0.0.0 + GAME_SERVER_PORT: 8000 + GAME_SERVER_CHANNEL_ID: 1 + GAME_SERVER_GROUP: 1 + HTTP_LISTEN_PORT: 17500 + REDIS_IP: redis + REDIS_PORT: 6379 + MQTT_BROKER_ADDRESS: mqtt + MQTT_BROKER_PORT: 1883 + WINGSEMU_MONGO_HOST: mongodb + WINGSEMU_MONGO_PORT: 27017 + WINGSEMU_MONGO_DB: wingsemu_logs + WINGSEMU_MONGO_USERNAME: ${MONGO_ROOT_USERNAME} + WINGSEMU_MONGO_PWD: ${MONGO_ROOT_PASSWORD} + command: ["/app/GameChannel.dll"] + volumes: + - ./resources:/app/resources:ro + - ./config:/app/config + ports: + - "8000:8000" + - "17500:17500" + + database: + build: + context: . + dockerfile: docker/Dockerfile.service + args: + SERVICE_DIR: database-server + container_name: wingsemu-database + restart: unless-stopped + depends_on: + master: + condition: service_started + postgres: + condition: service_healthy + redis: + condition: service_healthy + mqtt: + condition: service_healthy + environment: + DATABASE_SERVER_PORT: 29999 + MASTER_IP: master + MASTER_PORT: 20500 + DATABASE_IP: postgres + DATABASE_PORT: 5432 + DATABASE_NAME: ${POSTGRES_DB:-game} + DATABASE_USER: ${POSTGRES_USER:-postgres} + DATABASE_PASSWORD: ${POSTGRES_PASSWORD} + POSTGRES_DATABASE_IP: postgres + POSTGRES_DATABASE_PORT: 5432 + POSTGRES_DATABASE_NAME: ${POSTGRES_DB:-game} + POSTGRES_DATABASE_USERNAME: ${POSTGRES_USER:-postgres} + POSTGRES_DATABASE_PASSWORD: ${POSTGRES_PASSWORD} + REDIS_IP: redis + REDIS_PORT: 6379 + MQTT_BROKER_ADDRESS: mqtt + MQTT_BROKER_PORT: 1883 + command: ["/app/DatabaseServer.dll"] + ports: + - "29999:29999" + + bazaar: + build: + context: . + dockerfile: docker/Dockerfile.service + args: + SERVICE_DIR: bazaar-server + container_name: wingsemu-bazaar + restart: unless-stopped + depends_on: + master: + condition: service_started + database: + condition: service_started + redis: + condition: service_healthy + mqtt: + condition: service_healthy + environment: + BAZAAR_SERVER_PORT: 25555 + MASTER_IP: master + MASTER_PORT: 20500 + DATABASE_IP: postgres + DATABASE_PORT: 5432 + DATABASE_NAME: ${POSTGRES_DB:-game} + DATABASE_USER: ${POSTGRES_USER:-postgres} + DATABASE_PASSWORD: ${POSTGRES_PASSWORD} + DB_SERVER_IP: database + DB_SERVER_PORT: 29999 + REDIS_IP: redis + REDIS_PORT: 6379 + MQTT_BROKER_ADDRESS: mqtt + MQTT_BROKER_PORT: 1883 + command: ["/app/BazaarServer.dll"] + volumes: + - ./resources:/app/resources:ro + - ./config:/app/config + ports: + - "25555:25555" + + family: + build: + context: . + dockerfile: docker/Dockerfile.service + args: + SERVICE_DIR: family-server + container_name: wingsemu-family + restart: unless-stopped + depends_on: + master: + condition: service_started + database: + condition: service_started + redis: + condition: service_healthy + mqtt: + condition: service_healthy + environment: + FAMILY_SERVER_PORT: 26666 + MASTER_IP: master + MASTER_PORT: 20500 + DATABASE_IP: postgres + DATABASE_PORT: 5432 + DATABASE_NAME: ${POSTGRES_DB:-game} + DATABASE_USER: ${POSTGRES_USER:-postgres} + DATABASE_PASSWORD: ${POSTGRES_PASSWORD} + DB_SERVER_IP: database + DB_SERVER_PORT: 29999 + REDIS_IP: redis + REDIS_PORT: 6379 + MQTT_BROKER_ADDRESS: mqtt + MQTT_BROKER_PORT: 1883 + command: ["/app/FamilyServer.dll"] + volumes: + - ./resources:/app/resources:ro + - ./config:/app/config + ports: + - "26666:26666" + + relation: + build: + context: . + dockerfile: docker/Dockerfile.service + args: + SERVICE_DIR: relation-server + container_name: wingsemu-relation + restart: unless-stopped + depends_on: + master: + condition: service_started + database: + condition: service_started + redis: + condition: service_healthy + mqtt: + condition: service_healthy + environment: + RELATION_SERVER_PORT: 21111 + MASTER_IP: master + MASTER_PORT: 20500 + DATABASE_IP: postgres + DATABASE_PORT: 5432 + DATABASE_NAME: ${POSTGRES_DB:-game} + DATABASE_USER: ${POSTGRES_USER:-postgres} + DATABASE_PASSWORD: ${POSTGRES_PASSWORD} + DB_SERVER_IP: database + DB_SERVER_PORT: 29999 + REDIS_IP: redis + REDIS_PORT: 6379 + MQTT_BROKER_ADDRESS: mqtt + MQTT_BROKER_PORT: 1883 + command: ["/app/RelationServer.dll"] + volumes: + - ./resources:/app/resources:ro + - ./config:/app/config + ports: + - "21111:21111" + + mail: + build: + context: . + dockerfile: docker/Dockerfile.service + args: + SERVICE_DIR: mail-server + container_name: wingsemu-mail + restart: unless-stopped + depends_on: + master: + condition: service_started + database: + condition: service_started + redis: + condition: service_healthy + mqtt: + condition: service_healthy + environment: + MAIL_SERVER_PORT: 27777 + MASTER_IP: master + MASTER_PORT: 20500 + DATABASE_IP: postgres + DATABASE_PORT: 5432 + DATABASE_NAME: ${POSTGRES_DB:-game} + DATABASE_USER: ${POSTGRES_USER:-postgres} + DATABASE_PASSWORD: ${POSTGRES_PASSWORD} + DB_SERVER_IP: database + DB_SERVER_PORT: 29999 + REDIS_IP: redis + REDIS_PORT: 6379 + MQTT_BROKER_ADDRESS: mqtt + MQTT_BROKER_PORT: 1883 + command: ["/app/MailServer.dll"] + volumes: + - ./resources:/app/resources:ro + - ./config:/app/config + ports: + - "27777:27777" + + translation: + build: + context: . + dockerfile: docker/Dockerfile.service + args: + SERVICE_DIR: translation-server + container_name: wingsemu-translation + restart: unless-stopped + depends_on: + master: + condition: service_started + redis: + condition: service_healthy + mqtt: + condition: service_healthy + environment: + TRANSLATION_SERVER_PORT: 19999 + MASTER_IP: master + MASTER_PORT: 20500 + REDIS_IP: redis + REDIS_PORT: 6379 + MQTT_BROKER_ADDRESS: mqtt + MQTT_BROKER_PORT: 1883 + command: ["/app/TranslationsServer.dll"] + volumes: + - ./translations:/app/translations:ro + ports: + - "19999:19999" + + logs: + build: + context: . + dockerfile: docker/Dockerfile.service + args: + SERVICE_DIR: logs-server + container_name: wingsemu-logs + restart: unless-stopped + depends_on: + master: + condition: service_started + mongodb: + condition: service_healthy + redis: + condition: service_healthy + mqtt: + condition: service_healthy + environment: + LOGS_SERVER_PORT: 28888 + MASTER_IP: master + MASTER_PORT: 20500 + REDIS_IP: redis + REDIS_PORT: 6379 + WINGSEMU_MONGO_HOST: mongodb + WINGSEMU_MONGO_PORT: 27017 + WINGSEMU_MONGO_DB: wingsemu_logs + WINGSEMU_MONGO_USERNAME: ${MONGO_ROOT_USERNAME} + WINGSEMU_MONGO_PWD: ${MONGO_ROOT_PASSWORD} + MQTT_BROKER_ADDRESS: mqtt + MQTT_BROKER_PORT: 1883 + command: ["/app/LogsServer.dll"] + ports: + - "28888:28888" + + scheduler: + build: + context: . + dockerfile: docker/Dockerfile.service + args: + SERVICE_DIR: scheduler + container_name: wingsemu-scheduler + restart: unless-stopped + depends_on: + master: + condition: service_started + redis: + condition: service_healthy + mqtt: + condition: service_healthy + environment: + MASTER_IP: master + MASTER_PORT: 20500 + REDIS_IP: redis + REDIS_PORT: 6379 + MQTT_BROKER_ADDRESS: mqtt + MQTT_BROKER_PORT: 1883 + command: ["/app/Scheduler.dll"] + ports: + - "25000:25000" + volumes: postgres_data: redis_data: mongodb_data: + + + diff --git a/docker/.dockerignore b/docker/.dockerignore new file mode 100644 index 0000000..223929a --- /dev/null +++ b/docker/.dockerignore @@ -0,0 +1,5 @@ +.git +**/bin +**/obj +.env +memory diff --git a/docker/Dockerfile.service b/docker/Dockerfile.service new file mode 100644 index 0000000..ad6f02d --- /dev/null +++ b/docker/Dockerfile.service @@ -0,0 +1,11 @@ +FROM mcr.microsoft.com/dotnet/aspnet:5.0 + +ARG SERVICE_DIR +ARG SERVICE_DLL + +WORKDIR /app +COPY dist/${SERVICE_DIR}/ ./ + +ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=false + +ENTRYPOINT ["dotnet"] diff --git a/srcs/WingsAPI.Game/Configurations/BankReputationConfiguration.cs b/srcs/WingsAPI.Game/Configurations/BankReputationConfiguration.cs index 2e4f6c7..daef16b 100644 --- a/srcs/WingsAPI.Game/Configurations/BankReputationConfiguration.cs +++ b/srcs/WingsAPI.Game/Configurations/BankReputationConfiguration.cs @@ -18,17 +18,23 @@ public class BankReputationConfiguration : IBankReputationConfiguration public BankReputationConfiguration(BankReputationInfo bankReputationInfo) { - var bankRankTypes = new Dictionary(); - foreach (BankRankInfo bankRankInfo in bankReputationInfo.BankRanks) + bankReputationInfo ??= new BankReputationInfo { - foreach (ReputationType reputation in bankRankInfo.Reputations) + BankRanks = new List(), + BankPenalties = new List() + }; + + var bankRankTypes = new Dictionary(); + foreach (BankRankInfo bankRankInfo in bankReputationInfo.BankRanks ?? new List()) + { + foreach (ReputationType reputation in bankRankInfo.Reputations ?? new List()) { bankRankTypes.TryAdd(reputation, bankRankInfo); } } _ranksByReputation = bankRankTypes.ToImmutableDictionary(); - _penaltiesByReputation = bankReputationInfo.BankPenalties.ToImmutableDictionary(s => s.Reputation); + _penaltiesByReputation = (bankReputationInfo.BankPenalties ?? new List()).ToImmutableDictionary(s => s.Reputation); } public BankRankInfo GetBankRankInfo(ReputationType reputationType) => _ranksByReputation.GetOrDefault(reputationType); diff --git a/srcs/WingsAPI.Game/RainbowBattle/IRainbowBattleManager.cs b/srcs/WingsAPI.Game/RainbowBattle/IRainbowBattleManager.cs index db6a33b..2e12640 100644 --- a/srcs/WingsAPI.Game/RainbowBattle/IRainbowBattleManager.cs +++ b/srcs/WingsAPI.Game/RainbowBattle/IRainbowBattleManager.cs @@ -39,7 +39,9 @@ public class RainbowBattleManager : IRainbowBattleManager public RainbowBattleManager(RainbowBattleConfiguration rainbowBattleConfiguration) { var warnings = new List<(TimeSpan, int, TimeType)>(); - foreach (TimeSpan warning in rainbowBattleConfiguration.Warnings) + IEnumerable configuredWarnings = rainbowBattleConfiguration?.Warnings ?? new List(); + + foreach (TimeSpan warning in configuredWarnings) { TimeSpan time = TimeSpan.FromMinutes(5) - warning; bool isSec = time.TotalMinutes < 1; diff --git a/srcs/_plugins/Plugin.ResourceLoader/InMemoryMultilanguageService.cs b/srcs/_plugins/Plugin.ResourceLoader/InMemoryMultilanguageService.cs index b4a2726..5190342 100644 --- a/srcs/_plugins/Plugin.ResourceLoader/InMemoryMultilanguageService.cs +++ b/srcs/_plugins/Plugin.ResourceLoader/InMemoryMultilanguageService.cs @@ -72,10 +72,15 @@ namespace Plugin.ResourceLoader } Log.Info("[MULTILANGUAGE] Loading..."); - IReadOnlyList gameDialogTranslations = await _genericTranslationsLoader.LoadAsync(); + IReadOnlyList gameDialogTranslations = await _genericTranslationsLoader.LoadAsync() ?? Array.Empty(); foreach (GenericTranslationDto tmp in gameDialogTranslations) { - _cacheClient.Set(ToKey(tmp.Key, tmp.Language), tmp.Value); + if (tmp?.Key == null) + { + continue; + } + + _cacheClient.Set(ToKey(tmp.Key, tmp.Language), tmp.Value ?? string.Empty); } Log.Info($"[MULTILANGUAGE] loaded {gameDialogTranslations.Count.ToString()} generic translations"); diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Items/DropRarityConfigurationProvider.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Items/DropRarityConfigurationProvider.cs index 25c6356..627de98 100644 --- a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Items/DropRarityConfigurationProvider.cs +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/Event/Items/DropRarityConfigurationProvider.cs @@ -15,8 +15,8 @@ public class DropRarityConfigurationProvider : IDropRarityConfigurationProvider public DropRarityConfigurationProvider(DropRarityConfiguration dropRarityConfiguration, IRandomGenerator randomGenerator) { _randomGenerator = randomGenerator; - _equipment = dropRarityConfiguration.Equipment.OrderBy(s => s.Chance).ToList(); - _shells = dropRarityConfiguration.Shells.OrderBy(s => s.Chance).ToList(); + _equipment = (dropRarityConfiguration?.Equipment ?? new List()).OrderBy(s => s.Chance).ToList(); + _shells = (dropRarityConfiguration?.Shells ?? new List()).OrderBy(s => s.Chance).ToList(); } public sbyte GetRandomRarity(ItemType itemType) @@ -27,6 +27,11 @@ public class DropRarityConfigurationProvider : IDropRarityConfigurationProvider } List rarities = itemType == ItemType.Shell ? _shells : _equipment; + if (rarities.Count == 0) + { + return 0; + } + var randomBag = new RandomBag(_randomGenerator); foreach (RarityChance rarity in rarities) diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/GuriPlugin.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/GuriPlugin.cs index 9714238..30b86dd 100644 --- a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/GuriPlugin.cs +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/GuriPlugin.cs @@ -37,7 +37,7 @@ public class GuriPlugin : IGamePlugin } catch (Exception e) { - Log.Error("[GURI][FAIL_ADD]", e); + Log.Error($"[GURI][FAIL_ADD] HandlerType={handlerType?.FullName}", e); } } } diff --git a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemHandlerPlugin.cs b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemHandlerPlugin.cs index d0fdb00..676a4d0 100644 --- a/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemHandlerPlugin.cs +++ b/srcs/_plugins/WingsEmu.Plugins.BasicImplementation/ItemHandlerPlugin.cs @@ -37,7 +37,7 @@ public class ItemHandlerPlugin : IGamePlugin } catch (Exception e) { - Log.Error("[ITEM_USAGE][FAIL_ADD]", e); + Log.Error($"[ITEM_USAGE][FAIL_ADD] HandlerType={handlerType?.FullName}", e); } } @@ -56,7 +56,7 @@ public class ItemHandlerPlugin : IGamePlugin } catch (Exception e) { - Log.Error("[ITEM_USAGE][FAIL_ADD_VNUM]", e); + Log.Error($"[ITEM_USAGE][FAIL_ADD_VNUM] HandlerType={handlerType?.FullName}", e); } } } diff --git a/srcs/_plugins/WingsEmu.Plugins.PacketHandling/GamePacketHandlersGamePlugin.cs b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/GamePacketHandlersGamePlugin.cs index 7202210..fdf80fb 100644 --- a/srcs/_plugins/WingsEmu.Plugins.PacketHandling/GamePacketHandlersGamePlugin.cs +++ b/srcs/_plugins/WingsEmu.Plugins.PacketHandling/GamePacketHandlersGamePlugin.cs @@ -35,14 +35,27 @@ public class GamePacketHandlersGamePlugin : IGamePlugin continue; } - Type type = handlerType.BaseType.GenericTypeArguments[0]; + Type baseType = handlerType.BaseType; + if (baseType == null || !baseType.IsGenericType || baseType.GenericTypeArguments.Length == 0) + { + Log.Warn($"[GAME_HANDLERS][SKIP_INVALID_BASE] {handlerType.FullName}"); + continue; + } + + Type type = baseType.GenericTypeArguments[0]; + if (type == null) + { + Log.Warn($"[GAME_HANDLERS][SKIP_NULL_PACKET_TYPE] {handlerType.FullName}"); + continue; + } _handlers.Register(type, handler); - Log.Info($"[GAME_HANDLERS][ADD_HANDLER] {type.GetCustomAttribute().Identification}"); + string identification = type.GetCustomAttribute()?.Identification ?? type.Name; + Log.Info($"[GAME_HANDLERS][ADD_HANDLER] {identification}"); } catch (Exception e) { - Log.Error("[GAME_HANDLERS][FAIL_ADD]", e); + Log.Error($"[GAME_HANDLERS][FAIL_ADD] HandlerType={registeredPacketHandler?.HandlerType?.FullName}", e); // ignored } }