diff --git a/CMakeLists.txt b/CMakeLists.txt index 39a12bb..26f9ed8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -9,17 +9,10 @@ add_compile_options(-Wall -Wextra -Wpedantic) # Boost 및 의존성 찾기 find_package(Boost REQUIRED COMPONENTS system) find_package(OpenSSL REQUIRED) -find_package(Protobuf REQUIRED) # 컴파일 명령 추출 활성화 (LSP/IDE 용) set(CMAKE_EXPORT_COMPILE_COMMANDS ON) -# proto 컴파일 -file(GLOB PROTO_FILES "${CMAKE_CURRENT_SOURCE_DIR}/proto/*.proto") -protobuf_generate_cpp(PROTO_SRCS PROTO_HDRS ${PROTO_FILES}) - -add_custom_target(generate_proto DEPENDS ${PROTO_SRCS} ${PROTO_HDRS}) - add_executable(Server server/main.cpp server/NetworkService.cpp @@ -27,14 +20,14 @@ add_executable(Server server/PacketHandler.cpp server/SessionManager.cpp server/DatabaseManager.cpp - ${PROTO_SRCS} ${PROTO_HDRS} ) -target_include_directories(Server PRIVATE include ${CMAKE_CURRENT_BINARY_DIR}) -target_link_libraries(Server PRIVATE - Boost::system - OpenSSL::SSL - OpenSSL::Crypto - protobuf::libprotobuf -) +target_include_directories(Server PRIVATE include) +target_link_libraries(Server PRIVATE Boost::system OpenSSL::SSL OpenSSL::Crypto) +add_executable(Client + client/Client.cpp + client/main.cpp +) +target_include_directories(Client PRIVATE include) +target_link_libraries(Client PRIVATE Boost::system OpenSSL::SSL OpenSSL::Crypto) diff --git a/Dockerfile b/Dockerfile index 9146547..4a19a78 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,8 +5,7 @@ RUN apt-get update && apt-get install -y \ make \ g++ \ libboost-all-dev \ - libssl-dev \ - protobuf-compiler + libssl-dev COPY . /app WORKDIR /app diff --git a/README.md b/README.md index 7c07b60..31be888 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,9 @@ cmake --build build # 서버 실행 ./build/Server + +# 클라이언트 실행 +./build/Client ``` ## 아키텍처 구성 @@ -47,13 +50,9 @@ cmake --build build - PacketHandler: 패킷 ID별 로직 디스패칭 - SessionManager: 글로벌 세션 관리 및 브로드캐스팅 - DatabaseManager: 비동기 데이터베이스 작업 및 락 제어 -- Packet: Protobuf 기반 직렬화 프로토콜 (proto/Protocol.proto) - -## Python GUI 클라이언트 - -[README.md](https://git.bumpsoo.dev/bumpsoo/sword_game/src/branch/main/client/README.md) +- Packet: 바이너리 프로토콜 정의 (pragma pack(1)) ## TODO: 개선 사항 - 현재 서버 메모리에만 유지되는 검 강화 상태를 DB에 저장하여, 재접속 시에도 이전 상태가 그대로 복구되도록 구현. -- DB Connection Pool 도입 -- 랭킹 view 기능 추가 \ No newline at end of file +- Connection Pool 도입 +- protocol 변경 후 다른 언어로 구현된 클라이언트 대응 diff --git a/client/.gitignore b/client/.gitignore deleted file mode 100644 index ff5a986..0000000 --- a/client/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -Protocol_pb2.py -venv -__pycache__ \ No newline at end of file diff --git a/client/Client.cpp b/client/Client.cpp new file mode 100644 index 0000000..3c7f413 --- /dev/null +++ b/client/Client.cpp @@ -0,0 +1,233 @@ +#include "Client.h" +#include +#include + +BaseClient::BaseClient(boost::asio::io_context &io_context, + const std::string &host, const std::string &port) + : socket_(io_context) { + tcp::resolver resolver(io_context); + boost::asio::connect(socket_, resolver.resolve(host, port)); +} + +void BaseClient::SendPacket(PacketID id, const void *payload, + size_t payloadSize) { + PacketHeader header; + header.id = static_cast(id); + header.size = static_cast(payloadSize); + + std::vector buffer(sizeof(PacketHeader) + payloadSize); + std::memcpy(buffer.data(), &header, sizeof(PacketHeader)); + if (payload && payloadSize > 0) { + std::memcpy(buffer.data() + sizeof(PacketHeader), payload, payloadSize); + } + + boost::asio::write(socket_, boost::asio::buffer(buffer)); +} + +bool BaseClient::ReceiveHeader(PacketHeader &header) { + try { + boost::asio::read(socket_, boost::asio::buffer(&header, sizeof(header))); + return true; + } catch (...) { + return false; + } +} + +bool BaseClient::Login(const std::string &nickname) { + PKT_CS_Login loginPkt; + std::memset(loginPkt.nickname, 0, sizeof(loginPkt.nickname)); + std::strncpy(loginPkt.nickname, nickname.c_str(), + sizeof(loginPkt.nickname) - 1); + SendPacket(PacketID::CS_Login, &loginPkt, sizeof(loginPkt)); + std::cout << "로그인 요청 보냄: " << nickname << std::endl; + + // 로그인 결과 대기 + PacketHeader header; + if (!ReceiveHeader(header) || + header.id != static_cast(PacketID::SC_LoginResult)) { + std::cerr << "로그인 응답을 받지 못했습니다." << std::endl; + return false; + } + + PKT_SC_LoginResult res; + if (!ReceivePayload(res)) { + std::cerr << "로그인 응답 수신 실패" << std::endl; + return false; + } + + if (res.result == 1) { + std::cout << "로그인 성공!" << std::endl; + return true; + } else if (res.result == 0) { + std::cerr << "로그인 실패: 이미 접속 중인 유저입니다." << std::endl; + return false; + } else { + std::cerr << "로그인 실패: 알 수 없는 오류" << std::endl; + return false; + } +} + +void BaseClient::StartReceive() { + receiveThread_ = std::jthread(&BaseClient::ReceiveLoop, this); +} + +void BaseClient::StopReceive() { + boost::system::error_code ec; + socket_.shutdown(tcp::socket::shutdown_both, ec); + socket_.close(ec); + receiveThread_.request_stop(); +} + +void BaseClient::ReceiveLoop(std::stop_token stopToken) { + try { + while (!stopToken.stop_requested()) { + PacketHeader header; + if (!ReceiveHeader(header)) + break; + HandlePacket(header); + } + } catch (...) { + } +} + +void BaseClient::HandlePacket(const PacketHeader &header) { + PacketID id = static_cast(header.id); + switch (id) { + case PacketID::Chat: { + std::vector buffer(header.size + 1, 0); + boost::asio::read(socket_, boost::asio::buffer(buffer.data(), header.size)); + std::cout << "\n" << buffer.data() << std::endl; + } break; + case PacketID::SC_UpgradeResult: + HandleUpgradeResult(); + break; + case PacketID::SC_SellResult: + HandleSellResult(); + break; + default: + // 처리하지 않는 패킷은 페이로드만 건너뜀 + if (header.size > 0) { + std::vector dummy(header.size); + boost::asio::read(socket_, + boost::asio::buffer(dummy.data(), header.size)); + } + break; + } +} + +void BaseClient::HandleUpgradeResult() { + PKT_SC_UpgradeResult res; + if (ReceivePayload(res)) { + currentLevel_ = res.currentLevel; + currentGold_ = res.currentGold; + // 강화 결과 출력은 채팅으로 + } +} + +void BaseClient::HandleSellResult() { + PKT_SC_SellResult res; + if (ReceivePayload(res)) { + currentLevel_ = 0; + currentGold_ = res.totalGold; + // 판매 결과는 본인에게만 + std::cout << "\n>> 판매 완료: " << res.earnedGold << " 골드 획득!" + << std::endl; + } +} + +void InteractiveClient::Run() { + std::cout << "닉네임을 입력하세요: "; + std::cin >> nickname_; + if (!Login(nickname_)) { + std::cerr << "로그인에 실패하여 종료합니다." << std::endl; + return; + } + + // 로그인 후 비동기 수신 시작 + StartReceive(); + + while (true) { + if (currentLevel_ == 0) { + std::cout << "\n[현재 검 없음, 골드: " << currentGold_ + << "] 1. 검 구매 시도 (1000골드), 2. 종료\n입력: "; + } else { + std::cout << "\n[현재 검 레벨: " << currentLevel_ + << ", 골드: " << currentGold_ + << "] 1. 강화 시도, 2. 판매, 3. 종료\n입력: "; + } + + int choice; + if (!(std::cin >> choice)) + break; + + if (currentLevel_ == 0) { + if (choice == 1) { + SendPacket(PacketID::CS_UpgradeSword); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + } else if (choice == 2) { + break; + } + } else { + if (choice == 1) { + SendPacket(PacketID::CS_UpgradeSword); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + } else if (choice == 2) { + SendPacket(PacketID::CS_SellSword); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + } else if (choice == 3) { + break; + } + } + } + + StopReceive(); +} + +#include + +void ScenarioClient::Run() { + std::ifstream is(scenarioFile_); + if (!is.is_open()) { + std::cerr << "시나리오 파일을 열 수 없습니다: " << scenarioFile_ + << std::endl; + return; + } + + if (!(is >> nickname_)) + return; + if (!Login(nickname_)) { + std::cerr << "로그인에 실패하여 종료합니다." << std::endl; + return; + } + + // 비동기 수신 시작 + StartReceive(); + + std::string line; + while (is >> line) { + int choice = std::stoi(line); + std::cout << "\n시나리오 입력 실행: " << choice << std::endl; + + if (currentLevel_ == 0) { + if (choice == 1) { + SendPacket(PacketID::CS_UpgradeSword); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + } else if (choice == 2) { + break; + } + } else { + if (choice == 1) { + SendPacket(PacketID::CS_UpgradeSword); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + } else if (choice == 2) { + SendPacket(PacketID::CS_SellSword); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + } else if (choice == 3) { + break; + } + } + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + } + + StopReceive(); +} diff --git a/client/README.md b/client/README.md deleted file mode 100644 index 02253ea..0000000 --- a/client/README.md +++ /dev/null @@ -1,19 +0,0 @@ -## c++ cli 클라이언트 대신하는 python gui client - -- tkinter 사용 -- protocol buffer 사용 - -## protocol buffer 빌드 - -```bash -protoc --python_out=. Protocol.proto -I ../proto -``` - -## python 설치 및 실행 - -```bash -python3 -m venv venv -source venv/bin/activate -pip install -r requirements.txt -python main.py -``` \ No newline at end of file diff --git a/client/main.cpp b/client/main.cpp new file mode 100644 index 0000000..170acb8 --- /dev/null +++ b/client/main.cpp @@ -0,0 +1,44 @@ +#include "Client.h" +#include + +int main(int argc, char *argv[]) { + bool isScenario = false; + std::string nickname; + std::string scenarioFile; + + for (int i = 1; i < argc; ++i) { + std::string arg = argv[i]; + if (arg == "--scenario" && i + 1 < argc) { + isScenario = true; + scenarioFile = argv[++i]; + } else { + nickname = arg; + } + } + + try { + boost::asio::io_context io_context; + const char *env_host = std::getenv("SERVER_HOST"); + const char *env_port = std::getenv("SERVER_PORT"); + std::string host = env_host ? env_host : "127.0.0.1"; + std::string port = env_port ? env_port : "30000"; + + if (isScenario) { + if (scenarioFile.empty()) { + std::cerr + << "시나리오 파일명이 필요합니다. 사용법: --scenario <파일경로>" + << std::endl; + return 1; + } + ScenarioClient client(io_context, host, port, scenarioFile); + client.Run(); + } else { + InteractiveClient client(io_context, host, port); + client.Run(); + } + } catch (std::exception &e) { + std::cerr << "Exception: " << e.what() << "\n"; + } + + return 0; +} diff --git a/client/main.py b/client/main.py deleted file mode 100644 index 10df3b2..0000000 --- a/client/main.py +++ /dev/null @@ -1,196 +0,0 @@ -import socket -import threading -import struct -import customtkinter as ctk -from enum import IntEnum -import Protocol_pb2 as Protocol - -class PacketID(IntEnum): - Ping = 1 - CS_Login = 10 - SC_LoginResult = 11 - Chat = 20 - CS_UpgradeSword = 30 - SC_UpgradeResult = 31 - CS_SellSword = 35 - SC_SellResult = 36 - CS_RankingRequest = 40 - SC_RankingList = 41 - -class SwordGameClient(ctk.CTk): - def __init__(self): - super().__init__() - - self.title("C++ Sword Game - Python Client") - self.geometry("1200x800") - ctk.set_appearance_mode("dark") - - # 네트워크 관련 - self.socket = None - self.connected = False - self.nickname = "" - - # 게임 정보 - self.current_level = 0 - self.current_gold = 0 - - # UI 초기화 - self.setup_login_ui() - - def setup_login_ui(self): - self.login_frame = ctk.CTkFrame(self) - self.login_frame.pack(expand=True) - - self.label = ctk.CTkLabel(self.login_frame, text="검 키우기 온라인", font=("Arial", 24, "bold")) - self.label.pack(pady=20) - - self.nickname_entry = ctk.CTkEntry(self.login_frame, placeholder_text="닉네임 입력") - self.nickname_entry.pack(pady=10) - - self.login_button = ctk.CTkButton(self.login_frame, text="접속", command=self.connect_to_server) - self.login_button.pack(pady=20) - - def setup_game_ui(self): - for widget in self.winfo_children(): - widget.destroy() - - # 메인 레이아웃 - self.grid_columnconfigure(0, weight=1) - self.grid_columnconfigure(1, weight=2) - self.grid_rowconfigure(0, weight=1) - - # 왼쪽: 유저 정보 창 - self.info_frame = ctk.CTkFrame(self) - self.info_frame.grid(row=0, column=0, padx=20, pady=20, sticky="nsew") - - self.user_label = ctk.CTkLabel(self.info_frame, text=f"플레이어: {self.nickname}", font=("Arial", 16)) - self.user_label.pack(pady=10) - - self.gold_label = ctk.CTkLabel(self.info_frame, text="골드: 0", font=("Arial", 18, "bold"), text_color="#FFD700") - self.gold_label.pack(pady=5) - - self.level_label = ctk.CTkLabel(self.info_frame, text="검 레벨: 0", font=("Arial", 20, "bold")) - self.level_label.pack(pady=10) - - self.upgrade_btn = ctk.CTkButton(self.info_frame, text="강화 시도", fg_color="#28a745", hover_color="#218838", command=self.send_upgrade) - self.upgrade_btn.pack(pady=10, fill="x", padx=20) - - self.sell_btn = ctk.CTkButton(self.info_frame, text="판매 하기", fg_color="#dc3545", hover_color="#c82333", command=self.send_sell) - self.sell_btn.pack(pady=10, fill="x", padx=20) - - # 오른쪽: 채팅 창 - self.chat_frame = ctk.CTkFrame(self) - self.chat_frame.grid(row=0, column=1, padx=20, pady=20, sticky="nsew") - - self.chat_box = ctk.CTkTextbox(self.chat_frame, state="disabled") - self.chat_box.pack(expand=True, fill="both", padx=10, pady=10) - - self.chat_entry = ctk.CTkEntry(self.chat_frame, placeholder_text="메시지 입력...") - self.chat_entry.pack(fill="x", padx=10, pady=(0, 10)) - self.chat_entry.bind("", lambda e: self.send_chat()) - - # TODO: 채팅창 오른 쪽에 랭킹? - - def connect_to_server(self): - self.nickname = self.nickname_entry.get() - if not self.nickname: - return - - try: - self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - self.socket.connect(("127.0.0.1", 30000)) - self.connected = True - - threading.Thread(target=self.receive_loop, daemon=True).start() - - pkt = Protocol.CS_Login() - pkt.nickname = self.nickname - self.send_packet(PacketID.CS_Login, pkt) - - except Exception as e: - print(f"Connection failed: {e}") - self.nickname_entry.configure(placeholder_text="연결 실패!") - - def send_packet(self, packet_id, protobuf_obj=None): - if not self.socket: return - - payload = b"" - if protobuf_obj: - payload = protobuf_obj.SerializeToString() - - # Header: size (2 bytes) + id (2 bytes), Little Endian - header = struct.pack(' 0: - payload = self.socket.recv(size) - - self.handle_packet(pkt_id, payload) - except Exception as e: - print(f"Receive error: {e}") - finally: - self.connected = False - - def handle_packet(self, pkt_id, payload): - if pkt_id == PacketID.SC_LoginResult: - res = Protocol.SC_LoginResult() - res.ParseFromString(payload) - if res.success: - self.after(0, self.setup_game_ui) - else: - print("Login Failed") - - elif pkt_id == PacketID.SC_UpgradeResult: - res = Protocol.SC_UpgradeResult() - res.ParseFromString(payload) - self.current_level = res.current_level - self.current_gold = res.current_gold - self.after(0, self.update_stats) - - elif pkt_id == PacketID.SC_SellResult: - res = Protocol.SC_SellResult() - res.ParseFromString(payload) - self.current_level = 0 - self.current_gold = res.total_gold - self.after(0, self.update_stats) - self.after(0, lambda: self.log_chat(f"[시스템] 검 판매 완료! {res.earned_gold} 골드 획득.")) - - elif pkt_id == PacketID.Chat: - msg = payload.decode('utf-8') - self.after(0, lambda: self.log_chat(msg)) - - def update_stats(self): - self.level_label.configure(text=f"검 레벨: {self.current_level}") - self.gold_label.configure(text=f"골드: {self.current_gold:,}") - - def send_upgrade(self): - self.send_packet(PacketID.CS_UpgradeSword) - - def send_sell(self): - self.send_packet(PacketID.CS_SellSword) - - def send_chat(self): - msg = self.chat_entry.get() - if msg: - header = struct.pack(' #include #include #include -namespace google::protobuf { -class Message; -} - using boost::asio::ip::tcp; class BaseClient { @@ -18,10 +15,20 @@ public: virtual ~BaseClient() = default; - void SendPacket(PacketID id, const google::protobuf::Message *pkt = nullptr); + void SendPacket(PacketID id, const void *payload = nullptr, + size_t payloadSize = 0); bool ReceiveHeader(PacketHeader &header); + template bool ReceivePayload(T &payload) { + try { + boost::asio::read(socket_, boost::asio::buffer(&payload, sizeof(T))); + return true; + } catch (...) { + return false; + } + } + bool Login(const std::string &nickname); // 비동기 수신 시작 @@ -41,8 +48,8 @@ protected: void ReceiveLoop(std::stop_token stopToken); void HandlePacket(const PacketHeader &header); - void HandleUpgradeResult(const PacketHeader &header); - void HandleSellResult(const PacketHeader &header); + void HandleUpgradeResult(); + void HandleSellResult(); }; class InteractiveClient : public BaseClient { diff --git a/include/Packet.h b/include/Packet.h index a83127b..d73e4dd 100644 --- a/include/Packet.h +++ b/include/Packet.h @@ -46,3 +46,45 @@ namespace GameConfig { const uint32_t MAX_SWORD_LEVEL = 20; const uint64_t INITIAL_GOLD = 10000; } // namespace GameConfig + +// 고정 크기 데이터 전송을 위한 구조체 정의 +#pragma pack(push, 1) +struct PKT_CS_Login { + // 유저 닉네임 (최대 32자) + char nickname[32]; +}; + +struct PKT_SC_LoginResult { + // 0: 이미 접속 중, 1: 성공 + uint8_t result; +}; + +struct PKT_SC_UpgradeResult { + // 0: 파괴, 1: 성공, 2: 실패 + uint8_t result; + // 현재 강화 레벨 + uint32_t currentLevel; + // 현재 보유 골드 + uint64_t currentGold; +}; + +struct PKT_SC_SellResult { + // 판매 후 획득 골드 + uint64_t earnedGold; + // 현재 총 골드 + uint64_t totalGold; +}; + +struct RankingEntry { + // 유저 이름 (최대 32자) + char username[32]; + // 검 레벨 + uint32_t swordLevel; +}; + +struct PKT_SC_RankingList { + // 랭킹 리스트 개수 + uint32_t count; + // 이후 RankingEntry[count] 만큼 데이터가 따라옴 +}; +#pragma pack(pop) diff --git a/include/Session.h b/include/Session.h index 3d7e1fc..9a25f19 100644 --- a/include/Session.h +++ b/include/Session.h @@ -16,15 +16,14 @@ public: void Start(); void Send(std::span data); - template void SendPacket(PacketID id, const T &pkt) { - uint16_t size = static_cast(pkt.ByteSizeLong()); + template void SendPacket(PacketID id, const T &payload) { PacketHeader header; header.id = static_cast(id); - header.size = size; + header.size = sizeof(T); - std::vector buffer(sizeof(PacketHeader) + size); + std::vector buffer(sizeof(PacketHeader) + sizeof(T)); std::memcpy(buffer.data(), &header, sizeof(PacketHeader)); - pkt.SerializeToArray(buffer.data() + sizeof(PacketHeader), size); + std::memcpy(buffer.data() + sizeof(PacketHeader), &payload, sizeof(T)); Send(buffer); } diff --git a/proto/Protocol.proto b/proto/Protocol.proto deleted file mode 100644 index 08eaf1e..0000000 --- a/proto/Protocol.proto +++ /dev/null @@ -1,31 +0,0 @@ -syntax = "proto3"; - -package Protocol; - -message CS_Login { - string nickname = 1; -} - -message SC_LoginResult { - bool success = 1; -} - -message SC_UpgradeResult { - uint32 result = 1; - uint32 current_level = 2; - uint64 current_gold = 3; -} - -message SC_SellResult { - uint64 earned_gold = 1; - uint64 total_gold = 2; -} - -message RankingEntry { - string nickname = 1; - uint32 level = 2; -} - -message SC_RankingList { - repeated RankingEntry entries = 1; -} diff --git a/server/PacketHandler.cpp b/server/PacketHandler.cpp index 291d020..c294e33 100644 --- a/server/PacketHandler.cpp +++ b/server/PacketHandler.cpp @@ -1,7 +1,6 @@ #include "PacketHandler.h" #include "DatabaseManager.h" #include "Logger.h" -#include "Protocol.pb.h" #include "SessionManager.h" #include "SwordLogic.h" #include @@ -18,25 +17,27 @@ PacketHandler::HandlePacket(std::shared_ptr session, } break; case PacketID::CS_Login: { - Protocol::CS_Login pkt; - if (!pkt.ParseFromArray(packet.payload.data(), packet.payload.size())) { - Logger::Log("로그인 패킷 파싱 실패"); + if (packet.payload.size() < sizeof(PKT_CS_Login)) { + Logger::Log("로그인 패킷 크기가 올바르지 않습니다."); co_return; } - std::string nickname = pkt.nickname(); + const PKT_CS_Login *loginPkt = + reinterpret_cast(packet.payload.data()); + std::string nickname(loginPkt->nickname); + auto userData = co_await DatabaseManager::GetInstance().LoadUser(nickname); // 골드와 검 레벨 설정 (닉네임은 TryJoin에서 설정) session->SetGold(userData.gold); session->SetSwordLevel(userData.swordLevel); - Protocol::SC_LoginResult loginResult; + PKT_SC_LoginResult loginResult; // 중복 체크 if (!SessionManager::GetInstance().TryJoin(session, userData.nickname)) { Logger::Log("중복 로그인 거부: ", nickname); - loginResult.set_success(false); + loginResult.result = 0; session->SendPacket(PacketID::SC_LoginResult, loginResult); co_return; } @@ -45,7 +46,7 @@ PacketHandler::HandlePacket(std::shared_ptr session, " (Gold: ", session->GetGold(), ", Level: ", session->GetSwordLevel(), ")"); - loginResult.set_success(true); + loginResult.result = 1; session->SendPacket(PacketID::SC_LoginResult, loginResult); } break; @@ -54,14 +55,14 @@ PacketHandler::HandlePacket(std::shared_ptr session, uint64_t cost = SwordLogic::GetUpgradeCost(currentLevel); uint64_t currentGold = session->GetGold(); - Protocol::SC_UpgradeResult res; + PKT_SC_UpgradeResult res; if (currentGold < cost) { // 골드 부족 - res.set_result(2); + res.result = 2; } else { session->SetGold(currentGold - cost); uint8_t result = SwordLogic::TryUpgrade(currentLevel); - res.set_result(result); + res.result = result; // 성공 if (result == 1) { @@ -75,8 +76,8 @@ PacketHandler::HandlePacket(std::shared_ptr session, session->GetNickname(), session->GetGold(), session->GetSwordLevel()); } - res.set_current_level(session->GetSwordLevel()); - res.set_current_gold(session->GetGold()); + res.currentLevel = session->GetSwordLevel(); + res.currentGold = session->GetGold(); // 결과 패킷 전송 session->SendPacket(PacketID::SC_UpgradeResult, res); @@ -85,9 +86,9 @@ PacketHandler::HandlePacket(std::shared_ptr session, if (currentGold >= cost) { uint32_t attemptedLevel = currentLevel + 1; std::string resultText; - if (res.result() == 1) { + if (res.result == 1) { resultText = "를 달성했습니다!"; - } else if (res.result() == 0) { + } else if (res.result == 0) { resultText = " 시도 중 검이 파괴되었습니다!"; } else { resultText = " 달성에 실패했습니다!"; @@ -104,9 +105,8 @@ PacketHandler::HandlePacket(std::shared_ptr session, std::vector chatPayload(chatMsg.begin(), chatMsg.end()); SessionManager::GetInstance().Broadcast(chatHeader, chatPayload); - Logger::Log("강화 시도 [", session->GetNickname(), - "]: ", (int)res.result(), " (레벨: ", currentLevel, "->", - res.current_level(), ")"); + Logger::Log("강화 시도 [", session->GetNickname(), "]: ", (int)res.result, + " (레벨: ", currentLevel, "->", res.currentLevel, ")"); } } break; @@ -121,9 +121,9 @@ PacketHandler::HandlePacket(std::shared_ptr session, co_await DatabaseManager::GetInstance().SaveUser( session->GetNickname(), session->GetGold(), session->GetSwordLevel()); - Protocol::SC_SellResult res; - res.set_earned_gold(price); - res.set_total_gold(newGold); + PKT_SC_SellResult res; + res.earnedGold = price; + res.totalGold = newGold; session->SendPacket(PacketID::SC_SellResult, res); diff --git a/setup.sh b/setup.sh index 737e95d..60eaa56 100755 --- a/setup.sh +++ b/setup.sh @@ -2,15 +2,17 @@ set -e +echo "Starting project setup..." + if [ ! -d "build" ]; then mkdir build + echo "Created build directory." fi cd build cmake .. -DCMAKE_EXPORT_COMPILE_COMMANDS=ON -cmake --build . --target generate_proto +echo "CMake configuration complete." cd .. ln -sf build/compile_commands.json compile_commands.json -echo "완료" diff --git a/test_scenario.txt b/test_scenario.txt new file mode 100644 index 0000000..1291a8d --- /dev/null +++ b/test_scenario.txt @@ -0,0 +1,5 @@ +TestUser +1 +1 +2 +3