first commit
This commit is contained in:
85
.gitignore
vendored
Normal file
85
.gitignore
vendored
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
# ============================================================
|
||||||
|
# C# / .NET
|
||||||
|
# ============================================================
|
||||||
|
bin/
|
||||||
|
obj/
|
||||||
|
*.user
|
||||||
|
*.suo
|
||||||
|
*.userosscache
|
||||||
|
*.sln.docstates
|
||||||
|
*.nupkg
|
||||||
|
*.snupkg
|
||||||
|
*.log
|
||||||
|
[Ll]og/
|
||||||
|
[Ll]ogs/
|
||||||
|
*.tmp
|
||||||
|
*.temp
|
||||||
|
project.lock.json
|
||||||
|
project.fragment.lock.json
|
||||||
|
artifacts/
|
||||||
|
|
||||||
|
# NuGet
|
||||||
|
.nuget/
|
||||||
|
packages/
|
||||||
|
*.nupkg
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# IDE - Visual Studio
|
||||||
|
# ============================================================
|
||||||
|
.vs/
|
||||||
|
*.rsuser
|
||||||
|
*.MojoProj
|
||||||
|
*.pidb
|
||||||
|
*.svclog
|
||||||
|
*.scc
|
||||||
|
_Chutzpah*
|
||||||
|
ipch/
|
||||||
|
*.aps
|
||||||
|
*.ncb
|
||||||
|
*.opendb
|
||||||
|
*.opensdf
|
||||||
|
*.sdf
|
||||||
|
*.cachefile
|
||||||
|
*.VC.db
|
||||||
|
*.VC.opendb
|
||||||
|
$tf/
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# IDE - JetBrains (Rider / IntelliJ IDEA)
|
||||||
|
# ============================================================
|
||||||
|
.idea/
|
||||||
|
*.iml
|
||||||
|
*.iws
|
||||||
|
*.ipr
|
||||||
|
.idea_modules/
|
||||||
|
out/
|
||||||
|
atlassian-ide-plugin.xml
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# IDE - VS Code
|
||||||
|
# ============================================================
|
||||||
|
.vscode/
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# AI - Claude
|
||||||
|
# ============================================================
|
||||||
|
.claude/
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# OS
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
# Windows
|
||||||
|
Thumbs.db
|
||||||
|
ehthumbs.db
|
||||||
|
Desktop.ini
|
||||||
|
$RECYCLE.BIN/
|
||||||
|
*.lnk
|
||||||
|
|
||||||
|
# macOS
|
||||||
|
.DS_Store
|
||||||
|
.AppleDouble
|
||||||
|
.LSOverride
|
||||||
|
._*
|
||||||
|
.Spotlight-V100
|
||||||
|
.Trashes
|
||||||
361
ClientTester/.editorconfig
Normal file
361
ClientTester/.editorconfig
Normal file
@@ -0,0 +1,361 @@
|
|||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
end_of_line = lf
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 4
|
||||||
|
|
||||||
|
[*.cs]
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# 중괄호 Allman 스타일
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# Good: Bad:
|
||||||
|
# if (isAlive) if (isAlive) {
|
||||||
|
# { TakeDamage();
|
||||||
|
# TakeDamage(); }
|
||||||
|
# }
|
||||||
|
csharp_new_line_before_open_brace = all
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# 중괄호 항상 사용
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# Good: Bad:
|
||||||
|
# if (isAlive) if (isAlive)
|
||||||
|
# { TakeDamage();
|
||||||
|
# TakeDamage();
|
||||||
|
# }
|
||||||
|
csharp_prefer_braces = true:warning
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# 접근 제한자 항상 명시
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# Good: Bad:
|
||||||
|
# private int health; int health;
|
||||||
|
# public float moveSpeed; float moveSpeed;
|
||||||
|
dotnet_style_require_accessibility_modifiers = always:warning
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# this. 한정자 허용 (필드와 매개변수 이름이 같을 때 필요)
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# 허용: this.stateContext = stateContext; (매개변수와 필드명 동일 시)
|
||||||
|
# 불필요: this.Initialize(); (모호하지 않은 경우)
|
||||||
|
dotnet_style_qualification_for_field = false:suggestion
|
||||||
|
dotnet_style_qualification_for_property = false:suggestion
|
||||||
|
dotnet_style_qualification_for_method = false:suggestion
|
||||||
|
dotnet_style_qualification_for_event = false:suggestion
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# var 사용 금지
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# Good: Bad:
|
||||||
|
# Enemy enemy = GetEnemy(); var enemy = GetEnemy();
|
||||||
|
# int count = 0; var count = 0;
|
||||||
|
csharp_style_var_for_built_in_types = false:warning
|
||||||
|
csharp_style_var_when_type_is_apparent = false:warning
|
||||||
|
csharp_style_var_elsewhere = false:warning
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# null 체크 스타일 강제
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# Good: Bad:
|
||||||
|
# animator?.Play("Run"); if (animator != null) animator.Play("Run");
|
||||||
|
# string name = playerName ?? "Unknown"; string name = playerName != null ? playerName : "Unknown";
|
||||||
|
# if (obj is null) { } if (object.ReferenceEquals(obj, null)) { }
|
||||||
|
csharp_style_conditional_delegate_call = true:warning
|
||||||
|
dotnet_style_null_propagation = true:warning
|
||||||
|
dotnet_style_coalesce_expression = true:warning
|
||||||
|
dotnet_style_prefer_is_null_check_over_reference_equality_method = true:warning
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# 패턴 매칭 강제 (as + null 체크 대신 is 패턴 사용)
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# Good: Bad:
|
||||||
|
# if (obj is Enemy enemy) Enemy enemy = obj as Enemy;
|
||||||
|
# { if (enemy != null)
|
||||||
|
# enemy.TakeDamage(); {
|
||||||
|
# } enemy.TakeDamage();
|
||||||
|
# }
|
||||||
|
csharp_style_pattern_matching_over_is_with_cast_check = true:warning
|
||||||
|
csharp_style_pattern_matching_over_as_with_null_check = true:warning
|
||||||
|
# not 패턴 (C# 9+)
|
||||||
|
# Good: if (obj is not null) Bad: if (!(obj is null))
|
||||||
|
# Good: if (enemy is not Dead) Bad: if (!(enemy is Dead))
|
||||||
|
csharp_style_prefer_not_pattern = true:warning
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# throw 표현식 강제
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# Good:
|
||||||
|
# target = player ?? throw new ArgumentNullException(nameof(player));
|
||||||
|
#
|
||||||
|
# Bad:
|
||||||
|
# if (player == null) throw new ArgumentNullException(nameof(player));
|
||||||
|
# target = player;
|
||||||
|
csharp_style_throw_expression = true:warning
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# 인라인 변수 선언 강제
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# Good: Bad:
|
||||||
|
# TryGetComponent(out Rigidbody rb); Rigidbody rb;
|
||||||
|
# TryGetComponent(out rb);
|
||||||
|
csharp_style_inlined_variable_declaration = true:warning
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# 튜플 해체 선언
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# Good: Bad:
|
||||||
|
# var (x, y) = GetPosition(); var pos = GetPosition();
|
||||||
|
# var x = pos.x; var y = pos.y;
|
||||||
|
csharp_style_deconstructed_variable_declaration = true:suggestion
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# 간단한 using 선언문
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# Good: Bad:
|
||||||
|
# using var stream = File.Open(...); using (var stream = File.Open(...))
|
||||||
|
# {
|
||||||
|
# }
|
||||||
|
csharp_prefer_simple_using_statement = true:suggestion
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# 인덱스 / 범위 연산자 (C# 8+)
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# Good: items[^1] Bad: items[items.Length - 1]
|
||||||
|
# Good: items[1..3] Bad: items.Skip(1).Take(2)
|
||||||
|
csharp_style_prefer_index_from_end = true:suggestion
|
||||||
|
csharp_style_prefer_range_operator = true:suggestion
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# switch 표현식 권장
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# Good: Bad:
|
||||||
|
# string label = state switch string label;
|
||||||
|
# { switch (state)
|
||||||
|
# GameState.Playing => "Playing", {
|
||||||
|
# GameState.Paused => "Paused", case GameState.Playing: label = "Playing"; break;
|
||||||
|
# _ => "Unknown" case GameState.Paused: label = "Paused"; break;
|
||||||
|
# }; default: label = "Unknown"; break;
|
||||||
|
# }
|
||||||
|
csharp_style_prefer_switch_expression = true:suggestion
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# 불필요한 코드 제거
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# object initializer
|
||||||
|
# Good: Bad:
|
||||||
|
# Enemy enemy = new Enemy Enemy enemy = new Enemy();
|
||||||
|
# { enemy.hp = 100;
|
||||||
|
# hp = 100, enemy.name = "Goblin";
|
||||||
|
# name = "Goblin"
|
||||||
|
# };
|
||||||
|
#
|
||||||
|
# 미사용 반환값은 _ 로 명시적으로 버리기
|
||||||
|
# Good: Bad:
|
||||||
|
# _ = TryGetComponent(out Rigidbody rb); TryGetComponent(out Rigidbody rb);
|
||||||
|
dotnet_style_object_initializer = true:warning
|
||||||
|
dotnet_style_collection_initializer = true:warning
|
||||||
|
dotnet_remove_unnecessary_suppression_exclusions = true
|
||||||
|
csharp_style_unused_value_assignment_preference = discard_variable:warning
|
||||||
|
csharp_style_unused_value_expression_statement_preference = discard_variable:warning
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# 단순화 강제
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# auto property
|
||||||
|
# Good: Bad:
|
||||||
|
# public int Hp { get; private set; } private int hp;
|
||||||
|
# public int Hp { get { return hp; } }
|
||||||
|
#
|
||||||
|
# boolean 단순화
|
||||||
|
# Good: Bad:
|
||||||
|
# return isAlive; return isAlive == true;
|
||||||
|
#
|
||||||
|
# 삼항 연산자 (suggestion - 복잡한 경우 강제 안 함)
|
||||||
|
# Good: Bad:
|
||||||
|
# int damage = isCrit ? 200 : 100; int damage;
|
||||||
|
# if (isCrit) damage = 200;
|
||||||
|
# else damage = 100;
|
||||||
|
dotnet_style_prefer_auto_properties = true:warning
|
||||||
|
dotnet_style_prefer_simplified_boolean_expressions = true:warning
|
||||||
|
dotnet_style_prefer_conditional_expression_over_assignment = true:suggestion
|
||||||
|
dotnet_style_prefer_conditional_expression_over_return = true:suggestion
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# 복합 할당 연산자
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# Good: hp -= damage; Bad: hp = hp - damage;
|
||||||
|
# Good: score += point; Bad: score = score + point;
|
||||||
|
dotnet_style_prefer_compound_assignment = true:warning
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# 타입 예약어 강제 (BCL 타입명 대신 C# 예약어 사용)
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# Good: Bad:
|
||||||
|
# int count = 0; Int32 count = 0;
|
||||||
|
# string name = "Player"; String name = "Player";
|
||||||
|
# object obj = new Enemy(); Object obj = new Enemy();
|
||||||
|
dotnet_style_predefined_type_for_locals_parameters_members = true:warning
|
||||||
|
dotnet_style_predefined_type_for_member_access = true:warning
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# expression-bodied 프로퍼티/접근자 강제, 메서드/생성자/로컬함수 금지
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# Good: Bad:
|
||||||
|
# public int CurrentHp => currentHp; public int CurrentHp
|
||||||
|
# {
|
||||||
|
# get { return currentHp; }
|
||||||
|
# }
|
||||||
|
#
|
||||||
|
# 메서드 / 생성자 / 로컬 함수는 expression-bodied 금지
|
||||||
|
# Good: Bad:
|
||||||
|
# public void TakeDamage(int damage) public void TakeDamage(int damage) => hp -= damage;
|
||||||
|
# {
|
||||||
|
# hp -= damage;
|
||||||
|
# }
|
||||||
|
csharp_style_expression_bodied_properties = true:warning
|
||||||
|
csharp_style_expression_bodied_accessors = true:warning
|
||||||
|
csharp_style_expression_bodied_indexers = true:warning
|
||||||
|
csharp_style_expression_bodied_operators = true:warning
|
||||||
|
csharp_style_expression_bodied_lambdas = true:suggestion
|
||||||
|
csharp_style_expression_bodied_methods = false:warning
|
||||||
|
csharp_style_expression_bodied_constructors = false:warning
|
||||||
|
csharp_style_expression_bodied_local_functions = false:warning
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# 한 줄 블록 금지 (포맷터 적용 시 자동 정리)
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# Good: Bad:
|
||||||
|
# if (isAlive) if (isAlive) { TakeDamage(); }
|
||||||
|
# {
|
||||||
|
# TakeDamage();
|
||||||
|
# }
|
||||||
|
# 주의: 이 설정은 경고가 아닌 포맷터 실행 시에만 적용됩니다.
|
||||||
|
csharp_preserve_single_line_blocks = false
|
||||||
|
csharp_preserve_single_line_statements = false
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# using 정렬
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# Good:
|
||||||
|
# using System.Collections;
|
||||||
|
# using System.Collections.Generic;
|
||||||
|
# using UnityEngine;
|
||||||
|
dotnet_sort_system_directives_first = true
|
||||||
|
dotnet_separate_import_directive_groups = false
|
||||||
|
csharp_using_directive_placement = outside_namespace:warning
|
||||||
|
# 네임스페이스는 폴더 구조와 일치
|
||||||
|
# Good: Assets/Scripts/Player/ → namespace Project.Player
|
||||||
|
dotnet_style_namespace_match_folder = true:suggestion
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# 네이밍 규칙
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# 주의: 더 구체적인 규칙(const 등)을 먼저 선언해야 올바르게 적용됩니다.
|
||||||
|
|
||||||
|
# 상수: UPPER_SNAKE_CASE (const modifier로 가장 구체적 → 최우선)
|
||||||
|
# Good: const int MAX_LEVEL = 100; Bad: const int MaxLevel = 100;
|
||||||
|
# Good: const float DEFAULT_SPEED = 5f; Bad: const float defaultSpeed = 5f;
|
||||||
|
dotnet_naming_rule.constants.severity = warning
|
||||||
|
dotnet_naming_rule.constants.symbols = constants
|
||||||
|
dotnet_naming_rule.constants.style = upper_snake_case
|
||||||
|
|
||||||
|
dotnet_naming_symbols.constants.applicable_kinds = field
|
||||||
|
dotnet_naming_symbols.constants.required_modifiers = const
|
||||||
|
|
||||||
|
dotnet_naming_style.upper_snake_case.capitalization = all_upper
|
||||||
|
dotnet_naming_style.upper_snake_case.word_separator = _
|
||||||
|
|
||||||
|
# private / protected 필드: camelCase (Unity 스타일 통일)
|
||||||
|
# Good: private int currentHp; Bad: private int _currentHp;
|
||||||
|
# Good: protected float baseSpeed; Bad: protected float _baseSpeed;
|
||||||
|
dotnet_naming_rule.private_fields.severity = warning
|
||||||
|
dotnet_naming_rule.private_fields.symbols = private_fields
|
||||||
|
dotnet_naming_rule.private_fields.style = camel_case_style
|
||||||
|
|
||||||
|
dotnet_naming_symbols.private_fields.applicable_kinds = field
|
||||||
|
dotnet_naming_symbols.private_fields.applicable_accessibilities = private, protected, protected_internal
|
||||||
|
|
||||||
|
# public 필드: camelCase (Unity Inspector 노출 필드 기준 - 일반 C# 컨벤션과 다름)
|
||||||
|
# Good: public float moveSpeed; Bad: public float MoveSpeed;
|
||||||
|
# Good: public int maxHealth; Bad: public int MaxHealth;
|
||||||
|
dotnet_naming_rule.public_fields.severity = warning
|
||||||
|
dotnet_naming_rule.public_fields.symbols = public_fields
|
||||||
|
dotnet_naming_rule.public_fields.style = camel_case_style
|
||||||
|
|
||||||
|
dotnet_naming_symbols.public_fields.applicable_kinds = field
|
||||||
|
dotnet_naming_symbols.public_fields.applicable_accessibilities = public, internal
|
||||||
|
|
||||||
|
dotnet_naming_style.camel_case_style.capitalization = camel_case
|
||||||
|
|
||||||
|
# 인터페이스: IPascalCase
|
||||||
|
# Good: interface IEnemy { } Bad: interface Enemy { }
|
||||||
|
# Good: interface IDamageable { } Bad: interface Damageable { }
|
||||||
|
dotnet_naming_rule.interfaces.severity = warning
|
||||||
|
dotnet_naming_rule.interfaces.symbols = interfaces
|
||||||
|
dotnet_naming_rule.interfaces.style = prefix_i
|
||||||
|
|
||||||
|
dotnet_naming_symbols.interfaces.applicable_kinds = interface
|
||||||
|
|
||||||
|
dotnet_naming_style.prefix_i.capitalization = pascal_case
|
||||||
|
dotnet_naming_style.prefix_i.required_prefix = I
|
||||||
|
|
||||||
|
# 클래스 / 구조체 / 열거형: PascalCase
|
||||||
|
# Good: class PlayerController { } Bad: class playerController { }
|
||||||
|
# Good: enum GameState { } Bad: enum gameState { }
|
||||||
|
dotnet_naming_rule.types.severity = warning
|
||||||
|
dotnet_naming_rule.types.symbols = types
|
||||||
|
dotnet_naming_rule.types.style = pascal_case_style
|
||||||
|
|
||||||
|
dotnet_naming_symbols.types.applicable_kinds = class, struct, enum, delegate
|
||||||
|
|
||||||
|
dotnet_naming_style.pascal_case_style.capitalization = pascal_case
|
||||||
|
|
||||||
|
# 메서드 / 프로퍼티 / 이벤트: PascalCase
|
||||||
|
# Good: public void TakeDamage() { } Bad: public void takeDamage() { }
|
||||||
|
# Good: public int CurrentHp { } Bad: public int currentHp { }
|
||||||
|
# Good: public event Action OnDeath; Bad: public event Action onDeath;
|
||||||
|
dotnet_naming_rule.members.severity = warning
|
||||||
|
dotnet_naming_rule.members.symbols = members
|
||||||
|
dotnet_naming_rule.members.style = pascal_case_style
|
||||||
|
|
||||||
|
dotnet_naming_symbols.members.applicable_kinds = method, property, event
|
||||||
|
|
||||||
|
# 매개변수 / 로컬 변수: camelCase
|
||||||
|
# Good: void Init(int maxHp) { } Bad: void Init(int MaxHp) { }
|
||||||
|
# Good: float moveSpeed = 5f; Bad: float MoveSpeed = 5f;
|
||||||
|
dotnet_naming_rule.parameters_and_locals.severity = warning
|
||||||
|
dotnet_naming_rule.parameters_and_locals.symbols = parameters_and_locals
|
||||||
|
dotnet_naming_rule.parameters_and_locals.style = camel_case_style
|
||||||
|
|
||||||
|
dotnet_naming_symbols.parameters_and_locals.applicable_kinds = parameter, local
|
||||||
|
|
||||||
|
# enum 멤버: PascalCase
|
||||||
|
# Good: GameState.Playing Bad: GameState.playing
|
||||||
|
# Good: GameState.GameOver Bad: GameState.GAME_OVER
|
||||||
|
dotnet_naming_rule.enum_members.severity = warning
|
||||||
|
dotnet_naming_rule.enum_members.symbols = enum_members
|
||||||
|
dotnet_naming_rule.enum_members.style = pascal_case_style
|
||||||
|
|
||||||
|
dotnet_naming_symbols.enum_members.applicable_kinds = enum_member
|
||||||
|
|
||||||
|
[*.{json,yaml,yml}]
|
||||||
|
indent_size = 2
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
trim_trailing_whitespace = false
|
||||||
|
|
||||||
|
[*.{asmdef,asmref}]
|
||||||
|
indent_size = 4
|
||||||
|
|
||||||
|
[*.shader]
|
||||||
|
indent_size = 4
|
||||||
|
|
||||||
|
[*.{compute,hlsl,cginc}]
|
||||||
|
indent_size = 4
|
||||||
|
|
||||||
|
[*.{uss,uxml}]
|
||||||
|
indent_size = 4
|
||||||
16
ClientTester/ClientTester.sln
Normal file
16
ClientTester/ClientTester.sln
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
|
||||||
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EchoClientTester", "EchoClientTester\EchoClientTester.csproj", "{F093EF72-D2FA-4C17-AE64-49B6A9839EEB}"
|
||||||
|
EndProject
|
||||||
|
Global
|
||||||
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
Release|Any CPU = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||||
|
{F093EF72-D2FA-4C17-AE64-49B6A9839EEB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{F093EF72-D2FA-4C17-AE64-49B6A9839EEB}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{F093EF72-D2FA-4C17-AE64-49B6A9839EEB}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{F093EF72-D2FA-4C17-AE64-49B6A9839EEB}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
EndGlobal
|
||||||
18
ClientTester/EchoClientTester/EchoClientTester.csproj
Normal file
18
ClientTester/EchoClientTester/EchoClientTester.csproj
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<RootNamespace>ClientTester</RootNamespace>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="LiteNetLib" Version="2.0.2" />
|
||||||
|
<PackageReference Include="protobuf-net" Version="3.2.56" />
|
||||||
|
<PackageReference Include="Serilog" Version="4.3.1" />
|
||||||
|
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
using LiteNetLib.Utils;
|
||||||
|
using Serilog;
|
||||||
|
|
||||||
|
namespace ClientTester.EchoDummyService;
|
||||||
|
|
||||||
|
public class DummyClientService
|
||||||
|
{
|
||||||
|
private readonly List<DummyClients> _clients;
|
||||||
|
private readonly int _sendInterval;
|
||||||
|
|
||||||
|
public DummyClientService(int count, string ip, int port, string key, int sendIntervalMs = 1000)
|
||||||
|
{
|
||||||
|
_sendInterval = sendIntervalMs;
|
||||||
|
_clients = Enumerable.Range(0, count)
|
||||||
|
.Select(i => new DummyClients(i, ip, port, key))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
Log.Information("[SERVICE] {Count}개 클라이언트 생성 → {Ip}:{Port}", count, ip, port);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task RunAsync(CancellationToken ct)
|
||||||
|
{
|
||||||
|
await Task.WhenAll(
|
||||||
|
PollLoopAsync(ct),
|
||||||
|
SendLoopAsync(ct)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task PollLoopAsync(CancellationToken ct)
|
||||||
|
{
|
||||||
|
while (!ct.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
foreach (var c in _clients)
|
||||||
|
c.PollEvents();
|
||||||
|
|
||||||
|
try { await Task.Delay(15, ct); }
|
||||||
|
catch (OperationCanceledException) { break; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SendLoopAsync(CancellationToken ct)
|
||||||
|
{
|
||||||
|
try { await Task.Delay(500, ct); }
|
||||||
|
catch (OperationCanceledException) { return; }
|
||||||
|
|
||||||
|
int tick = 0;
|
||||||
|
|
||||||
|
while (!ct.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
int sent = 0;
|
||||||
|
|
||||||
|
foreach (var client in _clients)
|
||||||
|
{
|
||||||
|
client.SendPing();
|
||||||
|
if (client.Peer != null) sent++;
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.Information("[TICK {Tick:000}] {Sent}/{Total} 전송", tick, sent, _clients.Count);
|
||||||
|
tick++;
|
||||||
|
|
||||||
|
try { await Task.Delay(_sendInterval, ct); }
|
||||||
|
catch (OperationCanceledException) { break; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void PrintStats()
|
||||||
|
{
|
||||||
|
int totalSent = 0, totalRecv = 0;
|
||||||
|
int connected = 0;
|
||||||
|
|
||||||
|
Log.Information("════════════ Performance Report ════════════");
|
||||||
|
|
||||||
|
double totalAvgRtt = 0;
|
||||||
|
|
||||||
|
foreach (var c in _clients)
|
||||||
|
{
|
||||||
|
var stats = c.Peer?.Statistics;
|
||||||
|
long loss = stats?.PacketLoss ?? 0;
|
||||||
|
float lossPct = stats?.PacketLossPercent ?? 0f;
|
||||||
|
|
||||||
|
Log.Information(
|
||||||
|
"[Client {ClientId:00}] Sent={Sent} Recv={Recv} | Loss={Loss}({LossPct:F1}%) AvgRTT={AvgRtt:F3}ms LastRTT={LastRtt:F3}ms",
|
||||||
|
c.ClientId, c.SentCount, c.ReceivedCount, loss, lossPct, c.AvgRttMs, c.LastRttMs);
|
||||||
|
|
||||||
|
totalSent += c.SentCount;
|
||||||
|
totalRecv += c.ReceivedCount;
|
||||||
|
totalAvgRtt += c.AvgRttMs;
|
||||||
|
if (c.Peer != null) connected++;
|
||||||
|
}
|
||||||
|
|
||||||
|
double avgRtt = connected > 0 ? totalAvgRtt / connected : 0;
|
||||||
|
|
||||||
|
Log.Information("────────────────────────────────────────────");
|
||||||
|
Log.Information(
|
||||||
|
"[TOTAL] Sent={Sent} Recv={Recv} Connected={Connected}/{Total} AvgRTT={AvgRtt:F3}ms",
|
||||||
|
totalSent, totalRecv, connected, _clients.Count, avgRtt);
|
||||||
|
Log.Information("════════════════════════════════════════════");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Stop()
|
||||||
|
{
|
||||||
|
foreach (var c in _clients)
|
||||||
|
c.Stop();
|
||||||
|
|
||||||
|
Log.Information("[SERVICE] 모든 클라이언트 종료됨.");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
using System.Diagnostics;
|
||||||
|
using LiteNetLib;
|
||||||
|
using LiteNetLib.Utils;
|
||||||
|
using Serilog;
|
||||||
|
|
||||||
|
namespace ClientTester.EchoDummyService;
|
||||||
|
|
||||||
|
public class DummyClients
|
||||||
|
{
|
||||||
|
public NetManager Manager;
|
||||||
|
public EventBasedNetListener Listener;
|
||||||
|
public NetPeer? Peer;
|
||||||
|
public int ClientId;
|
||||||
|
|
||||||
|
// seq → 송신 타임스탬프 (Stopwatch tick)
|
||||||
|
private readonly Dictionary<int, long> _pendingPings = new();
|
||||||
|
private int _seqNumber;
|
||||||
|
|
||||||
|
// 통계
|
||||||
|
public int SentCount;
|
||||||
|
public int ReceivedCount;
|
||||||
|
public double LastRttMs;
|
||||||
|
public double TotalRttMs;
|
||||||
|
public int RttCount;
|
||||||
|
|
||||||
|
public DummyClients(int clientId, string ip, int port, string key)
|
||||||
|
{
|
||||||
|
ClientId = clientId;
|
||||||
|
Listener = new EventBasedNetListener();
|
||||||
|
Manager = new NetManager(Listener);
|
||||||
|
|
||||||
|
Listener.PeerConnectedEvent += peer =>
|
||||||
|
{
|
||||||
|
Peer = peer;
|
||||||
|
Log.Information("[Client {ClientId:00}] 연결됨", ClientId);
|
||||||
|
};
|
||||||
|
|
||||||
|
Listener.NetworkReceiveEvent += (peer, reader, channel, deliveryMethod) =>
|
||||||
|
{
|
||||||
|
var msg = reader.GetString();
|
||||||
|
|
||||||
|
// "ack:seq:{seqNum}" 파싱
|
||||||
|
var parts = msg.Split(':');
|
||||||
|
if (parts.Length == 3 && parts[0] == "ack" && parts[1] == "seq"
|
||||||
|
&& int.TryParse(parts[2], out int seq)
|
||||||
|
&& _pendingPings.TryGetValue(seq, out long sentTick))
|
||||||
|
{
|
||||||
|
double rttMs = (Stopwatch.GetTimestamp() - sentTick)
|
||||||
|
* 1000.0 / Stopwatch.Frequency;
|
||||||
|
LastRttMs = rttMs;
|
||||||
|
TotalRttMs += rttMs;
|
||||||
|
RttCount++;
|
||||||
|
_pendingPings.Remove(seq);
|
||||||
|
}
|
||||||
|
|
||||||
|
ReceivedCount++;
|
||||||
|
reader.Recycle();
|
||||||
|
};
|
||||||
|
|
||||||
|
Listener.PeerDisconnectedEvent += (peer, info) =>
|
||||||
|
{
|
||||||
|
Log.Warning("[Client {ClientId:00}] 연결 끊김: {Reason}", ClientId, info.Reason);
|
||||||
|
Peer = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
Manager.Start();
|
||||||
|
Manager.Connect(ip, port, key);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SendPing()
|
||||||
|
{
|
||||||
|
if (Peer is null) return;
|
||||||
|
|
||||||
|
int seq = _seqNumber++;
|
||||||
|
_pendingPings[seq] = Stopwatch.GetTimestamp();
|
||||||
|
|
||||||
|
var writer = new NetDataWriter();
|
||||||
|
writer.Put($"seq:{seq}");
|
||||||
|
Peer.Send(writer, DeliveryMethod.ReliableOrdered);
|
||||||
|
SentCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
public double AvgRttMs => RttCount > 0 ? TotalRttMs / RttCount : 0.0;
|
||||||
|
|
||||||
|
public void PollEvents() => Manager.PollEvents();
|
||||||
|
|
||||||
|
public void Stop() => Manager.Stop();
|
||||||
|
}
|
||||||
604
ClientTester/EchoClientTester/Packet/PacketBody.cs
Normal file
604
ClientTester/EchoClientTester/Packet/PacketBody.cs
Normal file
@@ -0,0 +1,604 @@
|
|||||||
|
using ProtoBuf;
|
||||||
|
|
||||||
|
namespace ClientTester.Packet;
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 공통 타입
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
[ProtoContract]
|
||||||
|
public class Vector3
|
||||||
|
{
|
||||||
|
[ProtoMember(1)]
|
||||||
|
public float X
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
|
||||||
|
[ProtoMember(2)]
|
||||||
|
public float Y
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
|
||||||
|
[ProtoMember(3)]
|
||||||
|
public float Z
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[ProtoContract]
|
||||||
|
public class PlayerInfo
|
||||||
|
{
|
||||||
|
[ProtoMember(1)]
|
||||||
|
public int PlayerId
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
|
||||||
|
[ProtoMember(2)]
|
||||||
|
public string Nickname
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
|
||||||
|
[ProtoMember(3)]
|
||||||
|
public int Level
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
|
||||||
|
[ProtoMember(4)]
|
||||||
|
public int Hp
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
|
||||||
|
[ProtoMember(5)]
|
||||||
|
public int MaxHp
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
|
||||||
|
[ProtoMember(6)]
|
||||||
|
public int Mp
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
|
||||||
|
[ProtoMember(7)]
|
||||||
|
public int MaxMp
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
|
||||||
|
[ProtoMember(8)]
|
||||||
|
public Vector3 Position
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
|
||||||
|
[ProtoMember(9)]
|
||||||
|
public float RotY
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[ProtoContract]
|
||||||
|
public class ItemInfo
|
||||||
|
{
|
||||||
|
[ProtoMember(1)]
|
||||||
|
public int ItemId
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
|
||||||
|
[ProtoMember(2)]
|
||||||
|
public int Count
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 인증
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
// RECV_TOKEN
|
||||||
|
[ProtoContract]
|
||||||
|
public class RecvTokenPacket
|
||||||
|
{
|
||||||
|
[ProtoMember(1)]
|
||||||
|
public string Token
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// LOAD_GAME
|
||||||
|
[ProtoContract]
|
||||||
|
public class LoadGamePacket
|
||||||
|
{
|
||||||
|
[ProtoMember(1)]
|
||||||
|
public bool IsAccepted
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
|
||||||
|
[ProtoMember(2)]
|
||||||
|
public PlayerInfo Player
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 로비
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
// INTO_LOBBY
|
||||||
|
[ProtoContract]
|
||||||
|
public class IntoLobbyPacket
|
||||||
|
{
|
||||||
|
[ProtoMember(1)]
|
||||||
|
public List<PlayerInfo> Players
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// EXIT_LOBBY
|
||||||
|
[ProtoContract]
|
||||||
|
public class ExitLobbyPacket
|
||||||
|
{
|
||||||
|
[ProtoMember(1)]
|
||||||
|
public int PlayerId
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 인스턴스 던전
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
public enum BossState
|
||||||
|
{
|
||||||
|
START,
|
||||||
|
END,
|
||||||
|
PHASE_CHANGE
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum BossResult
|
||||||
|
{
|
||||||
|
SUCCESS,
|
||||||
|
FAIL
|
||||||
|
}
|
||||||
|
|
||||||
|
// INTO_INSTANCE
|
||||||
|
[ProtoContract]
|
||||||
|
public class IntoInstancePacket
|
||||||
|
{
|
||||||
|
[ProtoMember(1)]
|
||||||
|
public int InstanceId
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
|
||||||
|
[ProtoMember(2)]
|
||||||
|
public int BossId
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
|
||||||
|
[ProtoMember(3)]
|
||||||
|
public List<int> PlayerIds
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UPDATE_BOSS
|
||||||
|
[ProtoContract]
|
||||||
|
public class UpdateBossPacket
|
||||||
|
{
|
||||||
|
[ProtoMember(1)]
|
||||||
|
public BossState State
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
|
||||||
|
[ProtoMember(2)]
|
||||||
|
public int Phase
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
|
||||||
|
[ProtoMember(3)]
|
||||||
|
public BossResult Result
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
} // END일 때만 유효
|
||||||
|
}
|
||||||
|
|
||||||
|
// REWARD_INSTANCE
|
||||||
|
[ProtoContract]
|
||||||
|
public class RewardInstancePacket
|
||||||
|
{
|
||||||
|
[ProtoMember(1)]
|
||||||
|
public int Exp
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
|
||||||
|
[ProtoMember(2)]
|
||||||
|
public List<ItemInfo> Items
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// EXIT_INSTANCE
|
||||||
|
[ProtoContract]
|
||||||
|
public class ExitInstancePacket
|
||||||
|
{
|
||||||
|
[ProtoMember(1)]
|
||||||
|
public int PlayerId
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 파티
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
public enum PartyUpdateType
|
||||||
|
{
|
||||||
|
CREATE,
|
||||||
|
DELETE
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum UserPartyUpdateType
|
||||||
|
{
|
||||||
|
JOIN,
|
||||||
|
LEAVE
|
||||||
|
}
|
||||||
|
|
||||||
|
// UPDATE_PARTY
|
||||||
|
[ProtoContract]
|
||||||
|
public class UpdatePartyPacket
|
||||||
|
{
|
||||||
|
[ProtoMember(1)]
|
||||||
|
public int PartyId
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
|
||||||
|
[ProtoMember(2)]
|
||||||
|
public PartyUpdateType Type
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
|
||||||
|
[ProtoMember(3)]
|
||||||
|
public int LeaderId
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UPDATE_USER_PARTY
|
||||||
|
[ProtoContract]
|
||||||
|
public class UpdateUserPartyPacket
|
||||||
|
{
|
||||||
|
[ProtoMember(1)]
|
||||||
|
public int PartyId
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
|
||||||
|
[ProtoMember(2)]
|
||||||
|
public int PlayerId
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
|
||||||
|
[ProtoMember(3)]
|
||||||
|
public UserPartyUpdateType Type
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 플레이어
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
public enum PlayerActionType
|
||||||
|
{
|
||||||
|
IDLE,
|
||||||
|
MOVE,
|
||||||
|
ATTACK,
|
||||||
|
SKILL,
|
||||||
|
DODGE,
|
||||||
|
DIE,
|
||||||
|
REVIVE
|
||||||
|
}
|
||||||
|
|
||||||
|
// TRANSFORM_PLAYER
|
||||||
|
[ProtoContract]
|
||||||
|
public class TransformPlayerPacket
|
||||||
|
{
|
||||||
|
[ProtoMember(1)]
|
||||||
|
public int PlayerId
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
|
||||||
|
[ProtoMember(2)]
|
||||||
|
public Vector3 Position
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
|
||||||
|
[ProtoMember(3)]
|
||||||
|
public float RotY
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ACTION_PLAYER
|
||||||
|
[ProtoContract]
|
||||||
|
public class ActionPlayerPacket
|
||||||
|
{
|
||||||
|
[ProtoMember(1)]
|
||||||
|
public int PlayerId
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
|
||||||
|
[ProtoMember(2)]
|
||||||
|
public PlayerActionType Action
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
|
||||||
|
[ProtoMember(3)]
|
||||||
|
public int SkillId
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
} // ATTACK, SKILL일 때
|
||||||
|
|
||||||
|
[ProtoMember(4)]
|
||||||
|
public int TargetId
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
} // 공격 대상
|
||||||
|
}
|
||||||
|
|
||||||
|
// STATE_PLAYER
|
||||||
|
[ProtoContract]
|
||||||
|
public class StatePlayerPacket
|
||||||
|
{
|
||||||
|
[ProtoMember(1)]
|
||||||
|
public int PlayerId
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
|
||||||
|
[ProtoMember(2)]
|
||||||
|
public int Hp
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
|
||||||
|
[ProtoMember(3)]
|
||||||
|
public int MaxHp
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
|
||||||
|
[ProtoMember(4)]
|
||||||
|
public int Mp
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
|
||||||
|
[ProtoMember(5)]
|
||||||
|
public int MaxMp
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// NPC
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
public enum NpcActionType
|
||||||
|
{
|
||||||
|
IDLE,
|
||||||
|
MOVE,
|
||||||
|
ATTACK,
|
||||||
|
SKILL,
|
||||||
|
DIE
|
||||||
|
}
|
||||||
|
|
||||||
|
// TRANSFORM_NPC
|
||||||
|
[ProtoContract]
|
||||||
|
public class TransformNpcPacket
|
||||||
|
{
|
||||||
|
[ProtoMember(1)]
|
||||||
|
public int NpcId
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
|
||||||
|
[ProtoMember(2)]
|
||||||
|
public Vector3 Position
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
|
||||||
|
[ProtoMember(3)]
|
||||||
|
public float RotY
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ACTION_NPC
|
||||||
|
[ProtoContract]
|
||||||
|
public class ActionNpcPacket
|
||||||
|
{
|
||||||
|
[ProtoMember(1)]
|
||||||
|
public int NpcId
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
|
||||||
|
[ProtoMember(2)]
|
||||||
|
public NpcActionType Action
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
|
||||||
|
[ProtoMember(3)]
|
||||||
|
public int PatternId
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
} // 사용 패턴/스킬 번호
|
||||||
|
|
||||||
|
[ProtoMember(4)]
|
||||||
|
public int TargetId
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// STATE_NPC
|
||||||
|
[ProtoContract]
|
||||||
|
public class StateNpcPacket
|
||||||
|
{
|
||||||
|
[ProtoMember(1)]
|
||||||
|
public int NpcId
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
|
||||||
|
[ProtoMember(2)]
|
||||||
|
public int Hp
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
|
||||||
|
[ProtoMember(3)]
|
||||||
|
public int MaxHp
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
|
||||||
|
[ProtoMember(4)]
|
||||||
|
public int Phase
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 데미지
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
// DAMAGE
|
||||||
|
[ProtoContract]
|
||||||
|
public class DamagePacket
|
||||||
|
{
|
||||||
|
[ProtoMember(1)]
|
||||||
|
public int AttackerId
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
|
||||||
|
[ProtoMember(2)]
|
||||||
|
public int TargetId
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
|
||||||
|
[ProtoMember(3)]
|
||||||
|
public int Amount
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
|
||||||
|
[ProtoMember(4)]
|
||||||
|
public bool IsCritical
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
}
|
||||||
61
ClientTester/EchoClientTester/Packet/PacketHeader.cs
Normal file
61
ClientTester/EchoClientTester/Packet/PacketHeader.cs
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
namespace ClientTester.Packet;
|
||||||
|
|
||||||
|
public enum PacketCode
|
||||||
|
{
|
||||||
|
NONE,
|
||||||
|
// 초기 클라이언트 시작시 jwt토큰 받아옴
|
||||||
|
RECV_TOKEN,
|
||||||
|
// jwt토큰 검증후 게임에 들어갈지 말지 (내 데이터도 전송)
|
||||||
|
LOAD_GAME,
|
||||||
|
|
||||||
|
// 마을(로비)진입시 모든 데이터 로드
|
||||||
|
INTO_LOBBY,
|
||||||
|
|
||||||
|
// 로비 나가기
|
||||||
|
EXIT_LOBBY,
|
||||||
|
|
||||||
|
// 인스턴스 던전 입장
|
||||||
|
INTO_INSTANCE,
|
||||||
|
|
||||||
|
// 결과 보상
|
||||||
|
REWARD_INSTANCE,
|
||||||
|
|
||||||
|
// 보스전 (시작, 종료)
|
||||||
|
UPDATE_BOSS,
|
||||||
|
|
||||||
|
// 인스턴스 던전 퇴장
|
||||||
|
EXIT_INSTANCE,
|
||||||
|
|
||||||
|
// 파티 (생성, 삭제)
|
||||||
|
UPDATE_PARTY,
|
||||||
|
|
||||||
|
// 파티 유저 업데이트(추가 삭제)
|
||||||
|
UPDATE_USER_PARTY,
|
||||||
|
|
||||||
|
// 플레이어 위치, 방향
|
||||||
|
TRANSFORM_PLAYER,
|
||||||
|
|
||||||
|
// 플레이어 행동 업데이트
|
||||||
|
ACTION_PLAYER,
|
||||||
|
|
||||||
|
// 플레이어 스테이트 업데이트
|
||||||
|
STATE_PLAYER,
|
||||||
|
|
||||||
|
// NPC 위치, 방향
|
||||||
|
TRANSFORM_NPC,
|
||||||
|
|
||||||
|
// NPC 행동 업데이트
|
||||||
|
ACTION_NPC,
|
||||||
|
|
||||||
|
// NPC 스테이트 업데이트
|
||||||
|
STATE_NPC,
|
||||||
|
|
||||||
|
// 데미지 UI 전달
|
||||||
|
DAMAGE
|
||||||
|
}
|
||||||
|
|
||||||
|
public class PacketHeader
|
||||||
|
{
|
||||||
|
public PacketCode Code;
|
||||||
|
public int BodyLength;
|
||||||
|
}
|
||||||
28
ClientTester/EchoClientTester/Program.cs
Normal file
28
ClientTester/EchoClientTester/Program.cs
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
using ClientTester.EchoDummyService;
|
||||||
|
using Serilog;
|
||||||
|
|
||||||
|
Log.Logger = new LoggerConfiguration()
|
||||||
|
.WriteTo.Console()
|
||||||
|
.CreateLogger();
|
||||||
|
|
||||||
|
const string SERVER_IP = "localhost";
|
||||||
|
const int SERVER_PORT = 9500;
|
||||||
|
const string CONNECTION_KEY = "game";
|
||||||
|
const int CLIENT_COUNT = 1000;
|
||||||
|
|
||||||
|
var cts = new CancellationTokenSource();
|
||||||
|
Console.CancelKeyPress += (_, e) =>
|
||||||
|
{
|
||||||
|
e.Cancel = true;
|
||||||
|
Log.Warning("[SHUTDOWN] Ctrl+C 감지, 종료 중...");
|
||||||
|
cts.Cancel();
|
||||||
|
};
|
||||||
|
|
||||||
|
var service = new DummyClientService(CLIENT_COUNT, SERVER_IP, SERVER_PORT, CONNECTION_KEY, 100);
|
||||||
|
|
||||||
|
await service.RunAsync(cts.Token);
|
||||||
|
|
||||||
|
service.PrintStats();
|
||||||
|
service.Stop();
|
||||||
|
|
||||||
|
Log.CloseAndFlush();
|
||||||
48
ClientTester/TempServer/Program.cs
Normal file
48
ClientTester/TempServer/Program.cs
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
using LiteNetLib;
|
||||||
|
using LiteNetLib.Utils;
|
||||||
|
|
||||||
|
const int PORT = 9500;
|
||||||
|
const string CONNECTION_KEY = "game";
|
||||||
|
|
||||||
|
var listener = new EventBasedNetListener();
|
||||||
|
var server = new NetManager(listener);
|
||||||
|
|
||||||
|
listener.ConnectionRequestEvent += request =>
|
||||||
|
{
|
||||||
|
request.AcceptIfKey(CONNECTION_KEY);
|
||||||
|
};
|
||||||
|
|
||||||
|
listener.PeerConnectedEvent += peer =>
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[CONNECT] {peer.Port} (ID:{peer.Id}, Total:{server.ConnectedPeersCount})");
|
||||||
|
};
|
||||||
|
|
||||||
|
listener.PeerDisconnectedEvent += (peer, info) =>
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[DISCONNECT] {peer.Port} Reason:{info.Reason} (Remaining:{server.ConnectedPeersCount})");
|
||||||
|
};
|
||||||
|
|
||||||
|
listener.NetworkReceiveEvent += (fromPeer, reader, channel, deliveryMethod) =>
|
||||||
|
{
|
||||||
|
var msg = reader.GetString();
|
||||||
|
Console.WriteLine($"[RECV] ID:{fromPeer.Id} → {msg}");
|
||||||
|
|
||||||
|
// 클라이언트 ReceivedCount 증가시키려면 응답 필요
|
||||||
|
var writer = new NetDataWriter();
|
||||||
|
writer.Put($"ack:{msg}");
|
||||||
|
fromPeer.Send(writer, deliveryMethod);
|
||||||
|
|
||||||
|
reader.Recycle();
|
||||||
|
};
|
||||||
|
|
||||||
|
server.Start(PORT);
|
||||||
|
Console.WriteLine($"[SERVER] Started on port {PORT}. Press Enter to stop.");
|
||||||
|
|
||||||
|
while (!Console.KeyAvailable)
|
||||||
|
{
|
||||||
|
server.PollEvents();
|
||||||
|
Thread.Sleep(15);
|
||||||
|
}
|
||||||
|
|
||||||
|
server.Stop();
|
||||||
|
Console.WriteLine("[SERVER] Stopped.");
|
||||||
14
ClientTester/TempServer/TempServer.csproj
Normal file
14
ClientTester/TempServer/TempServer.csproj
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="LiteNetLib" Version="2.0.2" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
26
MMOTestServer/.dockerignore
Normal file
26
MMOTestServer/.dockerignore
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
**/.dockerignore
|
||||||
|
**/.env
|
||||||
|
**/.git
|
||||||
|
**/.gitignore
|
||||||
|
**/.project
|
||||||
|
**/.settings
|
||||||
|
**/.toolstarget
|
||||||
|
**/.vs
|
||||||
|
**/.vscode
|
||||||
|
**/.idea
|
||||||
|
**/*.*proj.user
|
||||||
|
*.user
|
||||||
|
**/*.dbmdl
|
||||||
|
**/*.jfm
|
||||||
|
**/azds.yaml
|
||||||
|
**/bin
|
||||||
|
**/charts
|
||||||
|
**/docker-compose*
|
||||||
|
**/Dockerfile*
|
||||||
|
**/node_modules
|
||||||
|
**/npm-debug.log
|
||||||
|
**/obj
|
||||||
|
**/secrets.dev.yaml
|
||||||
|
**/values.dev.yaml
|
||||||
|
LICENSE
|
||||||
|
README.md
|
||||||
362
MMOTestServer/.editorconfig
Normal file
362
MMOTestServer/.editorconfig
Normal file
@@ -0,0 +1,362 @@
|
|||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
end_of_line = lf
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 4
|
||||||
|
max_line_length = 140
|
||||||
|
|
||||||
|
[*.cs]
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# 중괄호 Allman 스타일
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# Good: Bad:
|
||||||
|
# if (isAlive) if (isAlive) {
|
||||||
|
# { TakeDamage();
|
||||||
|
# TakeDamage(); }
|
||||||
|
# }
|
||||||
|
csharp_new_line_before_open_brace = all
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# 중괄호 항상 사용
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# Good: Bad:
|
||||||
|
# if (isAlive) if (isAlive)
|
||||||
|
# { TakeDamage();
|
||||||
|
# TakeDamage();
|
||||||
|
# }
|
||||||
|
csharp_prefer_braces = true:warning
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# 접근 제한자 항상 명시
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# Good: Bad:
|
||||||
|
# private int health; int health;
|
||||||
|
# public float moveSpeed; float moveSpeed;
|
||||||
|
dotnet_style_require_accessibility_modifiers = always:warning
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# this. 한정자 허용 (필드와 매개변수 이름이 같을 때 필요)
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# 허용: this.stateContext = stateContext; (매개변수와 필드명 동일 시)
|
||||||
|
# 불필요: this.Initialize(); (모호하지 않은 경우)
|
||||||
|
dotnet_style_qualification_for_field = false:suggestion
|
||||||
|
dotnet_style_qualification_for_property = false:suggestion
|
||||||
|
dotnet_style_qualification_for_method = false:suggestion
|
||||||
|
dotnet_style_qualification_for_event = false:suggestion
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# var 사용 금지
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# Good: Bad:
|
||||||
|
# Enemy enemy = GetEnemy(); var enemy = GetEnemy();
|
||||||
|
# int count = 0; var count = 0;
|
||||||
|
csharp_style_var_for_built_in_types = false:warning
|
||||||
|
csharp_style_var_when_type_is_apparent = false:warning
|
||||||
|
csharp_style_var_elsewhere = false:warning
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# null 체크 스타일 강제
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# Good: Bad:
|
||||||
|
# animator?.Play("Run"); if (animator != null) animator.Play("Run");
|
||||||
|
# string name = playerName ?? "Unknown"; string name = playerName != null ? playerName : "Unknown";
|
||||||
|
# if (obj is null) { } if (object.ReferenceEquals(obj, null)) { }
|
||||||
|
csharp_style_conditional_delegate_call = true:warning
|
||||||
|
dotnet_style_null_propagation = true:warning
|
||||||
|
dotnet_style_coalesce_expression = true:warning
|
||||||
|
dotnet_style_prefer_is_null_check_over_reference_equality_method = true:warning
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# 패턴 매칭 강제 (as + null 체크 대신 is 패턴 사용)
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# Good: Bad:
|
||||||
|
# if (obj is Enemy enemy) Enemy enemy = obj as Enemy;
|
||||||
|
# { if (enemy != null)
|
||||||
|
# enemy.TakeDamage(); {
|
||||||
|
# } enemy.TakeDamage();
|
||||||
|
# }
|
||||||
|
csharp_style_pattern_matching_over_is_with_cast_check = true:warning
|
||||||
|
csharp_style_pattern_matching_over_as_with_null_check = true:warning
|
||||||
|
# not 패턴 (C# 9+)
|
||||||
|
# Good: if (obj is not null) Bad: if (!(obj is null))
|
||||||
|
# Good: if (enemy is not Dead) Bad: if (!(enemy is Dead))
|
||||||
|
csharp_style_prefer_not_pattern = true:warning
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# throw 표현식 강제
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# Good:
|
||||||
|
# target = player ?? throw new ArgumentNullException(nameof(player));
|
||||||
|
#
|
||||||
|
# Bad:
|
||||||
|
# if (player == null) throw new ArgumentNullException(nameof(player));
|
||||||
|
# target = player;
|
||||||
|
csharp_style_throw_expression = true:warning
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# 인라인 변수 선언 강제
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# Good: Bad:
|
||||||
|
# TryGetComponent(out Rigidbody rb); Rigidbody rb;
|
||||||
|
# TryGetComponent(out rb);
|
||||||
|
csharp_style_inlined_variable_declaration = true:warning
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# 튜플 해체 선언
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# Good: Bad:
|
||||||
|
# var (x, y) = GetPosition(); var pos = GetPosition();
|
||||||
|
# var x = pos.x; var y = pos.y;
|
||||||
|
csharp_style_deconstructed_variable_declaration = true:suggestion
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# 간단한 using 선언문
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# Good: Bad:
|
||||||
|
# using var stream = File.Open(...); using (var stream = File.Open(...))
|
||||||
|
# {
|
||||||
|
# }
|
||||||
|
csharp_prefer_simple_using_statement = true:suggestion
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# 인덱스 / 범위 연산자 (C# 8+)
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# Good: items[^1] Bad: items[items.Length - 1]
|
||||||
|
# Good: items[1..3] Bad: items.Skip(1).Take(2)
|
||||||
|
csharp_style_prefer_index_from_end = true:suggestion
|
||||||
|
csharp_style_prefer_range_operator = true:suggestion
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# switch 표현식 권장
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# Good: Bad:
|
||||||
|
# string label = state switch string label;
|
||||||
|
# { switch (state)
|
||||||
|
# GameState.Playing => "Playing", {
|
||||||
|
# GameState.Paused => "Paused", case GameState.Playing: label = "Playing"; break;
|
||||||
|
# _ => "Unknown" case GameState.Paused: label = "Paused"; break;
|
||||||
|
# }; default: label = "Unknown"; break;
|
||||||
|
# }
|
||||||
|
csharp_style_prefer_switch_expression = true:suggestion
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# 불필요한 코드 제거
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# object initializer
|
||||||
|
# Good: Bad:
|
||||||
|
# Enemy enemy = new Enemy Enemy enemy = new Enemy();
|
||||||
|
# { enemy.hp = 100;
|
||||||
|
# hp = 100, enemy.name = "Goblin";
|
||||||
|
# name = "Goblin"
|
||||||
|
# };
|
||||||
|
#
|
||||||
|
# 미사용 반환값은 _ 로 명시적으로 버리기
|
||||||
|
# Good: Bad:
|
||||||
|
# _ = TryGetComponent(out Rigidbody rb); TryGetComponent(out Rigidbody rb);
|
||||||
|
dotnet_style_object_initializer = true:warning
|
||||||
|
dotnet_style_collection_initializer = true:warning
|
||||||
|
dotnet_remove_unnecessary_suppression_exclusions = true
|
||||||
|
csharp_style_unused_value_assignment_preference = discard_variable:warning
|
||||||
|
csharp_style_unused_value_expression_statement_preference = discard_variable:warning
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# 단순화 강제
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# auto property
|
||||||
|
# Good: Bad:
|
||||||
|
# public int Hp { get; private set; } private int hp;
|
||||||
|
# public int Hp { get { return hp; } }
|
||||||
|
#
|
||||||
|
# boolean 단순화
|
||||||
|
# Good: Bad:
|
||||||
|
# return isAlive; return isAlive == true;
|
||||||
|
#
|
||||||
|
# 삼항 연산자 (suggestion - 복잡한 경우 강제 안 함)
|
||||||
|
# Good: Bad:
|
||||||
|
# int damage = isCrit ? 200 : 100; int damage;
|
||||||
|
# if (isCrit) damage = 200;
|
||||||
|
# else damage = 100;
|
||||||
|
dotnet_style_prefer_auto_properties = true:warning
|
||||||
|
dotnet_style_prefer_simplified_boolean_expressions = true:warning
|
||||||
|
dotnet_style_prefer_conditional_expression_over_assignment = true:suggestion
|
||||||
|
dotnet_style_prefer_conditional_expression_over_return = true:suggestion
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# 복합 할당 연산자
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# Good: hp -= damage; Bad: hp = hp - damage;
|
||||||
|
# Good: score += point; Bad: score = score + point;
|
||||||
|
dotnet_style_prefer_compound_assignment = true:warning
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# 타입 예약어 강제 (BCL 타입명 대신 C# 예약어 사용)
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# Good: Bad:
|
||||||
|
# int count = 0; Int32 count = 0;
|
||||||
|
# string name = "Player"; String name = "Player";
|
||||||
|
# object obj = new Enemy(); Object obj = new Enemy();
|
||||||
|
dotnet_style_predefined_type_for_locals_parameters_members = true:warning
|
||||||
|
dotnet_style_predefined_type_for_member_access = true:warning
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# expression-bodied 프로퍼티/접근자 강제, 메서드/생성자/로컬함수 금지
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# Good: Bad:
|
||||||
|
# public int CurrentHp => currentHp; public int CurrentHp
|
||||||
|
# {
|
||||||
|
# get { return currentHp; }
|
||||||
|
# }
|
||||||
|
#
|
||||||
|
# 메서드 / 생성자 / 로컬 함수는 expression-bodied 금지
|
||||||
|
# Good: Bad:
|
||||||
|
# public void TakeDamage(int damage) public void TakeDamage(int damage) => hp -= damage;
|
||||||
|
# {
|
||||||
|
# hp -= damage;
|
||||||
|
# }
|
||||||
|
csharp_style_expression_bodied_properties = true:warning
|
||||||
|
csharp_style_expression_bodied_accessors = true:warning
|
||||||
|
csharp_style_expression_bodied_indexers = true:warning
|
||||||
|
csharp_style_expression_bodied_operators = true:warning
|
||||||
|
csharp_style_expression_bodied_lambdas = true:suggestion
|
||||||
|
csharp_style_expression_bodied_methods = false:warning
|
||||||
|
csharp_style_expression_bodied_constructors = false:warning
|
||||||
|
csharp_style_expression_bodied_local_functions = false:warning
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# 한 줄 블록 금지 (포맷터 적용 시 자동 정리)
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# Good: Bad:
|
||||||
|
# if (isAlive) if (isAlive) { TakeDamage(); }
|
||||||
|
# {
|
||||||
|
# TakeDamage();
|
||||||
|
# }
|
||||||
|
# 주의: 이 설정은 경고가 아닌 포맷터 실행 시에만 적용됩니다.
|
||||||
|
csharp_preserve_single_line_blocks = false
|
||||||
|
csharp_preserve_single_line_statements = false
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# using 정렬
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# Good:
|
||||||
|
# using System.Collections;
|
||||||
|
# using System.Collections.Generic;
|
||||||
|
# using UnityEngine;
|
||||||
|
dotnet_sort_system_directives_first = true
|
||||||
|
dotnet_separate_import_directive_groups = false
|
||||||
|
csharp_using_directive_placement = outside_namespace:warning
|
||||||
|
# 네임스페이스는 폴더 구조와 일치
|
||||||
|
# Good: Assets/Scripts/Player/ → namespace Project.Player
|
||||||
|
dotnet_style_namespace_match_folder = true:suggestion
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# 네이밍 규칙
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# 주의: 더 구체적인 규칙(const 등)을 먼저 선언해야 올바르게 적용됩니다.
|
||||||
|
|
||||||
|
# 상수: UPPER_SNAKE_CASE (const modifier로 가장 구체적 → 최우선)
|
||||||
|
# Good: const int MAX_LEVEL = 100; Bad: const int MaxLevel = 100;
|
||||||
|
# Good: const float DEFAULT_SPEED = 5f; Bad: const float defaultSpeed = 5f;
|
||||||
|
dotnet_naming_rule.constants.severity = warning
|
||||||
|
dotnet_naming_rule.constants.symbols = constants
|
||||||
|
dotnet_naming_rule.constants.style = upper_snake_case
|
||||||
|
|
||||||
|
dotnet_naming_symbols.constants.applicable_kinds = field
|
||||||
|
dotnet_naming_symbols.constants.required_modifiers = const
|
||||||
|
|
||||||
|
dotnet_naming_style.upper_snake_case.capitalization = all_upper
|
||||||
|
dotnet_naming_style.upper_snake_case.word_separator = _
|
||||||
|
|
||||||
|
# private / protected 필드: camelCase (Unity 스타일 통일)
|
||||||
|
# Good: private int currentHp; Bad: private int _currentHp;
|
||||||
|
# Good: protected float baseSpeed; Bad: protected float _baseSpeed;
|
||||||
|
dotnet_naming_rule.private_fields.severity = warning
|
||||||
|
dotnet_naming_rule.private_fields.symbols = private_fields
|
||||||
|
dotnet_naming_rule.private_fields.style = camel_case_style
|
||||||
|
|
||||||
|
dotnet_naming_symbols.private_fields.applicable_kinds = field
|
||||||
|
dotnet_naming_symbols.private_fields.applicable_accessibilities = private, protected, protected_internal
|
||||||
|
|
||||||
|
# public 필드: camelCase (Unity Inspector 노출 필드 기준 - 일반 C# 컨벤션과 다름)
|
||||||
|
# Good: public float moveSpeed; Bad: public float MoveSpeed;
|
||||||
|
# Good: public int maxHealth; Bad: public int MaxHealth;
|
||||||
|
dotnet_naming_rule.public_fields.severity = warning
|
||||||
|
dotnet_naming_rule.public_fields.symbols = public_fields
|
||||||
|
dotnet_naming_rule.public_fields.style = camel_case_style
|
||||||
|
|
||||||
|
dotnet_naming_symbols.public_fields.applicable_kinds = field
|
||||||
|
dotnet_naming_symbols.public_fields.applicable_accessibilities = public, internal
|
||||||
|
|
||||||
|
dotnet_naming_style.camel_case_style.capitalization = camel_case
|
||||||
|
|
||||||
|
# 인터페이스: IPascalCase
|
||||||
|
# Good: interface IEnemy { } Bad: interface Enemy { }
|
||||||
|
# Good: interface IDamageable { } Bad: interface Damageable { }
|
||||||
|
dotnet_naming_rule.interfaces.severity = warning
|
||||||
|
dotnet_naming_rule.interfaces.symbols = interfaces
|
||||||
|
dotnet_naming_rule.interfaces.style = prefix_i
|
||||||
|
|
||||||
|
dotnet_naming_symbols.interfaces.applicable_kinds = interface
|
||||||
|
|
||||||
|
dotnet_naming_style.prefix_i.capitalization = pascal_case
|
||||||
|
dotnet_naming_style.prefix_i.required_prefix = I
|
||||||
|
|
||||||
|
# 클래스 / 구조체 / 열거형: PascalCase
|
||||||
|
# Good: class PlayerController { } Bad: class playerController { }
|
||||||
|
# Good: enum GameState { } Bad: enum gameState { }
|
||||||
|
dotnet_naming_rule.types.severity = warning
|
||||||
|
dotnet_naming_rule.types.symbols = types
|
||||||
|
dotnet_naming_rule.types.style = pascal_case_style
|
||||||
|
|
||||||
|
dotnet_naming_symbols.types.applicable_kinds = class, struct, enum, delegate
|
||||||
|
|
||||||
|
dotnet_naming_style.pascal_case_style.capitalization = pascal_case
|
||||||
|
|
||||||
|
# 메서드 / 프로퍼티 / 이벤트: PascalCase
|
||||||
|
# Good: public void TakeDamage() { } Bad: public void takeDamage() { }
|
||||||
|
# Good: public int CurrentHp { } Bad: public int currentHp { }
|
||||||
|
# Good: public event Action OnDeath; Bad: public event Action onDeath;
|
||||||
|
dotnet_naming_rule.members.severity = warning
|
||||||
|
dotnet_naming_rule.members.symbols = members
|
||||||
|
dotnet_naming_rule.members.style = pascal_case_style
|
||||||
|
|
||||||
|
dotnet_naming_symbols.members.applicable_kinds = method, property, event
|
||||||
|
|
||||||
|
# 매개변수 / 로컬 변수: camelCase
|
||||||
|
# Good: void Init(int maxHp) { } Bad: void Init(int MaxHp) { }
|
||||||
|
# Good: float moveSpeed = 5f; Bad: float MoveSpeed = 5f;
|
||||||
|
dotnet_naming_rule.parameters_and_locals.severity = warning
|
||||||
|
dotnet_naming_rule.parameters_and_locals.symbols = parameters_and_locals
|
||||||
|
dotnet_naming_rule.parameters_and_locals.style = camel_case_style
|
||||||
|
|
||||||
|
dotnet_naming_symbols.parameters_and_locals.applicable_kinds = parameter, local
|
||||||
|
|
||||||
|
# enum 멤버: PascalCase
|
||||||
|
# Good: GameState.Playing Bad: GameState.playing
|
||||||
|
# Good: GameState.GameOver Bad: GameState.GAME_OVER
|
||||||
|
dotnet_naming_rule.enum_members.severity = warning
|
||||||
|
dotnet_naming_rule.enum_members.symbols = enum_members
|
||||||
|
dotnet_naming_rule.enum_members.style = pascal_case_style
|
||||||
|
|
||||||
|
dotnet_naming_symbols.enum_members.applicable_kinds = enum_member
|
||||||
|
|
||||||
|
[*.{json,yaml,yml}]
|
||||||
|
indent_size = 2
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
trim_trailing_whitespace = false
|
||||||
|
|
||||||
|
[*.{asmdef,asmref}]
|
||||||
|
indent_size = 4
|
||||||
|
|
||||||
|
[*.shader]
|
||||||
|
indent_size = 4
|
||||||
|
|
||||||
|
[*.{compute,hlsl,cginc}]
|
||||||
|
indent_size = 4
|
||||||
|
|
||||||
|
[*.{uss,uxml}]
|
||||||
|
indent_size = 4
|
||||||
27
MMOTestServer/MMOserver.sln
Normal file
27
MMOTestServer/MMOserver.sln
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
|
||||||
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MMOserver", "MMOserver\MMOserver.csproj", "{A1676221-8C43-4BA8-89B9-B5FD668632E8}"
|
||||||
|
EndProject
|
||||||
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{1F979F90-C11E-4BA5-B226-76DC1C052640}"
|
||||||
|
ProjectSection(SolutionItems) = preProject
|
||||||
|
compose.yaml = compose.yaml
|
||||||
|
EndProjectSection
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ServerLib", "ServerLib\ServerLib.csproj", "{7BE5A9C9-D4E2-4C51-8E45-972F487D2637}"
|
||||||
|
EndProject
|
||||||
|
Global
|
||||||
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
Release|Any CPU = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||||
|
{A1676221-8C43-4BA8-89B9-B5FD668632E8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{A1676221-8C43-4BA8-89B9-B5FD668632E8}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{A1676221-8C43-4BA8-89B9-B5FD668632E8}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{A1676221-8C43-4BA8-89B9-B5FD668632E8}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{7BE5A9C9-D4E2-4C51-8E45-972F487D2637}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{7BE5A9C9-D4E2-4C51-8E45-972F487D2637}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{7BE5A9C9-D4E2-4C51-8E45-972F487D2637}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{7BE5A9C9-D4E2-4C51-8E45-972F487D2637}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
EndGlobal
|
||||||
24
MMOTestServer/MMOserver/Dockerfile
Normal file
24
MMOTestServer/MMOserver/Dockerfile
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
FROM mcr.microsoft.com/dotnet/runtime:9.0 AS base
|
||||||
|
USER $APP_UID
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
|
||||||
|
ARG BUILD_CONFIGURATION=Release
|
||||||
|
WORKDIR /src
|
||||||
|
COPY ["MMOserver/MMOserver.csproj", "MMOserver/"]
|
||||||
|
RUN dotnet restore "MMOserver/MMOserver.csproj"
|
||||||
|
COPY . .
|
||||||
|
WORKDIR "/src/MMOserver"
|
||||||
|
RUN dotnet build "./MMOserver.csproj" -c $BUILD_CONFIGURATION -o /app/build
|
||||||
|
|
||||||
|
FROM build AS publish
|
||||||
|
ARG BUILD_CONFIGURATION=Release
|
||||||
|
RUN dotnet publish "./MMOserver.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
|
||||||
|
|
||||||
|
# UDP 9050 포트 열기
|
||||||
|
EXPOSE 9050/udp
|
||||||
|
|
||||||
|
FROM base AS final
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --from=publish /app/publish .
|
||||||
|
ENTRYPOINT ["dotnet", "MMOserver.dll"]
|
||||||
28
MMOTestServer/MMOserver/Game/GameServer.cs
Normal file
28
MMOTestServer/MMOserver/Game/GameServer.cs
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
using LiteNetLib;
|
||||||
|
using Serilog;
|
||||||
|
using ServerLib.Service;
|
||||||
|
|
||||||
|
namespace MMOserver.Game;
|
||||||
|
|
||||||
|
public class GameServer : ServerBase
|
||||||
|
{
|
||||||
|
public GameServer(int port, string connectionString) : base(port, connectionString)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnSessionConnected(NetPeer peer, long hashKey)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnSessionDisconnected(NetPeer peer, long hashKey, DisconnectInfo info)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void HandlePacket(NetPeer peer, long hashKey, ushort type, byte[] payload)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
}
|
||||||
23
MMOTestServer/MMOserver/MMOserver.csproj
Normal file
23
MMOTestServer/MMOserver/MMOserver.csproj
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="LiteNetLib" Version="2.0.2" />
|
||||||
|
<PackageReference Include="protobuf-net" Version="3.2.56" />
|
||||||
|
<Content Include="..\.dockerignore">
|
||||||
|
<Link>.dockerignore</Link>
|
||||||
|
</Content>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\ServerLib\ServerLib.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
604
MMOTestServer/MMOserver/Packet/PacketBody.cs
Normal file
604
MMOTestServer/MMOserver/Packet/PacketBody.cs
Normal file
@@ -0,0 +1,604 @@
|
|||||||
|
using ProtoBuf;
|
||||||
|
|
||||||
|
namespace ClientTester.Packet;
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 공통 타입
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
[ProtoContract]
|
||||||
|
public class Vector3
|
||||||
|
{
|
||||||
|
[ProtoMember(1)]
|
||||||
|
public float X
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
|
||||||
|
[ProtoMember(2)]
|
||||||
|
public float Y
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
|
||||||
|
[ProtoMember(3)]
|
||||||
|
public float Z
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[ProtoContract]
|
||||||
|
public class PlayerInfo
|
||||||
|
{
|
||||||
|
[ProtoMember(1)]
|
||||||
|
public int PlayerId
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
|
||||||
|
[ProtoMember(2)]
|
||||||
|
public string Nickname
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
|
||||||
|
[ProtoMember(3)]
|
||||||
|
public int Level
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
|
||||||
|
[ProtoMember(4)]
|
||||||
|
public int Hp
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
|
||||||
|
[ProtoMember(5)]
|
||||||
|
public int MaxHp
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
|
||||||
|
[ProtoMember(6)]
|
||||||
|
public int Mp
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
|
||||||
|
[ProtoMember(7)]
|
||||||
|
public int MaxMp
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
|
||||||
|
[ProtoMember(8)]
|
||||||
|
public Vector3 Position
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
|
||||||
|
[ProtoMember(9)]
|
||||||
|
public float RotY
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[ProtoContract]
|
||||||
|
public class ItemInfo
|
||||||
|
{
|
||||||
|
[ProtoMember(1)]
|
||||||
|
public int ItemId
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
|
||||||
|
[ProtoMember(2)]
|
||||||
|
public int Count
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 인증
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
// RECV_TOKEN
|
||||||
|
[ProtoContract]
|
||||||
|
public class RecvTokenPacket
|
||||||
|
{
|
||||||
|
[ProtoMember(1)]
|
||||||
|
public string Token
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// LOAD_GAME
|
||||||
|
[ProtoContract]
|
||||||
|
public class LoadGamePacket
|
||||||
|
{
|
||||||
|
[ProtoMember(1)]
|
||||||
|
public bool IsAccepted
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
|
||||||
|
[ProtoMember(2)]
|
||||||
|
public PlayerInfo Player
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 로비
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
// INTO_LOBBY
|
||||||
|
[ProtoContract]
|
||||||
|
public class IntoLobbyPacket
|
||||||
|
{
|
||||||
|
[ProtoMember(1)]
|
||||||
|
public List<PlayerInfo> Players
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// EXIT_LOBBY
|
||||||
|
[ProtoContract]
|
||||||
|
public class ExitLobbyPacket
|
||||||
|
{
|
||||||
|
[ProtoMember(1)]
|
||||||
|
public int PlayerId
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 인스턴스 던전
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
public enum BossState
|
||||||
|
{
|
||||||
|
START,
|
||||||
|
END,
|
||||||
|
PHASE_CHANGE
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum BossResult
|
||||||
|
{
|
||||||
|
SUCCESS,
|
||||||
|
FAIL
|
||||||
|
}
|
||||||
|
|
||||||
|
// INTO_INSTANCE
|
||||||
|
[ProtoContract]
|
||||||
|
public class IntoInstancePacket
|
||||||
|
{
|
||||||
|
[ProtoMember(1)]
|
||||||
|
public int InstanceId
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
|
||||||
|
[ProtoMember(2)]
|
||||||
|
public int BossId
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
|
||||||
|
[ProtoMember(3)]
|
||||||
|
public List<int> PlayerIds
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UPDATE_BOSS
|
||||||
|
[ProtoContract]
|
||||||
|
public class UpdateBossPacket
|
||||||
|
{
|
||||||
|
[ProtoMember(1)]
|
||||||
|
public BossState State
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
|
||||||
|
[ProtoMember(2)]
|
||||||
|
public int Phase
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
|
||||||
|
[ProtoMember(3)]
|
||||||
|
public BossResult Result
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
} // END일 때만 유효
|
||||||
|
}
|
||||||
|
|
||||||
|
// REWARD_INSTANCE
|
||||||
|
[ProtoContract]
|
||||||
|
public class RewardInstancePacket
|
||||||
|
{
|
||||||
|
[ProtoMember(1)]
|
||||||
|
public int Exp
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
|
||||||
|
[ProtoMember(2)]
|
||||||
|
public List<ItemInfo> Items
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// EXIT_INSTANCE
|
||||||
|
[ProtoContract]
|
||||||
|
public class ExitInstancePacket
|
||||||
|
{
|
||||||
|
[ProtoMember(1)]
|
||||||
|
public int PlayerId
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 파티
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
public enum PartyUpdateType
|
||||||
|
{
|
||||||
|
CREATE,
|
||||||
|
DELETE
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum UserPartyUpdateType
|
||||||
|
{
|
||||||
|
JOIN,
|
||||||
|
LEAVE
|
||||||
|
}
|
||||||
|
|
||||||
|
// UPDATE_PARTY
|
||||||
|
[ProtoContract]
|
||||||
|
public class UpdatePartyPacket
|
||||||
|
{
|
||||||
|
[ProtoMember(1)]
|
||||||
|
public int PartyId
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
|
||||||
|
[ProtoMember(2)]
|
||||||
|
public PartyUpdateType Type
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
|
||||||
|
[ProtoMember(3)]
|
||||||
|
public int LeaderId
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UPDATE_USER_PARTY
|
||||||
|
[ProtoContract]
|
||||||
|
public class UpdateUserPartyPacket
|
||||||
|
{
|
||||||
|
[ProtoMember(1)]
|
||||||
|
public int PartyId
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
|
||||||
|
[ProtoMember(2)]
|
||||||
|
public int PlayerId
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
|
||||||
|
[ProtoMember(3)]
|
||||||
|
public UserPartyUpdateType Type
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 플레이어
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
public enum PlayerActionType
|
||||||
|
{
|
||||||
|
IDLE,
|
||||||
|
MOVE,
|
||||||
|
ATTACK,
|
||||||
|
SKILL,
|
||||||
|
DODGE,
|
||||||
|
DIE,
|
||||||
|
REVIVE
|
||||||
|
}
|
||||||
|
|
||||||
|
// TRANSFORM_PLAYER
|
||||||
|
[ProtoContract]
|
||||||
|
public class TransformPlayerPacket
|
||||||
|
{
|
||||||
|
[ProtoMember(1)]
|
||||||
|
public int PlayerId
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
|
||||||
|
[ProtoMember(2)]
|
||||||
|
public Vector3 Position
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
|
||||||
|
[ProtoMember(3)]
|
||||||
|
public float RotY
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ACTION_PLAYER
|
||||||
|
[ProtoContract]
|
||||||
|
public class ActionPlayerPacket
|
||||||
|
{
|
||||||
|
[ProtoMember(1)]
|
||||||
|
public int PlayerId
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
|
||||||
|
[ProtoMember(2)]
|
||||||
|
public PlayerActionType Action
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
|
||||||
|
[ProtoMember(3)]
|
||||||
|
public int SkillId
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
} // ATTACK, SKILL일 때
|
||||||
|
|
||||||
|
[ProtoMember(4)]
|
||||||
|
public int TargetId
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
} // 공격 대상
|
||||||
|
}
|
||||||
|
|
||||||
|
// STATE_PLAYER
|
||||||
|
[ProtoContract]
|
||||||
|
public class StatePlayerPacket
|
||||||
|
{
|
||||||
|
[ProtoMember(1)]
|
||||||
|
public int PlayerId
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
|
||||||
|
[ProtoMember(2)]
|
||||||
|
public int Hp
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
|
||||||
|
[ProtoMember(3)]
|
||||||
|
public int MaxHp
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
|
||||||
|
[ProtoMember(4)]
|
||||||
|
public int Mp
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
|
||||||
|
[ProtoMember(5)]
|
||||||
|
public int MaxMp
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// NPC
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
public enum NpcActionType
|
||||||
|
{
|
||||||
|
IDLE,
|
||||||
|
MOVE,
|
||||||
|
ATTACK,
|
||||||
|
SKILL,
|
||||||
|
DIE
|
||||||
|
}
|
||||||
|
|
||||||
|
// TRANSFORM_NPC
|
||||||
|
[ProtoContract]
|
||||||
|
public class TransformNpcPacket
|
||||||
|
{
|
||||||
|
[ProtoMember(1)]
|
||||||
|
public int NpcId
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
|
||||||
|
[ProtoMember(2)]
|
||||||
|
public Vector3 Position
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
|
||||||
|
[ProtoMember(3)]
|
||||||
|
public float RotY
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ACTION_NPC
|
||||||
|
[ProtoContract]
|
||||||
|
public class ActionNpcPacket
|
||||||
|
{
|
||||||
|
[ProtoMember(1)]
|
||||||
|
public int NpcId
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
|
||||||
|
[ProtoMember(2)]
|
||||||
|
public NpcActionType Action
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
|
||||||
|
[ProtoMember(3)]
|
||||||
|
public int PatternId
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
} // 사용 패턴/스킬 번호
|
||||||
|
|
||||||
|
[ProtoMember(4)]
|
||||||
|
public int TargetId
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// STATE_NPC
|
||||||
|
[ProtoContract]
|
||||||
|
public class StateNpcPacket
|
||||||
|
{
|
||||||
|
[ProtoMember(1)]
|
||||||
|
public int NpcId
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
|
||||||
|
[ProtoMember(2)]
|
||||||
|
public int Hp
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
|
||||||
|
[ProtoMember(3)]
|
||||||
|
public int MaxHp
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
|
||||||
|
[ProtoMember(4)]
|
||||||
|
public int Phase
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 데미지
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
// DAMAGE
|
||||||
|
[ProtoContract]
|
||||||
|
public class DamagePacket
|
||||||
|
{
|
||||||
|
[ProtoMember(1)]
|
||||||
|
public int AttackerId
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
|
||||||
|
[ProtoMember(2)]
|
||||||
|
public int TargetId
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
|
||||||
|
[ProtoMember(3)]
|
||||||
|
public int Amount
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
|
||||||
|
[ProtoMember(4)]
|
||||||
|
public bool IsCritical
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
}
|
||||||
61
MMOTestServer/MMOserver/Packet/PacketHeader.cs
Normal file
61
MMOTestServer/MMOserver/Packet/PacketHeader.cs
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
namespace ClientTester.Packet;
|
||||||
|
|
||||||
|
public enum PacketCode
|
||||||
|
{
|
||||||
|
NONE,
|
||||||
|
// 초기 클라이언트 시작시 jwt토큰 받아옴
|
||||||
|
RECV_TOKEN,
|
||||||
|
// jwt토큰 검증후 게임에 들어갈지 말지 (내 데이터도 전송)
|
||||||
|
LOAD_GAME,
|
||||||
|
|
||||||
|
// 마을(로비)진입시 모든 데이터 로드
|
||||||
|
INTO_LOBBY,
|
||||||
|
|
||||||
|
// 로비 나가기
|
||||||
|
EXIT_LOBBY,
|
||||||
|
|
||||||
|
// 인스턴스 던전 입장
|
||||||
|
INTO_INSTANCE,
|
||||||
|
|
||||||
|
// 결과 보상
|
||||||
|
REWARD_INSTANCE,
|
||||||
|
|
||||||
|
// 보스전 (시작, 종료)
|
||||||
|
UPDATE_BOSS,
|
||||||
|
|
||||||
|
// 인스턴스 던전 퇴장
|
||||||
|
EXIT_INSTANCE,
|
||||||
|
|
||||||
|
// 파티 (생성, 삭제)
|
||||||
|
UPDATE_PARTY,
|
||||||
|
|
||||||
|
// 파티 유저 업데이트(추가 삭제)
|
||||||
|
UPDATE_USER_PARTY,
|
||||||
|
|
||||||
|
// 플레이어 위치, 방향
|
||||||
|
TRANSFORM_PLAYER,
|
||||||
|
|
||||||
|
// 플레이어 행동 업데이트
|
||||||
|
ACTION_PLAYER,
|
||||||
|
|
||||||
|
// 플레이어 스테이트 업데이트
|
||||||
|
STATE_PLAYER,
|
||||||
|
|
||||||
|
// NPC 위치, 방향
|
||||||
|
TRANSFORM_NPC,
|
||||||
|
|
||||||
|
// NPC 행동 업데이트
|
||||||
|
ACTION_NPC,
|
||||||
|
|
||||||
|
// NPC 스테이트 업데이트
|
||||||
|
STATE_NPC,
|
||||||
|
|
||||||
|
// 데미지 UI 전달
|
||||||
|
DAMAGE
|
||||||
|
}
|
||||||
|
|
||||||
|
public class PacketHeader
|
||||||
|
{
|
||||||
|
public PacketCode Code;
|
||||||
|
public int BodyLength;
|
||||||
|
}
|
||||||
46
MMOTestServer/MMOserver/Program.cs
Normal file
46
MMOTestServer/MMOserver/Program.cs
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
using MMOserver.Game;
|
||||||
|
using Serilog;
|
||||||
|
|
||||||
|
class Program
|
||||||
|
{
|
||||||
|
private static void Main()
|
||||||
|
{
|
||||||
|
string timestamp = DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss");
|
||||||
|
|
||||||
|
Log.Logger = new LoggerConfiguration()
|
||||||
|
.MinimumLevel.Debug()
|
||||||
|
.WriteTo.Console()
|
||||||
|
.WriteTo.File($"logs/log_{timestamp}.txt")
|
||||||
|
.CreateLogger();
|
||||||
|
|
||||||
|
Log.Information("Write Log Started");
|
||||||
|
|
||||||
|
int port = 9500;
|
||||||
|
string connectionString = "test";
|
||||||
|
GameServer gameServer = new GameServer(port, connectionString);
|
||||||
|
|
||||||
|
// Ctrl+C → Stop() 호출 → Run() 루프 탈출 → serverThread 종료
|
||||||
|
Console.CancelKeyPress += (_, e) =>
|
||||||
|
{
|
||||||
|
// 프로세스 즉시 종료 막기
|
||||||
|
e.Cancel = true;
|
||||||
|
gameServer.Stop();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 게임 서버 스레드 생성
|
||||||
|
Thread serverThread = new Thread(() =>
|
||||||
|
{
|
||||||
|
gameServer.Init();
|
||||||
|
gameServer.Run();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 게임 서버 스레드 시작
|
||||||
|
serverThread.Start();
|
||||||
|
|
||||||
|
// Run()이 끝날 때까지 대기
|
||||||
|
serverThread.Join();
|
||||||
|
|
||||||
|
// Log 종료
|
||||||
|
Log.CloseAndFlush();
|
||||||
|
}
|
||||||
|
}
|
||||||
53
MMOTestServer/ServerLib/Packet/PacketSerializer.cs
Normal file
53
MMOTestServer/ServerLib/Packet/PacketSerializer.cs
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
using ProtoBuf;
|
||||||
|
using Serilog;
|
||||||
|
|
||||||
|
namespace ServerLib.Packet
|
||||||
|
{
|
||||||
|
// 패킷 헤더 크기 4(패킷 타입, 패킷 길이)
|
||||||
|
|
||||||
|
public static class PacketSerializer
|
||||||
|
{
|
||||||
|
// 직렬화: 객체 → byte[]
|
||||||
|
public static byte[] Serialize<T>(ushort type, ushort size, T packet)
|
||||||
|
{
|
||||||
|
MemoryStream ms = new MemoryStream();
|
||||||
|
|
||||||
|
// 2바이트 패킷 타입 헤더
|
||||||
|
ms.WriteByte((byte)(type & 0xFF));
|
||||||
|
ms.WriteByte((byte)(type >> 8));
|
||||||
|
|
||||||
|
// 2바이트 패킷 길이 헤더
|
||||||
|
ms.WriteByte((byte)(size & 0xFF));
|
||||||
|
ms.WriteByte((byte)(size >> 8));
|
||||||
|
|
||||||
|
// protobuf 페이로드
|
||||||
|
Serializer.Serialize(ms, packet);
|
||||||
|
return ms.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 역직렬화: byte[] → (PacketType, payload bytes)
|
||||||
|
public static (ushort type, ushort size, byte[] payload) Deserialize(byte[] data)
|
||||||
|
{
|
||||||
|
if (data.Length < 4)
|
||||||
|
{
|
||||||
|
Log.Warning("[PacketHeader]의 길이가 4이하입니다.");
|
||||||
|
return (0, 0, null)!;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 길이체크도 필요함
|
||||||
|
|
||||||
|
ushort type = (ushort)(data[0] | (data[1] << 8));
|
||||||
|
ushort size = (ushort)(data[2] | (data[3] << 8));
|
||||||
|
byte[] payload = new byte[data.Length - 4];
|
||||||
|
Buffer.BlockCopy(data, 4, payload, 0, payload.Length);
|
||||||
|
return (type, size, payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 페이로드 → 특정 타입으로 역직렬화
|
||||||
|
public static T DeserializePayload<T>(byte[] payload)
|
||||||
|
{
|
||||||
|
MemoryStream ms = new MemoryStream(payload);
|
||||||
|
return Serializer.Deserialize<T>(ms);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
11
MMOTestServer/ServerLib/Packet/PacketType.cs
Normal file
11
MMOTestServer/ServerLib/Packet/PacketType.cs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
namespace ServerLib.Packet;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 패킷 타입 식별자 (2바이트 헤더)
|
||||||
|
/// Auth는 베이스에서 처리, 게임 패킷은 하위 프로젝트에서 추가 정의
|
||||||
|
/// </summary>
|
||||||
|
public enum PacketType : ushort
|
||||||
|
{
|
||||||
|
Auth = 1, // 클라 → 서버: 최초 인증 (HashKey 전달)
|
||||||
|
// 1000번 이상은 게임 패킷으로 예약
|
||||||
|
}
|
||||||
17
MMOTestServer/ServerLib/ServerLib.csproj
Normal file
17
MMOTestServer/ServerLib/ServerLib.csproj
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="LiteNetLib" Version="2.0.2" />
|
||||||
|
<PackageReference Include="protobuf-net" Version="3.2.56" />
|
||||||
|
<PackageReference Include="Serilog" Version="4.3.1" />
|
||||||
|
<PackageReference Include="Serilog.Sinks.Console" Version="6.1.1" />
|
||||||
|
<PackageReference Include="Serilog.Sinks.File" Version="7.0.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
239
MMOTestServer/ServerLib/Service/ServerBase.cs
Normal file
239
MMOTestServer/ServerLib/Service/ServerBase.cs
Normal file
@@ -0,0 +1,239 @@
|
|||||||
|
using System.Net;
|
||||||
|
using System.Net.Sockets;
|
||||||
|
using LiteNetLib;
|
||||||
|
using LiteNetLib.Utils;
|
||||||
|
using Serilog;
|
||||||
|
using ServerLib.Packet;
|
||||||
|
|
||||||
|
namespace ServerLib.Service;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 네트워킹 추상 베이스 (protobuf 없음)
|
||||||
|
///
|
||||||
|
/// 흐름:
|
||||||
|
/// OnPeerConnected → 대기 목록 등록
|
||||||
|
/// OnNetworkReceive → Auth 패킷(type=1)이면 HashKey(8byte long) 읽어 인증
|
||||||
|
/// → 이미 같은 HashKey 세션 있으면 이전 피어 끊고 재연결 (WiFi→LTE)
|
||||||
|
/// → 그 외 패킷은 HandlePacket() 으로 전달
|
||||||
|
/// OnPeerDisconnected → 세션/대기 목록에서 제거
|
||||||
|
///
|
||||||
|
/// 서브클래스 구현:
|
||||||
|
/// OnSessionConnected - 인증 완료 시
|
||||||
|
/// OnSessionDisconnected - 세션 정상 해제 시 (재연결 교체는 호출 안 함)
|
||||||
|
/// HandlePacket - 인증된 피어의 게임 패킷 처리
|
||||||
|
/// </summary>
|
||||||
|
public abstract class ServerBase : INetEventListener
|
||||||
|
{
|
||||||
|
protected NetManager netManager = null!;
|
||||||
|
|
||||||
|
// 인증 전 대기 피어 (peer.Id → NetPeer)
|
||||||
|
private readonly Dictionary<int, NetPeer> pendingPeers = new();
|
||||||
|
|
||||||
|
// 인증된 세션 (hashKey → NetPeer) 재연결 조회용
|
||||||
|
// peer → hashKey 역방향은 peer.Tag as Session 으로 대체
|
||||||
|
private readonly Dictionary<long, NetPeer> sessions = new();
|
||||||
|
|
||||||
|
// 핑 로그 출력 여부
|
||||||
|
public bool PingLogRtt { get; set; }
|
||||||
|
|
||||||
|
public int Port { get; }
|
||||||
|
public string ConnectionString { get; }
|
||||||
|
|
||||||
|
private bool isListening = false;
|
||||||
|
|
||||||
|
public ServerBase(int port, string connectionString)
|
||||||
|
{
|
||||||
|
Port = port;
|
||||||
|
ConnectionString = connectionString;
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual void Init(int pingInterval = 3000, int disconnectTimeout = 60000)
|
||||||
|
{
|
||||||
|
netManager = new NetManager(this)
|
||||||
|
{
|
||||||
|
AutoRecycle = true,
|
||||||
|
PingInterval = pingInterval,
|
||||||
|
DisconnectTimeout = disconnectTimeout,
|
||||||
|
};
|
||||||
|
isListening = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual void Run()
|
||||||
|
{
|
||||||
|
netManager.Start(Port);
|
||||||
|
Log.Information("[Server] 시작 Port={Port}", Port);
|
||||||
|
|
||||||
|
while (isListening)
|
||||||
|
{
|
||||||
|
netManager.PollEvents();
|
||||||
|
Thread.Sleep(15);
|
||||||
|
}
|
||||||
|
netManager.Stop();
|
||||||
|
Log.Information("[Server] 종료 Port={Port}", Port);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Stop()
|
||||||
|
{
|
||||||
|
isListening = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 클라이언트 연결 요청 수신 → Accept / Reject 결정
|
||||||
|
public void OnConnectionRequest(ConnectionRequest request)
|
||||||
|
{
|
||||||
|
// 벤 기능 추가? 한국 ip만?
|
||||||
|
if (request.AcceptIfKey(ConnectionString) == null)
|
||||||
|
{
|
||||||
|
Log.Debug("해당 클라이언트의 ConnectionKey={request.ConnectionKey}가 동일하지 않습니다", request.Data.ToString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 클라이언트가 연결 완료됐을 때 호출
|
||||||
|
public void OnPeerConnected(NetPeer peer)
|
||||||
|
{
|
||||||
|
pendingPeers[peer.Id] = peer;
|
||||||
|
Log.Debug("[Server] 대기 등록 PeerId={Id} IP={IP}", peer.Id, peer.Address);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 클라이언트가 연결 해제됐을 때 (타임아웃, 명시적 끊기 등)
|
||||||
|
public void OnPeerDisconnected(NetPeer peer, DisconnectInfo disconnectInfo)
|
||||||
|
{
|
||||||
|
pendingPeers.Remove(peer.Id);
|
||||||
|
|
||||||
|
if (peer.Tag is Session session)
|
||||||
|
{
|
||||||
|
// 현재 인증된 피어가 이 peer일 때만 세션 제거
|
||||||
|
// (재연결로 이미 교체된 경우엔 건드리지 않음)
|
||||||
|
if (sessions.TryGetValue(session.HashKey, out NetPeer? current) && current.Id == peer.Id)
|
||||||
|
{
|
||||||
|
sessions.Remove(session.HashKey);
|
||||||
|
Log.Information("[Server] 세션 해제 HashKey={Key} Reason={Reason}", session.HashKey, disconnectInfo.Reason);
|
||||||
|
OnSessionDisconnected(peer, session.HashKey, disconnectInfo);
|
||||||
|
}
|
||||||
|
peer.Tag = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 연결된 피어로부터 데이터 수신 시 핵심 콜백
|
||||||
|
public void OnNetworkReceive(NetPeer peer, NetPacketReader reader, byte channelNumber, DeliveryMethod deliveryMethod)
|
||||||
|
{
|
||||||
|
byte[] data = reader.GetRemainingBytes();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
(ushort type, ushort size, byte[] payload) = PacketSerializer.Deserialize(data);
|
||||||
|
|
||||||
|
// Auth 패킷은 베이스에서 처리 (raw 8-byte long, protobuf 불필요)
|
||||||
|
if (type == (ushort)PacketType.Auth)
|
||||||
|
{
|
||||||
|
HandleAuth(peer, payload);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 인증된 피어인지 확인
|
||||||
|
if (peer.Tag is not Session session)
|
||||||
|
{
|
||||||
|
// 추가로 벤 때려도 될듯
|
||||||
|
Log.Warning("[Server] 미인증 패킷 무시 PeerId={Id} Type={Type}", peer.Id, type);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
HandlePacket(peer, session.HashKey, type, payload);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Log.Error(ex, "[Server] 패킷 처리 오류 PeerId={Id}", peer.Id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 소켓 레벨 오류 발생 시
|
||||||
|
public void OnNetworkError(IPEndPoint endPoint, SocketError socketError)
|
||||||
|
{
|
||||||
|
Log.Error("[Server] 네트워크 오류 {EP} {Err}", endPoint, socketError);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 미연결 상태의 UDP 메시지 수신 (LAN 탐색, 브로드캐스트 등)
|
||||||
|
public void OnNetworkReceiveUnconnected(IPEndPoint endPoint, NetPacketReader reader, UnconnectedMessageType messageType)
|
||||||
|
{
|
||||||
|
// 혹시나 외부에서 이벤트 발생관련 수신이라면 여기에 구현? 경험치 배율 이런거
|
||||||
|
Log.Warning("[Server] 미연결 패킷 수신 {EP} 무시", endPoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 핑 갱신 시 (ms)
|
||||||
|
public void OnNetworkLatencyUpdate(NetPeer peer, int latency)
|
||||||
|
{
|
||||||
|
if (PingLogRtt)
|
||||||
|
{
|
||||||
|
// rtt 시간 출력
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Auth 처리 (내부) ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private void HandleAuth(NetPeer peer, byte[] payload)
|
||||||
|
{
|
||||||
|
if (payload.Length < sizeof(long))
|
||||||
|
{
|
||||||
|
Log.Warning("[Server] Auth 페이로드 크기 오류 PeerId={Id}", peer.Id);
|
||||||
|
peer.Disconnect();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
long hashKey = BitConverter.ToInt64(payload, 0);
|
||||||
|
|
||||||
|
if (sessions.TryGetValue(hashKey, out NetPeer? existing))
|
||||||
|
{
|
||||||
|
// WiFi → LTE 전환 등 재연결: 이전 피어 교체
|
||||||
|
existing.Tag = null;
|
||||||
|
sessions.Remove(hashKey);
|
||||||
|
Log.Information("[Server] 재연결 HashKey={Key} Old={Old} New={New}", hashKey, existing.Id, peer.Id);
|
||||||
|
existing.Disconnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
peer.Tag = new Session(hashKey, peer);
|
||||||
|
sessions[hashKey] = peer;
|
||||||
|
pendingPeers.Remove(peer.Id);
|
||||||
|
|
||||||
|
Log.Information("[Server] 인증 완료 HashKey={Key} PeerId={Id}", hashKey, peer.Id);
|
||||||
|
OnSessionConnected(peer, hashKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── 전송 헬퍼 ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
// NetDataWriter writer 풀처리 필요할듯
|
||||||
|
// peer에게 전송
|
||||||
|
protected void SendTo(NetPeer peer, byte[] data, DeliveryMethod method = DeliveryMethod.ReliableOrdered)
|
||||||
|
{
|
||||||
|
NetDataWriter writer = new NetDataWriter();
|
||||||
|
writer.Put(data);
|
||||||
|
peer.Send(writer, method);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 모두에게 전송
|
||||||
|
protected void Broadcast(byte[] data, DeliveryMethod method = DeliveryMethod.ReliableOrdered)
|
||||||
|
{
|
||||||
|
// 일단 channelNumber는 건드리지 않는다
|
||||||
|
NetDataWriter writer = new NetDataWriter();
|
||||||
|
writer.Put(data);
|
||||||
|
netManager.SendToAll(writer, 0, method);
|
||||||
|
}
|
||||||
|
|
||||||
|
// exclude 1개 제외 / 나 제외 정도
|
||||||
|
protected void BroadcastExcept(byte[] data, NetPeer exclude, DeliveryMethod method = DeliveryMethod.ReliableOrdered)
|
||||||
|
{
|
||||||
|
// 일단 channelNumber는 건드리지 않는다
|
||||||
|
NetDataWriter writer = new NetDataWriter();
|
||||||
|
writer.Put(data);
|
||||||
|
netManager.SendToAll(writer, 0, method, exclude);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── 서브클래스 구현 ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
// 인증(Auth) 완료 후 호출
|
||||||
|
protected abstract void OnSessionConnected(NetPeer peer, long hashKey);
|
||||||
|
|
||||||
|
// 세션 정상 해제 시 호출 (재연결 교체 시에는 호출되지 않음)
|
||||||
|
protected abstract void OnSessionDisconnected(NetPeer peer, long hashKey, DisconnectInfo info);
|
||||||
|
|
||||||
|
// 인증된 피어의 게임 패킷 수신 / payload는 헤더 제거된 raw bytes → 실행 프로젝트에서 protobuf 역직렬화
|
||||||
|
protected abstract void HandlePacket(NetPeer peer, long hashKey, ushort type, byte[] payload);
|
||||||
|
}
|
||||||
15
MMOTestServer/ServerLib/Service/Session.cs
Normal file
15
MMOTestServer/ServerLib/Service/Session.cs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
using LiteNetLib;
|
||||||
|
|
||||||
|
namespace ServerLib.Service;
|
||||||
|
|
||||||
|
public class Session
|
||||||
|
{
|
||||||
|
public long HashKey { get; init; }
|
||||||
|
public NetPeer Peer { get; set; }
|
||||||
|
|
||||||
|
public Session(long hashKey, NetPeer peer)
|
||||||
|
{
|
||||||
|
HashKey = hashKey;
|
||||||
|
Peer = peer;
|
||||||
|
}
|
||||||
|
}
|
||||||
24
MMOTestServer/ServerLib/Service/SessionManager.cs
Normal file
24
MMOTestServer/ServerLib/Service/SessionManager.cs
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
namespace ServerLib.Service;
|
||||||
|
|
||||||
|
public class SessionManager
|
||||||
|
{
|
||||||
|
public Dictionary<int, Session> Sessions
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SessionManager()
|
||||||
|
{
|
||||||
|
Sessions = new Dictionary<int, Session>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddSession(Session session)
|
||||||
|
{
|
||||||
|
Sessions.Add(session.GetHashCode(), session);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RemoveSession(Session session)
|
||||||
|
{
|
||||||
|
Sessions.Remove(session.GetHashCode());
|
||||||
|
}
|
||||||
|
}
|
||||||
9
MMOTestServer/compose.yaml
Normal file
9
MMOTestServer/compose.yaml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
services:
|
||||||
|
mmoserver:
|
||||||
|
image: mmoserver
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: MMOserver/Dockerfile
|
||||||
|
ports:
|
||||||
|
- "9050:9050/udp" # LiteNetLib UDP 포트
|
||||||
|
restart: unless-stopped
|
||||||
7
MMOTestServer/global.json
Normal file
7
MMOTestServer/global.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"sdk": {
|
||||||
|
"version": "9.0.0",
|
||||||
|
"rollForward": "latestMinor",
|
||||||
|
"allowPrerelease": false
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user