Stabilize Docker full stack and fix GameChannel handler startup nullrefs

This commit is contained in:
nizar 2026-02-24 00:45:18 +01:00
parent 6cb6b4eb7e
commit e2a5d4487e
16 changed files with 494 additions and 17 deletions

View file

@ -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

View file

@ -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

View file

@ -0,0 +1 @@
counters: []

View file

@ -0,0 +1 @@
counters: []

View file

@ -0,0 +1 @@
[]

View file

@ -0,0 +1 @@
[]

View file

@ -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:

5
docker/.dockerignore Normal file
View file

@ -0,0 +1,5 @@
.git
**/bin
**/obj
.env
memory

11
docker/Dockerfile.service Normal file
View file

@ -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"]

View file

@ -18,17 +18,23 @@ public class BankReputationConfiguration : IBankReputationConfiguration
public BankReputationConfiguration(BankReputationInfo bankReputationInfo)
{
var bankRankTypes = new Dictionary<ReputationType, BankRankInfo>();
foreach (BankRankInfo bankRankInfo in bankReputationInfo.BankRanks)
bankReputationInfo ??= new BankReputationInfo
{
foreach (ReputationType reputation in bankRankInfo.Reputations)
BankRanks = new List<BankRankInfo>(),
BankPenalties = new List<BankPenaltyInfo>()
};
var bankRankTypes = new Dictionary<ReputationType, BankRankInfo>();
foreach (BankRankInfo bankRankInfo in bankReputationInfo.BankRanks ?? new List<BankRankInfo>())
{
foreach (ReputationType reputation in bankRankInfo.Reputations ?? new List<ReputationType>())
{
bankRankTypes.TryAdd(reputation, bankRankInfo);
}
}
_ranksByReputation = bankRankTypes.ToImmutableDictionary();
_penaltiesByReputation = bankReputationInfo.BankPenalties.ToImmutableDictionary(s => s.Reputation);
_penaltiesByReputation = (bankReputationInfo.BankPenalties ?? new List<BankPenaltyInfo>()).ToImmutableDictionary(s => s.Reputation);
}
public BankRankInfo GetBankRankInfo(ReputationType reputationType) => _ranksByReputation.GetOrDefault(reputationType);

View file

@ -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<TimeSpan> configuredWarnings = rainbowBattleConfiguration?.Warnings ?? new List<TimeSpan>();
foreach (TimeSpan warning in configuredWarnings)
{
TimeSpan time = TimeSpan.FromMinutes(5) - warning;
bool isSec = time.TotalMinutes < 1;

View file

@ -72,10 +72,15 @@ namespace Plugin.ResourceLoader
}
Log.Info("[MULTILANGUAGE] Loading...");
IReadOnlyList<GenericTranslationDto> gameDialogTranslations = await _genericTranslationsLoader.LoadAsync();
IReadOnlyList<GenericTranslationDto> gameDialogTranslations = await _genericTranslationsLoader.LoadAsync() ?? Array.Empty<GenericTranslationDto>();
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");

View file

@ -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<RarityChance>()).OrderBy(s => s.Chance).ToList();
_shells = (dropRarityConfiguration?.Shells ?? new List<RarityChance>()).OrderBy(s => s.Chance).ToList();
}
public sbyte GetRandomRarity(ItemType itemType)
@ -27,6 +27,11 @@ public class DropRarityConfigurationProvider : IDropRarityConfigurationProvider
}
List<RarityChance> rarities = itemType == ItemType.Shell ? _shells : _equipment;
if (rarities.Count == 0)
{
return 0;
}
var randomBag = new RandomBag<RarityChance>(_randomGenerator);
foreach (RarityChance rarity in rarities)

View file

@ -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);
}
}
}

View file

@ -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);
}
}
}

View file

@ -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<PacketHeaderAttribute>().Identification}");
string identification = type.GetCustomAttribute<PacketHeaderAttribute>()?.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
}
}