Source code for peng3d.peng

#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
#  peng.py
#  
#  Copyright 2016 notna <notna@apparat.org>
#  
#  This program is free software; you can redistribute it and/or modify
#  it under the terms of the GNU General Public License as published by
#  the Free Software Foundation; either version 2 of the License, or
#  (at your option) any later version.
#  
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#  
#  You should have received a copy of the GNU General Public License
#  along with this program; if not, write to the Free Software
#  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
#  MA 02110-1301, USA.
#  
#  

__all__ = ["Peng","HeadlessPeng"]

import sys

import weakref
import inspect

#from . import window, config, keybind, pyglet_patch
from . import config, world, resource, i18n

_pyglet_patched = sys.version_info.major == 2 or not world._have_pyglet

[docs]class Peng(object): """ This Class should only be instantiated once per application, if you want to use multiple windows, see :py:meth:`createWindow()`\\ . An Instance of this class represents the whole Engine, with all accompanying state and window/world objects. Be sure to keep your instance accessible, as it will be needed to create most other classes. """ def __init__(self,cfg={}): global _pyglet_patched if world._have_pyglet: from . import pyglet_patch, keybind # Local import for compat with headless machines self.window = None self.pygletEventHandlers = {} self.rlPygletEventHandlers = {} self.rlPygletEventHandlersParams = { "on_mouse_motion": (0, 0, 0, 0), "on_resize": (0, 0), } self.rlPygletEventHandlersTriggered = { "on_mouse_motion": False, "on_resize": False, } self.eventHandlers = {} self.events_ignored = {} self.event_list = set() if cfg == {}: cfg = {} # To avoid bugs with default arguments self.cfg = config.Config(cfg,defaults=config.DEFAULT_CONFIG) if world._have_pyglet: self.keybinds = keybind.KeybindHandler(self) self.resourceMgr = None self.i18n = None self.t = lambda *args,**kwargs:self._t(*args,**kwargs) self.tl = lambda *args,**kwargs:self._tl(*args,**kwargs) self.addEventListener("peng3d:peng.exit",self.handler_exit) self.registerEventHandler("on_mouse_motion", self.on_mouse_motion) self.registerEventHandler("on_resize", self.on_resize) if not _pyglet_patched and self.cfg["pyglet.patch.patch_float2int"]: _pyglet_patched = True pyglet_patch.patch_float2int()
[docs] def createWindow(self,cls=None,caption_t=None,rsrc_class=resource.ResourceManager, *args,**kwargs): """ createWindow(cls=window.PengWindow, *args, **kwargs) Creates a new window using the supplied ``cls``\\ . If ``cls`` is not given, :py:class:`peng3d.window.PengWindow()` will be used. Any other positional or keyword arguments are passed to the class constructor. Note that this method currently does not support using multiple windows. .. todo:: Implement having multiple windows. """ if cls is None: from . import window cls = window.PengWindow if self.window is not None: raise RuntimeError("Window already created!") self.sendEvent("peng3d:window.create.pre",{"peng":self,"cls":cls}) if caption_t is not None: kwargs["caption"] = "Peng3d Application" self.window = cls(self,*args,**kwargs) self.sendEvent("peng3d:window.create.post",{"peng":self,"window":self.window}) if self.cfg["rsrc.enable"] and self.resourceMgr is None: self.sendEvent("peng3d:rsrc.init.pre",{"peng":self,"basepath":self.cfg["rsrc.basepath"]}) self.resourceMgr = rsrc_class(self,self.cfg["rsrc.basepath"]) self.rsrcMgr = self.resourceMgr self.sendEvent("peng3d:rsrc.init.post",{"peng":self,"rsrcMgr":self.resourceMgr}) if self.resourceMgr is not None and self.cfg["i18n.enable"] and self.i18n is None: self.sendEvent("peng3d:i18n.init.pre",{"peng":self}) self.i18n = i18n.TranslationManager(self) self._t = self.i18n.t self._tl = self.i18n.tl self.sendEvent("peng3d:i18n.init.post",{"peng":self,"i18n":self.i18n}) if caption_t is not None: self.window.set_caption(self.t(caption_t)) def f(): self.window.set_caption(self.t(caption_t)) self.i18n.addAction("setlang",f) return self.window if caption_t is not None: raise RuntimeError("Could not set translated window title since either the resource system or i18n has been disabled") return self.window
[docs] def run(self,evloop=None): """ Runs the application main loop. This method is blocking and needs to be called from the main thread to avoid OpenGL bugs that can occur. ``evloop`` may optionally be a subclass of :py:class:`pyglet.app.base.EventLoop` to replace the default event loop. """ # TODO: support more than one event loop self.sendEvent("peng3d:peng.run",{"peng":self,"window":self.window,"evloop":evloop}) self.window.run(evloop) self.sendEvent("peng3d:peng.exit",{"peng":self})
[docs] def sendPygletEvent(self,event_type,args,window=None): """ Handles a pyglet event. This method is called by :py:meth:`PengWindow.dispatch_event()` and handles all events. See :py:meth:`registerEventHandler()` for how to listen to these events. This method should be used to send pyglet events. For new code, it is recommended to use :py:meth:`sendEvent()` instead. For "tunneling" pyglet events, use event names of the format ``pyglet:<event>`` and for the data use ``{"args":<args as list>,"window":<window object or none>,"src":<event source>,"event_type":<event type>}`` Note that you should send pyglet events only via this method, the above event will be sent automatically. Do not use this method to send custom events, use :py:meth:`sendEvent` instead. """ args = list(args) self.sendEvent("pyglet:%s"%event_type,{"peng":self,"args":args,"window":window,"src":self,"event_type":event_type}) self.sendEvent("peng3d:pyglet",{"peng":self,"args":args,"window":window,"src":self,"event_type":event_type}) if event_type not in ["on_draw","on_mouse_motion"] and self.cfg["debug.events.dump"]: print("Event %s with args %s"%(event_type,args)) if event_type in self.pygletEventHandlers: for whandler in self.pygletEventHandlers[event_type]: # This allows for proper collection of deleted handler methods by using weak references handler = whandler() if handler is None: del self.pygletEventHandlers[event_type][self.pygletEventHandlers[event_type].index(whandler)] handler(*args)
[docs] def addPygletListener(self,event_type,handler): """ Registers an event handler. The specified callable handler will be called every time an event with the same ``event_type`` is encountered. All event arguments are passed as positional arguments. This method should be used to listen for pyglet events. For new code, it is recommended to use :py:meth:`addEventListener()` instead. See :py:meth:`handleEvent()` for information about tunneled pyglet events. For custom events, use :py:meth:`addEventListener()` instead. """ if self.cfg["debug.events.register"]: print("Registered Event: %s Handler: %s"%(event_type,handler)) if event_type not in self.pygletEventHandlers: self.pygletEventHandlers[event_type]=[] # Only a weak reference is kept if inspect.ismethod(handler): handler = weakref.WeakMethod(handler) else: handler = weakref.ref(handler) self.pygletEventHandlers[event_type].append(handler)
def addRateLimitedPygletListener(self, event_type, handler): if self.cfg["debug.events.register"]: print("Registered Rate Limited Event: %s Handler: %s" % (event_type, handler)) if event_type not in self.rlPygletEventHandlers: self.rlPygletEventHandlers[event_type] = [] # Only a weak reference is kept if inspect.ismethod(handler): handler = weakref.WeakMethod(handler) else: handler = weakref.ref(handler) self.rlPygletEventHandlers[event_type].append(handler) def _pumpRateLimitedEvents(self): for event_type in self.rlPygletEventHandlers: if self.rlPygletEventHandlersTriggered[event_type]: self.rlPygletEventHandlersTriggered[event_type] = False if event_type in self.rlPygletEventHandlers: args = list(self.rlPygletEventHandlersParams.get(event_type, [])) for whandler in self.rlPygletEventHandlers[event_type]: # This allows for proper collection of deleted handler methods by using weak references handler = whandler() if handler is None: del self.rlPygletEventHandlers[event_type][ self.rlPygletEventHandlers[event_type].index(whandler)] handler(*args) # For compatibility, deprecated handleEvent = sendPygletEvent registerEventHandler = addPygletListener registerRateLimitedEventHandler = addRateLimitedPygletListener
[docs] def sendEvent(self,event,data=None): """ Sends an event with attached data. ``event`` should be a string of format ``<namespace>:<category1>.<subcategory2>.<name>``\\ . There may be an arbitrary amount of subcategories. Also note that this format is not strictly enforced, but rather recommended by convention. ``data`` may be any Python Object, but it usually is a dictionary containing relevant parameters. For example, most built-in events use a dictionary containing at least the ``peng`` key set to an instance of this class. If there are no handlers for the event, a corresponding message will be printed to the log file. To prevent spam, the maximum amount of ignored messages can be configured via :confval:`events.maxignore` and defaults to 3. If the config value :confval:`debug.events.dumpfile` is a file path, the event type will be added to an internal list and be saved to the given file during program exit. """ if self.cfg["debug.events.dumpfile"]!="" and event not in self.event_list: self.event_list.add(event) if event not in self.eventHandlers: if event not in self.events_ignored or self.events_ignored[event]<=self.cfg["events.maxignore"]: # Prevents spamming logfile with ignored event messages # TODO: write to logfile # Needs a logging module first... self.events_ignored[event] = self.events_ignored.get(event,0)+1 return for handler in self.eventHandlers[event]: f = handler[0] try: f(event,data) except Exception: if not handler[1]: raise else: # TODO: write to logfile if self.cfg["events.removeonerror"]: self.delEventListener(event,f)
[docs] def addEventListener(self,event,func,raiseErrors=False): """ Adds a handler to the given event. A event may have an arbitrary amount of handlers, though assigning too many handlers may slow down event processing. For the format of ``event``\\ , see :py:meth:`sendEvent()`\\ . ``func`` is the handler which will be executed with two arguments, ``event_type`` and ``data``\\ , as supplied to :py:meth:`sendEvent()`\\ . If ``raiseErrors`` is True, exceptions caused by the handler will be re-raised. Defaults to ``False``\\ . """ if not isinstance(event,str): raise TypeError("Event types must always be strings") if event not in self.eventHandlers: self.eventHandlers[event]=[] self.eventHandlers[event].append([func,raiseErrors])
[docs] def delEventListener(self,event,func): """ Removes the given handler from the given event. If the event does not exist, a :py:exc:`NameError` is thrown. If the handler has not been registered previously, also a :py:exc:`NameError` will be thrown. """ if event not in self.eventHandlers: raise NameError("No handlers exist for event %s"%event) if [func,True] in self.eventHandlers[event]: del self.eventHandlers[event][self.eventHandlers[event].index(func)] elif [func,False] in self.eventHandler[event]: del self.eventHandlers[event][self.eventHandlers[event].index(func)] else: raise NameError("This handler is not registered for event %s"%event) if self.eventHandlers[event] == []: del self.eventHandlers[event]
def on_mouse_motion(self, x, y, dx, dy): _, _, odx, ody = self.rlPygletEventHandlersParams["on_mouse_motion"] self.rlPygletEventHandlersParams["on_mouse_motion"] = (x, y, dx+odx, dy+ody) self.rlPygletEventHandlersTriggered["on_mouse_motion"] = True def on_resize(self, width, height): self.rlPygletEventHandlersParams["on_resize"] = (width, height) self.rlPygletEventHandlersTriggered["on_resize"] = True def handler_exit(self,event,data): if self.cfg["debug.events.dumpfile"]!="": with open(self.cfg["debug.events.dumpfile"],"w") as f: f.write("\n".join(sorted(list(self.event_list)))) handler_exit.__noautodoc__ = True
[docs]class HeadlessPeng(object): """ Variant of peng that should work without having pyglet installed. This class is intended for use in servers as a drop-in replacement for the normal engine class. Note that this class is only in its beginnings and should not be used yet. """ def __init__(self,cfg={}): if "rsrc.enable" not in cfg: cfg["rsrc.enable"]=False super(HeadlessPeng,self).__init__(cfg)