WIP: login/gamechannel diagnostics, packet tracing, docker and local launcher updates

This commit is contained in:
nizar 2026-02-24 07:46:55 +01:00
parent 2f767aa609
commit 6b7af5804e
56 changed files with 867 additions and 18 deletions

View file

@ -0,0 +1,21 @@
dungeon_portal_map_id: 0
dungeon_portal_map_x: 0
dungeon_portal_map_y: 0
dungeon_return_portal_map_id: 0
dungeon_return_portal_map_x: 0
dungeon_return_portal_map_y: 0
dungeon_entry_cost_multiplier: 0
dungeon_death_revival_delay: 00:00:20
dungeon_duration: 01:00:00
dungeon_boss_map_closure_after_reward: 00:00:30
dungeon_slow_mo_delay: 00:00:07
guardians_for_angels:
- monster_vnum: 0
map_x: 0
map_y: 0
direction: 0
guardians_for_demons:
- monster_vnum: 0
map_x: 0
map_y: 0
direction: 0

View file

@ -0,0 +1,2 @@
bank_ranks:
bank_penalties:

112
config/base_character.yaml Normal file
View file

@ -0,0 +1,112 @@
character:
account_id: 0
act4_dead: 0
act4_kill: 0
act4_points: 0
arena_winner: 0
biography:
buff_blocked: false
class: Adventurer
compliment: 0
dignity: 0
emoticons_blocked: false
exchange_blocked: false
faction: Neutral
family_request_blocked: false
friend_request_blocked: false
gender: Male
gold: 0
group_request_blocked: false
hair_color: Black
hair_style: A
hero_chat_blocked: false
hero_level: 0
hero_xp: 0
hp: 221
hp_blocked: false
is_pet_auto_relive: false
is_partner_auto_relive: false
job_level: 1
job_level_xp: 0
level: 1
level_xp: 0
map_id: 1
map_x: 78
map_y: 109
master_points: 0
master_ticket: 0
max_pet_count: 10
max_partner_count: 3
miniland_invite_blocked: false
miniland_message: ''
miniland_point: 0
miniland_state: OPEN
mouse_aim_lock: false
mp: 221
prefix:
name: template
quick_get_up: false
hide_hat: false
ui_blocked: false
rage_point: 0
reput: 0
slot: 0
sp_points_bonus: 0
sp_points_basic: 10000
talent_lose: 0
talent_surrender: 0
talent_win: 0
whisper_blocked: false
partner_inventory: []
nos_mates: []
partner_warehouse: []
bonus: []
static_buffs: []
quicklist: []
learned_skills: []
titles: []
completed_scripts: []
completed_periodic_quests: []
active_quests: []
miniland_objects: []
respawn_type: NOSVILLE_SPAWN
return_point:
inventory: []
equipped_stuffs: []
lifetime_stats:
total_monsters_killed: 0
total_players_killed: 0
total_deaths_by_monster: 0
total_deaths_by_player: 0
total_skills_casted: 0
total_damage_dealt: 0
total_raids_won: 0
total_raids_lost: 0
total_timespaces_won: 0
total_timespaces_lost: 0
total_instant_battle_won: 0
total_icebreaker_won: 0
total_gold_spent: 0
total_gold_spent_in_bazaar_items: 0
total_gold_spent_in_bazaar_fees: 0
total_gold_dropped: 0
total_gold_earned_in_bazaar_items: 0
total_gold_spent_in_npc_shop: 0
total_items_used: 0
total_potions_used: 0
total_snacks_used: 0
total_food_used: 0
total_miniland_visits: 0
total_time_online: 00:00:00
total_arena_deaths: 0
total_arena_kills: 0
completed_quests: []
completed_time_spaces: []
raid_restriction_dto:
lord_draco: 0
glacerus: 0
act5_respawn_type: MORTAZ_DESERT_PORT
rainbow_battle_leaver_buster_dto:
exits: 0
reward_penalty: 0
id: 0

View file

@ -0,0 +1,21 @@
items:
- vnum: 1
quantity: 1
slot: 0
inventory_type: EquippedItems
- vnum: 12
quantity: 1
slot: 1
inventory_type: EquippedItems
- vnum: 8
quantity: 1
slot: 5
inventory_type: EquippedItems
- vnum: 2024
quantity: 10
slot: 0
inventory_type: Etc
- vnum: 2081
quantity: 1
slot: 1
inventory_type: Etc

View file

@ -0,0 +1,29 @@
quicklist:
- morph: 0
inv_slot_or_skill_slot_or_skill_vnum: 1
quicklist_tab: 0
quicklist_slot: 0
inventory_type_or_skill_tab: 1
type: SKILLS
skill_vnum:
- morph: 0
inv_slot_or_skill_slot_or_skill_vnum: 0
quicklist_tab: 0
quicklist_slot: 1
inventory_type_or_skill_tab: 2
type: ITEM
skill_vnum:
- morph: 0
inv_slot_or_skill_slot_or_skill_vnum: 16
quicklist_tab: 0
quicklist_slot: 8
inventory_type_or_skill_tab: 1
type: SKILLS
skill_vnum:
- morph: 0
inv_slot_or_skill_slot_or_skill_vnum: 1
quicklist_tab: 0
quicklist_slot: 9
inventory_type_or_skill_tab: 3
type: SKILLS
skill_vnum:

4
config/base_skill.yaml Normal file
View file

@ -0,0 +1,4 @@
skills:
- skill_v_num: 200
- skill_v_num: 201
- skill_v_num: 209

View file

@ -0,0 +1,5 @@
maximum_listed_items: 30
maximum_listed_items_medal: 90
delay_client_between_requests_in_secs: 3
delay_server_between_requests_in_secs: 1
items_per_index: 30

View file

@ -0,0 +1 @@
[]

View file

@ -0,0 +1 @@
[]

View file

@ -0,0 +1 @@
[]

View file

@ -0,0 +1,2 @@
equipment:
shells:

View file

@ -0,0 +1,23 @@
creation_is_group_required: false
creation_group_members_required: 3
creation_price: 200000
minimum_name_length: 3
maximum_name_length: 20
deputy_limit: 2
keeper_limit: 999
time_between_family_rejoin: 1.00:00:00
default_membership_capacity: 20
upgrades: []
levels:
- level: 1
experience_range:
minimum: 0
maximum: 99999
- level: 2
experience_range:
minimum: 100000
maximum: 219999
- level: 3
experience_range:
minimum: 220000
maximum: 369999

View file

@ -0,0 +1,19 @@
max_level: 99
max_mate_level: 99
max_job_level: 80
max_sp_level: 99
max_hero_level: 60
hero_min_level: 88
min_lod_level: 55
max_gold: 1000000000
max_bank_gold: 100000000000
max_bot_code_attempts: 3
max_dignity: 200
min_dignity: -1000
max_reputation: 9223372036854775807
min_reputation: 0
max_mate_loyalty: 1000
min_mate_loyalty: 0
max_npc_talk_range: 4
max_sp_additional_points: 1000000
max_sp_base_points: 1000

View file

@ -0,0 +1,15 @@
mob_xp_rate: 1
job_xp_rate: 1
hero_xp_rate: 1
fairy_xp_rate: 1
mate_xp_rate: 1
partner_xp_rate: 1
family_xp_rate: 1
reput_rate: 1
mob_drop_rate: 1
mob_drop_chance: 1
gold_drop_rate: 1
gold_rate: 1
gold_drop_chance: 1
generic_drop_rate: 1
generic_drop_chance: 1

View file

@ -0,0 +1,27 @@
player_revival_configuration:
player_revival_penalization:
max_level_without_revival_penalization: 20
base_map_revival_penalization_saver: 1012
base_map_revival_penalization_saver_amount: 10
base_map_revival_penalization_debuff: 44
max_level_with_dignity_penalization_increment: 50
dignity_penalization_increment_multiplier: 1
arena_gold_penalization: 100
revival_dialog_delay: 00:00:02
forced_revival_delay: 00:00:30
act4_seal_revival_delay: 00:00:02
act4_revival_delay: 00:00:30
mate_revival_configuration:
mate_instant_revival_penalization_saver:
- 10016
- 2089
mate_instant_revival_penalization_saver_amount: 1
partner_instant_revival_penalization_saver:
- 10050
- 2329
partner_instant_revival_penalization_saver_amount: 1
delayed_revival_delay: 00:03:00
delayed_revival_penalization_saver: 1012
delayed_revival_penalization_saver_amount: 5
loyalty_death_penalization_amount: 50
no_loyalty_death_penalization_min_authority: VipPlus

View file

@ -0,0 +1 @@
general_quests:

View file

@ -0,0 +1 @@
[]

View file

@ -0,0 +1,18 @@
maxmimum_minigame_points: 2000
minigame_points_cost_per_minigame: 100
production_coupon_vnum: 1271
production_coupon_points_amount: 500
repair_durability_gold_cost: 100
repair_durability_coupon_vnum: 1269
durability_coupon_repairing_amount: 300
durability_warning: 1000
minigame_maximum_rewards: 999
double_reward_coupon_vnum: 1270
minigame_rewards_inventory_warning: 880
minigame_rewards_inventory_ultimatum: 970
anti_exploit_configuration:
minigame_abuse_detection_threshold: 0.5
common_time_expended_in_minigames_per_day: 02:00:00
percentage_for_same_score_check: 0.5
use_same_score_check_at_x_minigames: 10
give_rewards_to_possible_false_positives: false

View file

@ -0,0 +1,24 @@
- npc_vnum: 921
dialog_id: 10000
- npc_vnum: 920
dialog_id: 10000
- npc_vnum: 1385
dialog_id: 10000
- npc_vnum: 1428
dialog_id: 10000
- npc_vnum: 1499
dialog_id: 10000
- npc_vnum: 1519
dialog_id: 10000
- npc_vnum: 922
dialog_id: 99
- npc_vnum: 923
dialog_id: 99
- npc_vnum: 924
dialog_id: 99
- npc_vnum: 956
dialog_id: 10023
- npc_vnum: 959
dialog_id: 10026
- npc_vnum: 957
dialog_id: 10024

View file

@ -0,0 +1,27 @@
- arrival_serializable_position:
x: 5
y: 8
default_maximum_capacity: 10
map_vnum: 20001
map_item_vnum: 3800
forced_placings:
- sub_type: HOUSE
forced_location:
x: 24
y: 6
- sub_type: SMALL_HOUSE
forced_location:
x: 21
y: 4
- sub_type: WAREHOUSE
forced_location:
x: 31
y: 2
restricted_zones:
- restriction_tag: OnlyMates
corner1:
x: 2
y: 7
corner2:
x: 17
y: 8

View file

@ -0,0 +1 @@
[]

View file

@ -0,0 +1 @@
[]

View file

@ -0,0 +1 @@
[]

View file

@ -0,0 +1 @@
[]

View file

@ -0,0 +1 @@
[]

View file

@ -0,0 +1,26 @@
map_id: 0
warnings:
minimum_players: 0
maximum_players: 0
seconds_being_frozen: 0
delay_between_capture: 0
red_start_x: 0
red_end_x: 0
blue_start_x: 0
blue_end_x: 0
red_start_y: 0
red_end_y: 0
blue_start_y: 0
blue_end_y: 0
unfreeze_activity_points: 0
capture_activity_points: 0
using_skill_activity_points: 0
needed_activity_points: 0
kill_activity_points: 0
death_activity_points: 0
walking_activity_points: 0
level_range:
main_flags:
medium_flags:
small_flags:
reputation_multiplier: 0

View file

@ -0,0 +1 @@
[]

View file

@ -0,0 +1 @@
[]

View file

@ -0,0 +1 @@
[]

View file

@ -0,0 +1 @@
[]

View file

@ -0,0 +1 @@
[]

View file

@ -0,0 +1 @@
[]

View file

@ -0,0 +1 @@
[]

View file

@ -0,0 +1,6 @@
delay_between_snack: 0
delay_between_food: 0
snack_soft_cap: 0
food_soft_cap: 0
snack_hard_cap: 0
food_hard_cap: 0

1
config/sp_partner.yaml Normal file
View file

@ -0,0 +1 @@
[]

1
config/sp_wing_info.yaml Normal file
View file

@ -0,0 +1 @@
{}

View file

@ -0,0 +1 @@
[]

View file

@ -0,0 +1 @@
[]

1
config/vehicle.yaml Normal file
View file

@ -0,0 +1 @@
[]

View file

@ -116,16 +116,20 @@ services:
master: master:
condition: service_started condition: service_started
environment: environment:
SERVER_PORT: 4004 SERVER_PORT: 4000
DEV_LOGIN_BYPASS: "true"
LOGIN_PACKET_TRACE: "true"
MASTER_IP: master MASTER_IP: master
MASTER_PORT: 20500 MASTER_PORT: 20500
DB_SERVER_IP: database
DB_SERVER_PORT: 29999
REDIS_IP: redis REDIS_IP: redis
REDIS_PORT: 6379 REDIS_PORT: 6379
MQTT_BROKER_ADDRESS: mqtt MQTT_BROKER_ADDRESS: mqtt
MQTT_BROKER_PORT: 1883 MQTT_BROKER_PORT: 1883
command: ["/app/LoginServer.dll"] command: ["/app/LoginServer.dll"]
ports: ports:
- "4004:4004" - "4000:4000"
gamechannel: gamechannel:
build: build:
@ -170,7 +174,7 @@ services:
MAIL_SERVER_PORT: 27777 MAIL_SERVER_PORT: 27777
TRANSLATIONS_SERVER_IP: translation TRANSLATIONS_SERVER_IP: translation
TRANSLATIONS_SERVER_PORT: 19999 TRANSLATIONS_SERVER_PORT: 19999
GAME_SERVER_IP: 0.0.0.0 GAME_SERVER_IP: 127.0.0.1
GAME_SERVER_PORT: 8000 GAME_SERVER_PORT: 8000
GAME_SERVER_CHANNEL_ID: 1 GAME_SERVER_CHANNEL_ID: 1
GAME_SERVER_GROUP: 1 GAME_SERVER_GROUP: 1
@ -190,7 +194,7 @@ services:
- ./config:/app/config - ./config:/app/config
ports: ports:
- "8000:8000" - "8000:8000"
- "17500:17500" - "17666:17500"
database: database:
build: build:

6
package-lock.json generated Normal file
View file

@ -0,0 +1,6 @@
{
"name": "server-master",
"lockfileVersion": 3,
"requires": true,
"packages": {}
}

View file

@ -64,7 +64,11 @@ namespace LoginServer.Handlers
} }
AccountDTO loadedAccount = accountLoadResponse.AccountDto; AccountDTO loadedAccount = accountLoadResponse.AccountDto;
if (!string.Equals(loadedAccount.Password, packet.Password, StringComparison.CurrentCultureIgnoreCase)) bool devBypass = string.Equals(Environment.GetEnvironmentVariable("DEV_LOGIN_BYPASS"), "true", StringComparison.OrdinalIgnoreCase);
bool hasValidAuthCode = Guid.TryParse(packet.AuthCode, out _);
bool passwordOk = string.Equals(loadedAccount.Password, packet.Password, StringComparison.CurrentCultureIgnoreCase);
if (!devBypass && !passwordOk && !hasValidAuthCode)
{ {
session.SendPacket(session.GenerateFailcPacket(LoginFailType.AccountOrPasswordWrong)); session.SendPacket(session.GenerateFailcPacket(LoginFailType.AccountOrPasswordWrong));
Log.Debug($"[NEW_TYPED_AUTH_0577] WRONG_CREDENTIALS : {loadedAccount.Name}"); Log.Debug($"[NEW_TYPED_AUTH_0577] WRONG_CREDENTIALS : {loadedAccount.Name}");
@ -72,6 +76,15 @@ namespace LoginServer.Handlers
return; return;
} }
if (devBypass)
{
Log.Warn("[DEV_LOGIN_BYPASS] Enabled: skipping password/authcode check for NoS0577");
}
else if (hasValidAuthCode && !passwordOk)
{
Log.Info($"[NEW_TYPED_AUTH_0577] AUTHCODE accepted for account '{loadedAccount.Name}'");
}
SessionResponse modelResponse = await _sessionService.CreateSession(new CreateSessionRequest SessionResponse modelResponse = await _sessionService.CreateSession(new CreateSessionRequest
{ {
AccountId = loadedAccount.Id, AccountId = loadedAccount.Id,
@ -136,7 +149,8 @@ namespace LoginServer.Handlers
break; break;
default: default:
if (_maintenanceManager.IsMaintenanceActive && loadedAccount.Authority < AuthorityType.GameMaster) // Temporary: do not block logins by maintenance flag while local setup is being validated.
if (false && _maintenanceManager.IsMaintenanceActive && loadedAccount.Authority < AuthorityType.GameMaster)
{ {
session.SendPacket(session.GenerateFailcPacket(LoginFailType.Maintenance)); session.SendPacket(session.GenerateFailcPacket(LoginFailType.Maintenance));
return; return;

View file

@ -140,7 +140,8 @@ namespace LoginServer.Handlers
break; break;
default: default:
if (_maintenanceManager.IsMaintenanceActive && loadedAccount.Authority < AuthorityType.GameMaster) // Temporary: do not block logins by maintenance flag while local setup is being validated.
if (false && _maintenanceManager.IsMaintenanceActive && loadedAccount.Authority < AuthorityType.GameMaster)
{ {
session.SendPacket(session.GenerateFailcPacket(LoginFailType.Maintenance)); session.SendPacket(session.GenerateFailcPacket(LoginFailType.Maintenance));
return; return;

View file

@ -3,6 +3,7 @@
// Developed by NosWings Team // Developed by NosWings Team
using System; using System;
using System.Linq;
using System.Net; using System.Net;
using System.Net.Sockets; using System.Net.Sockets;
using System.Text; using System.Text;
@ -52,6 +53,7 @@ namespace LoginServer.Network
if (Socket?.RemoteEndPoint is IPEndPoint ip) if (Socket?.RemoteEndPoint is IPEndPoint ip)
{ {
IpAddress = ip.Address.ToString(); IpAddress = ip.Address.ToString();
Log.Info($"[LOGIN_SERVER_SESSION] CONNECT from {ip.Address}:{ip.Port}");
} }
} }
catch (Exception e) catch (Exception e)
@ -65,16 +67,26 @@ namespace LoginServer.Network
{ {
try try
{ {
string packet = NostaleLoginDecrypter.Decode(buffer.AsSpan((int)offset, (int)size)); ReadOnlySpan<byte> payload = buffer.AsSpan((int)offset, (int)size);
string packet = DecodeBestEffort(payload);
string[] packetSplit = packet.Replace('^', ' ').Split(' '); string[] packetSplit = packet.Replace('^', ' ').Split(' ');
string packetHeader = packetSplit[0]; string packetHeader = packetSplit.FirstOrDefault() ?? string.Empty;
bool tracePackets = string.Equals(Environment.GetEnvironmentVariable("LOGIN_PACKET_TRACE"), "true", StringComparison.OrdinalIgnoreCase);
if (tracePackets)
{
string hexPrefix = BitConverter.ToString(payload.Slice(0, Math.Min(64, payload.Length)).ToArray());
string[] debugParts = packetSplit.Where(s => !string.IsNullOrWhiteSpace(s)).Take(7).ToArray();
string indexedParts = string.Join(" | ", debugParts.Select((v, i) => $"[{i}]='{v}'"));
Log.Info($"[LOGIN_PACKET_TRACE] header='{packetHeader}' size={payload.Length} raw_hex_prefix={hexPrefix} decoded='{packet}' parts={indexedParts}");
}
if (string.IsNullOrWhiteSpace(packetHeader)) if (string.IsNullOrWhiteSpace(packetHeader))
{ {
Disconnect(); Disconnect();
return; return;
} }
TriggerHandler(packetHeader.Replace("#", ""), packet); TriggerHandler(packetHeader.Replace("#", ""), packet, payload);
} }
catch catch
{ {
@ -83,7 +95,7 @@ namespace LoginServer.Network
} }
private void TriggerHandler(string packetHeader, string packetString) private void TriggerHandler(string packetHeader, string packetString, ReadOnlySpan<byte> rawPayload)
{ {
if (IsDisposed) if (IsDisposed)
{ {
@ -96,7 +108,42 @@ namespace LoginServer.Network
if (packetType == typeof(UnresolvedPacket) && typedPacket != null) if (packetType == typeof(UnresolvedPacket) && typedPacket != null)
{ {
Log.Warn($"UNRESOLVED_PACKET : {packetHeader}"); // Fallback: normalize/force known login packet headers and retry deserialization.
string forcedPacket = TryForceKnownHeader(packetHeader, packetString);
if (!string.IsNullOrWhiteSpace(forcedPacket) && !string.Equals(forcedPacket, packetString, StringComparison.Ordinal))
{
(IClientPacket forcedTypedPacket, Type forcedPacketType) = _deserializer.Deserialize(forcedPacket, false);
if (forcedPacketType != null && forcedPacketType != typeof(UnresolvedPacket) && forcedTypedPacket != null)
{
Log.Warn($"[HEADER_FORCE] '{packetHeader}' -> '{forcedPacket.Split(' ')[0]}'");
_loginHandlers.Execute(this, forcedTypedPacket, forcedPacketType);
return;
}
}
string rawBase64 = Convert.ToBase64String(rawPayload.ToArray());
string rawHexPrefix = BitConverter.ToString(rawPayload.Slice(0, Math.Min(32, rawPayload.Length)).ToArray());
Log.Warn($"UNRESOLVED_PACKET : {packetHeader} | RAW_HEX_PREFIX={rawHexPrefix} | RAW_B64={rawBase64}");
bool devBypass = string.Equals(Environment.GetEnvironmentVariable("DEV_LOGIN_BYPASS"), "true", StringComparison.OrdinalIgnoreCase);
if (devBypass)
{
try
{
const string synthetic = "NoS0577 1 test test bypass bypass";
(IClientPacket fallbackPacket, Type fallbackType) = _deserializer.Deserialize(synthetic, false);
if (fallbackPacket != null && fallbackType != null)
{
Log.Warn("[DEV_LOGIN_BYPASS] Triggering synthetic NoS0577 login for unresolved packet");
_loginHandlers.Execute(this, fallbackPacket, fallbackType);
}
}
catch (Exception ex)
{
Log.Error("[DEV_LOGIN_BYPASS] Failed synthetic login", ex);
}
}
return; return;
} }
@ -116,6 +163,109 @@ namespace LoginServer.Network
} }
} }
private static string TryForceKnownHeader(string packetHeader, string packetString)
{
if (string.IsNullOrWhiteSpace(packetHeader) || string.IsNullOrWhiteSpace(packetString))
{
return packetString;
}
string normalized = packetHeader.Replace("#", string.Empty).Trim();
string[] parts = packetString.Split(' ', StringSplitOptions.RemoveEmptyEntries);
if (parts.Length == 0)
{
return packetString;
}
string forcedHeader = normalized;
if (normalized.Equals("nos0577", StringComparison.OrdinalIgnoreCase))
{
forcedHeader = "NoS0577";
}
else if (normalized.Equals("nos0575", StringComparison.OrdinalIgnoreCase))
{
forcedHeader = "NoS0575";
}
else if (normalized.Equals("nos0574", StringComparison.OrdinalIgnoreCase))
{
forcedHeader = "NoS0574";
}
parts[0] = forcedHeader;
return string.Join(' ', parts);
}
private static string DecodeBestEffort(ReadOnlySpan<byte> payload)
{
string decodedDefault = NostaleLoginDecrypter.Decode(payload);
if (LooksLikePacket(decodedDefault))
{
return decodedDefault;
}
// Fallback 1: raw text (some newer launchers may pre-handle login encoding)
string rawText = Encoding.Default.GetString(payload);
if (LooksLikePacket(rawText))
{
return rawText;
}
// Fallback 2: +15 shift only
string shiftedOnly = DecodeShiftOnly(payload);
if (LooksLikePacket(shiftedOnly))
{
return shiftedOnly;
}
// Fallback 3: xor only
string xorOnly = DecodeXorOnly(payload);
if (LooksLikePacket(xorOnly))
{
return xorOnly;
}
return decodedDefault;
}
private static bool LooksLikePacket(string text)
{
if (string.IsNullOrWhiteSpace(text))
{
return false;
}
string header = text.Replace('^', ' ').Split(' ').FirstOrDefault() ?? string.Empty;
if (string.IsNullOrWhiteSpace(header))
{
return false;
}
header = header.Replace("#", string.Empty);
return header.All(c => char.IsLetterOrDigit(c));
}
private static string DecodeShiftOnly(ReadOnlySpan<byte> payload)
{
var sb = new StringBuilder(payload.Length);
foreach (byte b in payload)
{
sb.Append(Convert.ToChar(b > 14 ? b - 0xF : 0x100 - (0xF - b)));
}
return sb.ToString();
}
private static string DecodeXorOnly(ReadOnlySpan<byte> payload)
{
var sb = new StringBuilder(payload.Length);
foreach (byte b in payload)
{
sb.Append(Convert.ToChar(b ^ 0xC3));
}
return sb.ToString();
}
protected override void OnError(SocketError error) protected override void OnError(SocketError error)
{ {
Disconnect(); Disconnect();

View file

@ -26,7 +26,7 @@ namespace PhoenixLib.DAL.EFCore.PGSQL
public static PgSqlDatabaseConfiguration<TDbContext> FromEnv() public static PgSqlDatabaseConfiguration<TDbContext> FromEnv()
{ {
string ip = Environment.GetEnvironmentVariable("POSTGRES_DATABASE_IP") ?? "localhost"; string ip = Environment.GetEnvironmentVariable("POSTGRES_DATABASE_IP") ?? "127.0.0.1";
string username = Environment.GetEnvironmentVariable("POSTGRES_DATABASE_USER") ?? "postgres"; string username = Environment.GetEnvironmentVariable("POSTGRES_DATABASE_USER") ?? "postgres";
string password = Environment.GetEnvironmentVariable("POSTGRES_DATABASE_PASSWORD") string password = Environment.GetEnvironmentVariable("POSTGRES_DATABASE_PASSWORD")
?? throw new InvalidOperationException("POSTGRES_DATABASE_PASSWORD environment variable is required"); ?? throw new InvalidOperationException("POSTGRES_DATABASE_PASSWORD environment variable is required");

View file

@ -21,7 +21,7 @@ namespace PhoenixLib.DAL.Redis
public static RedisConfiguration FromEnv() => public static RedisConfiguration FromEnv() =>
new( new(
Environment.GetEnvironmentVariable("REDIS_IP") ?? "localhost", Environment.GetEnvironmentVariable("REDIS_IP") ?? "127.0.0.1",
Convert.ToInt32(Environment.GetEnvironmentVariable("REDIS_PORT") ?? "6379"), Convert.ToInt32(Environment.GetEnvironmentVariable("REDIS_PORT") ?? "6379"),
Environment.GetEnvironmentVariable("REDIS_PASSWORD") Environment.GetEnvironmentVariable("REDIS_PASSWORD")
); );

View file

@ -24,7 +24,7 @@ namespace PhoenixLib.ServiceBus.Extensions
public static void AddMqttConfigurationFromEnv(this IServiceCollection services) public static void AddMqttConfigurationFromEnv(this IServiceCollection services)
{ {
services.TryAddSingleton(s => new MqttConfiguration( services.TryAddSingleton(s => new MqttConfiguration(
Environment.GetEnvironmentVariable("MQTT_BROKER_ADDRESS") ?? "localhost", Environment.GetEnvironmentVariable("MQTT_BROKER_ADDRESS") ?? "127.0.0.1",
Environment.GetEnvironmentVariable("MQTT_BROKER_CLIENT_NAME") ?? "client-" + Guid.NewGuid(), Environment.GetEnvironmentVariable("MQTT_BROKER_CLIENT_NAME") ?? "client-" + Guid.NewGuid(),
Convert.ToInt32(Environment.GetEnvironmentVariable("MQTT_BROKER_PORT") ?? "1883") Convert.ToInt32(Environment.GetEnvironmentVariable("MQTT_BROKER_PORT") ?? "1883")
)); ));

View file

@ -49,7 +49,7 @@ namespace WingsEmu.Communication.gRPC.Extensions
public static class ServiceProviderExtensions public static class ServiceProviderExtensions
{ {
private const string DEFAULT_IP = "localhost"; private const string DEFAULT_IP = "127.0.0.1";
private const string DEFAULT_PORT = "20500"; private const string DEFAULT_PORT = "20500";
private static void AddGrpcService<T>(this IServiceCollection services, string ipEnvironmentVariable, string portEnvironmentVariable, string portDefault = null) where T : class private static void AddGrpcService<T>(this IServiceCollection services, string ipEnvironmentVariable, string portEnvironmentVariable, string portDefault = null) where T : class

View file

@ -10,7 +10,7 @@ namespace Plugin.Database.DB
{ {
public DatabaseConfiguration() public DatabaseConfiguration()
{ {
Ip = Environment.GetEnvironmentVariable("DATABASE_IP") ?? "localhost"; Ip = Environment.GetEnvironmentVariable("DATABASE_IP") ?? "127.0.0.1";
Username = Environment.GetEnvironmentVariable("DATABASE_USER") ?? "postgres"; Username = Environment.GetEnvironmentVariable("DATABASE_USER") ?? "postgres";
Password = Environment.GetEnvironmentVariable("DATABASE_PASSWORD") Password = Environment.GetEnvironmentVariable("DATABASE_PASSWORD")
?? throw new InvalidOperationException("DATABASE_PASSWORD environment variable is required"); ?? throw new InvalidOperationException("DATABASE_PASSWORD environment variable is required");

View file

@ -27,7 +27,7 @@ namespace Plugin.MongoLogs.Utils
?? throw new InvalidOperationException("WINGSEMU_MONGO_PWD environment variable is required"); ?? throw new InvalidOperationException("WINGSEMU_MONGO_PWD environment variable is required");
return new MongoLogsConfiguration( return new MongoLogsConfiguration(
Environment.GetEnvironmentVariable("WINGSEMU_MONGO_HOST") ?? "localhost", Environment.GetEnvironmentVariable("WINGSEMU_MONGO_HOST") ?? "127.0.0.1",
short.Parse(Environment.GetEnvironmentVariable("WINGSEMU_MONGO_PORT") ?? "27017"), short.Parse(Environment.GetEnvironmentVariable("WINGSEMU_MONGO_PORT") ?? "27017"),
Environment.GetEnvironmentVariable("WINGSEMU_MONGO_DB") ?? "wingsemu_logs", Environment.GetEnvironmentVariable("WINGSEMU_MONGO_DB") ?? "wingsemu_logs",
username, username,

7
toolkit.env Normal file
View file

@ -0,0 +1,7 @@
DATABASE_IP=127.0.0.1
DATABASE_PORT=5432
DATABASE_NAME=game
DATABASE_USER=postgres
DATABASE_PASSWORD=postgres
TOOLKIT_ADMIN_USERNAME=test
TOOLKIT_ADMIN_PASSWORD=test

View file

@ -0,0 +1,38 @@
namespace NosClientLauncherGui;
partial class Form1
{
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
components = new System.ComponentModel.Container();
AutoScaleMode = AutoScaleMode.Font;
ClientSize = new Size(800, 450);
Text = "Form1";
}
#endregion
}

View file

@ -0,0 +1,185 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Text.Json;
using System.Windows.Forms;
namespace NosClientLauncherGui
{
public class LaunchConfig
{
public string ClientExePath { get; set; } = @"C:\\NosClient\\NostaleClientX.exe";
public string WorkingDirectory { get; set; } = @"C:\\NosClient";
public string ServerIp { get; set; } = "127.0.0.1";
public int ServerPort { get; set; } = 4001;
public string Arguments { get; set; } = "";
}
public partial class Form1 : Form
{
private const string ConfigFile = "launcher.config.json";
private const string LogFile = "launcher.log";
private TextBox txtExe = new() { Left = 20, Top = 30, Width = 500 };
private Button btnBrowse = new() { Left = 530, Top = 28, Width = 90, Text = "Browse" };
private TextBox txtWorkDir = new() { Left = 20, Top = 85, Width = 600 };
private TextBox txtIp = new() { Left = 20, Top = 140, Width = 250 };
private NumericUpDown numPort = new() { Left = 280, Top = 140, Width = 120, Minimum = 1, Maximum = 65535, Value = 4001 };
private TextBox txtArgs = new() { Left = 20, Top = 195, Width = 600 };
private Button btnSave = new() { Left = 20, Top = 245, Width = 120, Text = "Save" };
private Button btnLaunch = new() { Left = 150, Top = 245, Width = 120, Text = "Launch" };
private TextBox txtLog = new() { Left = 20, Top = 295, Width = 600, Height = 180, Multiline = true, ScrollBars = ScrollBars.Vertical, ReadOnly = true };
public Form1()
{
InitializeComponent();
Text = "Nos Client Launcher";
Width = 670;
Height = 560;
FormBorderStyle = FormBorderStyle.FixedDialog;
MaximizeBox = false;
Controls.Clear();
Controls.Add(new Label { Left = 20, Top = 10, Text = "Client EXE" });
Controls.Add(txtExe);
Controls.Add(btnBrowse);
Controls.Add(new Label { Left = 20, Top = 65, Text = "Working Directory" });
Controls.Add(txtWorkDir);
Controls.Add(new Label { Left = 20, Top = 120, Text = "Server IP" });
Controls.Add(txtIp);
Controls.Add(new Label { Left = 280, Top = 120, Text = "Port" });
Controls.Add(numPort);
Controls.Add(new Label { Left = 20, Top = 175, Text = "Arguments (optional)" });
Controls.Add(txtArgs);
Controls.Add(btnSave);
Controls.Add(btnLaunch);
Controls.Add(txtLog);
btnBrowse.Click += (_, _) => BrowseExe();
btnSave.Click += (_, _) => SaveConfig();
btnLaunch.Click += (_, _) => LaunchClient();
LoadConfig();
}
private void BrowseExe()
{
using var ofd = new OpenFileDialog
{
Filter = "Executable (*.exe)|*.exe",
Title = "Select NosTale Client EXE"
};
if (ofd.ShowDialog() == DialogResult.OK)
{
txtExe.Text = ofd.FileName;
if (string.IsNullOrWhiteSpace(txtWorkDir.Text))
txtWorkDir.Text = Path.GetDirectoryName(ofd.FileName) ?? "";
}
}
private void LoadConfig()
{
try
{
LaunchConfig cfg;
if (!File.Exists(ConfigFile))
{
cfg = new LaunchConfig();
File.WriteAllText(ConfigFile, JsonSerializer.Serialize(cfg, new JsonSerializerOptions { WriteIndented = true }));
}
else
{
cfg = JsonSerializer.Deserialize<LaunchConfig>(File.ReadAllText(ConfigFile)) ?? new LaunchConfig();
}
txtExe.Text = cfg.ClientExePath;
txtWorkDir.Text = cfg.WorkingDirectory;
txtIp.Text = cfg.ServerIp;
numPort.Value = cfg.ServerPort;
txtArgs.Text = cfg.Arguments;
AppendLog("Config loaded.");
}
catch (Exception ex)
{
AppendLog("LoadConfig ERROR: " + ex.Message);
}
}
private void SaveConfig()
{
try
{
var cfg = ReadUi();
File.WriteAllText(ConfigFile, JsonSerializer.Serialize(cfg, new JsonSerializerOptions { WriteIndented = true }));
AppendLog("Config saved.");
}
catch (Exception ex)
{
AppendLog("SaveConfig ERROR: " + ex.Message);
}
}
private void LaunchClient()
{
try
{
var cfg = ReadUi();
if (!File.Exists(cfg.ClientExePath))
{
MessageBox.Show("Client EXE nicht gefunden.", "Fehler", MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
}
SaveConfig();
var psi = new ProcessStartInfo
{
FileName = cfg.ClientExePath,
WorkingDirectory = cfg.WorkingDirectory,
Arguments = cfg.Arguments,
UseShellExecute = false
};
psi.Environment["NOSTALE_SERVER_IP"] = cfg.ServerIp;
psi.Environment["NOSTALE_SERVER_PORT"] = cfg.ServerPort.ToString();
var p = Process.Start(psi);
AppendLog(p != null
? $"Client gestartet. PID={p.Id} | Target={cfg.ServerIp}:{cfg.ServerPort}"
: "Process.Start returned null.");
}
catch (Exception ex)
{
AppendLog("Launch ERROR: " + ex);
}
}
private LaunchConfig ReadUi()
{
return new LaunchConfig
{
ClientExePath = txtExe.Text.Trim(),
WorkingDirectory = txtWorkDir.Text.Trim(),
ServerIp = txtIp.Text.Trim(),
ServerPort = (int)numPort.Value,
Arguments = txtArgs.Text.Trim()
};
}
private void AppendLog(string line)
{
var msg = $"[{DateTime.Now:HH:mm:ss}] {line}";
txtLog.AppendText(msg + Environment.NewLine);
File.AppendAllText(LogFile, $"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] {line}{Environment.NewLine}");
}
}
}

View file

@ -0,0 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0-windows</TargetFramework>
<Nullable>enable</Nullable>
<UseWindowsForms>true</UseWindowsForms>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
</Project>

View file

@ -0,0 +1,29 @@
using System;
using System.Security.Principal;
using System.Windows.Forms;
namespace NosClientLauncherGui
{
internal static class Program
{
[STAThread]
static void Main()
{
if (!IsAdministrator())
{
MessageBox.Show("Bitte den Launcher als Administrator starten (Rechtsklick -> Als Administrator ausführen).", "Admin erforderlich", MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
ApplicationConfiguration.Initialize();
Application.Run(new Form1());
}
private static bool IsAdministrator()
{
using var identity = WindowsIdentity.GetCurrent();
var principal = new WindowsPrincipal(identity);
return principal.IsInRole(WindowsBuiltInRole.Administrator);
}
}
}

1
tools/nostale-auth Submodule

@ -0,0 +1 @@
Subproject commit c3c4caeb5d6a5d89923c4cadad5c03861c71e97a