diff options
Diffstat (limited to 'gui')
-rw-r--r-- | gui/__init__.py | 0 | ||||
-rw-r--r-- | gui/__init__.pyc | bin | 0 -> 154 bytes | |||
-rw-r--r-- | gui/__pycache__/__init__.cpython-37.pyc | bin | 0 -> 154 bytes | |||
-rw-r--r-- | gui/__pycache__/managui.cpython-37.pyc | bin | 0 -> 6859 bytes | |||
-rw-r--r-- | gui/chatlog.py | 86 | ||||
-rw-r--r-- | gui/handlers.py | 49 | ||||
-rw-r--r-- | gui/handlers.pyc | bin | 0 -> 2190 bytes | |||
-rw-r--r-- | gui/icon.png | bin | 0 -> 69430 bytes | |||
-rw-r--r-- | gui/managui.kv | 155 | ||||
-rw-r--r-- | gui/managui.py | 192 | ||||
-rw-r--r-- | gui/managui.pyc | bin | 0 -> 9104 bytes | |||
-rw-r--r-- | gui/pathfind.py | 91 | ||||
-rw-r--r-- | gui/plist.py | 54 | ||||
-rw-r--r-- | gui/tmxmap.py | 175 |
14 files changed, 802 insertions, 0 deletions
diff --git a/gui/__init__.py b/gui/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/gui/__init__.py diff --git a/gui/__init__.pyc b/gui/__init__.pyc Binary files differnew file mode 100644 index 0000000..de4ecef --- /dev/null +++ b/gui/__init__.pyc diff --git a/gui/__pycache__/__init__.cpython-37.pyc b/gui/__pycache__/__init__.cpython-37.pyc Binary files differnew file mode 100644 index 0000000..babd95b --- /dev/null +++ b/gui/__pycache__/__init__.cpython-37.pyc diff --git a/gui/__pycache__/managui.cpython-37.pyc b/gui/__pycache__/managui.cpython-37.pyc Binary files differnew file mode 100644 index 0000000..c15bd16 --- /dev/null +++ b/gui/__pycache__/managui.cpython-37.pyc diff --git a/gui/chatlog.py b/gui/chatlog.py new file mode 100644 index 0000000..aca9564 --- /dev/null +++ b/gui/chatlog.py @@ -0,0 +1,86 @@ +from kivy.event import EventDispatcher +from kivy.uix.abstractview import AbstractView +from kivy.properties import ListProperty, NumericProperty +from kivy.adapters.simplelistadapter import SimpleListAdapter +from kivy.utils import escape_markup +from kivy.clock import Clock +from kivy.uix.label import Label +# from utils import log_method + +from textutils import (links_to_markup, replace_emotes, preprocess, + remove_formatting) + + +class ChatLog(AbstractView, EventDispatcher): + + def __init__(self, **kwargs): + # Check for adapter argument + if 'adapter' not in kwargs: + list_adapter = SimpleListAdapter(data=[], cls=Label) + kwargs['adapter'] = list_adapter + + super(ChatLog, self).__init__(**kwargs) + + self._views = [] + + populate = self._trigger_populate = Clock.create_trigger( + self._populate, -1) + + fbind = self.fbind + fbind('adapter', populate) + fbind('size', populate) + fbind('pos', populate) + + max_lines = NumericProperty(100) + cut_lines = NumericProperty(10) + + def _populate(self, *args): + container = self.container + adapter = self.adapter + container.clear_widgets() + self._views = [] + + for index in range(adapter.get_count()): + item_view = adapter.get_view(index) + self.container.add_widget(item_view) + self._views.append(item_view) + + container.height = self._container_height() + + def _append(self, msg): + container = self.container + adapter = self.adapter + views = self._views + cl = self.cut_lines + + if len(views) >= self.max_lines: + container.clear_widgets(views[:cl]) + self._views = views[cl:] + adapter.data = adapter.data[cl:] + + adapter.data.append(msg) + item_view = adapter.get_view(adapter.get_count() - 1) + item_view.texture_update() + + self._views.append(item_view) + container.add_widget(item_view) + + def _container_height(self, *args): + h = 0 + for v in self._views: + h += v.height + return h + + def append_message(self, msg): + msg = preprocess(msg, (replace_emotes, + remove_formatting)) + msg = links_to_markup(escape_markup(msg)) + self._append(msg) + self.container.height = self._container_height() + self.children[0].scroll_y = 0 + + def msg_converter(self, index, msg): + b = (index % 2) * 0.04 + return {'text': msg, + 'width': self.width, + 'background_color': (0 + b, 0.17 + b, 0.21 + b, 1)} diff --git a/gui/handlers.py b/gui/handlers.py new file mode 100644 index 0000000..b6f9d0b --- /dev/null +++ b/gui/handlers.py @@ -0,0 +1,49 @@ + +import net.mapserv as mapserv +from loggers import debuglog +from utils import extends + + +__all__ = ['app'] + + +map_name = "" +app = None + + +@extends('smsg_whisper_response') +def send_whisper_result(data): + if data.code == 0: + last_nick = mapserv.last_whisper['to'] + app.root.chat_input.text = '/w "{}" '.format(last_nick) + app.root.chat_input.focus = True + + +@extends('smsg_player_warp') +def player_warp(data): + mapserv.cmsg_map_loaded() + m = "[warp] {} ({},{})".format(data.map, data.x, data.y) + debuglog.info(m) + + +@extends('smsg_char_map_info') +def char_map_info(data): + global map_name + map_name = data.map_name + + +@extends('smsg_map_login_success') +def map_login_success(data): + m = "[map] {} ({},{})".format(map_name, data.coor.x, data.coor.y) + debuglog.info(m) + mapserv.server.raw = True + mapserv.cmsg_map_loaded() + + +@extends('smsg_connection_problem') +def connection_problem(data): + error_codes = { + 2 : "Account already in use" + } + msg = error_codes.get(data.code, str(data.code)) + debuglog.error('Connection problem: %s', msg) diff --git a/gui/handlers.pyc b/gui/handlers.pyc Binary files differnew file mode 100644 index 0000000..8532460 --- /dev/null +++ b/gui/handlers.pyc diff --git a/gui/icon.png b/gui/icon.png Binary files differnew file mode 100644 index 0000000..3bea05d --- /dev/null +++ b/gui/icon.png diff --git a/gui/managui.kv b/gui/managui.kv new file mode 100644 index 0000000..7e41701 --- /dev/null +++ b/gui/managui.kv @@ -0,0 +1,155 @@ +#:import la kivy.adapters.simplelistadapter +#:import ChatLog gui.chatlog.ChatLog +#:import PlayersList gui.plist.PlayersList +#:import PlayersListItem gui.plist.PlayersListItem + + +<PlayersListItem>: + + canvas.before: + Color: + rgba: 0.05, 0.05, 0.05, 1 + Line: + points: [ self.x, self.y, self.x+self.width, self.y ] + + Label: + text: root.nick + + +<PlayersList>: + + canvas.before: + Color: + rgba: .25, .1, .9, 1 + Rectangle: + pos: self.pos + size: self.size + + +<RootWidget>: + mobile: True if self.width < dp(600) else False + + canvas.before: + Color: + rgba: 0.01, 0.33, 0.32, 1 + Rectangle: + pos: self.pos + size: self.size + + messages_log: id_chat_log + chat_input: id_chat_input + players_list: id_players_list + + BoxLayout: + orientation: 'horizontal' + + BoxLayout: + orientation: 'vertical' + + ChatLog: + id: id_chat_log + adapter: + la.SimpleListAdapter( + data=['Welcome to [ref=https://bitbucket.org/rumly111/manachat/][color=0000ff]ManaChat[/color][/ref]. Press F1 to show settings. Press ESCAPE to toggle menu.'], + template='ChatLogItem', + args_converter=self.msg_converter) + + TextInput: + id: id_chat_input + size_hint_y: None + height: '50dp' + # focus: True + multiline: False + on_text_validate: root.on_command_enter(args) + + PlayersList: + id: id_players_list + size_hint_x: None + width: '150dp' + + +[ChatLogItem@Label]: + + canvas.before: + Color: + rgba: ctx.background_color + Rectangle: + pos: self.pos + size: self.size + + text: ctx.text + width: ctx.width + text_size: self.width, None + size_hint: None, None + height: self.texture_size[1] + 10 + markup: True + on_ref_press: app.open_link(args[1]) + + +<ChatLog>: + container: container + ScrollView: + pos: root.pos + do_scroll_x: False + GridLayout: + cols: 1 + id: container + size_hint_y: None + + +<AboutPopup@Popup>: + title_size: '14dp' + title: 'About' + size_hint: 0.9, None + height: '140dp' + + Label: + id: lbl + text_size: self.width, None + size_hint_y: None + height: self.texture_size[1] + text: + '''ManaChat is a multi-purpose chat client for The Mana World MMORPG + Author: Joseph Botosh <rumly111@gmail.com> (TMW nickname: Travolta) + Licence: Gnu General Public Licence, rev. 2 + Homepage: [ref=https://bitbucket.org/rumly111/manachat/]https://bitbucket.org/rumly111/manachat/[/ref]''' + + markup: True + on_ref_press: app.open_link(args[1]) + + +<MenuPopup@Popup>: + title_size: '14dp' + title: 'ManaChat' + auto_dismiss: False + size_hint: None, None + width: "200dp" + height: "250dp" + + BoxLayout: + spacing: "2dp" + padding: "2dp" + orientation: "vertical" + + Button: + text: "Connect" + on_press: app.reconnect() + + Button: + text: "Config" + on_press: app.open_settings() + + Button: + text: "About" + on_press: app.show_about() + + Button: + text: "Exit" + on_press: app.stop() + + +<SettingPassword>: + Label: + text: '*' * len(root.value) if root.value else '' + pos: root.pos + font_size: '15sp' diff --git a/gui/managui.py b/gui/managui.py new file mode 100644 index 0000000..d6dd79a --- /dev/null +++ b/gui/managui.py @@ -0,0 +1,192 @@ +#!/usr/bin/python2 +# -- coding: utf-8 -- + +import asyncore +import logging +import webbrowser + +import kivy +kivy.require('1.9.1') + +from kivy.app import App +from kivy.clock import Clock +from kivy.core.window import Window +from kivy.properties import BooleanProperty +from kivy.uix.floatlayout import FloatLayout +from kivy.uix.popup import Popup + +from kivy.config import ConfigParser +config = ConfigParser() +config.read("manachat.ini") + +import monsterdb +import itemdb +import net +import net.mapserv as mapserv +import gui.handlers as handlers +import plugins +from commands import process_line +from net.onlineusers import OnlineUsers +from loggers import netlog, debuglog +from logicmanager import logic_manager + + +class DebugLogHandler(logging.Handler): + + def __init__(self, app, **kwargs): + self.app = app + super(self.__class__, self).__init__(**kwargs) + + def emit(self, record): + msg = self.format(record) + self.app.root.messages_log.append_message(msg) + + +class MenuPopup(Popup): + visible = BooleanProperty(False) + + def on_open(self, *args): + self.visible = True + + def on_dismiss(self, *args): + self.visible = False + + +class AboutPopup(Popup): + pass + + +class RootWidget(FloatLayout): + mobile = BooleanProperty(False) + + def _focus_chat_input(self, dt): + self.chat_input.focus = True + + def on_command_enter(self, *args): + process_line(self.chat_input.text.encode('utf-8')) + self.chat_input.text = '' + Clock.schedule_once(self._focus_chat_input, 0.1) # dirty hack :^) + + +class ManaGuiApp(App): + use_kivy_settings = BooleanProperty(False) + + def hook_keyboard(self, window, key, *largs): + if key == 27: + self.show_menu(not self._menu_popup.visible) + # self.stop() + return True + return False + + def on_start(self): + if config.getboolean('Other', 'log_network_packets'): + import os + import tempfile + + logfile = os.path.join(tempfile.gettempdir(), "netlog.txt") + netlog.setLevel(logging.INFO) + fh = logging.FileHandler(logfile, mode="w") + fmt = logging.Formatter("[%(asctime)s] %(message)s", + datefmt="%Y-%m-%d %H:%M:%S") + fh.setFormatter(fmt) + netlog.addHandler(fh) + + monsterdb.read_monster_db() + itemdb.load_itemdb('itemdb.txt') + + dbgh = DebugLogHandler(self) + dbgh.setFormatter(logging.Formatter("[%(asctime)s] %(message)s", + datefmt="%H:%M")) + debuglog.addHandler(dbgh) + debuglog.setLevel(logging.INFO) + + net2 = DebugLogHandler(self) + net2.setFormatter(logging.Formatter("[%(asctime)s] %(message)s", + datefmt="%H:%M")) + net2.setLevel(logging.ERROR) + netlog.addHandler(net2) + + plugins.load_plugins(config) + + handlers.app = self + + import chat + chat.pp_actions = () + + # self.reconnect() + + Clock.schedule_once(self.update_online_list, 0.2) + Clock.schedule_interval(self.update_online_list, 35) + Clock.schedule_interval(self.update_loop, 0) + Clock.schedule_once(self.show_menu, 1.5) + + def build(self): + self.icon = 'icon.png' + Window.bind(on_keyboard=self.hook_keyboard) + return RootWidget() + + def build_settings(self, settings): + from kivy.uix.settings import SettingString + + class SettingPassword(SettingString): + def _create_popup(self, instance): + SettingString._create_popup(self, instance) + self.textinput.password = True + + def add_widget(self, *largs): + if self.content is not None: + self.content.clear_widgets() + return SettingString.add_widget(self, *largs) + + settings.register_type('password', SettingPassword) + settings.add_json_panel('ManaChat', config, + filename='manachat.json') + + def update_loop(self, *l): + asyncore.loop(timeout=0, count=10) + logic_manager.tick() + + def on_pause(self): + return True + + def on_stop(self): + Clock.unschedule(self.update_loop) + Clock.unschedule(self.update_online_list) + + def open_link(self, link): + webbrowser.open(link) + + def update_online_list(self, *l): + lst = OnlineUsers.dl_online_list(config.get('Other', + 'online_txt_url')) + if lst is not None: + lst.sort() + self.root.players_list.items = lst + + def reconnect(self): + if mapserv.server is not None: + mapserv.cleanup() + + net.login(host=config.get('Server', 'host'), + port=config.getint('Server', 'port'), + username=config.get('Player', 'username'), + password=config.get('Player', 'password'), + charname=config.get('Player', 'charname')) + + if hasattr(self, '_menu_popup'): + self._menu_popup.dismiss() + + def show_about(self): + AboutPopup().open() + + def show_menu(self, show=True): + if not hasattr(self, '_menu_popup'): + self._menu_popup = MenuPopup() + if show: + self._menu_popup.open() + else: + self._menu_popup.dismiss() + + +if __name__ == "__main__": + ManaGuiApp().run() diff --git a/gui/managui.pyc b/gui/managui.pyc Binary files differnew file mode 100644 index 0000000..74b595d --- /dev/null +++ b/gui/managui.pyc diff --git a/gui/pathfind.py b/gui/pathfind.py new file mode 100644 index 0000000..2c870e1 --- /dev/null +++ b/gui/pathfind.py @@ -0,0 +1,91 @@ +import collections + + +class Queue: + def __init__(self): + self.elements = collections.deque() + + def empty(self): + return len(self.elements) == 0 + + def put(self, x): + self.elements.append(x) + + def get(self): + return self.elements.popleft() + + +class Graph: + def __init__(self, collisions): + self.collisions = collisions + self.width = len(collisions[0]) + self.height = len(collisions) + + def walkable(self, coor): + x, y = coor + if x < 0 or x > self.width - 1: + return False + if y < 0 or y > self.height - 1: + return False + if self.collisions[self.height - y - 1][x] > 0: + return False + return True + + def neighbors(self, coor): + n = [] + x, y = coor + + for dx, dy in ((-1, 0), (1, 0), (0, -1), (0, 1)): + nx = x + dx + ny = y + dy + if self.walkable((nx, ny)): + n.append((nx, ny)) + + for dx, dy in ((-1, -1), (-1, 1), (1, -1), (1, 1)): + nx = x + dx + ny = y + dy + if (self.walkable((nx, ny)) and + self.walkable((nx, y)) and + self.walkable((x, ny))): + n.append((nx, ny)) + + return n + + +def breadth_first_search(collisions, start, goal): + graph = Graph(collisions) + frontier = Queue() + frontier.put(start) + came_from = {} + came_from[start] = None + + while not frontier.empty(): + current = frontier.get() + + if current == goal: + break + + for next in graph.neighbors(current): + if next not in came_from: + frontier.put(next) + came_from[next] = current + + current = goal + path = [current] + while current != start: + current = came_from[current] + path.append(current) + path.reverse() + + # simplify path + spath = path[:1] + for i in range(1, len(path) - 1): + px, py = path[i - 1] + cx, cy = path[i] + nx, ny = path[i + 1] + if nx - cx == cx - px and ny - cy == cy - py: + continue + spath.append(path[i]) + spath.append(path[-1]) + + return spath diff --git a/gui/plist.py b/gui/plist.py new file mode 100644 index 0000000..a009a32 --- /dev/null +++ b/gui/plist.py @@ -0,0 +1,54 @@ +from kivy.app import App +from kivy.adapters.listadapter import ListAdapter +from kivy.uix.boxlayout import BoxLayout +from kivy.uix.floatlayout import FloatLayout +from kivy.properties import StringProperty, ListProperty +from kivy.uix.listview import ListView, ListItemLabel + + +class PlayersListItem(BoxLayout, ListItemLabel): + nick = StringProperty(allow_none=False) + + def on_touch_down(self, touch): + if not self.collide_point(*touch.pos): + return False + touch.grab(self) + return True + + def on_touch_up(self, touch): + if not self.collide_point(*touch.pos): + return False + if touch.grab_current is self: + app = App.get_running_app() + app.root.chat_input.text = '/w "{}" '.format(self.nick) + app.root.chat_input.focus = True + touch.ungrab(self) + return True + + +class PlayersList(FloatLayout): + items = ListProperty([]) + + def __init__(self, **kwargs): + super(PlayersList, self).__init__(**kwargs) + + player_args_coverter = lambda row_index, nick: {"nick" : nick, + "size_hint_y": None, "height": "30dp", "pos_hint_y": 0.9 } + + list_adapter = ListAdapter( + data=["Ginaria", "Celestia"], + args_converter=player_args_coverter, + selection_mode='single', + allow_empty_selection=True, + cls=PlayersListItem) + + list_view = ListView(adapter=list_adapter, + size_hint=(1.0, 1.0), + pos_hint={'center_x': 0.5}) + + def data_changed(instance, value): + list_adapter.data = value + + self.bind(items=data_changed) + + self.add_widget(list_view) diff --git a/gui/tmxmap.py b/gui/tmxmap.py new file mode 100644 index 0000000..800868e --- /dev/null +++ b/gui/tmxmap.py @@ -0,0 +1,175 @@ + +# import pytmx +import zipfile +from itertools import cycle, islice +try: + import cPickle as pickle +except ImportError: + import pickle + +from kivy.core.image import Image as CoreImage +from kivy.uix.widget import Widget +from kivy.uix.image import Image +from kivy.graphics.texture import Texture +from kivy.animation import Animation +from kivy.properties import (StringProperty, ObjectProperty, + NumericProperty, DictProperty) +from kivy.logger import Logger + +from pathfind import breadth_first_search +import net.mapserv as mapserv + + +def kivy_image_loader(filename, colorkey, **kwargs): + texture = CoreImage(filename).texture + + def loader(rect, flags): + x, y, w, h = rect + t = texture.get_region(x, + texture.height - y - h, + w, h) + + if flags.flipped_diagonally: + t.flip_horizontal() + t.flip_vertical() + elif flags.flipped_vertically: + t.flip_vertical() + elif flags.flipped_horizontally: + t.flip_horizontal() + + return t + + return loader + + +def create_map_texture(map_data, tile_size=4): + map_size = len(map_data[0]), len(map_data) + texture = Texture.create(size=(map_size[0] * tile_size, + map_size[1] * tile_size), + colorfmt='rgb') + + c = cycle('\x45\x46\x47') + s = islice(c, 3 * 4000) + buf = b''.join(s) + + for y, row in enumerate(map_data): + for x, cell in enumerate(row): + if cell > 0: + continue + + texture.blit_buffer(buf, size=(tile_size, tile_size), + colorfmt='rgb', + pos=(x * tile_size, y * tile_size)) + + texture.flip_vertical() + + return texture + + +class BeingWidget(Widget): + anim = ObjectProperty(None) + name = StringProperty() + + +class MapWidget(Image): + tile_size = NumericProperty(32) + player = ObjectProperty() + collisions = ObjectProperty(None) + current_attacks = DictProperty() + beings = DictProperty() + maps = {} + + def load_map(self, name, *args): + if name not in self.maps: + Logger.info("Caching map %s", name) + zf = zipfile.ZipFile('mapdb.zip', 'r') + self.maps[name] = pickle.load(zf.open(name + '.pickle')) + zf.close() + # m = pytmx.TiledMap(filename=filename) + # data = m.get_layer_by_name('Collision').data + texture = create_map_texture(self.maps[name]['collisions'], + self.tile_size) + self.texture = texture + self.size = texture.size + self.collisions = self.maps[name]['collisions'] + + def to_game_coords(self, pos): + ts = self.tile_size + height = len(self.collisions) + x = int(pos[0] // ts) + y = height - int(pos[1] // ts) - 1 + return x, y + + def from_game_coords(self, pos): + ts = self.tile_size + height = len(self.collisions) + x = pos[0] * ts + y = (height - pos[1] - 1) * ts + return x, y + + def move_being(self, being, gx, gy, speed=150): + ts = self.tile_size + ox = int(being.x // ts) + oy = int(being.y // ts) + gy = len(self.collisions) - gy - 1 + + try: + path = breadth_first_search(self.collisions, + (ox, oy), (gx, gy)) + except KeyError: + path = [] + + if being.anim: + being.anim.stop(being) + + being.anim = Animation(x=being.x, y=being.y, duration=0) + + for i in range(len(path) - 1): + cx, cy = path[i + 1] + px, py = path[i] + distance = max(abs(cx - px), abs(cy - py)) + being.anim += Animation(x=cx * ts, y=cy * ts, + duration=distance * speed / 1000.) + + being.anim.start(being) + + def get_attack_points(self, *bind_args): + points = [] + try: + player_id = mapserv.server.char_id + except AttributeError: + player_id = 0 + for (id1, id2) in self.current_attacks: + try: + # Temporary workaround + if id1 == player_id: + x1, y1 = self.player.pos + else: + x1, y1 = self.beings[id1].pos + if id2 == player_id: + x2, y2 = self.player.pos + else: + x2, y2 = self.beings[id2].pos + points.extend([x1, y1, x2, y2]) + except: + pass + + return points + + def get_attack_endpoints(self, *bind_args): + points = [] + try: + player_id = mapserv.server.char_id + except AttributeError: + player_id = 0 + for (_, target) in self.current_attacks: + try: + if target == player_id: + x, y = self.player.pos + else: + x, y = self.beings[target].pos + points.extend([x, y]) + except: + pass + + return points |