From 066181ec17606a3c8b648e5b6df3eadca7f2fbb2 Mon Sep 17 00:00:00 2001 From: Krofty Date: Tue, 25 Apr 2023 07:47:19 +0100 Subject: [PATCH 01/15] Initial trade functionality added --- dndserver/handlers/trade.py | 63 ++++++++++++++++++++++++++++++- dndserver/protocol.py | 74 +++++++++++++++++++++---------------- 2 files changed, 104 insertions(+), 33 deletions(-) diff --git a/dndserver/handlers/trade.py b/dndserver/handlers/trade.py index dad1781f..4e47742e 100644 --- a/dndserver/handlers/trade.py +++ b/dndserver/handlers/trade.py @@ -1,6 +1,13 @@ +import random + from dndserver.protos import PacketCommand as pc from dndserver.protos.Trade import (SS2C_TRADE_MEMBERSHIP_REQUIREMENT_RES, STRADE_MEMBERSHIP_REQUIREMENT, - SS2C_TRADE_MEMBERSHIP_RES) + SS2C_TRADE_MEMBERSHIP_RES, SS2C_TRADE_CHANNEL_LIST_RES, STRADE_CHANNEL, + SS2C_TRADE_CHANNEL_SELECT_RES, SS2C_TRADE_CHANNEL_CHAT_RES, STRADE_CHAT_S2C,) +from dndserver.protos.Chat import (SCHATDATA, SCHATDATA_PIECE, SCHATDATA_PIECE_ITEM, + SCHATDATA_PIECE_ITEM_PROPERTY) +from dndserver.protos.Character import SACCOUNT_NICKNAME, SCHARACTER_INFO + def get_trade_reqs(ctx, msg): @@ -10,5 +17,59 @@ def get_trade_reqs(ctx, msg): ) +def get_trade_channels(ctx, msg): + return SS2C_TRADE_CHANNEL_LIST_RES(isTrader=1, channels=[STRADE_CHANNEL(index=1, channelName="ChatRoomData:Id_ChatRoom_Trade_Barbarian", memberCount=219, roomType=1, groupIndex=1)]) + + +def select_trade_channel(ctx, msg): + return SS2C_TRADE_CHANNEL_SELECT_RES(result=pc.SUCCESS) + + +def chat_piece_item_property(): + return SCHATDATA_PIECE_ITEM_PROPERTY(pid="DesignDataItem:Id_Item_Torch_0001", pv=1) + + +def chat_piece_item(): + return SCHATDATA_PIECE_ITEM(uid=1, iid="DesignDataItem:Id_Item_Torch_0001", pp=[chat_piece_item_property()], sp=[chat_piece_item_property()]) + + +def chat_piece(): + data = SCHATDATA_PIECE() + data.chatStr = "hi" + data.chatDataPieceItem.CopyFrom(chat_piece_item()) + return data + + +def chat_data(): + nickname = SACCOUNT_NICKNAME( + originalNickName="krofty", + streamingModeNickName=f"Fighter#{random.randrange(1000000, 1700000)}" + ) + + data = SCHATDATA() + data.accountId = "1" + data.characterId = "1" + data.nickname.CopyFrom(nickname) + data.partyId = "1" + data.chatDataPieceArray.append(chat_piece()) + return data + + +def chat_S2C(): + data = STRADE_CHAT_S2C() + data.index = 1 + data.chatType = 1 + data.time = 219 + data.chatData.CopyFrom(chat_data()) + return data + + +def chat_request(ctx, msg): + return SS2C_TRADE_CHANNEL_CHAT_RES( + result=pc.SUCCESS, + chats=[chat_S2C()] + ) + + def process_membership(ctx, msg): return SS2C_TRADE_MEMBERSHIP_RES(result=pc.SUCCESS) diff --git a/dndserver/protocol.py b/dndserver/protocol.py index 5a6395d2..b2ad4ca1 100644 --- a/dndserver/protocol.py +++ b/dndserver/protocol.py @@ -29,37 +29,47 @@ def connectionLost(self, reason): def dataReceived(self, data: bytes) -> None: """Main loop for receiving request packets and sending response packets.""" - # TODO: Implement support for segemented packets based on the incoming data's length. - length, _id = struct.unpack(" Date: Tue, 25 Apr 2023 08:52:20 +0100 Subject: [PATCH 02/15] added basic chat functionality --- dndserver/handlers/trade.py | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/dndserver/handlers/trade.py b/dndserver/handlers/trade.py index 4e47742e..5b44ca29 100644 --- a/dndserver/handlers/trade.py +++ b/dndserver/handlers/trade.py @@ -3,7 +3,8 @@ from dndserver.protos import PacketCommand as pc from dndserver.protos.Trade import (SS2C_TRADE_MEMBERSHIP_REQUIREMENT_RES, STRADE_MEMBERSHIP_REQUIREMENT, SS2C_TRADE_MEMBERSHIP_RES, SS2C_TRADE_CHANNEL_LIST_RES, STRADE_CHANNEL, - SS2C_TRADE_CHANNEL_SELECT_RES, SS2C_TRADE_CHANNEL_CHAT_RES, STRADE_CHAT_S2C,) + SS2C_TRADE_CHANNEL_SELECT_RES, SS2C_TRADE_CHANNEL_CHAT_RES, STRADE_CHAT_S2C, + SC2S_TRADE_CHANNEL_CHAT_REQ) from dndserver.protos.Chat import (SCHATDATA, SCHATDATA_PIECE, SCHATDATA_PIECE_ITEM, SCHATDATA_PIECE_ITEM_PROPERTY) from dndserver.protos.Character import SACCOUNT_NICKNAME, SCHARACTER_INFO @@ -26,21 +27,21 @@ def select_trade_channel(ctx, msg): def chat_piece_item_property(): - return SCHATDATA_PIECE_ITEM_PROPERTY(pid="DesignDataItem:Id_Item_Torch_0001", pv=1) + return SCHATDATA_PIECE_ITEM_PROPERTY(pid="1", pv=1) def chat_piece_item(): - return SCHATDATA_PIECE_ITEM(uid=1, iid="DesignDataItem:Id_Item_Torch_0001", pp=[chat_piece_item_property()], sp=[chat_piece_item_property()]) + return SCHATDATA_PIECE_ITEM(uid=1, iid="1") -def chat_piece(): +def chat_piece(chatmsg): data = SCHATDATA_PIECE() - data.chatStr = "hi" - data.chatDataPieceItem.CopyFrom(chat_piece_item()) + data.chatStr = chatmsg + #data.chatDataPieceItem.CopyFrom(chat_piece_item()) return data -def chat_data(): +def chat_data(chatmsg): nickname = SACCOUNT_NICKNAME( originalNickName="krofty", streamingModeNickName=f"Fighter#{random.randrange(1000000, 1700000)}" @@ -51,23 +52,27 @@ def chat_data(): data.characterId = "1" data.nickname.CopyFrom(nickname) data.partyId = "1" - data.chatDataPieceArray.append(chat_piece()) + data.chatDataPieceArray.append(chat_piece(chatmsg)) return data -def chat_S2C(): +def chat_S2C(chatmsg): data = STRADE_CHAT_S2C() data.index = 1 data.chatType = 1 - data.time = 219 - data.chatData.CopyFrom(chat_data()) + data.time = 1 + data.chatData.CopyFrom(chat_data(chatmsg)) return data def chat_request(ctx, msg): + req = SC2S_TRADE_CHANNEL_CHAT_REQ() + req.ParseFromString(msg) + print(req) + chatmsg = req.chat.chatData.chatDataPieceArray[0].chatStr return SS2C_TRADE_CHANNEL_CHAT_RES( result=pc.SUCCESS, - chats=[chat_S2C()] + chats=[chat_S2C(chatmsg)] ) From 278363e7a980d5516baa1fb102b7e39d457a960d Mon Sep 17 00:00:00 2001 From: Krofty Date: Tue, 25 Apr 2023 09:21:29 +0100 Subject: [PATCH 03/15] Refactor plus additional chat features added --- dndserver/handlers/trade.py | 80 ++++++++++++++++--------------------- 1 file changed, 34 insertions(+), 46 deletions(-) diff --git a/dndserver/handlers/trade.py b/dndserver/handlers/trade.py index 5b44ca29..9586f42c 100644 --- a/dndserver/handlers/trade.py +++ b/dndserver/handlers/trade.py @@ -1,5 +1,3 @@ -import random - from dndserver.protos import PacketCommand as pc from dndserver.protos.Trade import (SS2C_TRADE_MEMBERSHIP_REQUIREMENT_RES, STRADE_MEMBERSHIP_REQUIREMENT, SS2C_TRADE_MEMBERSHIP_RES, SS2C_TRADE_CHANNEL_LIST_RES, STRADE_CHANNEL, @@ -7,8 +5,7 @@ SC2S_TRADE_CHANNEL_CHAT_REQ) from dndserver.protos.Chat import (SCHATDATA, SCHATDATA_PIECE, SCHATDATA_PIECE_ITEM, SCHATDATA_PIECE_ITEM_PROPERTY) -from dndserver.protos.Character import SACCOUNT_NICKNAME, SCHARACTER_INFO - +from dndserver.protos.Character import SACCOUNT_NICKNAME def get_trade_reqs(ctx, msg): @@ -26,53 +23,44 @@ def select_trade_channel(ctx, msg): return SS2C_TRADE_CHANNEL_SELECT_RES(result=pc.SUCCESS) -def chat_piece_item_property(): - return SCHATDATA_PIECE_ITEM_PROPERTY(pid="1", pv=1) - - -def chat_piece_item(): - return SCHATDATA_PIECE_ITEM(uid=1, iid="1") - - -def chat_piece(chatmsg): - data = SCHATDATA_PIECE() - data.chatStr = chatmsg - #data.chatDataPieceItem.CopyFrom(chat_piece_item()) - return data - - -def chat_data(chatmsg): - nickname = SACCOUNT_NICKNAME( - originalNickName="krofty", - streamingModeNickName=f"Fighter#{random.randrange(1000000, 1700000)}" - ) - - data = SCHATDATA() - data.accountId = "1" - data.characterId = "1" - data.nickname.CopyFrom(nickname) - data.partyId = "1" - data.chatDataPieceArray.append(chat_piece(chatmsg)) - return data - - -def chat_S2C(chatmsg): - data = STRADE_CHAT_S2C() - data.index = 1 - data.chatType = 1 - data.time = 1 - data.chatData.CopyFrom(chat_data(chatmsg)) - return data - - def chat_request(ctx, msg): req = SC2S_TRADE_CHANNEL_CHAT_REQ() req.ParseFromString(msg) - print(req) - chatmsg = req.chat.chatData.chatDataPieceArray[0].chatStr + + chat_type = req.chat.chatType + #target_account_id = req.chat.targetAccountId #currently unused + chat_str = req.chat.chatData.chatDataPieceArray[0].chatStr + uid = req.chat.chatData.chatDataPieceArray[0].chatDataPieceItem.uid + iid = req.chat.chatData.chatDataPieceArray[0].chatDataPieceItem.iid + pp_list = req.chat.chatData.chatDataPieceArray[0].chatDataPieceItem.pp + + property_list = [] + for pp in pp_list: + property_list.append(SCHATDATA_PIECE_ITEM_PROPERTY(pid=pp.pid, pv=pp.pv)) + + chat_piece_item_obj = SCHATDATA_PIECE_ITEM(uid=uid, iid=iid, pp=property_list) + + chat_piece = SCHATDATA_PIECE() + chat_piece.chatStr = chat_str + chat_piece.chatDataPieceItem.CopyFrom(chat_piece_item_obj) + + nickname = SACCOUNT_NICKNAME(originalNickName="Krofty", streamingModeNickName="") + chat_data = SCHATDATA() + chat_data.accountId = "1" + chat_data.characterId = "1" + chat_data.nickname.CopyFrom(nickname) + chat_data.partyId = "1" + chat_data.chatDataPieceArray.append(chat_piece) + + chat_trade = STRADE_CHAT_S2C() + chat_trade.index = 1 + chat_trade.chatType = chat_type + chat_trade.time = 1 + chat_trade.chatData.CopyFrom(chat_data) + return SS2C_TRADE_CHANNEL_CHAT_RES( result=pc.SUCCESS, - chats=[chat_S2C(chatmsg)] + chats=[chat_trade] ) From 76bbe4d815cc7e5a7931335679b5ac688a38cb99 Mon Sep 17 00:00:00 2001 From: Krofty Date: Tue, 25 Apr 2023 11:24:01 +0100 Subject: [PATCH 04/15] Added exit function --- dndserver/handlers/trade.py | 6 +++++- dndserver/protocol.py | 1 + 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/dndserver/handlers/trade.py b/dndserver/handlers/trade.py index 9586f42c..f43c6401 100644 --- a/dndserver/handlers/trade.py +++ b/dndserver/handlers/trade.py @@ -2,7 +2,7 @@ from dndserver.protos.Trade import (SS2C_TRADE_MEMBERSHIP_REQUIREMENT_RES, STRADE_MEMBERSHIP_REQUIREMENT, SS2C_TRADE_MEMBERSHIP_RES, SS2C_TRADE_CHANNEL_LIST_RES, STRADE_CHANNEL, SS2C_TRADE_CHANNEL_SELECT_RES, SS2C_TRADE_CHANNEL_CHAT_RES, STRADE_CHAT_S2C, - SC2S_TRADE_CHANNEL_CHAT_REQ) + SC2S_TRADE_CHANNEL_CHAT_REQ, SS2C_TRADE_CHANNEL_EXIT_RES) from dndserver.protos.Chat import (SCHATDATA, SCHATDATA_PIECE, SCHATDATA_PIECE_ITEM, SCHATDATA_PIECE_ITEM_PROPERTY) from dndserver.protos.Character import SACCOUNT_NICKNAME @@ -64,5 +64,9 @@ def chat_request(ctx, msg): ) +def exit(ctx, msg): + return SS2C_TRADE_CHANNEL_EXIT_RES(result=pc.SUCCESS) + + def process_membership(ctx, msg): return SS2C_TRADE_MEMBERSHIP_RES(result=pc.SUCCESS) diff --git a/dndserver/protocol.py b/dndserver/protocol.py index b2ad4ca1..229d1c3b 100644 --- a/dndserver/protocol.py +++ b/dndserver/protocol.py @@ -52,6 +52,7 @@ def dataReceived(self, data: bytes) -> None: pc.C2S_META_LOCATION_REQ: menu.process_location, pc.C2S_MERCHANT_LIST_REQ: merchant.get_merchant_list, pc.C2S_TRADE_CHANNEL_CHAT_REQ: trade.chat_request, + pc.C2S_TRADE_CHANNEL_EXIT_REQ: trade.exit, pc.C2S_TRADE_CHANNEL_LIST_REQ: trade.get_trade_channels, pc.C2S_TRADE_CHANNEL_SELECT_REQ: trade.select_trade_channel, pc.C2S_TRADE_MEMBERSHIP_REQUIREMENT_REQ: trade.get_trade_reqs, From f1be65c9d7d7d3e033a25643cad2c9c29cb99f74 Mon Sep 17 00:00:00 2001 From: Snaacky Date: Wed, 26 Apr 2023 02:11:11 -0400 Subject: [PATCH 05/15] Implemented User(), Party(), users are now able to join parties - Implemented User() and Party() for implementing user and party state. - Implemented party system up to accepting invites. There is currently a bug where the username isn't being rendered properly for the user. - Refactors throughout the codebase to support the new objects. --- dndserver/handlers/character.py | 25 +++--- dndserver/handlers/friends.py | 114 +++++++++-------------- dndserver/handlers/lobby.py | 29 ++---- dndserver/handlers/login.py | 23 +++-- dndserver/handlers/merchant.py | 8 +- dndserver/handlers/party.py | 154 ++++++++++++++++++++++++++++++++ dndserver/models.py | 3 + dndserver/objects/party.py | 20 +++++ dndserver/objects/user.py | 6 ++ dndserver/parties.py | 1 + dndserver/protocol.py | 31 +++---- dndserver/utils.py | 35 ++++++++ 12 files changed, 313 insertions(+), 136 deletions(-) create mode 100644 dndserver/handlers/party.py create mode 100644 dndserver/objects/party.py create mode 100644 dndserver/objects/user.py create mode 100644 dndserver/parties.py create mode 100644 dndserver/utils.py diff --git a/dndserver/handlers/character.py b/dndserver/handlers/character.py index 84c411b2..42ce8d12 100644 --- a/dndserver/handlers/character.py +++ b/dndserver/handlers/character.py @@ -21,7 +21,7 @@ def list_characters(ctx, msg): req = SC2S_ACCOUNT_CHARACTER_LIST_REQ() req.ParseFromString(msg) - query = db.query(Character).filter_by(user_id=sessions[ctx.transport]["user"].id).all() + query = db.query(Character).filter_by(user_id=sessions[ctx.transport].account.id).all() res = SS2C_ACCOUNT_CHARACTER_LIST_RES(totalCharacterCount=len(query), pageIndex=req.pageIndex) start = (res.pageIndex - 1) * 7 @@ -32,7 +32,7 @@ def list_characters(ctx, msg): characterId=str(result.id), nickName=SACCOUNT_NICKNAME( originalNickName=result.nickname, - streamingModeNickName=f"Fighter#{random.randrange(1000000, 1700000)}" + streamingModeNickName=result.streaming_nickname ), level=result.level, characterClass=CharacterClass(result.character_class).value, @@ -69,7 +69,7 @@ def create_character(ctx, msg): return res character = Character( - user_id=sessions[ctx.transport]["user"].id, + user_id=sessions[ctx.transport].account.id, nickname=req.nickName, gender=Gender(req.gender), character_class=CharacterClass(req.characterClass) @@ -88,7 +88,7 @@ def delete_character(ctx, msg): res = SS2C_ACCOUNT_CHARACTER_DELETE_RES(result=pc.SUCCESS) # Prevents characters from maliciously deleting others characters. - if query.user_id != sessions[ctx.transport]["user"].id: + if query.user_id != sessions[ctx.transport].user.id: res.result = pc.FAIL_GENERAL return res @@ -98,19 +98,21 @@ def delete_character(ctx, msg): def character_info(ctx, msg): """Occurs when the user loads into the lobby/tavern.""" - query = db.query(Character).filter_by(user_id=sessions[ctx.transport]["user"].id).first() + query = db.query(Character).filter_by(user_id=sessions[ctx.transport].account.id).first() + character = sessions[ctx.transport].character + res = SS2C_LOBBY_CHARACTER_INFO_RES( result=pc.SUCCESS, characterDataBase=SCHARACTER_INFO( accountId="1", nickName=SACCOUNT_NICKNAME( - originalNickName=query.nickname, - streamingModeNickName=f"Fighter#{random.randrange(1000000, 1700000)}" + originalNickName=character.nickname, + streamingModeNickName=character.streaming_nickname ), - characterClass=CharacterClass(query.character_class).value, - characterId=str(query.id), - gender=Gender(query.gender).value, - level=query.level, + characterClass=CharacterClass(character.character_class).value, + characterId=str(character.id), + gender=Gender(character.gender).value, + level=character.level, CharacterItemList=[ items.generate_helm(), items.generate_torch(), items.generate_lantern(), items.generate_sword(), items.generate_pants(), items.generate_tunic(), @@ -118,6 +120,7 @@ def character_info(ctx, msg): ] ) ) + return res def move_item(ctx, msg): diff --git a/dndserver/handlers/friends.py b/dndserver/handlers/friends.py index 9065800d..e6e3d1cf 100644 --- a/dndserver/handlers/friends.py +++ b/dndserver/handlers/friends.py @@ -1,14 +1,19 @@ -from dndserver.protos import Character as char -from dndserver.protos import Friend as friend -from dndserver.protos import Party as party +import random + +from dndserver.enums import CharacterClass, Gender +from dndserver.protos.Character import SACCOUNT_NICKNAME, SCHARACTER_FRIEND_INFO +from dndserver.protos.Friend import SC2S_FRIEND_FIND_REQ, SS2C_FRIEND_FIND_RES, SS2C_FRIEND_LIST_ALL_RES +from dndserver.protos import PacketCommand as pc +from dndserver.sessions import sessions +from dndserver.utils import get_user_by_nickname def list_friends(ctx, msg): - nickname = char.SACCOUNT_NICKNAME() + nickname = SACCOUNT_NICKNAME() nickname.originalNickName = "gay" - nickname.streamingModeNickName = "Fighter#11111" + nickname.streamingModeNickName = f"Fighter#{random.randrange(1000000, 1700000)}" - friend_info = char.SCHARACTER_FRIEND_INFO() + friend_info = SCHARACTER_FRIEND_INFO() friend_info.accountId = "2" friend_info.nickName.CopyFrom(nickname) friend_info.characterClass = "DesignDataPlayerCharacter:Id_PlayerCharacter_Fighter" @@ -19,7 +24,7 @@ def list_friends(ctx, msg): friend_info.PartyMemeberCount = 1 friend_info.PartyMaxMemeberCount = 3 - res = friend.SS2C_FRIEND_LIST_ALL_RES() # message SS2C_FRIEND_LIST_ALL_RES { + res = SS2C_FRIEND_LIST_ALL_RES() # message SS2C_FRIEND_LIST_ALL_RES { res.friendInfoList.extend([friend_info]) # repeated .DC.Packet.SCHARACTER_FRIEND_INFO friendInfoList = 1; res.loopFlag = 1 # uint32 loopFlag = 2; res.totalUserCount = 2 # uint32 totalUserCount = 3; @@ -30,73 +35,34 @@ def list_friends(ctx, msg): def find_user(ctx, msg): - nick = char.SACCOUNT_NICKNAME() - nick.originalNickName = "krofty" - nick.streamingModeNickName = "Fighter#00321" - nick.karmaRating = 666 - - friend_info = char.SCHARACTER_FRIEND_INFO() - friend_info.accountId = "2" - friend_info.nickName.CopyFrom(nick) - friend_info.characterClass = "DesignDataPlayerCharacter:Id_PlayerCharacter_Fighter" - friend_info.characterId = "2" - friend_info.gender = 2 - friend_info.level = 12 - friend_info.locationStatus = 2 - friend_info.PartyMemeberCount = 1 - friend_info.PartyMaxMemeberCount = 3 - - res = friend.SS2C_FRIEND_FIND_RES() - res.result = 1 - res.friendInfo.CopyFrom(friend_info) - return res - - -def party_invite(ctx, msg): - # message SC2S_PARTY_INVITE_REQ { - # .DC.Packet.SACCOUNT_NICKNAME findNickName = 1; - # string findAccountId = 2; - # string findCharacterId = 3; + # message SC2S_FRIEND_FIND_REQ { + # .DC.Packet.SACCOUNT_NICKNAME nickName = 1; # } - res = party.SS2C_PARTY_INVITE_RES() - res.result = 1 - return res - - -def party_invite_notify(ctx, msg): - res = party.SS2C_PARTY_INVITE_NOT() - - nick = char.SACCOUNT_NICKNAME() - nick.originalNickName = "krofty" - nick.streamingModeNickName = "Fighter#00321" - nick.karmaRating = 666 + req = SC2S_FRIEND_FIND_REQ() + req.ParseFromString(msg) + + res = SS2C_FRIEND_FIND_RES(result=pc.SUCCESS) + + # Makes it so users can't search for or invite themselves. + # if req.nickName.originalNickName == sessions[ctx.transport]["character"].nickname: + # return res + + _, session = get_user_by_nickname(nickname=req.nickName.originalNickName) + if session: + friend = SCHARACTER_FRIEND_INFO( + accountId=str(session.account.id), + nickName=SACCOUNT_NICKNAME( + originalNickName=session.character.nickname, + streamingModeNickName=session.character.streaming_nickname, + ), + characterClass=CharacterClass(session.character.character_class).value, + characterId=str(session.character.id), + gender=Gender(session.character.gender).value, + level=session.character.level, + locationStatus=1, # TODO: Remove the hardcoding from these bottom 3. + PartyMemeberCount=1, + PartyMaxMemeberCount=3 + ) + res.friendInfo.CopyFrom(friend) - res.InviteeAccountId = "1" - res.InviteeCharacterId = "1" - res.InviteeNickName.CopyFrom(nick) return res - - # message SS2C_PARTY_INVITE_RES { - # uint32 result = 1; - # } - - # message SS2C_PARTY_INVITE_NOT { - # .DC.Packet.SACCOUNT_NICKNAME InviteeNickName = 1; - # string InviteeAccountId = 2; - # string InviteeCharacterId = 3; - # } - - # message SC2S_PARTY_INVITE_ANSWER_REQ { - # uint32 inviteResult = 1; - # string returnAccountId = 2; - # } - - # message SS2C_PARTY_INVITE_ANSWER_RES { - # uint32 result = 1; - # } - - # message SS2C_PARTY_INVITE_ANSWER_RESULT_NOT { - # .DC.Packet.SACCOUNT_NICKNAME nickName = 1; - # uint32 inviteResult = 2; - # } - return diff --git a/dndserver/handlers/lobby.py b/dndserver/handlers/lobby.py index 2db2c580..cb9c46c7 100644 --- a/dndserver/handlers/lobby.py +++ b/dndserver/handlers/lobby.py @@ -1,11 +1,12 @@ from dndserver.database import db +from dndserver.objects.party import Party from dndserver.models import Character from dndserver.protos import PacketCommand as pc from dndserver.protos.Account import SC2S_LOBBY_ENTER_REQ, SS2C_LOBBY_ENTER_RES from dndserver.protos.Lobby import (SC2S_CHARACTER_SELECT_ENTER_REQ, SC2S_LOBBY_REGION_SELECT_REQ, SS2C_LOBBY_REGION_SELECT_RES, SS2C_CHARACTER_SELECT_ENTER_RES, - SC2S_LOBBY_GAME_DIFFICULTY_SELECT_REQ, SS2C_LOBBY_GAME_DIFFICULTY_SELECT_RES, - SC2S_OPEN_LOBBY_MAP_REQ) + SC2S_LOBBY_GAME_DIFFICULTY_SELECT_REQ, SS2C_LOBBY_GAME_DIFFICULTY_SELECT_RES) +from dndserver.parties import parties from dndserver.sessions import sessions @@ -13,9 +14,13 @@ def enter_lobby(ctx, msg): """Occurs when loading into the lobby from the character selection screen.""" req = SC2S_LOBBY_ENTER_REQ() req.ParseFromString(msg) + query = db.query(Character).filter_by(id=req.characterId).first() res = SS2C_LOBBY_ENTER_RES(result=pc.SUCCESS, accountId=str(query.user_id)) - sessions[ctx.transport]["character"] = query + + sessions[ctx.transport].character = query + sessions[ctx.transport].party = Party(_id=len(parties) + 1, player_1=sessions[ctx.transport]) + return res @@ -38,21 +43,5 @@ def start(ctx, msg): def enter_character_select(ctx, msg): """Occurs when client enter in the characters selection menu.""" res = SS2C_CHARACTER_SELECT_ENTER_RES() - res.result = 1 - return res - - -def map_select(ctx, msg): - """Occurs when client selects a map.""" - req = SC2S_LOBBY_GAME_DIFFICULTY_SELECT_REQ() - req.ParseFromString(msg) - res = SS2C_LOBBY_GAME_DIFFICULTY_SELECT_RES(result=pc.SUCCESS, gameDifficultyTypeIndex=req.gameDifficultyTypeIndex) - return res - - -def open_lobby_map(ctx, msg): - req = SC2S_OPEN_LOBBY_MAP_REQ() - req.ParseFromString(msg) - res = SC2S_OPEN_LOBBY_MAP_REQ(result=pc.SUCCESS) + res.result = pc.SUCCESS return res - diff --git a/dndserver/handlers/login.py b/dndserver/handlers/login.py index cb7a218a..0323a443 100644 --- a/dndserver/handlers/login.py +++ b/dndserver/handlers/login.py @@ -17,17 +17,17 @@ def process_login(ctx, msg): # TODO: Not all SS2C_ACCOUNT_LOGIN_RES fields are implemented. res = SS2C_ACCOUNT_LOGIN_RES(serverLocation=1) - user = db.query(Account).filter_by(username=req.loginId).first() - if not user: - user = Account( + account = db.query(Account).filter_by(username=req.loginId).first() + if not account: + account = Account( username=req.loginId, password=argon2.PasswordHasher().hash(req.password), secret_token=''.join(random.choices(string.ascii_uppercase + string.digits, k=21)) ) - user.save() + account.save() # TODO: Create new hwid objects and save them to the db here - res.secretToken = user.secret_token + res.secretToken = account.secret_token # Return FAIL_SHORT_ID_OR_PASSWORD on too short username/password. if len(req.loginId) <= 2 or len(req.password) <= 2: @@ -41,21 +41,20 @@ def process_login(ctx, msg): # Return FAIL_PASSWORD on invalid password. try: - argon2.PasswordHasher().verify(user.password, req.password) + argon2.PasswordHasher().verify(account.password, req.password) except argon2.exceptions.VerifyMismatchError: res.Result = res.FAIL_PASSWORD return res # Returns the respective SS2C_ACCOUNT_LOGIN_RES *__BAN_USER ban enum. - if user.ban_type: - res.Result = user.ban_type + if account.ban_type: + res.Result = account.ban_type return res - res.accountId = str(user.id) - info = SLOGIN_ACCOUNT_INFO(AccountID=str(user.id)) + res.accountId = str(account.id) + info = SLOGIN_ACCOUNT_INFO(AccountID=str(account.id)) res.AccountInfo.CopyFrom(info) - # Set the user object in session to indicate authentication and for further access. - sessions[ctx.transport]["user"] = user + sessions[ctx.transport].account = account return res diff --git a/dndserver/handlers/merchant.py b/dndserver/handlers/merchant.py index 1f592d88..b5ee589b 100644 --- a/dndserver/handlers/merchant.py +++ b/dndserver/handlers/merchant.py @@ -44,10 +44,10 @@ def get_buy_list(ctx, msg): # TODO: handle every trader differently # send stage 1 (start) - ctx.send(SS2C_MERCHANT_STOCK_BUY_ITEM_LIST_RES(result=1, loopMessageFlag=1, stockList=[])) + ctx.reply(SS2C_MERCHANT_STOCK_BUY_ITEM_LIST_RES(result=1, loopMessageFlag=1, stockList=[])) # send stage 2 (data the merchant is selling) - ctx.send( + ctx.reply( SS2C_MERCHANT_STOCK_BUY_ITEM_LIST_RES( result=1, loopMessageFlag=2, @@ -101,10 +101,10 @@ def get_sellback_list(ctx, msg): # merchant it also sends the sellbacks for the weaponsmith. # send stage 1 (start) - ctx.send(SS2C_MERCHANT_STOCK_SELL_BACK_ITEM_LIST_RES(result=1, loopMessageFlag=1, stockList=[])) + ctx.reply(SS2C_MERCHANT_STOCK_SELL_BACK_ITEM_LIST_RES(result=1, loopMessageFlag=1, stockList=[])) # send stage 2 (data the merchant is selling) - ctx.send( + ctx.reply( SS2C_MERCHANT_STOCK_SELL_BACK_ITEM_LIST_RES( result=1, loopMessageFlag=2, diff --git a/dndserver/handlers/party.py b/dndserver/handlers/party.py new file mode 100644 index 00000000..8106d613 --- /dev/null +++ b/dndserver/handlers/party.py @@ -0,0 +1,154 @@ +from loguru import logger + +from dndserver.enums import CharacterClass, Gender +from dndserver.objects.items import (generate_helm, generate_torch, generate_roundshield, + generate_lantern, generate_sword, generate_pants, + generate_tunic, generate_bandage) +from dndserver.protos.Character import SACCOUNT_NICKNAME, SCHARACTER_PARTY_INFO +from dndserver.protos.Party import (SS2C_PARTY_INVITE_NOT, SC2S_PARTY_INVITE_REQ, SC2S_PARTY_INVITE_ANSWER_REQ, + SS2C_PARTY_INVITE_RES, SS2C_PARTY_INVITE_ANSWER_RES, + SS2C_PARTY_INVITE_ANSWER_RESULT_NOT, SS2C_PARTY_MEMBER_INFO_NOT) +from dndserver.protos import PacketCommand as pc +from dndserver.sessions import sessions +from dndserver.utils import get_party_by_account_id, get_user_by_account_id, get_user_by_nickname, make_header + + +def party_invite(ctx, msg): + """Occurs when a user sends a party to another user.""" + # message SC2S_PARTY_INVITE_REQ { + # .DC.Packet.SACCOUNT_NICKNAME findNickName = 1; + # string findAccountId = 2; + # string findCharacterId = 3; + # } + req = SC2S_PARTY_INVITE_REQ() + req.ParseFromString(msg) + res = SS2C_PARTY_INVITE_RES(result=pc.SUCCESS) + send_invite_notification(ctx, req) + return res + + +def accept_invite(ctx, msg): + """Occurs when a user accepts a party invite.""" + # req.returnAccountId == inviter + req = SC2S_PARTY_INVITE_ANSWER_REQ() + req.ParseFromString(msg) + logger.debug(req) + + res = SS2C_PARTY_INVITE_ANSWER_RES(result=pc.SUCCESS) + logger.debug(res) + + # send a notification to the inviter that the invitee accepted + send_accept_notification(ctx, req) + + # delete empty party if the user joining the party was the only member + if len(sessions[ctx.transport].party.players) == 1: + del sessions[ctx.transport].party + + # add user to the inviters party object + party = get_party_by_account_id(int(req.returnAccountId)) # todo: we're storing the first player as a transport and the next as a user object + party.add_member(sessions[ctx.transport]) + + # set the invitees party to the inviters party + sessions[ctx.transport].party = party + + for user in party.players: + send_party_info_notification(party, user) + + return res + + +def send_invite_notification(ctx, req): + notify = SS2C_PARTY_INVITE_NOT( + InviteeNickName=SACCOUNT_NICKNAME( + originalNickName=sessions[ctx.transport].character.nickname, + streamingModeNickName=sessions[ctx.transport].character.streaming_nickname, + karmaRating=sessions[ctx.transport].character.karma_rating + ), + InviteeAccountId=str(sessions[ctx.transport].account.id), + InviteeCharacterId=str(sessions[ctx.transport].character.id) + ) + + # TODO: This can probably be refactored in a cleaner way in protocol.py. + header = make_header(notify) + transport, _ = get_user_by_nickname(nickname=req.findNickName.originalNickName) + transport.write(header + notify.SerializeToString()) + + +def send_accept_notification(ctx, req): + transport, _ = get_user_by_account_id(int(req.returnAccountId)) + notify = SS2C_PARTY_INVITE_ANSWER_RESULT_NOT( + nickName=SACCOUNT_NICKNAME( + originalNickName=sessions[ctx.transport].character.nickname, + streamingModeNickName=sessions[ctx.transport].character.streaming_nickname, + karmaRating=sessions[ctx.transport].character.karma_rating + ), + inviteResult=pc.SUCCESS + ) + header = make_header(notify) + transport.write(header + notify.SerializeToString()) + + +def send_party_info_notification(party, user): + # message SS2C_PARTY_MEMBER_INFO_NOT { + # repeated .DC.Packet.SCHARACTER_PARTY_INFO playPartyUserInfoData = 1; + # } + + # message SCHARACTER_PARTY_INFO { + # string accountId = 1; + # .DC.Packet.SACCOUNT_NICKNAME nickName = 2; + # string characterClass = 3; + # string characterId = 4; + # uint32 gender = 5; + # uint32 level = 6; + # uint32 isPartyLeader = 7; + # uint32 isReady = 8; + # uint32 isInGame = 9; + # repeated .DC.Packet.SItem equipItemList = 10; + # uint32 partyIdx = 11; + # } + + notify = SS2C_PARTY_MEMBER_INFO_NOT() + for user in party.players: + nick = SACCOUNT_NICKNAME( + originalNickName=user.character.nickname, + streamingModeNickName=user.character.streaming_nickname, + karmaRating=user.character.karma_rating + ) + info = SCHARACTER_PARTY_INFO() + info.accountId = str(user.account.id) + info.nickName.CopyFrom(nick) + info.characterClass = CharacterClass(user.character.character_class).value + info.characterId = str(user.character.id) + info.gender = Gender(user.character.gender).value + info.level = user.character.level + info.isPartyLeader = True if party.leader == user else False + info.isReady = 0 # Need to unhardcode these 2 + info.isInGame = 0 + info.equipItemList.extend([ + generate_helm(), generate_torch(), generate_roundshield(), + generate_lantern(), generate_sword(), generate_pants(), + generate_tunic(), generate_bandage() + ]) + info.partyIdx = party.id + notify.playPartyUserInfoData.append(info) + + header = make_header(notify) + for user in party.players: + transport, _ = get_user_by_account_id(user.account.id) + transport.write(header + notify.SerializeToString()) + # header = make_header(notify) + # transport.write(header + notify.SerializeToString()) + + # SCHARACTER_PARTY_INFO { + # string accountId = 1; + # .DC.Packet.SACCOUNT_NICKNAME nickName = 2; + # string characterClass = 3; + # string characterId = 4; + # uint32 gender = 5; + # uint32 level = 6; + # uint32 isPartyLeader = 7; + # uint32 isReady = 8; + # uint32 isInGame = 9; + # repeated .DC.Packet.SItem equipItemList = 10; + # uint32 partyIdx = 11; + # } \ No newline at end of file diff --git a/dndserver/models.py b/dndserver/models.py index 395e6365..403bddd2 100644 --- a/dndserver/models.py +++ b/dndserver/models.py @@ -1,3 +1,5 @@ +import random + import arrow from sqlalchemy import Column from sqlalchemy.ext.declarative import declarative_base @@ -37,6 +39,7 @@ class Character(base): created_at = Column(ArrowType, default=arrow.utcnow()) level = Column(Integer, default=1) karma_rating = Column(Integer, default=0) + streaming_nickname = Column(String(15), default=f"Fighter#{random.randrange(1000000, 1700000)}") def save(self): db.add(self) diff --git a/dndserver/objects/party.py b/dndserver/objects/party.py new file mode 100644 index 00000000..54b8d551 --- /dev/null +++ b/dndserver/objects/party.py @@ -0,0 +1,20 @@ +from dndserver.objects.user import User + + +class Party(): + def __init__(self, _id: int, player_1: User) -> None: + self.id = _id + self.players = [player_1] + self.leader = player_1 + + def add_member(self, user: User): + """Add member to the party.""" + if len(self.players) == 3: + raise Exception("No room in that party for any more players.") + self.players.append(user) + + def remove_member(self, user: User): + """Remove member from the party.""" + if user not in self.players: + raise Exception("User is not in that party.") + self.players.remove(user) diff --git a/dndserver/objects/user.py b/dndserver/objects/user.py new file mode 100644 index 00000000..4e3e9d52 --- /dev/null +++ b/dndserver/objects/user.py @@ -0,0 +1,6 @@ +class User(): + def __init__(self) -> None: + self.account = {} + self.character = {} + self.party = {} + self.state = {} diff --git a/dndserver/parties.py b/dndserver/parties.py new file mode 100644 index 00000000..9107ae0c --- /dev/null +++ b/dndserver/parties.py @@ -0,0 +1 @@ +parties = {} diff --git a/dndserver/protocol.py b/dndserver/protocol.py index 5af810c4..709dc921 100644 --- a/dndserver/protocol.py +++ b/dndserver/protocol.py @@ -3,9 +3,11 @@ from loguru import logger from twisted.internet.protocol import Factory, Protocol -from dndserver.handlers import character, friends, lobby, login, trade, menu, merchant +from dndserver.handlers import character, friends, lobby, login, party, trade, menu, merchant +from dndserver.objects.user import User from dndserver.protos import PacketCommand as pc from dndserver.sessions import sessions +from dndserver.utils import make_header class GameFactory(Factory): @@ -20,7 +22,8 @@ def __init__(self) -> None: def connectionMade(self) -> None: """Event for when a client connects to the server.""" logger.debug(f"Received connection from: {self.transport.client[0]}:{self.transport.client[1]}") - sessions[self.transport] = {"user": None} + user = User() + sessions[self.transport] = user def connectionLost(self, reason): """Event for when a client disconnects from the server.""" @@ -46,16 +49,15 @@ def dataReceived(self, data: bytes) -> None: pc.C2S_CUSTOMIZE_CHARACTER_INFO_REQ: character.character_info, pc.C2S_INVENTORY_SINGLE_UPDATE_REQ: character.move_item, pc.C2S_LOBBY_ENTER_REQ: lobby.enter_lobby, - pc.C2S_OPEN_LOBBY_MAP_REQ: lobby.open_lobby_map, pc.C2S_CHARACTER_SELECT_ENTER_REQ: lobby.enter_character_select, - pc.C2S_LOBBY_GAME_DIFFICULTY_SELECT_REQ: lobby.map_select, pc.C2S_FRIEND_LIST_ALL_REQ: friends.list_friends, pc.C2S_FRIEND_FIND_REQ: friends.find_user, - pc.C2S_PARTY_INVITE_REQ: friends.party_invite, pc.C2S_META_LOCATION_REQ: menu.process_location, pc.C2S_MERCHANT_LIST_REQ: merchant.get_merchant_list, pc.C2S_MERCHANT_STOCK_BUY_ITEM_LIST_REQ: merchant.get_buy_list, pc.C2S_MERCHANT_STOCK_SELL_BACK_ITEM_LIST_REQ: merchant.get_sellback_list, + pc.C2S_PARTY_INVITE_REQ: party.party_invite, + pc.C2S_PARTY_INVITE_ANSWER_REQ: party.accept_invite, pc.C2S_TRADE_MEMBERSHIP_REQUIREMENT_REQ: trade.get_trade_reqs, pc.C2S_TRADE_MEMBERSHIP_REQ: trade.process_membership, } @@ -68,7 +70,7 @@ def dataReceived(self, data: bytes) -> None: return self.heartbeat() res = handlers[handler[0]](self, msg) - self.send(res) + self.reply(msg=res) # remove the data we have processed data = data[length:] @@ -77,13 +79,12 @@ def heartbeat(self): """Send a D&D keepalive packet.""" self.transport.write(pc.SS2C_ALIVE_RES().SerializeToString()) - def make_header(self, msg: bytes): - """Create a D&D packet header.""" - # header: 00 00 00 00 - packet_type = type(msg).__name__.replace("SS2C", "S2C").replace("SC2S", "C2S") - return struct.pack(" 00 00 00 00 + packet_type = type(msg).__name__.replace("SS2C", "S2C").replace("SC2S", "C2S") + return struct.pack(" Date: Fri, 28 Apr 2023 05:22:11 +0100 Subject: [PATCH 06/15] Return fix --- dndserver/handlers/character.py | 1 + 1 file changed, 1 insertion(+) diff --git a/dndserver/handlers/character.py b/dndserver/handlers/character.py index 5b3b52dc..9dfbf018 100644 --- a/dndserver/handlers/character.py +++ b/dndserver/handlers/character.py @@ -162,6 +162,7 @@ def character_info(ctx, msg): ], ), ) + return res def get_experience(ctx, msg): From e04784c1bdbb43e8cf37f99abb03c9c0a300afc8 Mon Sep 17 00:00:00 2001 From: itzandroidtab Date: Fri, 12 May 2023 22:19:47 +0200 Subject: [PATCH 07/15] added requirements for trade membership --- dndserver/handlers/trade.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/dndserver/handlers/trade.py b/dndserver/handlers/trade.py index ff410ab3..21e1a9e0 100644 --- a/dndserver/handlers/trade.py +++ b/dndserver/handlers/trade.py @@ -13,12 +13,23 @@ ) from dndserver.protos.Chat import SCHATDATA, SCHATDATA_PIECE, SCHATDATA_PIECE_ITEM, SCHATDATA_PIECE_ITEM_PROPERTY from dndserver.protos.Character import SACCOUNT_NICKNAME +from dndserver.protos.Defines import Define_Trade def get_trade_reqs(ctx, msg): return SS2C_TRADE_MEMBERSHIP_REQUIREMENT_RES( # TODO: Unsure what these values are actually supposed to look like. - requirements=[STRADE_MEMBERSHIP_REQUIREMENT(memberShipType=1, memberShipValue=1)] + requirements=[ + STRADE_MEMBERSHIP_REQUIREMENT( + memberShipType=Define_Trade.Requirement_Type.MINIMUM_LEVEL, memberShipValue=5 + ), + STRADE_MEMBERSHIP_REQUIREMENT( + memberShipType=Define_Trade.Requirement_Type.INITIATION_FEE, memberShipValue=0 + ), + STRADE_MEMBERSHIP_REQUIREMENT( + memberShipType=Define_Trade.Requirement_Type.COST_PER_TRADE, memberShipValue=15 + ), + ] ) From 1b5c952ca497042a6ab3d6adbd0f0f11652c0503 Mon Sep 17 00:00:00 2001 From: itzandroidtab Date: Fri, 12 May 2023 22:20:01 +0200 Subject: [PATCH 08/15] bugfix merge with map select --- dndserver/handlers/lobby.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dndserver/handlers/lobby.py b/dndserver/handlers/lobby.py index 284a7252..e1c65571 100644 --- a/dndserver/handlers/lobby.py +++ b/dndserver/handlers/lobby.py @@ -71,5 +71,5 @@ def open_map_select(ctx, msg): req = SC2S_OPEN_LOBBY_MAP_REQ() req.ParseFromString(msg) - res = SS2C_OPEN_LOBBY_MAP_RES(result=pc.SUCCESS) + res = SS2C_OPEN_LOBBY_MAP_RES() return res From afa2ed082454f60f6f8a7419017963135cda49bc Mon Sep 17 00:00:00 2001 From: itzandroidtab Date: Tue, 16 May 2023 23:24:03 +0200 Subject: [PATCH 09/15] added basic trader support --- dndserver/handlers/trade.py | 744 +++++++++++++++++++++++++++++++++--- dndserver/models.py | 1 + dndserver/objects/trade.py | 15 + dndserver/persistent.py | 1 + dndserver/protocol.py | 21 +- dndserver/server.py | 1 + 6 files changed, 734 insertions(+), 49 deletions(-) create mode 100644 dndserver/objects/trade.py diff --git a/dndserver/handlers/trade.py b/dndserver/handlers/trade.py index 21e1a9e0..15083f5c 100644 --- a/dndserver/handlers/trade.py +++ b/dndserver/handlers/trade.py @@ -1,3 +1,10 @@ +import arrow + +from dndserver.database import db +from dndserver.handlers import inventory +from dndserver.handlers import character as HCharacter +from dndserver.models import Item, ItemAttribute +from dndserver.persistent import sessions from dndserver.protos import PacketCommand as pc from dndserver.protos.Trade import ( SS2C_TRADE_MEMBERSHIP_REQUIREMENT_RES, @@ -5,94 +12,743 @@ SS2C_TRADE_MEMBERSHIP_RES, SS2C_TRADE_CHANNEL_LIST_RES, STRADE_CHANNEL, + SC2S_TRADE_CHANNEL_SELECT_REQ, SS2C_TRADE_CHANNEL_SELECT_RES, SS2C_TRADE_CHANNEL_CHAT_RES, STRADE_CHAT_S2C, SC2S_TRADE_CHANNEL_CHAT_REQ, SS2C_TRADE_CHANNEL_EXIT_RES, + SC2S_TRADE_REQUEST_REQ, + SS2C_TRADE_REQUEST_RES, + SS2C_TRADE_REQUEST_NOT, + SC2S_TRADE_ANSWER_REQ, + SS2C_TRADE_ANSWER_RES, + SS2C_TRADE_ANSWER_REFUSAL_NOT, + SS2C_TRADING_BEGIN_NOT, + STRADING_USER_INFO, + SC2S_TRADING_ITEM_UPDATE_REQ, + SS2C_TRADING_ITEM_UPDATE_RES, + SC2S_TRADING_CHAT_REQ, + SS2C_TRADING_CHAT_RES, + SS2C_TRADING_CLOSE_RES, + SS2C_TRADE_CHANNEL_USER_UPDATE_NOT, + STRADE_CHANNEL_USER_UPDATE_INFO, + SC2S_TRADING_READY_REQ, + SS2C_TRADING_READY_RES, + SS2C_TRADING_READY_NOT, + SS2C_TRADING_CONFIRM_NOT, + SS2C_TRADING_CONFIRM_CANCEL_RES, + SC2S_TRADING_CONFIRM_READY_REQ, + SS2C_TRADING_CONFIRM_READY_RES, + SS2C_TRADING_CONFIRM_READY_NOT, + SS2C_TRADING_RESULT_NOT, ) from dndserver.protos.Chat import SCHATDATA, SCHATDATA_PIECE, SCHATDATA_PIECE_ITEM, SCHATDATA_PIECE_ITEM_PROPERTY +from dndserver.protos.Character import SCHARACTER_TRADE_INFO from dndserver.protos.Character import SACCOUNT_NICKNAME -from dndserver.protos.Defines import Define_Trade +from dndserver.protos.Defines import Define_Trade, Define_Message, Define_Item +from dndserver.objects.trade import Trade, TradeParty +from dndserver.persistent import trades +from dndserver.enums import classes + +trade_fee = 15 +initial_trade_fee = 25 + +channel_names = ["Fighter", "Barbarian", "Rogue", "Ranger", "Wizard", "Cleric", "Bard", "Utility", "Misc"] +channels = [] + +# create the channels on startup +for index, name in enumerate(channel_names): + channels.append({"name": name, "index": index + 1, "clients": []}) + + +def get_current_channel(ctx): + """Helper function to get the current channel the user is in""" + for ch in channels: + if ctx in ch["clients"]: + return ch + + return None + + +def get_user_in_channel(channel, account_id): + """Helper function to find the transport of a account id""" + # search for the other player we want to send the notification to + for client in channel["clients"]: + if sessions[client.transport].account.id != int(account_id): + continue + + return client + + return None + + +def find_trade(ctx): + """Helper function to find the current trade""" + for trade in trades: + if trade.user0.ctx == ctx: + return (trade, trade.user0, trade.user1) + elif trade.user1.ctx == ctx: + return (trade, trade.user1, trade.user0) + + return (None, None, None) + + +def broadcast_chat(ctx, msg): + """Helper function to broadcast a chat message to all the participants in a channel""" + # Broadcast the message to other clients + res = SS2C_TRADE_CHANNEL_CHAT_RES(result=pc.SUCCESS, chats=msg) + + # Find the client's channel + channel = get_current_channel(ctx) + + if channel: + for client in channel["clients"]: + # Send the chat message to each client in the channel except the sender + if client != ctx: + client.reply(res) + + +def leave_channel(ctx, channel): + """Helper function to have a player 'leave' a channel""" + # remove the player from the client list + channel["clients"].remove(ctx) + char = sessions[ctx.transport].character + + # send a notification to all the players in the channel + for client in channel["clients"]: + client.reply( + SS2C_TRADE_CHANNEL_USER_UPDATE_NOT( + updates=[ + STRADE_CHANNEL_USER_UPDATE_INFO( + trader=get_trader_info(char, str(sessions[ctx.transport].account.id)), + updateFlag=Define_Message.UpdateFlag.DELETE, + ) + ] + ) + ) + + +def cleanup(ctx): + """Helper function to cleanup anything left when the client crashes or alt-f4s""" + # check if the user is in any active trades + trade, _, other = find_trade(ctx) + + # if we found a trade cancel everything + if trade: + other.ctx.reply(SS2C_TRADING_CONFIRM_CANCEL_RES(result=pc.SUCCESS)) + other.ctx.reply(SS2C_TRADING_CLOSE_RES(result=pc.SUCCESS)) + + # find the client's channel + channel = get_current_channel(ctx) + + if channel: + leave_channel(ctx, channel) + + +def get_trader_info(char, account_id): + """Helper function to create trader information from the character and account id""" + nickname = SACCOUNT_NICKNAME(originalNickName=char.nickname, streamingModeNickName=char.streaming_nickname) + + trader = SCHARACTER_TRADE_INFO() + trader.accountId = account_id + trader.nickName.CopyFrom(nickname) + trader.characterClass = classes.CharacterClass(char.character_class).value + trader.characterId = str(char.id) + trader.gender = classes.Gender(char.gender).value + trader.level = char.level + trader.characterLocation = 1 + + return trader + + +def get_trading_info(ctx): + """Helper function to get the trading info for a user""" + char = sessions[ctx.transport].character + nickname = SACCOUNT_NICKNAME(originalNickName=char.nickname, streamingModeNickName=char.streaming_nickname) + + trader = STRADING_USER_INFO() + trader.nickName.CopyFrom(nickname) + trader.accountId = str(sessions[ctx.transport].account.id) + + return trader + + +def create_chat_data(ctx, req): + """Helper function to create chat data""" + character = sessions[ctx.transport].character + + chat_str = req.chat.chatData.chatDataPieceArray[0].chatStr + uid = req.chat.chatData.chatDataPieceArray[0].chatDataPieceItem.uid + iid = req.chat.chatData.chatDataPieceArray[0].chatDataPieceItem.iid + pp_list = req.chat.chatData.chatDataPieceArray[0].chatDataPieceItem.pp + + property_list = [] + for pp in pp_list: + property_list.append(SCHATDATA_PIECE_ITEM_PROPERTY(pid=pp.pid, pv=pp.pv)) + + chat_piece_item_obj = SCHATDATA_PIECE_ITEM(uid=uid, iid=iid, pp=property_list) + + chat_piece = SCHATDATA_PIECE() + chat_piece.chatStr = chat_str + chat_piece.chatDataPieceItem.CopyFrom(chat_piece_item_obj) + + nickName = SACCOUNT_NICKNAME( + originalNickName=character.nickname, streamingModeNickName=character.streaming_nickname + ) + chat_data = SCHATDATA() + chat_data.accountId = str(sessions[ctx.transport].account.id) + chat_data.characterId = str(sessions[ctx.transport].character.id) + chat_data.nickname.CopyFrom(nickName) + chat_data.partyId = str(sessions[ctx.transport].party.id) + chat_data.chatDataPieceArray.append(chat_piece) + + return chat_data + + +def get_all_gold(character_id, exclude=[]): + """Helper that gets all the gold items in the inventory of a user""" + items = inventory.get_all_items(character_id) + gold_items = [] + total = 0 + + for item, _ in items: + # skip all the items that are not gold + if "Id_Item_GoldCoins" not in item.item_id and inventory.get_inv_limit(item.item_id) == 0: + continue + + if (item, []) in exclude: + continue + + # we have a item that contains/is gold. Add it to the amount and the item list + if inventory.get_inv_limit(item.item_id): + total += item.inv_count + else: + total += item.quantity + + gold_items.append(item) + + return total, gold_items + + +def has_gold_amount(character_id, amount, exclude=[]): + """Helper to check if a the user has at least amount of gold""" + total, _ = get_all_gold(character_id, exclude) + + return total >= amount + + +def deduct_gold(character_id, deduct_amount): + """Helper function to deduct gold from the user""" + total, items = get_all_gold(character_id) + + # check if we have enough gold + if total < deduct_amount: + return False + + # remove the gold from the inventory + for item in items: + # get the amount of gold the item has + count = item.inv_count if inventory.get_inv_limit(item.item_id) else item.quantity + + # get the amount we should remove from this item + quantity = min(count, deduct_amount) + deduct_amount -= quantity + + if not inventory.get_inv_limit(item.item_id): + # we have a stack. Remove the quantity from the item + item.quantity -= quantity + + # if we have a stack check if we still have something in the stack. Otherwise remove the item + if item.quantity == 0: + inventory.delete_item(character_id, item, True) + + else: + item.inv_count -= quantity + + # check if we have removed enough gold + if deduct_amount <= 0: + break + + return True + + +def get_empty_slot(character_id, size=(1, 1)): + """Helper function get a empty slot in the inventory""" + # get the items from the bag and sort them + items = inventory.get_all_items(character_id, Define_Item.InventoryId.BAG) + items.sort(key=lambda i: i[0].slot_id, reverse=True) + + for index, (item, _) in zip(range(50), items): + # TODO: get the size of the current item and use the size of the item we want to place + if index != item.slot_id: + return (Define_Item.InventoryId.BAG, index) + + # do the same thing for the storage + items = inventory.get_all_items(character_id, Define_Item.InventoryId.STORAGE).sort( + key=lambda i: i[0].slot_id, reverse=True + ) + + for index, (item, _) in zip(range(240), items): + # TODO: get the size of the current item and use the size of the item we want to place + if index != item.slot_id: + return (Define_Item.InventoryId.STORAGE, index) + + # we have no space return None + return (None, None) def get_trade_reqs(ctx, msg): + """Occurs when the user is not a trader and opens the trade screen""" return SS2C_TRADE_MEMBERSHIP_REQUIREMENT_RES( - # TODO: Unsure what these values are actually supposed to look like. requirements=[ STRADE_MEMBERSHIP_REQUIREMENT( memberShipType=Define_Trade.Requirement_Type.MINIMUM_LEVEL, memberShipValue=5 ), STRADE_MEMBERSHIP_REQUIREMENT( - memberShipType=Define_Trade.Requirement_Type.INITIATION_FEE, memberShipValue=0 + memberShipType=Define_Trade.Requirement_Type.INITIATION_FEE, memberShipValue=initial_trade_fee ), STRADE_MEMBERSHIP_REQUIREMENT( - memberShipType=Define_Trade.Requirement_Type.COST_PER_TRADE, memberShipValue=15 + memberShipType=Define_Trade.Requirement_Type.COST_PER_TRADE, memberShipValue=trade_fee ), ] ) -def get_trade_channels(ctx, msg): - return SS2C_TRADE_CHANNEL_LIST_RES( - isTrader=1, - channels=[ +def get_channels(ctx, msg): + """Occurs when the user is a trader and opens the trader screen""" + res = SS2C_TRADE_CHANNEL_LIST_RES() + res.isTrader = sessions[ctx.transport].character.is_trader + + # add all the available channels + for ch in channels: + res.channels.append( STRADE_CHANNEL( - index=1, - channelName="ChatRoomData:Id_ChatRoom_Trade_Barbarian", - memberCount=219, - roomType=1, + index=ch["index"], + channelName="ChatRoomData:Id_ChatRoom_Trade_" + ch["name"], + memberCount=len(ch["clients"]), + roomType=1, # TODO: change this to a room type (Define_Chat.RoomType) groupIndex=1, ) - ], - ) + ) + + return res + + +def select_channel(ctx, msg): + """Occurs when the user selects a trading channel""" + req = SC2S_TRADE_CHANNEL_SELECT_REQ() + req.ParseFromString(msg) + + # check if we have a valid channel + if req.index == 0 or req.index > len(channels): + return SS2C_TRADE_CHANNEL_SELECT_RES(result=pc.FAIL_TRADE_REQUEST_NOT_FOUND_CHANNEL) + + # do not update anything if we already have the client in the channel list (for some reason it sometimes + # sends this message 2x) + if ctx in channels[req.index - 1]["clients"]: + return SS2C_TRADE_CHANNEL_SELECT_RES(result=pc.SUCCESS) + + # send a notification to all the players in the channel + char = sessions[ctx.transport].character + for client in channels[req.index - 1]["clients"]: + client.reply( + SS2C_TRADE_CHANNEL_USER_UPDATE_NOT( + updates=[ + STRADE_CHANNEL_USER_UPDATE_INFO( + trader=get_trader_info(char, str(sessions[ctx.transport].account.id)), + updateFlag=Define_Message.UpdateFlag.INSERT, + ) + ] + ) + ) + # send all the current users to the new player + notify = SS2C_TRADE_CHANNEL_USER_UPDATE_NOT() + for client in channels[req.index - 1]["clients"]: + char = sessions[client.transport].character -def select_trade_channel(ctx, msg): + trader = STRADE_CHANNEL_USER_UPDATE_INFO( + trader=get_trader_info(char, str(sessions[client.transport].account.id)), + updateFlag=Define_Message.UpdateFlag.INSERT, + ) + notify.updates.append(trader) + + ctx.reply(notify) + + # add the user to the channel list + channels[req.index - 1]["clients"].append(ctx) return SS2C_TRADE_CHANNEL_SELECT_RES(result=pc.SUCCESS) -def chat_request(ctx, msg): - req = SC2S_TRADE_CHANNEL_CHAT_REQ() +def exit_channel(ctx, msg): + """Occurs when the player exits a channel""" + # find the client's channel + channel = get_current_channel(ctx) + + if not channel: + # return a success anyway. Do not block a exit + return SS2C_TRADE_CHANNEL_EXIT_RES(result=pc.SUCCESS) + + # leave the channel + leave_channel(ctx, channel) + + # send a delete for every character to the character that is leaving + notify = SS2C_TRADE_CHANNEL_USER_UPDATE_NOT() + for client in channel["clients"]: + char = sessions[client.transport].character + + trader = STRADE_CHANNEL_USER_UPDATE_INFO( + trader=get_trader_info(char, str(sessions[client.transport].account.id)), + updateFlag=Define_Message.UpdateFlag.DELETE, + ) + notify.updates.append(trader) + + ctx.reply(notify) + + return SS2C_TRADE_CHANNEL_EXIT_RES(result=pc.SUCCESS) + + +def trade_request(ctx, msg): + """Occurs when a trade request is send by a user""" + req = SC2S_TRADE_REQUEST_REQ() req.ParseFromString(msg) - chat_type = req.chat.chatType - # target_account_id = req.chat.targetAccountId #currently unused - chat_str = req.chat.chatData.chatDataPieceArray[0].chatStr - uid = req.chat.chatData.chatDataPieceArray[0].chatDataPieceItem.uid - iid = req.chat.chatData.chatDataPieceArray[0].chatDataPieceItem.iid - pp_list = req.chat.chatData.chatDataPieceArray[0].chatDataPieceItem.pp + # check if we have enough gold + if not has_gold_amount(sessions[ctx.transport].character.id, trade_fee): + return SS2C_TRADE_REQUEST_RES(result=pc.FAIL_TRADE_REQUIREMENT_SHORTAGE_GOLD) - property_list = [] - for pp in pp_list: - property_list.append(SCHATDATA_PIECE_ITEM_PROPERTY(pid=pp.pid, pv=pp.pv)) + # get the channel we are in + channel = get_current_channel(ctx) - chat_piece_item_obj = SCHATDATA_PIECE_ITEM(uid=uid, iid=iid, pp=property_list) + if channel is None: + return SS2C_TRADE_REQUEST_RES(result=pc.FAIL_GENERAL) - chat_piece = SCHATDATA_PIECE() - chat_piece.chatStr = chat_str - chat_piece.chatDataPieceItem.CopyFrom(chat_piece_item_obj) + # find the other user in the current channel + other = get_user_in_channel(channel, req.accountId) - nickname = SACCOUNT_NICKNAME(originalNickName="Krofty", streamingModeNickName="") - chat_data = SCHATDATA() - chat_data.accountId = "1" - chat_data.characterId = "1" - chat_data.nickname.CopyFrom(nickname) - chat_data.partyId = "1" - chat_data.chatDataPieceArray.append(chat_piece) + if other is None: + return SS2C_TRADE_REQUEST_RES(result=pc.FAIL_GENERAL) + + # check if the other has enough gold + if not has_gold_amount(sessions[other.transport].character.id, trade_fee): + return SS2C_TRADE_REQUEST_RES(result=pc.FAIL_TRADE_REQUIREMENT_SHORTAGE_GOLD) + + # send a trade notification to the player + notify = SS2C_TRADE_REQUEST_NOT() + notify.accountId = str(sessions[ctx.transport].account.id) + notify.nickName.CopyFrom(req.nickName) + other.reply(notify) + + return SS2C_TRADE_REQUEST_RES(result=pc.SUCCESS, requestNickName=req.nickName) + + +def cancel_trade(ctx, msg): + """Occurs when the user cancels a trade""" + # get the other player to send a stop message + trade, _, other = find_trade(ctx) + + if trade and other: + other.ctx.reply(SS2C_TRADING_CLOSE_RES(result=pc.SUCCESS)) + + # remove the trade + trades.remove(trade) + + return SS2C_TRADING_CLOSE_RES(result=pc.SUCCESS) + + +def ready(ctx, msg): + """Occurs when a user presses ready on the first trader menu""" + req = SC2S_TRADING_READY_REQ() + req.ParseFromString(msg) + + # get all the parties in the trade + trade, current, other = find_trade(ctx) + + if not all([trade, current, other]): + return SS2C_TRADING_READY_RES(result=pc.FAIL_GENERAL) + + # check if we have enough gold in our inventory + if req.isReady and not has_gold_amount(sessions[ctx.transport].character.id, trade_fee, current.inventory): + return SS2C_TRADING_READY_RES(result=pc.FAIL_TRADING_READY_SHORTAGE_GOLD) + + # send the ready change to all the parties + for client in [current, other]: + notify = SS2C_TRADING_READY_NOT() + notify.readyUserInfo.CopyFrom(get_trading_info(ctx)) + notify.isReady = req.isReady + + client.ctx.reply(notify) + + # update the internal state of the trade + current.is_ready = req.isReady + + # check if we need to send the confirm message + if current.is_ready and other.is_ready: + # clear the is ready flags to reuse them for the confirm message + current.is_ready = False + other.is_ready = False + + # TODO: add the correct items in the packet. For some reason the game accepts it without any + # issue if it doesnt have it. + notify = SS2C_TRADING_CONFIRM_NOT() + # notify.target = + # notify.mine = + + # send the confirm message to all parties + for client in [current, other]: + client.ctx.reply(notify) + + return SS2C_TRADING_READY_RES(result=pc.SUCCESS) + + +def confirm(ctx, msg): + """Occurs when a user presses confirm on the second trader menu""" + req = SC2S_TRADING_CONFIRM_READY_REQ() + req.ParseFromString(msg) + + # get all the parties in the trade + trade, current, other = find_trade(ctx) + + if not all([trade, current, other]): + return SS2C_TRADING_READY_RES(result=pc.FAIL_GENERAL) + + # send the ready change to all the parties + for client in [current, other]: + notify = SS2C_TRADING_CONFIRM_READY_NOT() + notify.readyUserInfo.CopyFrom(get_trading_info(ctx)) + notify.isReady = req.isReady + + client.ctx.reply(notify) + + # update the internal state of the trade + current.is_ready = req.isReady + ctx.reply(SS2C_TRADING_CONFIRM_READY_RES(result=pc.SUCCESS)) + + # check if we need to send the confirm message + if current.is_ready and other.is_ready: + notify = SS2C_TRADING_RESULT_NOT() + notify.result = pc.SUCCESS + + # remove the gold from the inventory of both parties + for client in [current, other]: + deduct_gold(sessions[client.ctx.transport].character.id, trade_fee) + + # send the confirm message to all parties + for client in [current, other]: + client.ctx.reply(notify) + + # TODO: This should be done proper + # change the owner of the items and move the item to a empty location + for item in current.inventory: + inventory_id, slot_id = get_empty_slot(sessions[other.ctx.transport].character.id) + + item[0].character_id = sessions[other.ctx.transport].character.id + item[0].location_id = inventory_id + item[0].slot_id = slot_id + for item in other.inventory: + inventory_id, slot_id = get_empty_slot(sessions[other.ctx.transport].character.id) + + item[0].character_id = sessions[current.ctx.transport].character.id + item[0].location_id = inventory_id + item[0].slot_id = slot_id + + # update the items on both sides + for client in [current, other]: + client.ctx.reply(HCharacter.character_info(client.ctx, bytearray())) + + # exit the trade after we have updated everything + for client in [current, other]: + client.ctx.reply(SS2C_TRADING_CLOSE_RES(result=pc.SUCCESS)) + + # remove the trade from the trade list + trades.remove(trade) + + return None + + +def cancel_confirm(ctx, msg): + """Occurs when a user cancels a trade in the confirm stage""" + # get the current trade + _, current, other = find_trade(ctx) + + # send the cancel to the other party + if other: + other.ctx.reply(SS2C_TRADING_CONFIRM_CANCEL_RES(result=pc.SUCCESS)) + + # clear the ready state of both parties + current.is_ready = False + other.is_ready = False + + return SS2C_TRADING_CONFIRM_CANCEL_RES(result=pc.SUCCESS) + + +def accept_invite(ctx, msg): + """Occurs when the user accepts/refuses a trading invite""" + req = SC2S_TRADE_ANSWER_REQ() + req.ParseFromString(msg) + + # get the channel we are in + channel = get_current_channel(ctx) + + if channel is None: + return SS2C_TRADE_REQUEST_RES(result=pc.FAIL_GENERAL) + + # find the player the request came from + other = get_user_in_channel(channel, req.accountId) + + if other is None: + return SS2C_TRADE_ANSWER_RES(result=pc.FAIL_GENERAL) + + # send the notification to the other player + if req.selectFlag != Define_Message.SelectFlag.OK: + notify = SS2C_TRADE_ANSWER_REFUSAL_NOT() + notify.accountId = str(sessions[ctx.transport].account.id) + notify.nickName.CopyFrom(req.nickName) + + other.reply(notify) + else: + # add the users to the trading dictionary + trade = Trade(TradeParty(ctx), TradeParty(other)) + trades.append(trade) + + # get the other party + target_info = STRADING_USER_INFO() + target_info.nickName.CopyFrom(req.nickName) + target_info.accountId = req.accountId + + # create the notification + notify = SS2C_TRADING_BEGIN_NOT() + notify.target.CopyFrom(target_info) + notify.mine.CopyFrom(get_trading_info(ctx)) + notify.tradeFee = trade_fee + + # TODO: get the actual reset time + notify.moveResetTimeSec = 3 + + # send the notification to all the players + for client in [ctx, other]: + client.reply(notify) + + return SS2C_TRADE_ANSWER_RES(result=pc.SUCCESS) + + +def move_item(ctx, msg): + """Occurs when the user moves items in/out of the trading inventory""" + req = SC2S_TRADING_ITEM_UPDATE_REQ() + req.ParseFromString(msg) + + # get the other client + _, current, other = find_trade(ctx) + + if other is None: + return SS2C_TRADING_ITEM_UPDATE_RES(result=pc.FAIL_GENERAL) + + char = sessions[ctx.transport].character + + # get the item at the location + item = db.query(Item).filter_by(character_id=char.id).filter_by(id=req.uniqueId).first() + + if item is None: + return SS2C_TRADING_ITEM_UPDATE_RES(result=pc.FAIL_GENERAL) + + attributes = db.query(ItemAttribute).filter_by(item_id=item.id).all() + + # update the trade inventory with the item + if req.updateFlag == Define_Message.UpdateFlag.INSERT: + current.inventory.append((item, attributes)) + elif req.updateFlag == Define_Message.UpdateFlag.DELETE: + current.inventory.remove((item, attributes)) + + # clear the states of all users and send a notification + current.is_ready = False + other.is_ready = False + + # send the ready change to all the parties + for client in [current, other]: + notify = SS2C_TRADING_READY_NOT() + notify.readyUserInfo.CopyFrom(get_trading_info(client.ctx)) + notify.isReady = client.is_ready + + for client in [current, other]: + client.ctx.reply(notify) + + # create the proto item with the correct slot id + pItem = HCharacter.item_to_proto_item(item, attributes) + pItem.slotId = req.slotId + + res = SS2C_TRADING_ITEM_UPDATE_RES() + res.result = pc.SUCCESS + res.updateUserInfo.CopyFrom(get_trading_info(ctx)) + res.updateFlag = req.updateFlag + res.updateItem.CopyFrom(pItem) + + # send the response to the other client + other.ctx.reply(res) + + # send the response to the client + return res + + +def private_chat(ctx, msg): + """Occurs when the user sends a message in the trading chat""" + req = SC2S_TRADING_CHAT_REQ() + req.ParseFromString(msg) + + # search for the current trade we are doing + trade, _, other = find_trade(ctx) + + chat_trade = STRADE_CHAT_S2C() + chat_trade.index = trade.id + chat_trade.chatType = req.chat.chatType + chat_trade.time = int(arrow.utcnow().timestamp()) + chat_trade.chatData.CopyFrom(create_chat_data(ctx, req)) + + # check if we found it + if all([trade, other]): + # send the message to the other party + other.ctx.reply(SS2C_TRADING_CHAT_RES(result=pc.SUCCESS, chat=chat_trade)) + + return None + + +def chat(ctx, msg): + """Occurs when a user sends a message in the channel chat""" + req = SC2S_TRADE_CHANNEL_CHAT_REQ() + req.ParseFromString(msg) chat_trade = STRADE_CHAT_S2C() chat_trade.index = 1 - chat_trade.chatType = chat_type - chat_trade.time = 1 - chat_trade.chatData.CopyFrom(chat_data) + chat_trade.chatType = req.chat.chatType + chat_trade.time = int(arrow.utcnow().timestamp()) + chat_trade.chatData.CopyFrom(create_chat_data(ctx, req)) + + # Broadcast the message to other clients + broadcast_chat(ctx, [chat_trade]) return SS2C_TRADE_CHANNEL_CHAT_RES(result=pc.SUCCESS, chats=[chat_trade]) -def exit(ctx, msg): - return SS2C_TRADE_CHANNEL_EXIT_RES(result=pc.SUCCESS) +def process_membership(ctx, msg): + char = sessions[ctx.transport].character + # check if the level of the user is high enough + if char.level < 5: + return SS2C_TRADE_MEMBERSHIP_RES(result=pc.FAIL_TRADE_REQUIREMENT_SHORTAGE_LV) -def process_membership(ctx, msg): - return SS2C_TRADE_MEMBERSHIP_RES(result=pc.SUCCESS) + # check if the user is a trader already + if char.is_trader: + return SS2C_TRADE_MEMBERSHIP_RES(result=pc.FAIL_TRADE_ALREADY_MEMBERSHIP) + + # Try to deduct the gold. Send a error if the user does not have enough gold + if not deduct_gold(char.id, initial_trade_fee): + return SS2C_TRADE_MEMBERSHIP_RES(result=pc.FAIL_TRADE_REQUIREMENT_SHORTAGE_GOLD) + + # the user has become a trader. Update in the database + sessions[ctx.transport].character.is_trader = True + + # send the return state + ctx.reply(SS2C_TRADE_MEMBERSHIP_RES(result=pc.SUCCESS)) + + # update the player inventory + return HCharacter.character_info(ctx, bytearray()) diff --git a/dndserver/models.py b/dndserver/models.py index c33681bf..1a3e60ef 100644 --- a/dndserver/models.py +++ b/dndserver/models.py @@ -40,6 +40,7 @@ class Character(base): experience = Column(Integer, default=0) karma_rating = Column(Integer, default=0) streaming_nickname = Column(String(15)) + is_trader = Column(Boolean, default=False) perk0 = Column(String, default="") perk1 = Column(String, default="") diff --git a/dndserver/objects/trade.py b/dndserver/objects/trade.py new file mode 100644 index 00000000..854b404e --- /dev/null +++ b/dndserver/objects/trade.py @@ -0,0 +1,15 @@ +from dndserver.persistent import trades + + +class TradeParty: + def __init__(self, ctx=None) -> None: + self.ctx = ctx + self.is_ready = False + self.inventory = [] + + +class Trade: + def __init__(self, user0=TradeParty(), user1=TradeParty()) -> None: + self.id = len(trades) + 1 + self.user0 = user0 + self.user1 = user1 diff --git a/dndserver/persistent.py b/dndserver/persistent.py index a5b5cfac..a9c0578f 100644 --- a/dndserver/persistent.py +++ b/dndserver/persistent.py @@ -1,2 +1,3 @@ parties = [] sessions = {} +trades = [] diff --git a/dndserver/protocol.py b/dndserver/protocol.py index f15685ba..c8429742 100644 --- a/dndserver/protocol.py +++ b/dndserver/protocol.py @@ -44,6 +44,7 @@ def connectionLost(self, reason): # cleanup anything left behind from the gathering hall gatheringhall.cleanup(self) + trade.cleanup(self) del sessions[self.transport] @@ -108,14 +109,22 @@ def dataReceived(self, data: bytes) -> None: pc.C2S_PARTY_INVITE_REQ: party.party_invite, pc.C2S_PARTY_EXIT_REQ: party.leave_party, pc.C2S_PARTY_INVITE_ANSWER_REQ: party.accept_invite, - pc.C2S_TRADE_CHANNEL_CHAT_REQ: trade.chat_request, - pc.C2S_TRADE_CHANNEL_EXIT_REQ: trade.exit, - pc.C2S_TRADE_CHANNEL_LIST_REQ: trade.get_trade_channels, - pc.C2S_TRADE_CHANNEL_SELECT_REQ: trade.select_trade_channel, + pc.C2S_TRADE_CHANNEL_CHAT_REQ: trade.chat, + pc.C2S_TRADE_CHANNEL_EXIT_REQ: trade.exit_channel, + pc.C2S_TRADE_CHANNEL_LIST_REQ: trade.get_channels, + pc.C2S_TRADE_CHANNEL_SELECT_REQ: trade.select_channel, pc.C2S_PARTY_READY_REQ: party.set_ready_state, pc.C2S_PARTY_MEMBER_KICK_REQ: party.kick_member, pc.C2S_TRADE_MEMBERSHIP_REQUIREMENT_REQ: trade.get_trade_reqs, pc.C2S_TRADE_MEMBERSHIP_REQ: trade.process_membership, + pc.C2S_TRADE_REQUEST_REQ: trade.trade_request, + pc.C2S_TRADE_ANSWER_REQ: trade.accept_invite, + pc.C2S_TRADING_CHAT_REQ: trade.private_chat, + pc.C2S_TRADING_CLOSE_REQ: trade.cancel_trade, + pc.C2S_TRADING_ITEM_UPDATE_REQ: trade.move_item, + pc.C2S_TRADING_READY_REQ: trade.ready, + pc.C2S_TRADING_CONFIRM_CANCEL_REQ: trade.cancel_confirm, + pc.C2S_TRADING_CONFIRM_READY_REQ: trade.confirm, pc.C2S_RANKING_RANGE_REQ: ranking.get_ranking, pc.C2S_RANKING_CHARACTER_REQ: ranking.get_character_ranking, pc.C2S_GATHERING_HALL_CHANNEL_CHAT_REQ: gatheringhall.chat, @@ -133,7 +142,9 @@ def dataReceived(self, data: bytes) -> None: return self.heartbeat() res = handlers[handler[0]](self, msg) - self.reply(msg=res) + + if res is not None: + self.reply(msg=res) def heartbeat(self): """Send a D&D keepalive packet.""" diff --git a/dndserver/server.py b/dndserver/server.py index c3979070..fe4a8ead 100644 --- a/dndserver/server.py +++ b/dndserver/server.py @@ -10,6 +10,7 @@ from dndserver.protocol import GameFactory from dndserver.console import console + async def main(): """Entrypoint where the server first initializes""" signal.signal(signal.SIGINT, signal.default_int_handler) From c55d7f0349abeebb2db21616c920dbe6966b3a6d Mon Sep 17 00:00:00 2001 From: itzandroidtab Date: Tue, 16 May 2023 23:24:12 +0200 Subject: [PATCH 10/15] added alembic file --- ..._added_flag_for_if_the_user_is_a_trader.py | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 alembic/versions/5f041eb74995_added_flag_for_if_the_user_is_a_trader.py diff --git a/alembic/versions/5f041eb74995_added_flag_for_if_the_user_is_a_trader.py b/alembic/versions/5f041eb74995_added_flag_for_if_the_user_is_a_trader.py new file mode 100644 index 00000000..0c8159b1 --- /dev/null +++ b/alembic/versions/5f041eb74995_added_flag_for_if_the_user_is_a_trader.py @@ -0,0 +1,33 @@ +"""Added flag for if the user is a trader + +Revision ID: 5f041eb74995 +Revises: b53c793562c7 +Create Date: 2023-05-16 21:27:28.795934 + +""" +from alembic import op +import sqlalchemy as sa +import sqlalchemy_utils + + +# revision identifiers, used by Alembic. +revision = "5f041eb74995" +down_revision = "b53c793562c7" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table("characters", schema=None) as batch_op: + batch_op.add_column(sa.Column("is_trader", sa.Boolean(), nullable=True, server_default="0")) + + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table("characters", schema=None) as batch_op: + batch_op.drop_column("is_trader") + + # ### end Alembic commands ### From aebd4b00833eb1d441b331140a160bce2d8a02bd Mon Sep 17 00:00:00 2001 From: itzandroidtab Date: Tue, 16 May 2023 23:42:21 +0200 Subject: [PATCH 11/15] added missing docstring --- dndserver/handlers/trade.py | 1 + 1 file changed, 1 insertion(+) diff --git a/dndserver/handlers/trade.py b/dndserver/handlers/trade.py index 15083f5c..b5370081 100644 --- a/dndserver/handlers/trade.py +++ b/dndserver/handlers/trade.py @@ -730,6 +730,7 @@ def chat(ctx, msg): def process_membership(ctx, msg): + """Occurs when the user tries to become a trader""" char = sessions[ctx.transport].character # check if the level of the user is high enough From 01cfe64a71571c7c7706db7676819274a5b1ca69 Mon Sep 17 00:00:00 2001 From: itzandroidtab Date: Tue, 16 May 2023 23:44:32 +0200 Subject: [PATCH 12/15] reordered the party packets in protocol --- dndserver/protocol.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dndserver/protocol.py b/dndserver/protocol.py index c8429742..c121ea00 100644 --- a/dndserver/protocol.py +++ b/dndserver/protocol.py @@ -109,12 +109,12 @@ def dataReceived(self, data: bytes) -> None: pc.C2S_PARTY_INVITE_REQ: party.party_invite, pc.C2S_PARTY_EXIT_REQ: party.leave_party, pc.C2S_PARTY_INVITE_ANSWER_REQ: party.accept_invite, + pc.C2S_PARTY_READY_REQ: party.set_ready_state, + pc.C2S_PARTY_MEMBER_KICK_REQ: party.kick_member, pc.C2S_TRADE_CHANNEL_CHAT_REQ: trade.chat, pc.C2S_TRADE_CHANNEL_EXIT_REQ: trade.exit_channel, pc.C2S_TRADE_CHANNEL_LIST_REQ: trade.get_channels, pc.C2S_TRADE_CHANNEL_SELECT_REQ: trade.select_channel, - pc.C2S_PARTY_READY_REQ: party.set_ready_state, - pc.C2S_PARTY_MEMBER_KICK_REQ: party.kick_member, pc.C2S_TRADE_MEMBERSHIP_REQUIREMENT_REQ: trade.get_trade_reqs, pc.C2S_TRADE_MEMBERSHIP_REQ: trade.process_membership, pc.C2S_TRADE_REQUEST_REQ: trade.trade_request, From 33fd52084b020912546001af5ba5a4e93cae9012 Mon Sep 17 00:00:00 2001 From: itzandroidtab Date: Wed, 17 May 2023 00:00:25 +0200 Subject: [PATCH 13/15] cleanup get_empty_slot function --- dndserver/handlers/trade.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/dndserver/handlers/trade.py b/dndserver/handlers/trade.py index b5370081..d357767b 100644 --- a/dndserver/handlers/trade.py +++ b/dndserver/handlers/trade.py @@ -278,20 +278,21 @@ def get_empty_slot(character_id, size=(1, 1)): # get the items from the bag and sort them items = inventory.get_all_items(character_id, Define_Item.InventoryId.BAG) items.sort(key=lambda i: i[0].slot_id, reverse=True) + items.extend([(None, None)] * (50 - len(items))) - for index, (item, _) in zip(range(50), items): + for index, (item, _) in enumerate(items): # TODO: get the size of the current item and use the size of the item we want to place - if index != item.slot_id: - return (Define_Item.InventoryId.BAG, index) + if item is None or index != item.slot_id: + return (Define_Item.InventoryId.BAG, index + 1) # do the same thing for the storage - items = inventory.get_all_items(character_id, Define_Item.InventoryId.STORAGE).sort( - key=lambda i: i[0].slot_id, reverse=True - ) + items = inventory.get_all_items(character_id, Define_Item.InventoryId.STORAGE) + items.sort(key=lambda i: i[0].slot_id, reverse=True) + items.extend([(None, None)] * (240 - len(items))) - for index, (item, _) in zip(range(240), items): + for index, (item, _) in enumerate(items): # TODO: get the size of the current item and use the size of the item we want to place - if index != item.slot_id: + if item is None or index != item.slot_id: return (Define_Item.InventoryId.STORAGE, index) # we have no space return None From 0395dbc3920165536b813cb8ec4dbdbf00268ed4 Mon Sep 17 00:00:00 2001 From: itzandroidtab Date: Thu, 18 May 2023 14:49:53 +0200 Subject: [PATCH 14/15] bugfix skipping the first slot --- dndserver/handlers/trade.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dndserver/handlers/trade.py b/dndserver/handlers/trade.py index d357767b..d1755af8 100644 --- a/dndserver/handlers/trade.py +++ b/dndserver/handlers/trade.py @@ -283,7 +283,7 @@ def get_empty_slot(character_id, size=(1, 1)): for index, (item, _) in enumerate(items): # TODO: get the size of the current item and use the size of the item we want to place if item is None or index != item.slot_id: - return (Define_Item.InventoryId.BAG, index + 1) + return (Define_Item.InventoryId.BAG, index) # do the same thing for the storage items = inventory.get_all_items(character_id, Define_Item.InventoryId.STORAGE) From eac13fd429e396e1dd259f943bba351279494aaf Mon Sep 17 00:00:00 2001 From: itzandroidtab Date: Tue, 23 May 2023 23:07:01 +0200 Subject: [PATCH 15/15] reworked the trade id and added type hinting --- dndserver/handlers/trade.py | 72 ++++++++++++++++++++----------------- dndserver/objects/trade.py | 8 +++-- 2 files changed, 45 insertions(+), 35 deletions(-) diff --git a/dndserver/handlers/trade.py b/dndserver/handlers/trade.py index ce8403f6..56c07e2a 100644 --- a/dndserver/handlers/trade.py +++ b/dndserver/handlers/trade.py @@ -1,9 +1,10 @@ import arrow +from typing import Optional, Tuple, List from dndserver.database import db from dndserver.handlers import inventory from dndserver.handlers import character as HCharacter -from dndserver.models import Item, ItemAttribute +from dndserver.models import Item, ItemAttribute, Character from dndserver.persistent import sessions from dndserver.protos import PacketCommand as pc from dndserver.protos.Trade import ( @@ -44,10 +45,10 @@ SS2C_TRADING_RESULT_NOT, ) from dndserver.protos.Chat import SCHATDATA, SCHATDATA_PIECE, SCHATDATA_PIECE_ITEM, SCHATDATA_PIECE_ITEM_PROPERTY -from dndserver.protos.Character import SCHARACTER_TRADE_INFO -from dndserver.protos.Character import SACCOUNT_NICKNAME -from dndserver.protos.Defines import Define_Trade, Define_Message, Define_Item +from dndserver.protos.Character import SCHARACTER_TRADE_INFO, SACCOUNT_NICKNAME +from dndserver.protos.Defines import Define_Trade, Define_Message, Define_Item, Define_Equipment from dndserver.objects.trade import Trade, TradeParty +from dndserver.objects.user import User from dndserver.persistent import trades from dndserver.enums import classes @@ -62,7 +63,7 @@ channels.append({"name": name, "index": index + 1, "clients": []}) -def get_current_channel(ctx): +def get_current_channel(ctx) -> dict: """Helper function to get the current channel the user is in""" for ch in channels: if ctx in ch["clients"]: @@ -71,7 +72,7 @@ def get_current_channel(ctx): return None -def get_user_in_channel(channel, account_id): +def get_user_in_channel(channel: dict, account_id: int) -> User: """Helper function to find the transport of a account id""" # search for the other player we want to send the notification to for client in channel["clients"]: @@ -83,7 +84,7 @@ def get_user_in_channel(channel, account_id): return None -def find_trade(ctx): +def find_trade(ctx) -> Tuple[Trade, TradeParty, TradeParty]: """Helper function to find the current trade""" for trade in trades: if trade.user0.ctx == ctx: @@ -94,7 +95,7 @@ def find_trade(ctx): return (None, None, None) -def broadcast_chat(ctx, msg): +def broadcast_chat(ctx, msg: STRADE_CHAT_S2C) -> None: """Helper function to broadcast a chat message to all the participants in a channel""" # Broadcast the message to other clients res = SS2C_TRADE_CHANNEL_CHAT_RES(result=pc.SUCCESS, chats=msg) @@ -109,7 +110,7 @@ def broadcast_chat(ctx, msg): client.reply(res) -def leave_channel(ctx, channel): +def leave_channel(ctx, channel: int) -> None: """Helper function to have a player 'leave' a channel""" # remove the player from the client list channel["clients"].remove(ctx) @@ -129,7 +130,7 @@ def leave_channel(ctx, channel): ) -def cleanup(ctx): +def cleanup(ctx) -> None: """Helper function to cleanup anything left when the client crashes or alt-f4s""" # check if the user is in any active trades trade, _, other = find_trade(ctx) @@ -146,7 +147,7 @@ def cleanup(ctx): leave_channel(ctx, channel) -def get_trader_info(char, account_id): +def get_trader_info(char: Character, account_id: int) -> SCHARACTER_TRADE_INFO: """Helper function to create trader information from the character and account id""" nickname = SACCOUNT_NICKNAME(originalNickName=char.nickname, streamingModeNickName=char.streaming_nickname) @@ -162,7 +163,7 @@ def get_trader_info(char, account_id): return trader -def get_trading_info(ctx): +def get_trading_info(ctx) -> STRADING_USER_INFO: """Helper function to get the trading info for a user""" char = sessions[ctx.transport].character nickname = SACCOUNT_NICKNAME(originalNickName=char.nickname, streamingModeNickName=char.streaming_nickname) @@ -174,7 +175,7 @@ def get_trading_info(ctx): return trader -def create_chat_data(ctx, req): +def create_chat_data(ctx, req: SC2S_TRADE_CHANNEL_CHAT_REQ | SC2S_TRADING_CHAT_REQ) -> SCHATDATA: """Helper function to create chat data""" character = sessions[ctx.transport].character @@ -206,7 +207,7 @@ def create_chat_data(ctx, req): return chat_data -def get_all_gold(character_id, exclude=[]): +def get_all_gold(character_id: int, exclude: List[Tuple[Item, List[ItemAttribute]]] = []) -> Tuple[int, List[Item]]: """Helper that gets all the gold items in the inventory of a user""" items = inventory.get_all_items(character_id) gold_items = [] @@ -231,14 +232,14 @@ def get_all_gold(character_id, exclude=[]): return total, gold_items -def has_gold_amount(character_id, amount, exclude=[]): +def has_gold_amount(character_id: int, amount: int, exclude: List[Tuple[Item, List[ItemAttribute]]] = []) -> bool: """Helper to check if a the user has at least amount of gold""" total, _ = get_all_gold(character_id, exclude) return total >= amount -def deduct_gold(character_id, deduct_amount): +def deduct_gold(character_id: int, deduct_amount: int) -> bool: """Helper function to deduct gold from the user""" total, items = get_all_gold(character_id) @@ -273,11 +274,14 @@ def deduct_gold(character_id, deduct_amount): return True -def get_empty_slot(character_id, size=(1, 1)): +def get_empty_slot( + character_id: int, size: Tuple[int, int] = (1, 1) +) -> Tuple[Define_Item.InventoryId, Define_Equipment.SlotId]: """Helper function get a empty slot in the inventory""" - # get the items from the bag and sort them + # get the items from the bag and sort them (extend the list with the amount of + # items that fit in the inventory) items = inventory.get_all_items(character_id, Define_Item.InventoryId.BAG) - items.sort(key=lambda i: i[0].slot_id, reverse=True) + items.sort(key=lambda i: i[0].slot_id, reverse=False) items.extend([(None, None)] * (50 - len(items))) for index, (item, _) in enumerate(items): @@ -287,7 +291,7 @@ def get_empty_slot(character_id, size=(1, 1)): # do the same thing for the storage items = inventory.get_all_items(character_id, Define_Item.InventoryId.STORAGE) - items.sort(key=lambda i: i[0].slot_id, reverse=True) + items.sort(key=lambda i: i[0].slot_id, reverse=False) items.extend([(None, None)] * (240 - len(items))) for index, (item, _) in enumerate(items): @@ -316,7 +320,7 @@ def get_trade_reqs(ctx, msg: bytes) -> SS2C_TRADE_MEMBERSHIP_REQUIREMENT_RES: ) -def get_channels(ctx, msg): +def get_channels(ctx, msg: bytes) -> SS2C_TRADE_CHANNEL_LIST_RES: """Occurs when the user is a trader and opens the trader screen""" res = SS2C_TRADE_CHANNEL_LIST_RES() res.isTrader = sessions[ctx.transport].character.is_trader @@ -336,7 +340,7 @@ def get_channels(ctx, msg): return res -def select_channel(ctx, msg): +def select_channel(ctx, msg: bytes) -> SS2C_TRADE_CHANNEL_SELECT_RES: """Occurs when the user selects a trading channel""" req = SC2S_TRADE_CHANNEL_SELECT_REQ() req.ParseFromString(msg) @@ -382,7 +386,7 @@ def select_channel(ctx, msg): return SS2C_TRADE_CHANNEL_SELECT_RES(result=pc.SUCCESS) -def exit_channel(ctx, msg): +def exit_channel(ctx, msg: bytes) -> SS2C_TRADE_CHANNEL_EXIT_RES: """Occurs when the player exits a channel""" # find the client's channel channel = get_current_channel(ctx) @@ -410,7 +414,7 @@ def exit_channel(ctx, msg): return SS2C_TRADE_CHANNEL_EXIT_RES(result=pc.SUCCESS) -def trade_request(ctx, msg): +def trade_request(ctx, msg: bytes) -> SS2C_TRADE_REQUEST_RES: """Occurs when a trade request is send by a user""" req = SC2S_TRADE_REQUEST_REQ() req.ParseFromString(msg) @@ -444,7 +448,7 @@ def trade_request(ctx, msg): return SS2C_TRADE_REQUEST_RES(result=pc.SUCCESS, requestNickName=req.nickName) -def cancel_trade(ctx, msg): +def cancel_trade(ctx, msg: bytes) -> SS2C_TRADING_CLOSE_RES: """Occurs when the user cancels a trade""" # get the other player to send a stop message trade, _, other = find_trade(ctx) @@ -458,7 +462,7 @@ def cancel_trade(ctx, msg): return SS2C_TRADING_CLOSE_RES(result=pc.SUCCESS) -def ready(ctx, msg): +def ready(ctx, msg: bytes) -> SS2C_TRADING_READY_RES: """Occurs when a user presses ready on the first trader menu""" req = SC2S_TRADING_READY_REQ() req.ParseFromString(msg) @@ -503,7 +507,7 @@ def ready(ctx, msg): return SS2C_TRADING_READY_RES(result=pc.SUCCESS) -def confirm(ctx, msg): +def confirm(ctx, msg: bytes) -> Optional[SS2C_TRADING_CONFIRM_READY_RES]: """Occurs when a user presses confirm on the second trader menu""" req = SC2S_TRADING_CONFIRM_READY_REQ() req.ParseFromString(msg) @@ -512,7 +516,7 @@ def confirm(ctx, msg): trade, current, other = find_trade(ctx) if not all([trade, current, other]): - return SS2C_TRADING_READY_RES(result=pc.FAIL_GENERAL) + return SS2C_TRADING_CONFIRM_READY_RES(result=pc.FAIL_GENERAL) # send the ready change to all the parties for client in [current, other]: @@ -524,6 +528,8 @@ def confirm(ctx, msg): # update the internal state of the trade current.is_ready = req.isReady + + # TODO: check if we can send this after we have processed everything ctx.reply(SS2C_TRADING_CONFIRM_READY_RES(result=pc.SUCCESS)) # check if we need to send the confirm message @@ -568,7 +574,7 @@ def confirm(ctx, msg): return None -def cancel_confirm(ctx, msg): +def cancel_confirm(ctx, msg: bytes) -> SS2C_TRADING_CONFIRM_CANCEL_RES: """Occurs when a user cancels a trade in the confirm stage""" # get the current trade _, current, other = find_trade(ctx) @@ -584,7 +590,7 @@ def cancel_confirm(ctx, msg): return SS2C_TRADING_CONFIRM_CANCEL_RES(result=pc.SUCCESS) -def accept_invite(ctx, msg): +def accept_invite(ctx, msg: bytes) -> SS2C_TRADE_ANSWER_RES: """Occurs when the user accepts/refuses a trading invite""" req = SC2S_TRADE_ANSWER_REQ() req.ParseFromString(msg) @@ -634,7 +640,7 @@ def accept_invite(ctx, msg): return SS2C_TRADE_ANSWER_RES(result=pc.SUCCESS) -def move_item(ctx, msg): +def move_item(ctx, msg: bytes) -> SS2C_TRADING_ITEM_UPDATE_RES: """Occurs when the user moves items in/out of the trading inventory""" req = SC2S_TRADING_ITEM_UPDATE_REQ() req.ParseFromString(msg) @@ -691,7 +697,7 @@ def move_item(ctx, msg): return res -def private_chat(ctx, msg): +def private_chat(ctx, msg: bytes) -> None: """Occurs when the user sends a message in the trading chat""" req = SC2S_TRADING_CHAT_REQ() req.ParseFromString(msg) @@ -713,7 +719,7 @@ def private_chat(ctx, msg): return None -def chat(ctx, msg): +def chat(ctx, msg: bytes) -> SS2C_TRADE_CHANNEL_CHAT_RES: """Occurs when a user sends a message in the channel chat""" req = SC2S_TRADE_CHANNEL_CHAT_REQ() req.ParseFromString(msg) diff --git a/dndserver/objects/trade.py b/dndserver/objects/trade.py index 854b404e..872f97bf 100644 --- a/dndserver/objects/trade.py +++ b/dndserver/objects/trade.py @@ -1,4 +1,5 @@ -from dndserver.persistent import trades +# starting id for the trading +trade_id = 0 class TradeParty: @@ -10,6 +11,9 @@ def __init__(self, ctx=None) -> None: class Trade: def __init__(self, user0=TradeParty(), user1=TradeParty()) -> None: - self.id = len(trades) + 1 + global trade_id + trade_id += 1 + self.id = trade_id + self.user0 = user0 self.user1 = user1