Edit: Found out my issue wasn't Flask-SocketIO at all. Had my Player class pointing to a default dictionary and every instance of the object was modifying the default dictionary instead of making a new one. Will mark as solved once it allows me to.
I am trying to build a small custom text RPG engine using Flask-SocketIO as the communication interface between the client and the server. I am currently trying to add "player" objects to a "room" object's contents attribute when a player moves into a room (and remove that object when they subsequently leave). However, if more than one client is connected to the server, once multiple players begin to move around, it adds EVERY player to each room any player enters. I have a feeling that it's connected to the way I'm using Flask-SocketIO event handling to pass client commands to player/room methods, but I'm unsure of what exactly is happening. I feel like any time a client sends data, it's triggering that method to add a player to a room's contents for every player, but I haven't seen any other duplication for any of my other methods.
Players can be moved around without error, and speaking and looking both function as expected. I'm somewhat at a loss here. Any help or advice is much appreciated.
Connection Event
client_list = [] #List of clients currently connected
world = objects.World() #Instatiating world class to hold all rooms, players, and characters
@socketio.on('connect')
def connect():
active_player = current_user.accounts.filter(PlayerAccount.is_active == True).first() #Pulls the active player information
if not active_player.player_info: #Checks to see if the active player is a new player
player = objects.Player(id=active_player.id, name=active_player.player_name, description="A newborn player, fresh to the world.", account=active_player.user_id)
print(f'id = {player.id}, name = {player.name}, description = {player.description}, health = {player.health}, level = {player.level}, stats = {player.stats}, location = {player.location}, inventory = {player.inventory}') #Creates a new player object
active_player.player_info = dill.dumps(player) #Pickles and writes new player object to active player info
active_player.save() #Saves pickled data to player database
else:
player = dill.loads(active_player.player_info) #Loads pickled data in to the player
username = player.name
location = player.location
player.session_id = request.sid
client_list.append(player.session_id)
world.players.update({player.id: player})
print(f'client list is {client_list}')
print(f'players connected is {world.players}')
session['player_id'] = player.id
join_room(location)
print(player.location)
print(player, world.rooms[location].name, world.rooms[location].contents['Players'])
socketio.emit('event', {'message': f'{username} has connected to the server'})
Event for Client Sending Commands and Method Routing Functions
@socketio.event
def client(data):
current_player = events.world.players[session.get('player_id')]
current_room = events.world.rooms[current_player.location]
content = {
'player': current_player,
'room': current_room,
'command': data['command'],
'data': data['data'],
}
if content['command'] == 'say':
say(content['player'], content['data'])
if content['command'] in ['move', 'go', 'north', 'south', 'east', 'west', 'n', 's', 'e', 'w']:
if not content['data']:
content['data'] = content['command']
if content['data'] == 'n':
content['data'] = 'north'
if content['data'] == 's':
content['data'] = 'south'
if content['data'] == 'e':
content['data'] = 'east'
if content['data'] == 'w':
content['data'] = 'west'
move(player=content['player'], direction=content['data'], room=content['room'])
if content['command'] == 'look' or content['command'] == 'l':
look(player=content['player'], data=content['data'], room=content['room'])
if content['command'] == 'test':
test(content['player'], content['data'])
if content['command'] == 'save':
save(content['player'], content['data'])
def say(player, data):
player.speak(data)
def move(player, direction, room):
player.move(direction=direction, room=room)
def look(player, room, data=''):
player.look(data=data, room=room)
Object Classes for World, Room, and Player
class World():
def __init__(self) -> None:
with open('app/data/room_db.pkl', 'rb') as dill_file:
rooms = dill.load(dill_file)
self.rooms = rooms
self.players = {}
def world_test(self):
print(f'World initialized with {self.rooms}')
socketio.emit('event', {'message': self.rooms['0,0'].description})
def world_save(self):
with open('app/data/world_db.pkl', 'wb') as dill_file:
dill.dump(self, dill_file)
socketio.emit('event', {'message': 'world saved'})
def room_save(self):
with open('app/data/room_db.pkl', 'wb') as dill_file:
dill.dump(self.rooms, dill_file)
socketio.emit('event', {'message': 'rooms saved'})
#Overall class for any interactable object in the world
class Entity():
def __init__(self, name, description) -> None:
self.name = name #Shorthand name for an entity
self.description = description #Every entity needs to be able to be looked at
#Test function currently, but every entity needs to be able to describe itself when looked at
def describe(self):
pass
#Class for rooms. Rooms should contain all other objects (NPCs, Items, Players, anything else that gets added)
class Room(Entity):
id = itertools.count()
def __init__(self, name, description, position, exits, icon, contents={'NPCs': {}, 'Players': {}, 'Items': {}}) -> None:
super().__init__(name, description)
self.id = next(Room.id)
self.position = position #Coordinates in the grid system for a room, will be used when a character moves rooms
self.exits = exits #List of rooms that are connected to this room. Should be N,S,E,W but may expand so a player can "move/go shop or someting along those lines"
self.icon = icon #Icon for the world map, should consist of two ASCII characters (ie: "/\" for a mountain)
self.contents = contents #Dictionary containing all NPCs, Players, and Items currently in the room. Values will be modified depending on character movement, NPC generation, and item movement
def describe_contents(self, caller):
output = ''
print('test')
print(f'room contents is {self.contents["Players"]}')
return output
#Broad class for any entity capable of independent and autonomous action that affects the world in some way
default_stats = {
'strength': 10,
'endurance': 10,
'intelligence': 10,
'wisdom': 10,
'charisma': 10,
'agility': 10
}
class Character(Entity):
def __init__(self, name, description, health=100, level=1, location='0,0', stats=default_stats, deceased=False, inventory = []) -> None:
super().__init__(name, description)
self.health = health #All characters should have a health value
self.level = level #All characters should have a level value
self.location = location #All characters should have a location, reflecting their current room and referenced when moving
self.stats = stats #All characters should have a stat block.
self.deceased = deceased #Indicator of if a character is alive or not. If True, inventory can be looted
self.inventory = inventory #List of items in character's inventory. May swap to a dictionary of lists so items can be placed in categories
#Class that users control to interact with the world. Unsure if I need to have this mixed in with the models side or if it would be easier to pickle the entire class and pass that to the database?
class Player(Character):
def __init__(self, id, account, name, description, health=100, level=1, location='0,0', stats=default_stats, deceased=False, inventory=[]) -> None:
super().__init__(name, description, health, level, location, stats, deceased, inventory)
self.id = id
self.account = account #User account associated with the player character
self.session_id = '' #Session ID so messages can be broadcast to players without other members of a room or server seeing the message. Session ID is unique to every connection, so part of the connection process must be to assign the new value to the player's session_id
def connection(self):
events.world.rooms[self.location].contents['Players'].update({self.id: self})
def disconnection(self):
pass
def look(self, data, room):
if data == '':
socketio.emit('event', {'message': self.location}, to=self.session_id)
socketio.emit('event', {'message': room.description}, to=self.session_id)
socketio.emit('event', {'message': room.describe_contents(self)}, to=self.session_id)
else:
socketio.emit('event', {'message': 'this will eventually be a call to a class\'s .description to return a look statement.'}, to=self.session_id)
def speak(self, data):
socketio.emit('event', {'message': f'{self.name} says "{data}"'}, room=self.location, include_self=False)
socketio.emit('event', {'message': f'You say "{data}"'}, to=self.session_id)
def move(self, direction, room):
if direction not in room.exits:
socketio.emit('event', {'message': 'You can\'t go that way.'}, to=self.session_id)
return
leave_room(self.location)
socketio.emit('event', {'message': f'{self.name} moves towards the {direction}'}, room=self.location)
if self.id in room.contents['Players']:
print(f"{room.contents['Players'][self.id].name} removed from {room.name}, object: {id(room)}")
print(events.world)
del room.contents['Players'][self.id]
print(room.contents)
lat = int(self.location[:self.location.index(',')])
lon = int(self.location[self.location.index(',')+1:])
if direction == 'n' or direction == 'north':
lon += 1
socketio.emit('event', {'message': 'You move towards the north'}, to=self.session_id)
if direction == 's' or direction == 'south':
lon -= 1
socketio.emit('event', {'message': 'You move towards the south'}, to=self.session_id)
if direction == 'e' or direction == 'east':
lat += 1
socketio.emit('event', {'message': 'You move towards the east'}, to=self.session_id)
if direction == 'w' or direction == 'west':
lat -= 1
socketio.emit('event', {'message': 'You move towards the west'}, to=self.session_id)
new_location = f'{lat},{lon}'
came_from = [i for i in events.world.rooms[new_location].exits if events.world.rooms[new_location].exits[i]==self.location]
socketio.emit('event', {'message': f'{self.name} arrives from the {came_from[0]}'}, room=new_location)
socketio.sleep(.5)
self.location = new_location
join_room(self.location)
events.world.rooms[self.location].contents['Players'][self.id] = self
socketio.emit('event', {'message': events.world.rooms[self.location].description}, to=self.session_id)
Solved my own problem. Was caused because every room object instance was pointing to the same default dictionary and using the values.