diff --git a/commands/asy.py b/commands/asy.py new file mode 100644 index 0000000..c1c5d1a --- /dev/null +++ b/commands/asy.py @@ -0,0 +1,14 @@ +from typing import TYPE_CHECKING +from response import ExtensionResponse + +if TYPE_CHECKING: + from server import CafeServer + from asyncio import StreamWriter + + +async def handle_asy(server: 'CafeServer', client: 'StreamWriter') -> None: + address = client.get_extra_info('peername') + player = server.clients[address] + + response = ExtensionResponse('asy', '-1', '0', str(player.cash), str(player.gold)) + await server.send_response(client, response) diff --git a/commands/ebu.py b/commands/ebu.py index f7cffec..62356f7 100644 --- a/commands/ebu.py +++ b/commands/ebu.py @@ -70,14 +70,15 @@ async def handle_ebu(server: 'CafeServer', client: 'StreamWriter', *params: str) new_object.pos = [int(pos_x), int(pos_y)] new_object.rotation = int(rotation) - player.cafe.objects.append(new_object) - if int(obj_id) in DOOR_IDs: for obj in player.cafe.objects: if obj.id in DOOR_IDs: player.cafe.furniture_inventory[obj.id] = 1 player.cafe.objects.remove(obj) + player.cafe.objects.append(new_object) + + if int(obj_id) in DOOR_IDs: player.cafe.player_start = player.cafe.get_start_pos() server.db.update_cafe(player.id, diff --git a/commands/edi.py b/commands/edi.py index db4f9c2..a153213 100644 --- a/commands/edi.py +++ b/commands/edi.py @@ -1,9 +1,11 @@ from typing import TYPE_CHECKING + +from commands.asy import handle_asy +from objects.cafe_object import ObjectType +from objects.customer import spawn_cycle +from objects.waiter import spawn_waiters from response import ExtensionResponse from asyncio import create_task -from objects.waiter import spawn_multiple -from objects.customer import spawn_cycle -from objects.cafe_object import ObjectType if TYPE_CHECKING: from server import CafeServer @@ -18,25 +20,51 @@ async def handle_edi(server: 'CafeServer', client: 'StreamWriter', *params: str) match int(params[1]): case 0: - create_task(spawn_multiple(server, player.cafe)) - player.customer_cycle = create_task(spawn_cycle(server, player.cafe)) player.cafe.connect_chairs() + create_task(spawn_waiters(server, client)) + + print(f'Created a new customer cycle\nUsed this player\'s Cafe: {player.avatar.username}') + player.cafe.customer_cycle = create_task(spawn_cycle(server, client)) + case 1: + vending_machine = None + for obj in player.cafe.objects: + if obj.type == ObjectType.VENDING: + vending_machine = obj + for waiter in player.cafe.waiters: if waiter.task: waiter.task.cancel() - if player.customer_cycle: - player.customer_cycle.cancel() + if player.cafe.customer_cycle: + player.cafe.customer_cycle.cancel() player.customer_cycle = None for customer in player.cafe.customers: if customer.task: customer.task.cancel() + # TO UPDATE THE VENDING MACHINE AMOUNT IN VISUAL TOO! + # THE ASY NEED TO SYNC THE MONEY! + # THE XP IS NOT SYNCED WITH THE CLIENT :( + + if customer.action == 9: + await customer._use_vending_machine(server, player, vending_machine) + + if vending_machine and vending_machine.fast_food_amount < 1: + vending_machine.fast_food_id = -1 + server.db.update_cafe(player.id, objects=player.cafe.get_objects_as_json()) + + await handle_asy(server, client) + + player.cafe.customers = [] + for table in player.cafe.available_tables: table.busy = False + chair = table.connected_chair + chair.dish_id = -1 + chair.dish_status = 0 response = ExtensionResponse('edi', '-1', '0', params[1], str(pos[0]), str(pos[1]), '') await server.send_response(client, response) diff --git a/commands/ffc.py b/commands/ffc.py new file mode 100644 index 0000000..f03ba00 --- /dev/null +++ b/commands/ffc.py @@ -0,0 +1,51 @@ +from typing import TYPE_CHECKING +from response import ExtensionResponse +from utils.items import get_fast_food_info +if TYPE_CHECKING: + from server import CafeServer + from asyncio import StreamWriter + + +async def handle_ffc(server: 'CafeServer', client: 'StreamWriter', *params: str) -> None: + address = client.get_extra_info('peername') + player = server.clients[address] + + pos_x = int(params[1]) + pos_y = int(params[2]) + drink_id = int(params[3]) + + drink = get_fast_food_info(drink_id) + if not drink: + return + + cost_cash = drink['cash'] + cost_gold = drink['gold'] + amount = drink['amount'] + + if not cost_cash == 0: + if player.cash < cost_cash: + response = ExtensionResponse('ffc', '-1', '4') + await server.send_response(client, response) + return + else: + player.cash -= cost_cash + server.db.update_player(player.id, cash=player.cash) + + if not cost_gold == 0: + if player.gold < cost_gold: + response = ExtensionResponse('ffc', '-1', '4') + await server.send_response(client, response) + return + else: + player.gold -= cost_gold + server.db.update_player(player.id, gold=player.gold) + + obj = player.cafe.get_object(pos_x, pos_y) + obj.fast_food_id = drink_id + obj.fast_food_amount = amount + + server.db.update_cafe(player.id, objects=player.cafe.get_objects_as_json()) + + for user in player.cafe.clients: + response = ExtensionResponse('ffc', '-1', '0', params[1], params[2], str(drink_id)) + await server.send_response(user, response) diff --git a/commands/ffn.py b/commands/ffn.py new file mode 100644 index 0000000..a4f4fcd --- /dev/null +++ b/commands/ffn.py @@ -0,0 +1,75 @@ +from math import ceil +from typing import TYPE_CHECKING + +from objects.cafe import get_time +from response import ExtensionResponse + +from utils.items import get_fast_food_info + +if TYPE_CHECKING: + from server import CafeServer + from asyncio import StreamWriter + + +class ErrorCodes: + noFrosty_goShop = '999' + allServed = '103' + notReachable = '36' + + +async def handle_ffn(server: 'CafeServer', client: 'StreamWriter', *params: str) -> None: + address = client.get_extra_info('peername') + player = server.clients[address] + + customer_id = int(params[1]) + obj_pos_x = int(params[2]) + obj_pos_y = int(params[3]) + + obj = player.cafe.get_object(obj_pos_x, obj_pos_y) + + if not obj: + # not really need, the game sends before the server, but we should keep it + response = ExtensionResponse('ffn', '-1', ErrorCodes.noFrosty_goShop) + await server.send_response(client, response) + return + + if not obj.fast_food_amount > 0: + response = ExtensionResponse('ffn', '-1', ErrorCodes.allServed) + await server.send_response(client, response) + return + + facing_pos = [0, 0] + + nearby_pos = obj.get_nearby_pos() + for pos, rotation in nearby_pos.items(): + if rotation == obj.rotation: + facing_pos = pos + + if get_time(player.cafe, facing_pos) == -1: + response = ExtensionResponse('ffn', '-1', ErrorCodes.notReachable) + await server.send_response(client, response) + return + + for customer in player.cafe.customers: + if customer.id == customer_id: + customer.action = 9 + break + + fast_food_info = get_fast_food_info(obj.fast_food_id) + income = fast_food_info.get('income') + xp = fast_food_info.get('xp') + rating = fast_food_info.get('rating') + + player.cafe.update_rating(rating) + + player.cash += income + player.xp += ceil(xp * (player.get_level() + 18) / 190) + + obj.fast_food_amount -= 1 + + server.db.update_player(player.id, cash=player.cash, xp=player.xp) + + server.db.update_cafe(player.id, rating=player.cafe.rating, objects=player.cafe.get_objects_as_json()) + + response = ExtensionResponse('ffn', '-1', '0', str(customer_id), str(obj_pos_x), str(obj_pos_y)) + await server.send_response(client, response) diff --git a/commands/jca.py b/commands/jca.py index 95e7c9b..d9aa1cc 100644 --- a/commands/jca.py +++ b/commands/jca.py @@ -1,12 +1,13 @@ from typing import TYPE_CHECKING + +from commands.asy import handle_asy +from objects.waiter import spawn_waiters, spawn_waiters_to_client from response import ExtensionResponse from commands.sgc import handle_sgc from commands.ifr import handle_ifr from commands.ein import handle_ein -from objects.waiter import spawn_multiple -from objects.customer import spawn_cycle, Action -from objects.cafe_object import CafeObject, ObjectType from asyncio import create_task +from objects.customer import spawn_cycle, spawn_customers_to_client if TYPE_CHECKING: from server import CafeServer @@ -15,6 +16,8 @@ async def handle_jca(server: 'CafeServer', client: 'StreamWriter', *params: str) -> None: wanted_player_id = int(params[2]) + address = client.get_extra_info('peername') + player = server.clients[address] check_player = [player for player in server.players if player.id == wanted_player_id] if check_player: @@ -31,51 +34,14 @@ async def handle_jca(server: 'CafeServer', client: 'StreamWriter', *params: str) await handle_ifr(server, client, *params) await handle_ein(server, client, *params) - create_task(spawn_multiple(server, wanted_player.cafe)) - - # TODO: Broken for other player's cafe - # Tries to use player's cafe objects instead of the other player's - if not wanted_player.customer_cycle: - print(f'created waiter cycle\nused this player\'s cafe: f{wanted_player.avatar.username}') - # TODO: add random customers - wanted_player.customer_cycle = create_task(spawn_cycle(server, wanted_player.cafe)) - else: - customers = wanted_player.cafe.customers - - print(f'customers: {customers}') - - for customer in customers: - spawn_info = '+'.join(map(str, [ - customer.id, # Customer ID - 0, # NPC type (0: Customer) - 0, # Favourite dish - -1, # Dish ID - 0, # Is thirsty - customer.avatar.to_string(True) # Avatar - ])) - - response_spawn = ExtensionResponse('nav', '-1', '0', spawn_info) + if wanted_player.id == player.id: + await handle_asy(server, client) - location = wanted_player.cafe.get_object(*customer.pos) - - if isinstance(location, CafeObject) and location.type == ObjectType.CHAIR: - sit_down_info = '+'.join(map(str, [ - customer.id, # Customer ID - Action.SIT_DOWN, # Action type (2: Sit down) - customer.pos[0], # X - customer.pos[1] # Y - ])) - - response = ExtensionResponse('nac', '-1', '0', sit_down_info) - - else: - insert_info = '+'.join(map(str, [ - str(customer.id), # Customer ID - Action.INSERT # Action type (0: Insert) - ])) - - response = ExtensionResponse('nac', '-1', '0', insert_info) - - await server.send_response(client, response_spawn) - await server.send_response(client, response) + if not wanted_player.cafe.customer_cycle: + print(f'Created a new customer cycle\nUsed this player\'s Cafe: {wanted_player.avatar.username}') + wanted_player.cafe.customer_cycle = create_task(spawn_cycle(server, wanted_player.client)) + create_task(spawn_waiters(server, wanted_player.client)) + else: + create_task(spawn_customers_to_client(server, client, wanted_player.cafe)) + create_task(spawn_waiters_to_client(server, client, wanted_player.cafe)) diff --git a/commands/lbu.py b/commands/lbu.py new file mode 100644 index 0000000..ab818d9 --- /dev/null +++ b/commands/lbu.py @@ -0,0 +1,154 @@ +import random +from datetime import datetime, timezone +from math import floor +from random import choice +from typing import TYPE_CHECKING +from objects.cafe import get_time + +from commands.asy import handle_asy +from objects.cafe_object import ObjectType +from response import ExtensionResponse +from utils.items import get_dish_info + +if TYPE_CHECKING: + from server import CafeServer + from asyncio import StreamWriter + + +async def handle_lbu(server: 'CafeServer', client: 'StreamWriter') -> None: + address = client.get_extra_info('peername') + player = server.clients[address] + + max_minutes = 10000 + minutes_past = player.get_minutes_since_last_login() + + if minutes_past > max_minutes: + minutes_past = max_minutes + + sold_dishes = 0 + total_income = 0 + + if minutes_past > 0: + available_counters = [obj for obj in player.cafe.objects if + obj.type == ObjectType.COUNTER and obj.dish_amount > 0 + and get_time(player.cafe, obj.pos) != -1] + + if available_counters: + rating = player.cafe.rating + + waiters_for_serve = len([waiter for waiter in player.cafe.waiters if waiter.priority > 0]) + + if rating < 150: + should_sold = floor((minutes_past / 1.5) * waiters_for_serve) # LOWER IT TO SELL MORE, INCREASE TO SELL LESS. + elif 150 <= rating < 350: + should_sold = floor((minutes_past / 1.3) * waiters_for_serve) # LOWER IT TO SELL MORE, INCREASE TO SELL LESS. + elif 350 <= rating < 500: + should_sold = floor((minutes_past / 1.1) * waiters_for_serve) # LOWER IT TO SELL MORE, INCREASE TO SELL LESS. + else: + should_sold = floor((minutes_past / 0.7) * waiters_for_serve) # LOWER IT TO SELL MORE, INCREASE TO SELL LESS. + + total_income = 0 + total_sold_dishes = 0 + + dishes_info: dict[int, int] = {} + + dish_info_cache = {} + + for _ in range(should_sold): + if not available_counters: + break + + counter = choice(available_counters) + dish_id = counter.dish_id + + # Need to cache it, because it's slow to get the dish income on every loop + # And on a big loop, the client froze + if dish_id not in dish_info_cache: + dish_info_cache[dish_id] = get_dish_info(dish_id) + + dish_info = dish_info_cache[dish_id] + + counter.dish_amount -= 1 + total_sold_dishes += 1 + total_income += dish_info.get('income') + + dishes_info[dish_id] = dishes_info.setdefault(dish_id, 0) + 1 + + if counter.dish_amount <= 0: + counter.dish_id = -1 + available_counters.remove(counter) + + player.cash += total_income + + print(f'[ [LBU] OFFLINE SELLING] Player sold a total of: {total_sold_dishes} dishes.' + f'Time past since last login: ' + f'{minutes_past}m. Total income: {total_income}$') + + sold_dishes = sum(amount for dish_id, amount in dishes_info.items()) + + # If the player sold dishes, and the player daily login + if sold_dishes > 0 and player.check_daily_login(): + current_time = datetime.now(timezone.utc).strftime("%y-%d-%m %H:%M") + player.daily_login = current_time + + daily_cash_bonus = 500 * ((player.get_level() // 10) + 1) + player.cash += daily_cash_bonus + + random_fancy = random.randint(1401, 1409) + if random_fancy not in player.cafe.fridge_inventory: + player.cafe.fridge_inventory[random_fancy] = 1 + else: + player.cafe.fridge_inventory[random_fancy] += 1 + + daily_login_bonus_str = f'1906+{daily_cash_bonus}#{random_fancy}+1' + + response = ExtensionResponse('lbu', '-1', '0', f'{daily_login_bonus_str}#1901+{total_income}', + str(sold_dishes)) + await server.send_response(client, response) + + server.db.update_player(player.id, cash=player.cash, daily_login=player.daily_login) + server.db.update_cafe(player.id, objects=player.cafe.get_objects_as_json(), + fridge_inv=player.cafe.build_fridge_inventory()) + + await handle_asy(server, client) + + return + + # If the player sold dishes, but not daily login + elif sold_dishes > 0 and not player.check_daily_login(): + response = ExtensionResponse('lbu', '-1', '0', f'1901+{total_income}', + str(sold_dishes)) + await server.send_response(client, response) + + server.db.update_player(player.id, cash=player.cash) + server.db.update_cafe(player.id, objects=player.cafe.get_objects_as_json()) + + await handle_asy(server, client) + + return + + # If the player not sold dishes, but daily login + elif sold_dishes == 0 and player.check_daily_login(): + current_time = datetime.now(timezone.utc).strftime("%y-%d-%m %H:%M") + player.daily_login = current_time + + daily_cash_bonus = 500 * ((player.get_level() // 10) + 1) + player.cash += daily_cash_bonus + + random_fancy = random.randint(1401, 1409) + if random_fancy not in player.cafe.fridge_inventory: + player.cafe.fridge_inventory[random_fancy] = 1 + else: + player.cafe.fridge_inventory[random_fancy] += 1 + + daily_login_bonus_str = f'1906+{daily_cash_bonus}#{random_fancy}+1' + + response = ExtensionResponse('lbu', '-1', '0', f'{daily_login_bonus_str}') + await server.send_response(client, response) + + server.db.update_player(player.id, cash=player.cash, daily_login=player.daily_login) + server.db.update_cafe(player.id, fridge_inv=player.cafe.build_fridge_inventory()) + + await handle_asy(server, client) + + return diff --git a/commands/lgn.py b/commands/lgn.py index bd7d83a..3a99e95 100644 --- a/commands/lgn.py +++ b/commands/lgn.py @@ -1,4 +1,8 @@ +from datetime import datetime, timezone from typing import TYPE_CHECKING + +from commands.lbu import handle_lbu +from commands.pin import handle_pin from response import ExtensionResponse, SystemResponse from commands.rlu import handle_rlu from commands.gui import handle_gui @@ -48,6 +52,19 @@ async def handle_lgn(server: 'CafeServer', client: 'StreamWriter', *params: str) await handle_rlu(server, client, 'lgn', *params) + if not player.daily_login: + current_time = datetime.now(timezone.utc).strftime("%y-%d-%m %H:%M") + player.daily_login = current_time + server.db.update_player(player.id, daily_login=player.daily_login) + + # RESETS WHEN 24H PAST + # !! NEED TO CALL BEFORE GUI !! + if player.check_daily_login(): + player.instant_cookings = 0 + player.played_wheel = False + player.open_jobs = 5 + server.db.update_player(player.id, instant_cookings=0, played_wheel=0, open_jobs=5) + await handle_gui(server, client, *params) await handle_sbc(server, client, *params) @@ -55,3 +72,7 @@ async def handle_lgn(server: 'CafeServer', client: 'StreamWriter', *params: str) await handle_lmi(server, client, *params) await handle_ifr(server, client, *params) + + await handle_lbu(server, client) + + await handle_pin(server, client) diff --git a/commands/ncu.py b/commands/ncu.py new file mode 100644 index 0000000..4542846 --- /dev/null +++ b/commands/ncu.py @@ -0,0 +1,29 @@ +from typing import TYPE_CHECKING +from response import ExtensionResponse + +if TYPE_CHECKING: + from server import CafeServer + from asyncio import StreamWriter + + +async def handle_ncu(server: 'CafeServer', client: 'StreamWriter', *params: str) -> None: + address = client.get_extra_info('peername') + player = server.clients[address] + + selected_waiter = int(params[1]) + new_name = params[2] + new_priority = int(params[3]) + + for waiter in player.cafe.waiters: + if waiter.id == selected_waiter: + waiter.name = new_name + waiter.avatar.username = new_name + waiter.priority = new_priority + + response = ExtensionResponse('ncu', '-1', '0', str(selected_waiter), new_name, str(new_priority)) + + for user in player.cafe.clients: + await server.send_response(user, response) + + server.db.update_cafe(player.id, waiters=player.cafe.get_waiters_as_json()) + \ No newline at end of file diff --git a/commands/nhi.py b/commands/nhi.py index 07ef18a..9e5c658 100644 --- a/commands/nhi.py +++ b/commands/nhi.py @@ -1,6 +1,7 @@ +from objects.avatar import Avatar +from objects.waiter import Waiter, get_random_counter from response import ExtensionResponse from typing import TYPE_CHECKING -from objects.waiter import Waiter from asyncio import create_task if TYPE_CHECKING: @@ -16,22 +17,25 @@ async def handle_nhi(server: 'CafeServer', client: 'StreamWriter', *params: str) player = server.clients[address] npc_id = player.cafe.waiters[-1].id + 1 if player.cafe.waiters else 0 - npc_name = params[1] - npc_type = params[2] # 2: man, 1: woman + name = params[1] + gender = int(params[2]) # 2: man, 1: woman - response = ExtensionResponse('nhi', '0', '0', npc_name, npc_type) + avatar = Avatar.get_random_avatar(name, gender) + + response = ExtensionResponse('nhi', '0', '0', name, str(gender)) await server.send_response(client, response) - new_waiter = Waiter(int(npc_id), npc_name, int(npc_type), player.cafe) + new_waiter = Waiter(npc_id, name, gender, 50, avatar) + player.cafe.waiters.append(new_waiter) - # TODO: Save new waiter to database server.db.update_cafe(player.id, waiters=player.cafe.get_waiters_as_json()) - # task = create_task(new_waiter.spawn(server, client)) - # new_waiter.task = task - # task.add_done_callback(new_waiter.remove_task) + new_waiter.task = create_task(new_waiter.spawn(server, player.cafe)) + + await new_waiter.task - await new_waiter.spawn(server) + new_waiter.task = create_task(new_waiter.go_to_counter(server, player.cafe, + get_random_counter(player.cafe))) - # await task + await new_waiter.task diff --git a/commands/pin.py b/commands/pin.py new file mode 100644 index 0000000..dfe6a35 --- /dev/null +++ b/commands/pin.py @@ -0,0 +1,27 @@ +from typing import TYPE_CHECKING +from response import ExtensionResponse +import asyncio + +if TYPE_CHECKING: + from server import CafeServer + from asyncio import StreamWriter + + +async def handle_pin(server: 'CafeServer', client: 'StreamWriter') -> None: + address = client.get_extra_info('peername') + player = server.clients.get(address) + + if not player: + client.close() + + while True: + try: + response = ExtensionResponse('pin', '-1') + await server.send_response(client, response) + await asyncio.sleep(60) + except Exception as e: + print(f"[ERROR] Cant send response, because the client is gone") + print(f"[ERROR] An error occurred: {e}") + break + + client.close() diff --git a/commands/sgc.py b/commands/sgc.py index 5f8ef68..705c618 100644 --- a/commands/sgc.py +++ b/commands/sgc.py @@ -20,24 +20,6 @@ async def handle_sgc(server: 'CafeServer', client: 'StreamWriter', cmd: str, *pa await handle_juq(server, user, client) await handle_juq(server, client, user) - if player.room and player.room != server.marketplace and not player.room.clients: - for waiter in player.cafe.waiters: - if waiter.task: - waiter.task.cancel() - - if player.customer_cycle: - player.customer_cycle.cancel() - player.customer_cycle = None - - for customer in player.cafe.customers: - if customer.task: - customer.task.cancel() - - for table in player.cafe.available_tables: - table.busy = False - - player.room.customers = [] - if cmd == 'jca': existing_player = [player for player in server.players if player.id == int(params[2])] if existing_player: diff --git a/database.py b/database.py index c18e6e3..328fb8a 100644 --- a/database.py +++ b/database.py @@ -41,18 +41,20 @@ def _create_player_by_params(self, player_args: tuple[int, str]) -> Player: player_args[16]) player.cafe = self.get_cafe(player_args[0]) player.parse_mastery(player_args[17]) + player.last_login = (player_args[18]) + player.daily_login = (player_args[19]) return player def get_cafe(self, player_id: int) -> Cafe: - self.cursor.execute("SELECT owner_name, rating, luxury, expansion_id, tiles, objects, fridge_inv, furniture_inv, waiters FROM cafe WHERE player_id=?", (player_id,)) + self.cursor.execute("SELECT owner_name, luxury, rating, expansion_id, tiles, objects, fridge_inv, furniture_inv, waiters FROM cafe WHERE player_id=?", (player_id,)) cafe_args = self.cursor.fetchone() cafe = Cafe() cafe.user_id = player_id cafe.player_id = player_id cafe.owner_name = cafe_args[0] - cafe.rating = cafe_args[1] - cafe.luxury = cafe_args[2] + cafe.luxury = cafe_args[1] + cafe.update_rating(cafe_args[2]) cafe.expansion_id = cafe_args[3] cafe.size = cafe.expansion_id + 8 cafe.parse_tiles(cafe_args[4]) @@ -76,13 +78,13 @@ def try_login(self, name_or_email: str, password: str) -> tuple[int, str]: return 0, values[2] def get_player(self, username: str) -> Player: - self.cursor.execute("SELECT id, cash, gold, xp ,instant_cookings, open_jobs, played_wheel, allow_friend_requests, allow_emails, email_verified, new_gifts, username, gender, skin_color, top_color, hair_color, legs_color, mastery FROM player WHERE username=?", (username,)) + self.cursor.execute("SELECT id, cash, gold, xp ,instant_cookings, open_jobs, played_wheel, allow_friend_requests, allow_emails, email_verified, new_gifts, username, gender, skin_color, top_color, hair_color, legs_color, mastery, last_login, daily_login FROM player WHERE username=?", (username,)) player_args = self.cursor.fetchone() return self._create_player_by_params(player_args) def get_player_by_id(self, player_id: int) -> Player: - self.cursor.execute("SELECT id, cash, gold, xp ,instant_cookings, open_jobs, played_wheel, allow_friend_requests, allow_emails, email_verified, new_gifts, username, gender, skin_color, top_color, hair_color, legs_color, mastery FROM player WHERE id=?", (player_id,)) + self.cursor.execute("SELECT id, cash, gold, xp ,instant_cookings, open_jobs, played_wheel, allow_friend_requests, allow_emails, email_verified, new_gifts, username, gender, skin_color, top_color, hair_color, legs_color, mastery, last_login, daily_login FROM player WHERE id=?", (player_id,)) player_args = self.cursor.fetchone() return self._create_player_by_params(player_args) diff --git a/main.py b/main.py index 4be6f8d..786e4e9 100644 --- a/main.py +++ b/main.py @@ -1,12 +1,24 @@ from server import CafeServer from asyncio import run +import signal +import sys + + +def signal_handler(sig, frame): + print(f'\n[INFO] Shutting down server...') + server.stop() + sys.exit(0) + if __name__ == '__main__': server = CafeServer() + signal.signal(signal.SIGINT, signal_handler) + signal.signal(signal.SIGTERM, signal_handler) + try: run(server.start()) except KeyboardInterrupt: - server.stop() print(f'\n[INFO] Shutting down server...') + server.stop() exit(0) diff --git a/objects/avatar.py b/objects/avatar.py index d5ee6e8..01aba2f 100644 --- a/objects/avatar.py +++ b/objects/avatar.py @@ -1,3 +1,4 @@ +from random import choice, randint from typing import Literal parts = { @@ -64,3 +65,15 @@ def from_string(cls, string: str) -> 'Avatar': obj.legs_color = int(color) return obj + + @classmethod + def get_random_avatar(cls, name: str, gender: int = None) -> 'Avatar': + if not gender: + gender = choice([1, 2]) + + skin_color = randint(0, 7) + top_color = randint(0, 16) + hair_color = randint(0, 14) + legs_color = randint(0, 16) + + return Avatar(name, gender, skin_color, top_color, hair_color, legs_color) diff --git a/objects/cafe.py b/objects/cafe.py index 3bfa65f..cd6c414 100644 --- a/objects/cafe.py +++ b/objects/cafe.py @@ -1,17 +1,20 @@ -from typing import List, Literal, Union, Dict, TYPE_CHECKING +from copy import deepcopy +from typing import List, Literal, Union, TYPE_CHECKING, Optional from objects.cafe_object import CafeObject, ObjectType -from objects.waiter import Waiter from consts import * from astar import AStar import json +from objects.customer import Customer +from objects.waiter import Waiter + if TYPE_CHECKING: from asyncio import StreamWriter from objects.customer import Customer # TODO: Tidy up this function -def get_time(cafe: 'Cafe', chair: 'CafeObject') -> float: +def get_time(cafe: 'Cafe', end_pos) -> float: cafe_size = cafe.size obstacles = [[0 for _ in range(cafe_size)] for _ in range(cafe_size)] @@ -22,10 +25,10 @@ def get_time(cafe: 'Cafe', chair: 'CafeObject') -> float: for obj in cafe.objects: obstacles[obj.pos[0]][obj.pos[1]] = 1 - obstacles[chair.pos[0]][chair.pos[1]] = 0 + obstacles[end_pos[0]][end_pos[1]] = 0 start_pos = tuple(cafe.get_start_pos()) - path = AStar(obstacles, start_pos, tuple(chair.pos)).solve() + path = AStar(obstacles, start_pos, tuple(end_pos)).solve() if path: distance = len(path) - 1 @@ -60,6 +63,7 @@ def __init__(self): self.waiters: list['Waiter'] = [] self.customers: list['Customer'] = [] + self.customer_cycle: Optional['Task'] = None def build_tiles(self) -> str: return '+'.join(['+'.join([str(tile) for tile in row]) for row in self.tiles]) @@ -84,7 +88,13 @@ def parse_objects(self, objects: str) -> None: self.fridge_capacity = self.get_fridge_capacity() def get_objects_as_json(self) -> str: - objects = [obj.to_dict() for obj in self.objects] + objects = [] + objects_copy = deepcopy(self.objects) + for obj in objects_copy: + if obj.type == ObjectType.CHAIR: + obj.dish_id = -1 + obj.dish_status = 0 + objects.append(obj.to_dict()) json_str = json.dumps(objects) return json_str @@ -102,8 +112,9 @@ def get_waiters_as_json(self) -> str: return json_str def set_waiters_from_json(self, json_str: str) -> None: - waiters = json.loads(json_str) - self.waiters = [Waiter.from_dict(waiter, self) for waiter in waiters] + if json_str: + waiters = json.loads(json_str) + self.waiters = [Waiter.from_dict(waiter) for waiter in waiters] def get_object(self, x: int, y: int) -> Union['CafeObject', None]: for obj in self.objects: @@ -193,7 +204,7 @@ def connect_chairs(self) -> None: for table in tables: if (item := nearby_pos.get(tuple(table.pos))) is not None: - if chair.rotation == item and (get_time(self, chair) != -1): + if chair.rotation == item and (get_time(self, chair.pos) != -1): table.connected_chair = chair self.available_tables.append(table) @@ -203,6 +214,62 @@ def connect_chairs(self) -> None: print(f'I connected {len(self.available_tables)} chairs to tables') print(f'Connected table\'s pos: {[table.pos for table in self.available_tables]}\n') + # Getting the closest pos (Like: From the door, to the another object) + # This need to get where's the waiter is standing. Its always choose the sortest path. + def get_shortest_pos(self, first_pos, second_pos): + cafe_size = self.size + obstacles = [[0 for _ in range(cafe_size)] for _ in range(cafe_size)] + + for i in range(cafe_size): + obstacles[i][0] = 1 + obstacles[0][i] = 1 + + for obj in self.objects: + obstacles[obj.pos[0]][obj.pos[1]] = 1 + + obstacles[second_pos[0]][second_pos[1]] = 0 + + path = AStar(obstacles, tuple(first_pos), tuple(second_pos)).solve() + + if path: + path.pop() + return path[-1] + + return None + + # Getting nearby free spaces around the object + def get_nearby_free_spaces(self, object_pos: list[int]): + free_spaces = [] + + nearby_pos = [ + [object_pos[0] - 1, object_pos[1]], + [object_pos[0], object_pos[1] - 1], + [object_pos[0] + 1, object_pos[1]], + [object_pos[0], object_pos[1] + 1] + ] + + for pos in nearby_pos: + obj = self.get_object(int(pos[0]), int(pos[1])) + + if obj is None: + free_spaces.append(pos) + + return free_spaces + + def update_rating(self, rating: int): + self.rating += rating + if self.rating < 10: + self.rating = 10 + elif self.rating > 1000: + self.rating = 1000 + + # https://github.com/GGR-Devs/Cafe-Main/blob/2911a43b4d0972d2f1386edd782d3382f031f36d/com/goodgamestudios/cafe/model/components/CafeLevelData.as#L195 + # The game always check if the rating is smaller than the luxury_rating. If yes, then its update to this value. + luxury_rating = min(int((1 + 0.05 * self.luxury) * 10), 500) + + if self.rating < luxury_rating: + self.rating = luxury_rating + @classmethod def get_starting_cafe(cls) -> 'Cafe': cafe = cls() @@ -211,8 +278,10 @@ def get_starting_cafe(cls) -> 'Cafe': cafe.expansion_id = 0 cafe.size = cafe.expansion_id + 8 cafe.tiles = [[1 for _ in range(cafe.size)] for _ in range(cafe.size)] - cafe.parse_tiles('7+101+101+101+101+101+101+101+101+4+4+4+4+4+4+4+101+4+4+4+4+4+4+4+101+4+4+4+4+4+4+4+101+1+1+1+4+4+4+4+101+1+1+1+4+4+4+4+101+1+1+1+4+4+4+4+101+1+1+1+4+4+4+4') - cafe.parse_objects('3+0+901+0#5+0+901+0#5+1+601+3+-1+0#0+2+201+0#5+2+401+0#7+2+601+3+-1+0#7+3+401+0#1+4+351+0#5+4+401+0#1+5+252+0+-1#3+5+301+0+-1+0#5+5+601+1+-1+0#7+5+401+0#1+6+252+0+-1#3+6+301+0+-1+0#7+6+601+1+-1+0#1+7+252+0+-1+0+0+0') + cafe.parse_tiles( + '7+101+101+101+101+101+101+101+101+4+4+4+4+4+4+4+101+4+4+4+4+4+4+4+101+4+4+4+4+4+4+4+101+1+1+1+4+4+4+4+101+1+1+1+4+4+4+4+101+1+1+1+4+4+4+4+101+1+1+1+4+4+4+4') + cafe.parse_objects( + '3+0+901+0#5+0+901+0#5+1+601+3+-1+0#0+2+201+0#5+2+401+0#7+2+601+3+-1+0#7+3+401+0#1+4+351+0#5+4+401+0#1+5+252+0+-1#3+5+301+0+-1+0#5+5+601+1+-1+0#7+5+401+0#1+6+252+0+-1#3+6+301+0+-1+0#7+6+601+1+-1+0#1+7+252+0+-1+0+0+0') cafe.player_start = cafe.get_start_pos() cafe.fridge_capacity = cafe.get_fridge_capacity() @@ -237,7 +306,8 @@ def get_marketplace(cls) -> 'Cafe': marketplace.size = 12 marketplace.background = 1502 marketplace.tiles = [[1 for _ in range(marketplace.size)] for _ in range(marketplace.size)] - marketplace.parse_tiles('7+7+7+7+7+7+7+7+7+7+7+7+7+3+3+3+3+3+3+3+3+3+3+3+7+3+3+3+3+3+3+3+3+3+3+3+7+3+3+3+3+3+3+3+3+3+3+3+7+3+3+3+7+7+7+3+3+3+3+3+7+3+3+3+7+7+7+3+3+3+3+3+7+3+3+3+7+7+7+3+3+3+3+3+7+3+3+3+3+3+3+3+3+3+3+3+7+3+3+3+3+3+3+3+3+3+3+3+7+3+3+3+3+3+3+3+3+3+3+3+7+3+3+3+3+3+3+3+3+3+3+3+7+3+3+3+3+3+3+3+3+3+3+3') + marketplace.parse_tiles( + '7+7+7+7+7+7+7+7+7+7+7+7+7+3+3+3+3+3+3+3+3+3+3+3+7+3+3+3+3+3+3+3+3+3+3+3+7+3+3+3+3+3+3+3+3+3+3+3+7+3+3+3+7+7+7+3+3+3+3+3+7+3+3+3+7+7+7+3+3+3+3+3+7+3+3+3+7+7+7+3+3+3+3+3+7+3+3+3+3+3+3+3+3+3+3+3+7+3+3+3+3+3+3+3+3+3+3+3+7+3+3+3+3+3+3+3+3+3+3+3+7+3+3+3+3+3+3+3+3+3+3+3+7+3+3+3+3+3+3+3+3+3+3+3') marketplace.parse_objects('6+6+511#5+11+515#1+9+512#11+12+513#12+11+514') marketplace.player_start = [1, 2] diff --git a/objects/cafe_object.py b/objects/cafe_object.py index 72b4f87..bf70e86 100644 --- a/objects/cafe_object.py +++ b/objects/cafe_object.py @@ -64,6 +64,7 @@ def get_nearby_pos(self) -> dict[tuple[int], int]: def change_rotation(self) -> None: pass + def to_dict(self) -> dict: obj_dict = { 'pos': self.pos, diff --git a/objects/customer.py b/objects/customer.py index c70ddf7..f3b6ce1 100644 --- a/objects/customer.py +++ b/objects/customer.py @@ -1,22 +1,37 @@ -from random import randint, choice -from typing import TYPE_CHECKING, Optional -from response import ExtensionResponse -from objects.avatar import Avatar -from objects.cafe_object import ObjectType -from asyncio import sleep, current_task, wait_for, create_task +import asyncio +from asyncio import create_task, sleep, Task, current_task +from random import randint, choice, uniform, random +from typing import Optional, List, TYPE_CHECKING + from astar import AStar +from objects.avatar import Avatar +from objects.cafe_object import CafeObject, ObjectType +from objects.waiter import Waiter +from response import ExtensionResponse +from utils.items import get_dish_info if TYPE_CHECKING: - from asyncio import StreamWriter, Task from server import CafeServer - from objects.cafe import Cafe - from objects.player import Player - from objects.waiter import Waiter - from objects.cafe_object import CafeObject + from asyncio import StreamWriter -# TODO: Tidy up this function -def get_time(cafe: 'Cafe', chair: 'CafeObject') -> float: +class Action: + INSERT = 0 + WALK_TO_CHAIR = 1 + SIT_DOWN = 2 + EAT = 3 + LEAVE = 4 + FAST_FOOD = 8 + GOTO_VENDING_MACHINE = 9 + LEAVE_COMPLETE = 41 + + +class WaiterTask: + CLEAN = 0 + SERVE = 1 + + +def get_time(cafe, start_pos: list[int], end_pos: list[int]) -> float: cafe_size = cafe.size obstacles = [[0 for _ in range(cafe_size)] for _ in range(cafe_size)] @@ -27,243 +42,455 @@ def get_time(cafe: 'Cafe', chair: 'CafeObject') -> float: for obj in cafe.objects: obstacles[obj.pos[0]][obj.pos[1]] = 1 - obstacles[chair.pos[0]][chair.pos[1]] = 0 + obstacles[end_pos[0]][end_pos[1]] = 0 - start_pos = tuple(cafe.get_start_pos()) - path = AStar(obstacles, start_pos, tuple(chair.pos)).solve() + path = AStar(obstacles, tuple(start_pos), tuple(end_pos)).solve() if path: distance = len(path) - 1 - time = distance / 1.95 + time = distance / 1.85 # OLD -> 1.95 TESTED: 1.90 (customers should need to wait few sec before sitting down) + + # print(f'[CUSTOMER-ASTAR]: Start: {start_pos} End: {end_pos}') + # print(f'[CUSTOMER-ASTAR]: Time = {time}s') + # print(f'[CUSTOMER-ASTAR]: Distance = {distance}') + # print(f'[CUSTOMER-ASTAR]: Path = {path}') + else: time = -1 return time -def _get_random_avatar() -> Avatar: - gender = choice([1, 2]) +def get_random_table(cafe) -> Optional['CafeObject']: + eligible_tables = cafe.available_tables + free_tables = [table for table in eligible_tables if not table.busy] + + return choice(free_tables) if free_tables else None - skin_color = randint(0, 7) - top_color = randint(0, 16) - hair_color = randint(0, 14) - legs_color = randint(0, 16) - return Avatar('customer', gender, skin_color, top_color, hair_color, legs_color) +def check_for_foods(cafe): + counters = [obj for obj in cafe.objects if obj.type == ObjectType.COUNTER and get_time(cafe, cafe.get_start_pos(), + obj.pos) != -1] + counters_with_food = [counter for counter in counters if counter.dish_id != -1] + if counters_with_food: + return True -def _get_random_table(cafe: 'Cafe') -> Optional['CafeObject']: - eligible_tables = cafe.available_tables - nonbusy_tables = [table for table in eligible_tables if not table.busy] + return False - return choice(nonbusy_tables) if nonbusy_tables else None +def get_random_waiter(cafe, task) -> Optional['Waiter']: + waiters = cafe.waiters + free_waiters = [waiter for waiter in waiters if not waiter.task] + relevant_waiters = [waiter for waiter in free_waiters if waiter.priority != (0 if task == WaiterTask.SERVE else 100)] -def _get_random_waiter(cafe: 'Cafe') -> Optional['Waiter']: - nonbusy_waiters = [waiter for waiter in cafe.waiters if not waiter.task] + return choice(relevant_waiters) if relevant_waiters else None - return choice(nonbusy_waiters) if nonbusy_waiters else None +async def wait_for_table(cafe) -> Optional['CafeObject']: + for _ in range(10): + table = get_random_table(cafe) + if table: + return table + await sleep(1) + return None -def _check_for_foods(cafe: 'Cafe') -> bool: - counters = [obj for obj in cafe.objects if obj.type == ObjectType.COUNTER] - counters_with_food = [counter for counter in counters if counter.dish_id != -1] - return True if counters_with_food else False +async def wait_for_cleaning(server, player, table): + while True: + waiter = get_random_waiter(player.cafe, task=WaiterTask.CLEAN) + if waiter: + waiter.task = create_task(waiter.clean_table(server, player.cafe, table)) + waiter.task.add_done_callback(waiter.remove_task) + break + await sleep(0.1) -class Action: - INSERT = 0 - WALK_TO_CHAIR = 1 - SIT_DOWN = 2 - EAT = 3 - LEAVE = 4 - LEAVE_COMPLETE = 41 +async def spawn_customers_to_client(server, client, wanted_cafe): + for customer in wanted_cafe.customers: + if customer.action not in [Action.LEAVE, Action.GOTO_VENDING_MACHINE, Action.FAST_FOOD]: + spawn_info = '+'.join(map(str, [ + customer.id, # Customer ID + 0, # NPC type (0: Customer) + 0, # Favourite dish + -1, # Dish ID + 0, # Is thirsty + customer.avatar.to_string(True) # Avatar + ])) -async def spawn_cycle(server: 'CafeServer', cafe: 'Cafe') -> None: - while True: - new_id = cafe.customers[-1].id + 1 if cafe.customers else 100 - new_customer = Customer(new_id, cafe) + action = customer.action + + match customer.action: + # If the customer is walking, sending sit down to visitor client + case Action.WALK_TO_CHAIR: + action = Action.SIT_DOWN + + insert_info = '+'.join(map(str, [ + customer.id, # Customer ID + action, # Action type + customer.pos[0], # X + customer.pos[1] # Y + ])) - cafe.customers.append(new_customer) - create_task(new_customer.start(server)) + # NAV does not split by '#' + response_spawn = ExtensionResponse('nav', '-1', '0', spawn_info) - await sleep(randint(10, 10)) + # NAC uses split by '#' + # https://github.com/GGR-Devs/Cafe-Main/blob/2911a43b4d0972d2f1386edd782d3382f031f36d/com/goodgamestudios/cafe/commands/NACCommand.as#L29 + response_insert = ExtensionResponse('nac', '-1', '0', insert_info) + + await server.send_response(client, response_spawn) + await server.send_response(client, response_insert) class Customer: - def __init__(self, customer_id: int, player_cafe: 'Cafe'): + def __init__(self, customer_id: int): self.id: int = customer_id - self.avatar: Avatar = _get_random_avatar() - self.pos: list[int] = [0, 0] + self.avatar: Avatar = Avatar.get_random_avatar('Customer') + self.pos: List[int] = [0, 0] + self.task: Optional[asyncio.Task] = None - self.cafe: 'Cafe' = player_cafe - self.task: Optional['Task'] = None + self.dish = -1 + self.action = Action.INSERT + self.is_thirsty = 0 - print(f"CUSTOMER WITH ID {self.id} CREATED") + async def start(self, server, client): + address = client.get_extra_info('peername') + player = server.clients[address] - async def _spawn(self, server: 'CafeServer') -> None: - # TODO: Add favourite dish - spawn_info = '+'.join(map(str, [ - self.id, # Customer ID - 0, # NPC type (0: Customer) - 0, # Favourite dish - -1, # Dish ID - 0, # Is thirsty - self.avatar.to_string(True) # Avatar + insert_info = '+'.join(map(str, [ + str(self.id), # Customer ID + Action.INSERT # Action type (0: Insert) ])) + response_insert = ExtensionResponse('nac', '-1', '0', insert_info) + + if player.get_level() >= 6: + random_number = randint(1, 100) + if random_number < 5: + self.is_thirsty = 1 + + await self.spawn(server, player) + + for user in player.cafe.clients: + await server.send_response(user, response_insert) + + async def spawn(self, server, player): + self.task = current_task() + self.task.add_done_callback(self.remove_task) + insert_info = '+'.join(map(str, [ str(self.id), # Customer ID Action.INSERT # Action type (0: Insert) ])) - response_spawn = ExtensionResponse('nav', '-1', '0', spawn_info) response_insert = ExtensionResponse('nac', '-1', '0', insert_info) - self.pos = self.cafe.get_start_pos() + await self._spawn(server, player) - for user in self.cafe.clients: - await server.send_response(user, response_spawn) + self.pos = player.cafe.get_start_pos() + + for user in player.cafe.clients: await server.send_response(user, response_insert) - async def _wait_for_food(self, server: 'CafeServer') -> None: - while not _check_for_foods(self.cafe): - await sleep(0.1) + # print(f"[CUSTOMER] New spawned: {self.id}") - random_waiter = await self._wait_for_waiter() + await sleep(2) - random_waiter.task = create_task(random_waiter.start_serving_for(server, self)) - await random_waiter.task + if self.action == Action.GOTO_VENDING_MACHINE: + for obj in player.cafe.objects: + if obj.type == ObjectType.VENDING: + await self.go_to_vending_machine(server, player, obj) + break + return - random_waiter.task = create_task(random_waiter.go_to_counter(server)) - random_waiter.task.add_done_callback(random_waiter.remove_task) + await self.wait_for_table(server, player) - async def _wait_for_waiter(self) -> 'Waiter': - while not (waiter := _get_random_waiter(self.cafe)): - await sleep(0.1) + async def _spawn(self, server, player): + spawn_info = '+'.join(map(str, [ + self.id, # Customer ID + 0, # NPC type (0: Customer) + 0, # Favourite = Waiters priority + -1, # Dish ID + self.is_thirsty, # Is thirsty + self.avatar.to_string(True) # Avatar + ])) - return waiter + response_spawn = ExtensionResponse('nav', '-1', '0', spawn_info) - async def _wait_for_table(self) -> 'CafeObject': - while not (table := _get_random_table(self.cafe)): - await sleep(0.1) + for user in player.cafe.clients: + await server.send_response(user, response_spawn) - table.busy = True + async def wait_for_table(self, server, player): + self.task = current_task() + self.task.add_done_callback(self.remove_task) - return table + # print(f"[CUSTOMER] Started waiting for table: {self.id}") - @staticmethod - async def wait_for_cleaning(self, server: 'CafeServer', table: 'CafeObject') -> None: - while not (waiter := _get_random_waiter(self.cafe)): - await sleep(0.1) + for _ in range(10): + table = get_random_table(player.cafe) + if table: + # print(f"[CUSTOMER] Got a free table: {self.id}") + table.busy = True + await self.go_to_chair(server, player, table) + return + await sleep(1) - waiter.task = create_task(waiter.clean_table(server, table)) - waiter.task.add_done_callback(waiter.remove_task) + # print(f"[CUSTOMER] No free table found, leaving: {self.id}") + player.cafe.update_rating(-2) + server.db.update_cafe(player.id, rating=player.cafe.rating) - await waiter.task + await self.leave(server, player) - async def start(self, server: 'CafeServer') -> None: + async def go_to_chair(self, server, player, table): self.task = current_task() self.task.add_done_callback(self.remove_task) - await self._spawn(server) + chair = table.connected_chair - await sleep(2) + self.pos = chair.pos - table = _get_random_table(self.cafe) - if not table: - try: - print('Waiting for chair') - table = await wait_for(self._wait_for_table(), timeout=10) - except TimeoutError: - print('No chair, leaving') - await self.leave(server) - return - else: - table.busy = True + self.action = Action.WALK_TO_CHAIR - chair = table.connected_chair + await self._go_to_chair(server, player, chair) - await self.go_to_chair(server, chair) + walking_time = get_time(player.cafe, player.cafe.get_start_pos(), chair.pos) + # print(f"[CUSTOMER] Started going to table: {self.id} pos: {chair.pos} walking time: {walking_time}s") - walking_time = get_time(self.cafe, chair) - print(f'Calculated time: {walking_time}') await sleep(walking_time) - await self.sit_down(server, chair) + # print(f"[CUSTOMER] Arrived to the chair: {self.id}") + + if self.action == Action.GOTO_VENDING_MACHINE: + for obj in player.cafe.objects: + if obj.type == ObjectType.VENDING: + table.busy = False + await self.go_to_vending_machine(server, player, obj) + break + return + + await self.sit_down(server, player, table) + + async def sit_down(self, server, player, table): + self.task = current_task() + self.task.add_done_callback(self.remove_task) + + await self._sit_down(server, player) + + await self.wait_for_waiter(server, player, table) + + async def _sit_down(self, server, player): + self.action = Action.SIT_DOWN + + response_info = '+'.join(map(str, [ + self.id, # Customer ID + Action.SIT_DOWN, # Action type (2: Sit down) + self.pos[0], # X + self.pos[1] # Y + ])) + + response = ExtensionResponse('nac', '-1', '0', response_info) + + for user in player.cafe.clients: + await server.send_response(user, response) + + async def wait_for_waiter(self, server, player, table): + self.task = current_task() + self.task.add_done_callback(self.remove_task) + + # print(f"[CUSTOMER] Started waiting for waiter...") + waiter = None + for _ in range(25): + if check_for_foods(player.cafe): + waiter = get_random_waiter(player.cafe, task=WaiterTask.SERVE) + if waiter: + break + + # print(f"[CUSTOMER] Waiting for waiter...") + await sleep(1) + + if not waiter: + # print(f"[CUSTOMER] Couldn't get a waiter, leaving: {self.id}") - try: - await wait_for(self._wait_for_food(server), timeout=25) - except TimeoutError: - print('No food, leaving') - await self.leave(server) table.busy = False + player.cafe.update_rating(-2) + server.db.update_cafe(player.id, rating=player.cafe.rating) + + await self.leave(server, player) return - await self.eat(server, chair) + elif waiter: + waiter.task = create_task(waiter.serve_food(server, player, self, table)) + # print(f"[CUSTOMER] Got a waiter: {self.id} Waiter: {waiter.avatar.username} Pos: {waiter.pos}") + + async def eat(self, server, player, table): + self.task = current_task() + self.task.add_done_callback(self.remove_task) + + self.action = Action.EAT + + # print(f'[CUSTOMER] Started eating: {self.id} Dish: {self.dish}') + + await self._eat(server, player) + await sleep(25) - await self.leave(server) + chair = table.connected_chair + + chair.dish_status = 3 - await self.wait_for_cleaning(self, server, table) + dish_info = get_dish_info(self.dish) + income = dish_info.get('income') - async def go_to_chair(self, server: 'CafeServer', chair: 'CafeObject') -> None: - self.pos = chair.pos + player.cash += income + + server.db.update_player(player.id, cash=player.cash) + + player.cafe.update_rating(1) + server.db.update_cafe(player.id, rating=player.cafe.rating) + + create_task(wait_for_cleaning(server, player, table)) + await self.leave(server, player) + + async def _eat(self, server, player): response_info = '+'.join(map(str, [ - self.id, # Customer ID - Action.WALK_TO_CHAIR, # Action type (1: Walk to chair) - chair.pos[0], # X - chair.pos[1] # Y + self.id, # Customer ID + Action.EAT, # Action type (3: Eat) + self.pos[0], # X + self.pos[1] # Y ])) response = ExtensionResponse('nac', '-1', '0', response_info) - for user in self.cafe.clients: + for user in player.cafe.clients: await server.send_response(user, response) - async def sit_down(self, server: 'CafeServer', chair: 'CafeObject') -> None: + async def _go_to_chair(self, server, player, chair): response_info = '+'.join(map(str, [ - self.id, # Customer ID - Action.SIT_DOWN, # Action type (2: Sit down) - chair.pos[0], # X - chair.pos[1] # Y + self.id, # Customer ID + Action.WALK_TO_CHAIR, # Action type (1: Walk to chair) + chair.pos[0], # X + chair.pos[1] # Y ])) response = ExtensionResponse('nac', '-1', '0', response_info) - for user in self.cafe.clients: + for user in player.cafe.clients: await server.send_response(user, response) - async def eat(self, server: 'CafeServer', chair: 'CafeObject') -> None: + async def go_to_vending_machine(self, server, player, vending_machine): + self.task = current_task() + self.task.add_done_callback(self.remove_task) + + walking_time = get_time(player.cafe, self.pos, vending_machine.pos) + + await self._go_to_vending_machine(server, player, vending_machine) + + await sleep(walking_time) + + await self.use_vending_machine(server, player, vending_machine) + + async def _go_to_vending_machine(self, server, player, vending_machine): response_info = '+'.join(map(str, [ - self.id, # Customer ID - Action.EAT, # Action type (2: Sit down) - chair.pos[0], # X - chair.pos[1] # Y + self.id, # Customer ID + Action.GOTO_VENDING_MACHINE, # Action type (9: GOTO_VENDING_MACHINE) + vending_machine.pos[0], # X + vending_machine.pos[1] # Y ])) response = ExtensionResponse('nac', '-1', '0', response_info) - for user in self.cafe.clients: + for user in player.cafe.clients: await server.send_response(user, response) - async def leave(self, server: 'CafeServer') -> None: + async def use_vending_machine(self, server, player, vending_machine): + self.task = current_task() + self.task.add_done_callback(self.remove_task) + + self.action = Action.FAST_FOOD + + await self._use_vending_machine(server, player, vending_machine) + + facing_pos = vending_machine.pos + + nearby_pos = vending_machine.get_nearby_pos() + for pos, rotation in nearby_pos.items(): + if rotation == vending_machine.rotation: + facing_pos = pos + + self.pos = facing_pos + + if vending_machine.fast_food_amount < 1: + vending_machine.fast_food_id = -1 + server.db.update_cafe(player.id, objects=player.cafe.get_objects_as_json()) + + await sleep(2) + + await self.leave(server, player) + + async def _use_vending_machine(self, server, player, vending_machine): response_info = '+'.join(map(str, [ - self.id, # Customer ID - Action.LEAVE # Action type (4: Leave) + self.id, # Customer ID + Action.FAST_FOOD, # Action type (8: Use fast food) + vending_machine.pos[0], # X + vending_machine.pos[1] # Y ])) response = ExtensionResponse('nac', '-1', '0', response_info) - self.cafe.customers.remove(self) + for user in player.cafe.clients: + await server.send_response(user, response) - self.pos = self.cafe.get_start_pos() + async def leave(self, server, player): + self.action = Action.LEAVE - for user in self.cafe.clients: - await server.send_response(user, response) + insert_info = '+'.join(map(str, [ + str(self.id), # Customer ID + Action.LEAVE # Action type (4: Leave) + ])) + + response_insert = ExtensionResponse('nac', '-1', '0', insert_info) + + for user in player.cafe.clients: + await server.send_response(user, response_insert) + + if self.pos == player.cafe.get_start_pos(): + walking_time = 0 + else: + walking_time = get_time(player.cafe, self.pos, player.cafe.get_start_pos()) + + # Need to remove after when the customer avatar got deleted from the game. + await sleep(walking_time + 3) + + player.cafe.customers.remove(self) def remove_task(self, _: 'Task' = None): self.task = None + + +async def spawn_cycle(server, client) -> None: + address = client.get_extra_info('peername') + player = server.clients[address] + + while True: + rating = player.cafe.rating + + if rating < 150: # Customers spawning fast for a new player, because the rating go up fast. + spawn_interval = uniform(10, 20) + elif 150 <= rating < 350: + spawn_interval = uniform(4.5, 8) + elif 350 <= rating < 500: + spawn_interval = uniform(3.5, 6) + else: + spawn_interval = uniform(0.5, 5) + + await sleep(spawn_interval) + + new_id = player.cafe.customers[-1].id + 1 if player.cafe.customers else 100 + new_customer = Customer(new_id) + player.cafe.customers.append(new_customer) + + new_customer.task = create_task(new_customer.start(server, client)) + new_customer.task.add_done_callback(new_customer.remove_task) diff --git a/objects/player.py b/objects/player.py index 277f733..76e158e 100644 --- a/objects/player.py +++ b/objects/player.py @@ -1,11 +1,13 @@ +import json + from objects.avatar import Avatar from utils.items import get_dish_info from math import ceil, floor from typing import TYPE_CHECKING, Optional +from datetime import datetime, timezone, timedelta if TYPE_CHECKING: from objects.cafe import Cafe - from asyncio import Task class Player: @@ -30,8 +32,8 @@ def __init__(self): self.pos = None self.mastery: dict[int, int] = {num: 0 for num in range(1201, 1256)} - - self.customer_cycle: Optional['Task'] = None + self.last_login: str = '' + self.daily_login: str = '' def build_mastery(self) -> str: pairs = [] @@ -95,15 +97,37 @@ def get_mastery_duration(self, item_id: int) -> int: else: return floor(duration * 0.95) * 60 + def get_minutes_since_last_login(self) -> int: + if not self.last_login: + return 0 + + last_login = datetime.strptime(self.last_login, "%y-%d-%m %H:%M").replace(tzinfo=timezone.utc) + current_time = datetime.now(timezone.utc) + time_diff = current_time - last_login + minutes_diff = time_diff.total_seconds() // 60 + return floor(minutes_diff) + + def get_level(self) -> int: + if self.xp < 90: + return floor(pow(floor(self.xp / 10), 1 / 2)) + return floor(pow(floor(self.xp / 5), 1 / 3.72)) + + def check_daily_login(self): + current_time = datetime.now(timezone.utc) + daily_login = datetime.strptime(self.daily_login, "%y-%d-%m %H:%M").replace(tzinfo=timezone.utc) + if current_time - daily_login >= timedelta(hours=24): + return True + return False + def to_response(self, *params: str) -> list[str]: return [ - *params, str(self.id), str(self.id), '0', str(self.cash), str(self.gold), str(self.xp), str(self.instant_cookings), + *params, str(self.id), str(self.id), '0', str(self.cash), str(self.gold), str(self.xp), + str(self.instant_cookings), str(self.open_jobs), '0', str(int(self.played_wheel)), '0', str(int(self.allow_friend_requests)), str(int(self.allow_emails)), str(int(self.email_verified)), str(self.new_gifts), self.avatar.to_string() ] @classmethod - def new_user(cls, username: str): + def new_user(cls): player = cls() - player.avatar = Avatar(username, 2, 1, 1, 1, 1) return player diff --git a/objects/waiter.py b/objects/waiter.py index 9e60fd1..0c6c947 100644 --- a/objects/waiter.py +++ b/objects/waiter.py @@ -1,258 +1,351 @@ -from enum import Enum -from asyncio import sleep, current_task, gather -from random import randint, choice +from asyncio import current_task, sleep, Task, gather, create_task +from random import choice from typing import TYPE_CHECKING, Optional -from response import ExtensionResponse -from objects.avatar import Avatar -from objects.cafe_object import ObjectType -from objects.customer import Customer + from astar import AStar +from objects.avatar import Avatar +from objects.cafe_object import ObjectType, CafeObject +from response import ExtensionResponse if TYPE_CHECKING: - from asyncio import Task from server import CafeServer - from objects.cafe import Cafe - from objects.cafe_object import CafeObject + from asyncio import StreamWriter -def _get_random_avatar(name: str, gender: int) -> Avatar: - skin_color = randint(0, 7) - hair_color = randint(0, 14) +class Priority: + CLEANING = 0 + BOTH = 50 + SERVING = 100 - return Avatar(name, gender, skin_color, 0, hair_color, 0) + +class Action: + INSERT = 0 + MOVE_TO_COUNTER = 5 + FEED = 6 + CLEAN = 7 -def _get_random_counter(cafe: 'Cafe') -> 'CafeObject': - counters = [obj for obj in cafe.objects if obj.type == ObjectType.COUNTER] +def get_random_counter(cafe) -> 'CafeObject': + counters = [obj for obj in cafe.objects if obj.type == ObjectType.COUNTER and get_time(cafe, cafe.get_start_pos(), + obj.pos) != -1] counters_with_food = [counter for counter in counters if counter.dish_id != -1] - if counters_with_food: - return choice(counters_with_food) - return choice(counters) + if counters: + if counters_with_food: + return choice(counters_with_food) + return choice(counters) -class Priority(Enum): - CLEANING = 0 - SERVING = 1 - BOTH = 2 +def get_time(cafe, start_pos: list[int], end_pos: list[int]) -> float: + cafe_size = cafe.size + obstacles = [[0 for _ in range(cafe_size)] for _ in range(cafe_size)] + for i in range(cafe_size): + obstacles[i][0] = 1 + obstacles[0][i] = 1 + for obj in cafe.objects: + obstacles[obj.pos[0]][obj.pos[1]] = 1 -class Action: - INSERT = 0 - MOVE_TO_COUNTER = 5 - FEED = 6 - CLEAN = 7 + obstacles[start_pos[0]][start_pos[1]] = 0 + obstacles[end_pos[0]][end_pos[1]] = 0 + path = AStar(obstacles, start_pos, tuple(end_pos)).solve() -async def spawn_multiple(server: 'CafeServer', cafe: 'Cafe') -> None: - waiter_responses = [] - for waiter in cafe.waiters: - waiter.task = current_task() - waiter.task.add_done_callback(waiter.remove_task) + if path: + distance = len(path) - 1 + time = distance / 2.30 # WAITERS ARE FASTER THAN CUSTOMERS (40->45) - waiter_responses.append('+'.join(map(str, [ - waiter.id, # Waiter ID - 1, # NPC type (1: Waiter) - 0, # Favourite (unnecessary for waiters) - -1, # Dish ID (unnecessary for waiters) - waiter.avatar.to_string(True) # Avatar - ]))) + # print(f'[WAITER-ASTAR]: Start: {start_pos} End: {end_pos}') + # print(f'[WAITER-ASTAR]: Time = {time}s') + # print(f'[WAITER-ASTAR]: Distance = {distance}') + # print(f'[WAITER-ASTAR]: Path = {path}') + else: + time = -1 - response_add = ExtensionResponse('nav', '-1', '0', *waiter_responses) + return time - for user in cafe.clients: - await server.send_response(user, response_add) - for waiter in cafe.waiters: - insert_info = '+'.join(map(str, [ - waiter.id, # Waiter ID - Action.INSERT # Action type (0: insert) - ])) +async def spawn_waiters(server, client) -> None: + address = client.get_extra_info('peername') + player = server.clients[address] - response_insert = ExtensionResponse('nac', '-1', '0', insert_info) - await server.send_response(user, response_insert) + await gather(*(waiter.spawn(server, player.cafe) for waiter in player.cafe.waiters)) + + counter = get_random_counter(player.cafe) + + # PATH FOUND TO COUNTER + if counter: + for waiter in player.cafe.waiters: + waiter.counter = counter + + await gather(*[waiter.go_to_counter(server, player.cafe, + counter) for waiter in player.cafe.waiters]) + + +async def spawn_waiters_to_client(server, client, wanted_cafe): + for waiter in wanted_cafe.waiters: + add_info = '+'.join(map(str, [ + waiter.id, # Waiter ID + 1, # NPC type (1: Waiter) + waiter.priority, # Priority + -1, # Dish ID (unnecessary for waiters) + waiter.avatar.to_string(True), # Avatar + ])) - await sleep(2) + counter = waiter.counter - for user in cafe.clients: - await gather(*[waiter.go_to_counter(server) for waiter in cafe.waiters]) + # PATH FOUND TO COUNTER + if counter is not None: + pos = counter.pos + else: + # PATH NOT FOUND TO COUNTER + pos = wanted_cafe.get_start_pos() + + insert_info = '+'.join(map(str, [ + waiter.id, # Waiter ID + Action.MOVE_TO_COUNTER, # Action type + pos[0], # X + pos[1] # Y + ])) + + response_add = ExtensionResponse('nav', '-1', '0', add_info) + response_insert = ExtensionResponse('nac', '-1', '0', insert_info) + + await server.send_response(client, response_add) + await server.send_response(client, response_insert) class Waiter: - def __init__(self, waiter_id: int, name: str, gender: str, cafe: 'Cafe', priority: Priority = Priority.BOTH, avatar: Avatar = None): + def __init__(self, waiter_id: int, name: str, gender: int, priority: int, + avatar: Avatar = None): self.id: int = waiter_id self.name: str = name self.gender: int = gender - self.priority: Priority = priority - self.avatar: Avatar = avatar if avatar else _get_random_avatar(name, gender) + self.priority = priority + self.avatar: Avatar = avatar if avatar else Avatar.get_random_avatar(name, gender) self.pos: list[int] = [0, 0] + self.counter = None - self.cafe: 'Cafe' = cafe - self.task: Optional['Task'] = None + self.dish = -1 + self.action = Action.INSERT - async def _spawn(self, server: 'CafeServer') -> None: - self.pos = self.cafe.get_start_pos() + self.task: Optional['Task'] = None - add_info = '+'.join(map(str, [ - self.id, # Waiter ID - 1, # NPC type (1: Waiter) - 0, # Favourite (unnecessary for waiters) - -1, # Dish ID (unnecessary for waiters) - self.avatar.to_string(True) # Avatar - ])) + async def spawn(self, server, player_cafe) -> None: + self.task = current_task() + self.task.add_done_callback(self.remove_task) insert_info = '+'.join(map(str, [ - self.id, # Waiter ID - Action.INSERT # Action type (0: insert) + self.id, # Waiter ID + Action.INSERT # Action type (0: Insert) ])) - response_add = ExtensionResponse('nav', '-1', '0', add_info) response_insert = ExtensionResponse('nac', '-1', '0', insert_info) - for user in self.cafe.clients: - await server.send_response(user, response_add) + await self._spawn(server, player_cafe) + + for user in player_cafe.clients: await server.send_response(user, response_insert) - async def _feed(self, server: 'CafeServer', customer: 'Customer'): - self.pos = customer.pos + await sleep(2) - feed_info = '+'.join(map(str, [ - self.id, # Waiter ID - Action.FEED, # Action type (6: feed) - customer.pos[0], # Customer X - customer.pos[1], # Customer Y + async def _spawn(self, server, player_cafe) -> None: + self.pos = player_cafe.get_start_pos() + + add_info = '+'.join(map(str, [ + self.id, # Waiter ID + 1, # NPC type (1: Waiter) + self.priority, # Priority + -1, # Dish ID (unnecessary for waiters) + self.avatar.to_string(True), # Avatar ])) - response = ExtensionResponse('nac', '-1', '0', feed_info) + response_add = ExtensionResponse('nav', '-1', '0', add_info) - for user in self.cafe.clients: - await server.send_response(user, response) + for user in player_cafe.clients: + await server.send_response(user, response_add) - async def _clean(self, server: 'CafeServer', chair: 'CafeObject') -> None: - self.pos = chair.pos + async def go_to_counter(self, server, player_cafe, counter=None): + self.action = Action.MOVE_TO_COUNTER - info = '+'.join(map(str, [ - self.id, # Waiter ID - Action.CLEAN, # Action type (7: clean) - chair.pos[0], # Customer X - chair.pos[1], # Customer Y - ])) + self.task = current_task() + self.task.add_done_callback(self.remove_task) - response = ExtensionResponse('nac', '-1', '0', info) + if not counter: + counter = get_random_counter(player_cafe) - for user in self.cafe.clients: - await server.send_response(user, response) + # PATH FOUND TO COUNTER + if counter: + response_info = '+'.join(map(str, [ + self.id, # Waiter ID + Action.MOVE_TO_COUNTER, # Action type (5: go to counter) + counter.pos[0], # Counter X + counter.pos[1] # Counter Y + ])) - @staticmethod - def _get_time(cafe: 'Cafe', pos_start: list[int], pos_end: list[int]) -> float: - cafe_size = cafe.size - obstacles = [[0 for _ in range(cafe_size)] for _ in range(cafe_size)] + response = ExtensionResponse('nac', '-1', '0', response_info) - for i in range(cafe_size): - obstacles[i][0] = 1 - obstacles[0][i] = 1 + for user in player_cafe.clients: + await server.send_response(user, response) - for obj in cafe.objects: - obstacles[obj.pos[0]][obj.pos[1]] = 1 + self.counter = counter - obstacles[pos_start[0]][pos_start[1]] = 0 - obstacles[pos_end[0]][pos_end[1]] = 0 + walking_time = get_time(player_cafe, self.pos, counter.pos) - path = AStar(obstacles, pos_start, tuple(pos_end)).solve() - distance = len(path) - 1 - time = distance / 1.95 + # print(f"[WAITER] Walking time: {walking_time}") - return time + await sleep(walking_time + 0.5) - async def clean_table(self, server: 'CafeServer', table: 'CafeObject') -> None: - await self._clean(server, table.connected_chair) + self.pos = player_cafe.get_shortest_pos(self.pos, counter.pos) - time_to_wait = self._get_time(self.cafe, self.pos, table.connected_chair.pos) - await sleep(time_to_wait + 2) + # print(f"[WAITER] Waiter should stay at: {self.pos}") - table.busy = False + async def serve_food(self, server, player, customer, table): + self.task = current_task() + self.task.add_done_callback(self.remove_task) + + # print(f'[WAITER] Called to serve food: {self.avatar.username} to customer: {customer.id}') + + # print(f'[WAITER] Waiter counter is: {self.counter.pos} Dish id: {self.counter.dish_id} Amount: { + # self.counter.dish_amount}') + + if self.counter.dish_id == -1: + # print(f'[WAITER] Arr! Waiter: {self.avatar.username} has staying at an empty counter! Trying to find + # another...') + + new_counter = get_random_counter(player.cafe) + if new_counter.dish_id == -1: + # print(f'[WAITER] No empty counters found! Calling customer to start waiting again. Waiter task + # exit, bye!') + customer.task = create_task(customer.wait_for_waiter(server, player, table)) + + return + + else: + # print(f'[WAITER] Waiter is going to the new counter {new_counter.pos}') + self.task = self.go_to_counter(server, player.cafe, new_counter) + await self.task + + # print(f'[WAITER] Waiter is arrived to the new counter {new_counter.pos}') + + # print(f'[WAITER] Calling again to serve the dish to the customer: {customer.id}') + await self.serve_food(server, player, customer, table) + + # Cant cause loop, only the player can cause loop by deleting the dishes always + + else: + self.action = Action.FEED + + self.dish = self.counter.dish_id + self.counter.dish_amount -= 1 + + if self.counter.dish_amount <= 0: + self.counter.dish_id = -1 + # print(f"[WAITER] Counter is empty") + + # print(f"[WAITER] Waiter: {self.avatar.username} has dish: {self.dish} in their hands") + # print(f"[WAITER] Counter data: DISH ID: {self.counter.dish_id} AMOUNT LEFT: {self.counter.dish_amount}") + + server.db.update_cafe(player.id, objects=player.cafe.get_objects_as_json()) + + response_info = '+'.join(map(str, [ + self.id, # Waiter ID + Action.FEED, # Action type (6: feed) + customer.pos[0], # Customer X + customer.pos[1] # Customer Y + ])) - counter = await self.go_to_counter(server) + response = ExtensionResponse('nac', '-1', '0', response_info) - time_to_wait = self._get_time(self.cafe, self.pos, counter.pos) - await sleep(time_to_wait + 2) + for user in player.cafe.clients: + await server.send_response(user, response) - async def go_to_counter(self, server: 'CafeServer', counter_pos: Optional[list[int]] = None) -> 'CafeObject': - counter = self.cafe.get_object(*counter_pos) if counter_pos else _get_random_counter(self.cafe) + chair = table.connected_chair - self.pos = counter.pos + chair.dish_id = self.dish + chair.dish_status = 1 + + time_to_wait = get_time(player.cafe, self.pos, customer.pos) + + # print(f"[WAITER] Time to waiter: {self.id} arrive to the customer: {customer.id} is: {time_to_wait}s") + + await sleep(time_to_wait + 0.5) + + customer.dish = self.dish + + self.dish = -1 + + self.pos = player.cafe.get_shortest_pos(self.pos, customer.pos) + + # print(f"[WAITER] Waiter: {self.id} is arrived to: {customer.id}") + + customer.task = create_task(customer.eat(server, player, table)) + + await self.go_to_counter(server, player.cafe) + + async def clean_table(self, server, player_cafe, table): + # print(f"[WAITER] Waiter {self.avatar.username} is assigned to clean table {table.pos}") + self.action = Action.CLEAN response_info = '+'.join(map(str, [ - self.id, # Waiter ID - Action.MOVE_TO_COUNTER, # Action type (5: go to counter) - counter.pos[0], # Counter X - counter.pos[1] # Counter Y + self.id, # Waiter ID + Action.CLEAN, # Action type (7: clean) + table.connected_chair.pos[0], # Chair X + table.connected_chair.pos[1] # Chair Y ])) + chair = table.connected_chair + + time_to_walk = get_time(player_cafe, self.pos, chair.pos) + response = ExtensionResponse('nac', '-1', '0', response_info) - for user in self.cafe.clients: + for user in player_cafe.clients: await server.send_response(user, response) - time_to_wait = self._get_time(self.cafe, self.cafe.get_start_pos(), counter.pos) - await sleep(time_to_wait + 1) + chair.dish_id = -1 + chair.dish_status = 0 - return counter + await sleep(time_to_walk) - def remove_task(self, _: 'Task' = None): - self.task = None + # print(f"[WAITER] Waiter {self.avatar.username} is started to clean the table: {table.pos}") - async def spawn(self, server: 'CafeServer') -> None: - self.task = current_task() - self.task.add_done_callback(self.remove_task) + self.pos = player_cafe.get_shortest_pos(self.pos, chair.pos) - await self._spawn(server) await sleep(2) - counter = await self.go_to_counter(server) - async def start_serving_for(self, server: 'CafeServer', customer: 'Customer') -> None: - counter = self.cafe.get_object(*self.pos) - counter.dish_amount -= 1 + # print(f"[WAITER] Waiter is cleaned the table: {1 + 1}") - if counter.dish_amount == 0: - counter.dish_id = -1 + # print(f"[WAITER] Table cleaning finished, going back...") - await self._feed(server, customer) + table.busy = False - time_to_wait = self._get_time(self.cafe, self.pos, customer.pos) - await sleep(time_to_wait + 2) + await self.go_to_counter(server, player_cafe) + + def remove_task(self, _: 'Task' = None): + # if self.task: + # print(f"[WAITER] Waiter: {self.avatar.username} Task finished: {self.task.get_coro()}") + self.task = None def to_dict(self): waiter_dict = { 'waiter_id': self.id, 'name': self.name, 'gender': self.gender, - 'priority': self.priority.value, + 'priority': self.priority, 'avatar': self.avatar.to_string() } return waiter_dict @classmethod - def from_dict(cls, waiter_dict: dict, cafe: 'Cafe') -> 'Waiter': + def from_dict(cls, waiter_dict: dict) -> 'Waiter': waiter = Waiter( waiter_dict.get('waiter_id'), waiter_dict.get('name'), waiter_dict.get('gender'), - cafe, - Priority(waiter_dict.get('priority')), + waiter_dict.get('priority'), Avatar.from_string(waiter_dict.get('avatar')) ) return waiter - - @classmethod - def create_starting_waiter(cls, cafe) -> 'Waiter': - gender = randint(1, 2) - - if gender == 2: - name = choice(["Oskar", "James", "Jeffrey", "Tom", "William"]) - else: - name = choice(["Jane", "Lucy", "Emma", "Stacey", "Becky"]) - - return cls(0, name, gender, cafe) diff --git a/server.py b/server.py index 0086471..8589a05 100644 --- a/server.py +++ b/server.py @@ -1,5 +1,8 @@ import sys import socket +from asyncio import wait_for +from datetime import datetime, timezone + from database import Database from command import handle_command from request import Request, RequestType @@ -7,6 +10,16 @@ from objects.cafe import Cafe from typing import TYPE_CHECKING import asyncio +import logging + +logger = logging.getLogger(__name__) +logger.setLevel(logging.INFO) +ch = logging.StreamHandler() +ch.setLevel(logging.INFO) +formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s') +ch.setFormatter(formatter) +logger.addHandler(ch) +logging.basicConfig(filename=f'server_log.log', level=logging.INFO) if TYPE_CHECKING: from objects.player import Player @@ -38,7 +51,7 @@ async def send_response(connection, response: Response): response = response.raw_response connection.write(response.encode()) await connection.drain() - print('[SENT] {}'.format(response.strip('\x00'))) + # print('[SENT] {}'.format(response.strip('\x00'))) def bind_socket(self) -> None: try: @@ -52,11 +65,20 @@ def bind_socket(self) -> None: async def handle_client(self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter) -> None: tasks = [] + connection_timeout = 600 + + logger.info(f"[CONNECTED] {writer.get_extra_info('peername')}") while not writer.is_closing() and self.running: try: - request_str = (await reader.read(1024)).decode() + request_str = (await wait_for(reader.read(1024), timeout=connection_timeout)).decode() + except ConnectionResetError: + logger.info(f"[ERROR] {writer.get_extra_info('peername')} connection reset") + writer.close() + break + except TimeoutError: + logger.info(f"[DISCONNECTED] {writer.get_extra_info('peername')} due to inactivation") writer.close() break @@ -65,17 +87,20 @@ async def handle_client(self, reader: asyncio.StreamReader, writer: asyncio.Stre request = Request(request_str.strip('\x00')) - print(f'\n[RECEIVED] {request.raw_request}') + # print(f'\n[RECEIVED] {request.raw_request}') if request.type == RequestType.DISCONNECT: + logger.info(f"[DISCONNECTED] {writer.get_extra_info('peername')} initiated disconnect") writer.close() elif request.type == RequestType.POLICY_FILE_REQUEST: + # logger.info(f"[POLICY FILE REQUEST] {writer.get_extra_info('peername')} requested policy") response = SystemResponse(request.cmd) await self.send_response(writer, response) writer.close() elif request.type == RequestType.XML and request.cmd != 'login': + # logger.info(f"[XML REQUEST] {writer.get_extra_info('peername')} sent XML request (cmd: {request.cmd})") response = SystemResponse(request.cmd) await self.send_response(writer, response) @@ -86,39 +111,55 @@ async def handle_client(self, reader: asyncio.StreamReader, writer: asyncio.Stre peername = writer.get_extra_info('peername') if player := self.clients.get(peername): + logger.info(f"[DISCONNECTING PLAYER] {peername} is being disconnected") + if player.room: player.room.clients.remove(writer) - for client in player.room.clients: await self.send_response(client, ExtensionResponse('juq', '-1', '0', str(player.id))) player.room = None + logger.info(f"[CANCELLING NPC] Cancelling NPC tasks") + for waiter in player.cafe.waiters: if waiter.task: waiter.task.cancel() - if player.customer_cycle: - player.customer_cycle.cancel() - player.customer_cycle = None for table in player.cafe.available_tables: table.busy = False for customer in player.cafe.customers: - if customer.task: + if customer.task and not customer.task.done(): customer.task.cancel() + if player.cafe.customer_cycle: + player.cafe.customer_cycle.cancel() + player.cafe.customer_cycle = None + + if not player.cafe.customer_cycle: + logger.info(f"[CANCELLING NPC] Cancelled NPC tasks!") + player.online = False + current_time = datetime.now(timezone.utc).strftime("%y-%d-%m %H:%M") + player.last_login = current_time + + self.db.update_player(player.id, last_login=player.last_login) + + self.players.remove(self.clients[peername]) del self.clients[peername] for task in tasks: + logger.info(f"[CANCELLING TASK] Cancelling task: {task}") task.cancel() + logger.info(f"[DISCONNECTED] {peername} successfully disconnected and cleaned up") print(f'[DISCONNECTED] {peername}') async def start(self) -> None: + logger.info('Server started') # self.socket.listen(self.max_conn) self.running = True @@ -144,5 +185,16 @@ async def start(self) -> None: # # asyncio.run(self.handle_client(client_connection, client_address)) def stop(self) -> None: + for client in list(self.clients.keys()): + player = self.clients[client] + + current_time = datetime.now(timezone.utc).strftime("%y-%d-%m %H:%M") + player.last_login = current_time + + self.db.update_player(player.id, last_login=player.last_login) + + print(f'[DISCONNECTED] {client}') + del self.clients[client] + self.running = False self.socket.close() diff --git a/utils/items.py b/utils/items.py index 9776d33..05ca524 100644 --- a/utils/items.py +++ b/utils/items.py @@ -38,7 +38,10 @@ def get_dish_info(dish_id: int) -> dict: 'ingredients': ingredients, 'duration': int(wod.attrib.get('duration', 0)), 'amount': int(wod.attrib.get('servings', 0)), - 'xp': int(wod.attrib.get('xp', 0)), + 'level': int(wod.attrib.get('level', 0)), + 'events': int(wod.attrib.get('events', 0)), + 'income': int(wod.attrib.get('incomePerServing', 0)), + 'xp': int(wod.attrib.get('xp', 0)) } return {} @@ -56,3 +59,34 @@ def get_expansion(exp_id: int) -> dict: } return {} + + +def get_ingredient(ing_id: int) -> dict: + root = tree.getroot() + + for wod in root.findall('wod'): + if int(wod.attrib.get('id')) == ing_id: + return { + 'level': int(wod.attrib.get('level', 0)), + 'gold': int(wod.attrib.get('gold', 0)), + 'events': int(wod.attrib.get('events', 0)), + } + + return {} + + +def get_fast_food_info(drink_id: int) -> dict: + root = tree.getroot() + + for wod in root.findall("wod"): + if int(wod.attrib.get("id")) == drink_id: + return { + "cash": int(wod.attrib.get("cash", 0)), + "gold": int(wod.attrib.get("gold", 0)), + "xp": int(wod.attrib.get("xp", 0)), + "income": int(wod.attrib.get("incomePerServing", 0)), + "amount": int(wod.attrib.get("servings", 0)), + "rating": int(wod.attrib.get("ratingBonus", 0)), + } + + return {}