Search code examples
pythonflaskgame-engineflask-socketio

Flask Socket-IO custom objects getting added regardless of client action


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)

Solution

  • Solved my own problem. Was caused because every room object instance was pointing to the same default dictionary and using the values.