tmp
This commit is contained in:
parent
a88b22b177
commit
263bb3e3af
10 changed files with 374 additions and 117 deletions
|
|
@ -9,10 +9,15 @@ add_compile_options(-Wall -Wextra -Wpedantic)
|
||||||
# Boost 및 의존성 찾기
|
# Boost 및 의존성 찾기
|
||||||
find_package(Boost REQUIRED COMPONENTS system)
|
find_package(Boost REQUIRED COMPONENTS system)
|
||||||
find_package(OpenSSL REQUIRED)
|
find_package(OpenSSL REQUIRED)
|
||||||
|
find_package(Protobuf REQUIRED)
|
||||||
|
|
||||||
# 컴파일 명령 추출 활성화 (LSP/IDE 용)
|
# 컴파일 명령 추출 활성화 (LSP/IDE 용)
|
||||||
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
|
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
|
||||||
|
|
||||||
|
# proto 파일로부터 소스 코드(.pb.h, .pb.cc) 생성
|
||||||
|
file(GLOB PROTO_FILES "${CMAKE_CURRENT_SOURCE_DIR}/proto/*.proto")
|
||||||
|
protobuf_generate_cpp(PROTO_SRCS PROTO_HDRS ${PROTO_FILES})
|
||||||
|
|
||||||
add_executable(Server
|
add_executable(Server
|
||||||
server/main.cpp
|
server/main.cpp
|
||||||
server/NetworkService.cpp
|
server/NetworkService.cpp
|
||||||
|
|
@ -20,14 +25,26 @@ add_executable(Server
|
||||||
server/PacketHandler.cpp
|
server/PacketHandler.cpp
|
||||||
server/SessionManager.cpp
|
server/SessionManager.cpp
|
||||||
server/DatabaseManager.cpp
|
server/DatabaseManager.cpp
|
||||||
|
${PROTO_SRCS} ${PROTO_HDRS}
|
||||||
)
|
)
|
||||||
|
|
||||||
target_include_directories(Server PRIVATE include)
|
target_include_directories(Server PRIVATE include ${CMAKE_CURRENT_BINARY_DIR})
|
||||||
target_link_libraries(Server PRIVATE Boost::system OpenSSL::SSL OpenSSL::Crypto)
|
target_link_libraries(Server PRIVATE
|
||||||
|
Boost::system
|
||||||
|
OpenSSL::SSL
|
||||||
|
OpenSSL::Crypto
|
||||||
|
protobuf::libprotobuf
|
||||||
|
)
|
||||||
|
|
||||||
add_executable(Client
|
add_executable(Client
|
||||||
client/Client.cpp
|
client/Client.cpp
|
||||||
client/main.cpp
|
client/main.cpp
|
||||||
|
${PROTO_SRCS} ${PROTO_HDRS}
|
||||||
|
)
|
||||||
|
target_include_directories(Client PRIVATE include ${CMAKE_CURRENT_BINARY_DIR})
|
||||||
|
target_link_libraries(Client PRIVATE
|
||||||
|
Boost::system
|
||||||
|
OpenSSL::SSL
|
||||||
|
OpenSSL::Crypto
|
||||||
|
protobuf::libprotobuf
|
||||||
)
|
)
|
||||||
target_include_directories(Client PRIVATE include)
|
|
||||||
target_link_libraries(Client PRIVATE Boost::system OpenSSL::SSL OpenSSL::Crypto)
|
|
||||||
|
|
|
||||||
17
README.md
17
README.md
|
|
@ -50,9 +50,22 @@ cmake --build build
|
||||||
- PacketHandler: 패킷 ID별 로직 디스패칭
|
- PacketHandler: 패킷 ID별 로직 디스패칭
|
||||||
- SessionManager: 글로벌 세션 관리 및 브로드캐스팅
|
- SessionManager: 글로벌 세션 관리 및 브로드캐스팅
|
||||||
- DatabaseManager: 비동기 데이터베이스 작업 및 락 제어
|
- DatabaseManager: 비동기 데이터베이스 작업 및 락 제어
|
||||||
- Packet: 바이너리 프로토콜 정의 (pragma pack(1))
|
- Packet: Protobuf 기반 직렬화 프로토콜 (proto/Protocol.proto)
|
||||||
|
|
||||||
|
## Python GUI 클라이언트
|
||||||
|
`CustomTkinter`와 `Protobuf`를 사용한 현대적인 GUI 클라이언트입니다.
|
||||||
|
|
||||||
|
### 요구 사항
|
||||||
|
```bash
|
||||||
|
pip install customtkinter protobuf
|
||||||
|
```
|
||||||
|
|
||||||
|
### 실행 방법
|
||||||
|
```bash
|
||||||
|
python3 python_client/main.py
|
||||||
|
```
|
||||||
|
|
||||||
## TODO: 개선 사항
|
## TODO: 개선 사항
|
||||||
- 현재 서버 메모리에만 유지되는 검 강화 상태를 DB에 저장하여, 재접속 시에도 이전 상태가 그대로 복구되도록 구현.
|
- 현재 서버 메모리에만 유지되는 검 강화 상태를 DB에 저장하여, 재접속 시에도 이전 상태가 그대로 복구되도록 구현.
|
||||||
- Connection Pool 도입
|
- Connection Pool 도입
|
||||||
- protocol 변경 후 다른 언어로 구현된 클라이언트 대응
|
- 웹(WebSocket) 클라이언트 대응을 위한 Boost.Beast 적용
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
#include "Client.h"
|
#include "Client.h"
|
||||||
|
#include "Protocol.pb.h"
|
||||||
|
#include <google/protobuf/message.h>
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
#include <thread>
|
#include <thread>
|
||||||
|
|
||||||
|
|
@ -9,16 +11,20 @@ BaseClient::BaseClient(boost::asio::io_context &io_context,
|
||||||
boost::asio::connect(socket_, resolver.resolve(host, port));
|
boost::asio::connect(socket_, resolver.resolve(host, port));
|
||||||
}
|
}
|
||||||
|
|
||||||
void BaseClient::SendPacket(PacketID id, const void *payload,
|
void BaseClient::SendPacket(PacketID id, const google::protobuf::Message *pkt) {
|
||||||
size_t payloadSize) {
|
uint16_t payloadSize = 0;
|
||||||
|
if (pkt) {
|
||||||
|
payloadSize = static_cast<uint16_t>(pkt->ByteSizeLong());
|
||||||
|
}
|
||||||
|
|
||||||
PacketHeader header;
|
PacketHeader header;
|
||||||
header.id = static_cast<uint16_t>(id);
|
header.id = static_cast<uint16_t>(id);
|
||||||
header.size = static_cast<uint16_t>(payloadSize);
|
header.size = payloadSize;
|
||||||
|
|
||||||
std::vector<uint8_t> buffer(sizeof(PacketHeader) + payloadSize);
|
std::vector<uint8_t> buffer(sizeof(PacketHeader) + payloadSize);
|
||||||
std::memcpy(buffer.data(), &header, sizeof(PacketHeader));
|
std::memcpy(buffer.data(), &header, sizeof(PacketHeader));
|
||||||
if (payload && payloadSize > 0) {
|
if (pkt) {
|
||||||
std::memcpy(buffer.data() + sizeof(PacketHeader), payload, payloadSize);
|
pkt->SerializeToArray(buffer.data() + sizeof(PacketHeader), payloadSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
boost::asio::write(socket_, boost::asio::buffer(buffer));
|
boost::asio::write(socket_, boost::asio::buffer(buffer));
|
||||||
|
|
@ -34,11 +40,9 @@ bool BaseClient::ReceiveHeader(PacketHeader &header) {
|
||||||
}
|
}
|
||||||
|
|
||||||
bool BaseClient::Login(const std::string &nickname) {
|
bool BaseClient::Login(const std::string &nickname) {
|
||||||
PKT_CS_Login loginPkt;
|
Protocol::CS_Login loginPkt;
|
||||||
std::memset(loginPkt.nickname, 0, sizeof(loginPkt.nickname));
|
loginPkt.set_nickname(nickname);
|
||||||
std::strncpy(loginPkt.nickname, nickname.c_str(),
|
SendPacket(PacketID::CS_Login, &loginPkt);
|
||||||
sizeof(loginPkt.nickname) - 1);
|
|
||||||
SendPacket(PacketID::CS_Login, &loginPkt, sizeof(loginPkt));
|
|
||||||
std::cout << "로그인 요청 보냄: " << nickname << std::endl;
|
std::cout << "로그인 요청 보냄: " << nickname << std::endl;
|
||||||
|
|
||||||
// 로그인 결과 대기
|
// 로그인 결과 대기
|
||||||
|
|
@ -49,20 +53,20 @@ bool BaseClient::Login(const std::string &nickname) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
PKT_SC_LoginResult res;
|
Protocol::SC_LoginResult res;
|
||||||
if (!ReceivePayload(res)) {
|
std::vector<uint8_t> payload(header.size);
|
||||||
std::cerr << "로그인 응답 수신 실패" << std::endl;
|
boost::asio::read(socket_, boost::asio::buffer(payload.data(), header.size));
|
||||||
|
if (!res.ParseFromArray(payload.data(), header.size)) {
|
||||||
|
std::cerr << "로그인 응답 파싱 실패" << std::endl;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (res.result == 1) {
|
if (res.success()) {
|
||||||
std::cout << "로그인 성공!" << std::endl;
|
std::cout << "로그인 성공!" << std::endl;
|
||||||
return true;
|
return true;
|
||||||
} else if (res.result == 0) {
|
|
||||||
std::cerr << "로그인 실패: 이미 접속 중인 유저입니다." << std::endl;
|
|
||||||
return false;
|
|
||||||
} else {
|
} else {
|
||||||
std::cerr << "로그인 실패: 알 수 없는 오류" << std::endl;
|
std::cerr << "로그인 실패: 이미 접속 중인 유저거나 오류가 발생했습니다."
|
||||||
|
<< std::endl;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -99,10 +103,10 @@ void BaseClient::HandlePacket(const PacketHeader &header) {
|
||||||
std::cout << "\n" << buffer.data() << std::endl;
|
std::cout << "\n" << buffer.data() << std::endl;
|
||||||
} break;
|
} break;
|
||||||
case PacketID::SC_UpgradeResult:
|
case PacketID::SC_UpgradeResult:
|
||||||
HandleUpgradeResult();
|
HandleUpgradeResult(header);
|
||||||
break;
|
break;
|
||||||
case PacketID::SC_SellResult:
|
case PacketID::SC_SellResult:
|
||||||
HandleSellResult();
|
HandleSellResult(header);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
// 처리하지 않는 패킷은 페이로드만 건너뜀
|
// 처리하지 않는 패킷은 페이로드만 건너뜀
|
||||||
|
|
@ -115,22 +119,26 @@ void BaseClient::HandlePacket(const PacketHeader &header) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void BaseClient::HandleUpgradeResult() {
|
void BaseClient::HandleUpgradeResult(const PacketHeader &header) {
|
||||||
PKT_SC_UpgradeResult res;
|
Protocol::SC_UpgradeResult res;
|
||||||
if (ReceivePayload(res)) {
|
std::vector<uint8_t> payload(header.size);
|
||||||
currentLevel_ = res.currentLevel;
|
boost::asio::read(socket_, boost::asio::buffer(payload.data(), header.size));
|
||||||
currentGold_ = res.currentGold;
|
if (res.ParseFromArray(payload.data(), header.size)) {
|
||||||
|
currentLevel_ = res.current_level();
|
||||||
|
currentGold_ = res.current_gold();
|
||||||
// 강화 결과 출력은 채팅으로
|
// 강화 결과 출력은 채팅으로
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void BaseClient::HandleSellResult() {
|
void BaseClient::HandleSellResult(const PacketHeader &header) {
|
||||||
PKT_SC_SellResult res;
|
Protocol::SC_SellResult res;
|
||||||
if (ReceivePayload(res)) {
|
std::vector<uint8_t> payload(header.size);
|
||||||
|
boost::asio::read(socket_, boost::asio::buffer(payload.data(), header.size));
|
||||||
|
if (res.ParseFromArray(payload.data(), header.size)) {
|
||||||
currentLevel_ = 0;
|
currentLevel_ = 0;
|
||||||
currentGold_ = res.totalGold;
|
currentGold_ = res.total_gold();
|
||||||
// 판매 결과는 본인에게만
|
// 판매 결과는 본인에게만
|
||||||
std::cout << "\n>> 판매 완료: " << res.earnedGold << " 골드 획득!"
|
std::cout << "\n>> 판매 완료: " << res.earned_gold() << " 골드 획득!"
|
||||||
<< std::endl;
|
<< std::endl;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,14 @@
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "Packet.h"
|
#include "Packet.h"
|
||||||
#include <atomic>
|
|
||||||
#include <boost/asio.hpp>
|
#include <boost/asio.hpp>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <thread>
|
#include <thread>
|
||||||
|
|
||||||
|
namespace google::protobuf {
|
||||||
|
class Message;
|
||||||
|
}
|
||||||
|
|
||||||
using boost::asio::ip::tcp;
|
using boost::asio::ip::tcp;
|
||||||
|
|
||||||
class BaseClient {
|
class BaseClient {
|
||||||
|
|
@ -15,20 +18,10 @@ public:
|
||||||
|
|
||||||
virtual ~BaseClient() = default;
|
virtual ~BaseClient() = default;
|
||||||
|
|
||||||
void SendPacket(PacketID id, const void *payload = nullptr,
|
void SendPacket(PacketID id, const google::protobuf::Message *pkt = nullptr);
|
||||||
size_t payloadSize = 0);
|
|
||||||
|
|
||||||
bool ReceiveHeader(PacketHeader &header);
|
bool ReceiveHeader(PacketHeader &header);
|
||||||
|
|
||||||
template <typename T> 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);
|
bool Login(const std::string &nickname);
|
||||||
|
|
||||||
// 비동기 수신 시작
|
// 비동기 수신 시작
|
||||||
|
|
@ -48,8 +41,8 @@ protected:
|
||||||
void ReceiveLoop(std::stop_token stopToken);
|
void ReceiveLoop(std::stop_token stopToken);
|
||||||
void HandlePacket(const PacketHeader &header);
|
void HandlePacket(const PacketHeader &header);
|
||||||
|
|
||||||
void HandleUpgradeResult();
|
void HandleUpgradeResult(const PacketHeader &header);
|
||||||
void HandleSellResult();
|
void HandleSellResult(const PacketHeader &header);
|
||||||
};
|
};
|
||||||
|
|
||||||
class InteractiveClient : public BaseClient {
|
class InteractiveClient : public BaseClient {
|
||||||
|
|
|
||||||
|
|
@ -46,45 +46,3 @@ namespace GameConfig {
|
||||||
const uint32_t MAX_SWORD_LEVEL = 20;
|
const uint32_t MAX_SWORD_LEVEL = 20;
|
||||||
const uint64_t INITIAL_GOLD = 10000;
|
const uint64_t INITIAL_GOLD = 10000;
|
||||||
} // namespace GameConfig
|
} // 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)
|
|
||||||
|
|
|
||||||
|
|
@ -16,14 +16,15 @@ public:
|
||||||
void Start();
|
void Start();
|
||||||
void Send(std::span<const uint8_t> data);
|
void Send(std::span<const uint8_t> data);
|
||||||
|
|
||||||
template <typename T> void SendPacket(PacketID id, const T &payload) {
|
template <typename T> void SendPacket(PacketID id, const T &pkt) {
|
||||||
|
uint16_t size = static_cast<uint16_t>(pkt.ByteSizeLong());
|
||||||
PacketHeader header;
|
PacketHeader header;
|
||||||
header.id = static_cast<uint16_t>(id);
|
header.id = static_cast<uint16_t>(id);
|
||||||
header.size = sizeof(T);
|
header.size = size;
|
||||||
|
|
||||||
std::vector<uint8_t> buffer(sizeof(PacketHeader) + sizeof(T));
|
std::vector<uint8_t> buffer(sizeof(PacketHeader) + size);
|
||||||
std::memcpy(buffer.data(), &header, sizeof(PacketHeader));
|
std::memcpy(buffer.data(), &header, sizeof(PacketHeader));
|
||||||
std::memcpy(buffer.data() + sizeof(PacketHeader), &payload, sizeof(T));
|
pkt.SerializeToArray(buffer.data() + sizeof(PacketHeader), size);
|
||||||
|
|
||||||
Send(buffer);
|
Send(buffer);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
31
proto/Protocol.proto
Normal file
31
proto/Protocol.proto
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
syntax = "proto3";
|
||||||
|
|
||||||
|
package Protocol;
|
||||||
|
|
||||||
|
message CS_Login {
|
||||||
|
string nickname = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message SC_LoginResult {
|
||||||
|
bool success = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message SC_UpgradeResult {
|
||||||
|
uint32 result = 1; // 0: 파괴, 1: 성공, 2: 실패
|
||||||
|
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;
|
||||||
|
}
|
||||||
35
python_client/Protocol_pb2.py
Normal file
35
python_client/Protocol_pb2.py
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
||||||
|
# source: Protocol.proto
|
||||||
|
"""Generated protocol buffer code."""
|
||||||
|
from google.protobuf.internal import builder as _builder
|
||||||
|
from google.protobuf import descriptor as _descriptor
|
||||||
|
from google.protobuf import descriptor_pool as _descriptor_pool
|
||||||
|
from google.protobuf import symbol_database as _symbol_database
|
||||||
|
# @@protoc_insertion_point(imports)
|
||||||
|
|
||||||
|
_sym_db = _symbol_database.Default()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0eProtocol.proto\x12\x08Protocol\"\x1c\n\x08\x43S_Login\x12\x10\n\x08nickname\x18\x01 \x01(\t\"!\n\x0eSC_LoginResult\x12\x0f\n\x07success\x18\x01 \x01(\x08\"O\n\x10SC_UpgradeResult\x12\x0e\n\x06result\x18\x01 \x01(\r\x12\x15\n\rcurrent_level\x18\x02 \x01(\r\x12\x14\n\x0c\x63urrent_gold\x18\x03 \x01(\x04\"8\n\rSC_SellResult\x12\x13\n\x0b\x65\x61rned_gold\x18\x01 \x01(\x04\x12\x12\n\ntotal_gold\x18\x02 \x01(\x04\"/\n\x0cRankingEntry\x12\x10\n\x08nickname\x18\x01 \x01(\t\x12\r\n\x05level\x18\x02 \x01(\r\"9\n\x0eSC_RankingList\x12\'\n\x07\x65ntries\x18\x01 \x03(\x0b\x32\x16.Protocol.RankingEntryb\x06proto3')
|
||||||
|
|
||||||
|
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals())
|
||||||
|
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'Protocol_pb2', globals())
|
||||||
|
if _descriptor._USE_C_DESCRIPTORS == False:
|
||||||
|
|
||||||
|
DESCRIPTOR._options = None
|
||||||
|
_CS_LOGIN._serialized_start=28
|
||||||
|
_CS_LOGIN._serialized_end=56
|
||||||
|
_SC_LOGINRESULT._serialized_start=58
|
||||||
|
_SC_LOGINRESULT._serialized_end=91
|
||||||
|
_SC_UPGRADERESULT._serialized_start=93
|
||||||
|
_SC_UPGRADERESULT._serialized_end=172
|
||||||
|
_SC_SELLRESULT._serialized_start=174
|
||||||
|
_SC_SELLRESULT._serialized_end=230
|
||||||
|
_RANKINGENTRY._serialized_start=232
|
||||||
|
_RANKINGENTRY._serialized_end=279
|
||||||
|
_SC_RANKINGLIST._serialized_start=281
|
||||||
|
_SC_RANKINGLIST._serialized_end=338
|
||||||
|
# @@protoc_insertion_point(module_scope)
|
||||||
201
python_client/main.py
Normal file
201
python_client/main.py
Normal file
|
|
@ -0,0 +1,201 @@
|
||||||
|
import socket
|
||||||
|
import threading
|
||||||
|
import struct
|
||||||
|
import customtkinter as ctk
|
||||||
|
from enum import IntEnum
|
||||||
|
import Protocol_pb2 as Protocol
|
||||||
|
|
||||||
|
# 패킷 ID 정의 (C++과 동일하게)
|
||||||
|
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("800x500")
|
||||||
|
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()
|
||||||
|
|
||||||
|
# 메인 레이아웃 (Grid)
|
||||||
|
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("<Return>", lambda e: self.send_chat())
|
||||||
|
|
||||||
|
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('<HH', len(payload), int(packet_id))
|
||||||
|
self.socket.sendall(header + payload)
|
||||||
|
|
||||||
|
def receive_loop(self):
|
||||||
|
try:
|
||||||
|
while self.connected:
|
||||||
|
# Header 읽기 (4 bytes)
|
||||||
|
header_data = self.socket.recv(4)
|
||||||
|
if not header_data: break
|
||||||
|
|
||||||
|
size, pkt_id = struct.unpack('<HH', header_data)
|
||||||
|
|
||||||
|
# Payload 읽기
|
||||||
|
payload = b""
|
||||||
|
if size > 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 + Raw String)
|
||||||
|
header = struct.pack('<HH', len(msg.encode('utf-8')), int(PacketID.Chat))
|
||||||
|
self.socket.sendall(header + msg.encode('utf-8'))
|
||||||
|
self.chat_entry.delete(0, 'end')
|
||||||
|
|
||||||
|
def log_chat(self, msg):
|
||||||
|
self.chat_box.configure(state="normal")
|
||||||
|
self.chat_box.insert("end", msg + "\n")
|
||||||
|
self.chat_box.see("end")
|
||||||
|
self.chat_box.configure(state="disabled")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app = SwordGameClient()
|
||||||
|
app.mainloop()
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
#include "PacketHandler.h"
|
#include "PacketHandler.h"
|
||||||
#include "DatabaseManager.h"
|
#include "DatabaseManager.h"
|
||||||
#include "Logger.h"
|
#include "Logger.h"
|
||||||
|
#include "Protocol.pb.h"
|
||||||
#include "SessionManager.h"
|
#include "SessionManager.h"
|
||||||
#include "SwordLogic.h"
|
#include "SwordLogic.h"
|
||||||
#include <boost/asio/use_awaitable.hpp>
|
#include <boost/asio/use_awaitable.hpp>
|
||||||
|
|
@ -17,27 +18,25 @@ PacketHandler::HandlePacket(std::shared_ptr<Session> session,
|
||||||
} break;
|
} break;
|
||||||
|
|
||||||
case PacketID::CS_Login: {
|
case PacketID::CS_Login: {
|
||||||
if (packet.payload.size() < sizeof(PKT_CS_Login)) {
|
Protocol::CS_Login pkt;
|
||||||
Logger::Log("로그인 패킷 크기가 올바르지 않습니다.");
|
if (!pkt.ParseFromArray(packet.payload.data(), packet.payload.size())) {
|
||||||
|
Logger::Log("로그인 패킷 파싱 실패");
|
||||||
co_return;
|
co_return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const PKT_CS_Login *loginPkt =
|
std::string nickname = pkt.nickname();
|
||||||
reinterpret_cast<const PKT_CS_Login *>(packet.payload.data());
|
|
||||||
std::string nickname(loginPkt->nickname);
|
|
||||||
|
|
||||||
auto userData = co_await DatabaseManager::GetInstance().LoadUser(nickname);
|
auto userData = co_await DatabaseManager::GetInstance().LoadUser(nickname);
|
||||||
|
|
||||||
// 골드와 검 레벨 설정 (닉네임은 TryJoin에서 설정)
|
// 골드와 검 레벨 설정 (닉네임은 TryJoin에서 설정)
|
||||||
session->SetGold(userData.gold);
|
session->SetGold(userData.gold);
|
||||||
session->SetSwordLevel(userData.swordLevel);
|
session->SetSwordLevel(userData.swordLevel);
|
||||||
|
|
||||||
PKT_SC_LoginResult loginResult;
|
Protocol::SC_LoginResult loginResult;
|
||||||
|
|
||||||
// 중복 체크
|
// 중복 체크
|
||||||
if (!SessionManager::GetInstance().TryJoin(session, userData.nickname)) {
|
if (!SessionManager::GetInstance().TryJoin(session, userData.nickname)) {
|
||||||
Logger::Log("중복 로그인 거부: ", nickname);
|
Logger::Log("중복 로그인 거부: ", nickname);
|
||||||
loginResult.result = 0;
|
loginResult.set_success(false);
|
||||||
session->SendPacket(PacketID::SC_LoginResult, loginResult);
|
session->SendPacket(PacketID::SC_LoginResult, loginResult);
|
||||||
co_return;
|
co_return;
|
||||||
}
|
}
|
||||||
|
|
@ -46,7 +45,7 @@ PacketHandler::HandlePacket(std::shared_ptr<Session> session,
|
||||||
" (Gold: ", session->GetGold(),
|
" (Gold: ", session->GetGold(),
|
||||||
", Level: ", session->GetSwordLevel(), ")");
|
", Level: ", session->GetSwordLevel(), ")");
|
||||||
|
|
||||||
loginResult.result = 1;
|
loginResult.set_success(true);
|
||||||
session->SendPacket(PacketID::SC_LoginResult, loginResult);
|
session->SendPacket(PacketID::SC_LoginResult, loginResult);
|
||||||
} break;
|
} break;
|
||||||
|
|
||||||
|
|
@ -55,14 +54,14 @@ PacketHandler::HandlePacket(std::shared_ptr<Session> session,
|
||||||
uint64_t cost = SwordLogic::GetUpgradeCost(currentLevel);
|
uint64_t cost = SwordLogic::GetUpgradeCost(currentLevel);
|
||||||
uint64_t currentGold = session->GetGold();
|
uint64_t currentGold = session->GetGold();
|
||||||
|
|
||||||
PKT_SC_UpgradeResult res;
|
Protocol::SC_UpgradeResult res;
|
||||||
if (currentGold < cost) {
|
if (currentGold < cost) {
|
||||||
// 골드 부족
|
// 골드 부족
|
||||||
res.result = 2;
|
res.set_result(2);
|
||||||
} else {
|
} else {
|
||||||
session->SetGold(currentGold - cost);
|
session->SetGold(currentGold - cost);
|
||||||
uint8_t result = SwordLogic::TryUpgrade(currentLevel);
|
uint8_t result = SwordLogic::TryUpgrade(currentLevel);
|
||||||
res.result = result;
|
res.set_result(result);
|
||||||
|
|
||||||
// 성공
|
// 성공
|
||||||
if (result == 1) {
|
if (result == 1) {
|
||||||
|
|
@ -76,8 +75,8 @@ PacketHandler::HandlePacket(std::shared_ptr<Session> session,
|
||||||
session->GetNickname(), session->GetGold(), session->GetSwordLevel());
|
session->GetNickname(), session->GetGold(), session->GetSwordLevel());
|
||||||
}
|
}
|
||||||
|
|
||||||
res.currentLevel = session->GetSwordLevel();
|
res.set_current_level(session->GetSwordLevel());
|
||||||
res.currentGold = session->GetGold();
|
res.set_current_gold(session->GetGold());
|
||||||
|
|
||||||
// 결과 패킷 전송
|
// 결과 패킷 전송
|
||||||
session->SendPacket(PacketID::SC_UpgradeResult, res);
|
session->SendPacket(PacketID::SC_UpgradeResult, res);
|
||||||
|
|
@ -86,9 +85,9 @@ PacketHandler::HandlePacket(std::shared_ptr<Session> session,
|
||||||
if (currentGold >= cost) {
|
if (currentGold >= cost) {
|
||||||
uint32_t attemptedLevel = currentLevel + 1;
|
uint32_t attemptedLevel = currentLevel + 1;
|
||||||
std::string resultText;
|
std::string resultText;
|
||||||
if (res.result == 1) {
|
if (res.result() == 1) {
|
||||||
resultText = "를 달성했습니다!";
|
resultText = "를 달성했습니다!";
|
||||||
} else if (res.result == 0) {
|
} else if (res.result() == 0) {
|
||||||
resultText = " 시도 중 검이 파괴되었습니다!";
|
resultText = " 시도 중 검이 파괴되었습니다!";
|
||||||
} else {
|
} else {
|
||||||
resultText = " 달성에 실패했습니다!";
|
resultText = " 달성에 실패했습니다!";
|
||||||
|
|
@ -105,8 +104,9 @@ PacketHandler::HandlePacket(std::shared_ptr<Session> session,
|
||||||
std::vector<uint8_t> chatPayload(chatMsg.begin(), chatMsg.end());
|
std::vector<uint8_t> chatPayload(chatMsg.begin(), chatMsg.end());
|
||||||
SessionManager::GetInstance().Broadcast(chatHeader, chatPayload);
|
SessionManager::GetInstance().Broadcast(chatHeader, chatPayload);
|
||||||
|
|
||||||
Logger::Log("강화 시도 [", session->GetNickname(), "]: ", (int)res.result,
|
Logger::Log("강화 시도 [", session->GetNickname(),
|
||||||
" (레벨: ", currentLevel, "->", res.currentLevel, ")");
|
"]: ", (int)res.result(), " (레벨: ", currentLevel, "->",
|
||||||
|
res.current_level(), ")");
|
||||||
}
|
}
|
||||||
} break;
|
} break;
|
||||||
|
|
||||||
|
|
@ -121,9 +121,9 @@ PacketHandler::HandlePacket(std::shared_ptr<Session> session,
|
||||||
co_await DatabaseManager::GetInstance().SaveUser(
|
co_await DatabaseManager::GetInstance().SaveUser(
|
||||||
session->GetNickname(), session->GetGold(), session->GetSwordLevel());
|
session->GetNickname(), session->GetGold(), session->GetSwordLevel());
|
||||||
|
|
||||||
PKT_SC_SellResult res;
|
Protocol::SC_SellResult res;
|
||||||
res.earnedGold = price;
|
res.set_earned_gold(price);
|
||||||
res.totalGold = newGold;
|
res.set_total_gold(newGold);
|
||||||
|
|
||||||
session->SendPacket(PacketID::SC_SellResult, res);
|
session->SendPacket(PacketID::SC_SellResult, res);
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue