feat: c++ pragma 방식 => protocol buffer로 직렬화 수정 및 클라이언트 python gui 앱으로 변경
This commit is contained in:
parent
a88b22b177
commit
cd192d4ec4
14 changed files with 305 additions and 371 deletions
3
client/.gitignore
vendored
Normal file
3
client/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
Protocol_pb2.py
|
||||
venv
|
||||
__pycache__
|
||||
|
|
@ -1,233 +0,0 @@
|
|||
#include "Client.h"
|
||||
#include <iostream>
|
||||
#include <thread>
|
||||
|
||||
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<uint16_t>(id);
|
||||
header.size = static_cast<uint16_t>(payloadSize);
|
||||
|
||||
std::vector<uint8_t> 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<uint16_t>(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<PacketID>(header.id);
|
||||
switch (id) {
|
||||
case PacketID::Chat: {
|
||||
std::vector<char> 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<uint8_t> 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 <fstream>
|
||||
|
||||
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();
|
||||
}
|
||||
19
client/README.md
Normal file
19
client/README.md
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
## 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
|
||||
```
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
#include "Client.h"
|
||||
#include <iostream>
|
||||
|
||||
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;
|
||||
}
|
||||
196
client/main.py
Normal file
196
client/main.py
Normal file
|
|
@ -0,0 +1,196 @@
|
|||
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("<Return>", 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('<HH', len(payload), int(packet_id))
|
||||
self.socket.sendall(header + payload)
|
||||
|
||||
def receive_loop(self):
|
||||
try:
|
||||
while self.connected:
|
||||
header_data = self.socket.recv(4)
|
||||
if not header_data: break
|
||||
|
||||
size, pkt_id = struct.unpack('<HH', header_data)
|
||||
|
||||
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 = 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()
|
||||
4
client/requirements.txt
Normal file
4
client/requirements.txt
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
customtkinter==5.2.2
|
||||
darkdetect==0.8.0
|
||||
packaging==26.0
|
||||
protobuf==6.33.5
|
||||
Loading…
Add table
Add a link
Reference in a new issue