Continue modularization
The changes includes: * Modularize argument parser * Let peer handle FPS based on abstract timer * Allow specifying configuration file * Implement printing default configurations
This commit is contained in:
parent
aa6878e80d
commit
2503006d07
|
@ -1,4 +1,29 @@
|
|||
"""Axuy is a minimalist first-person shooter."""
|
||||
# package initialization
|
||||
# Copyright (C) 2019 Nguyễn Gia Phong
|
||||
#
|
||||
# This file is part of Axuy
|
||||
#
|
||||
# Axuy is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Axuy 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 Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with Axuy. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
__doc__ = """Axuy is a minimalist peer-to-peer first-person shooter.
|
||||
|
||||
This package provides abstractions for writing custom front-ends and AIs.
|
||||
All classes and helper functions are exposed at the package level.
|
||||
|
||||
Some superclasses may define abstract methods which must be overridden
|
||||
in derived classes. Subclasses only document newly introduced attributes.
|
||||
"""
|
||||
|
||||
from .misc import *
|
||||
from .pico import *
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
# main loop
|
||||
# Copyright (C) 2019 Nguyễn Gia Phong
|
||||
#
|
||||
# This file is part of Axuy
|
||||
#
|
||||
# Axuy is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Axuy 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 Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with Axuy. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
__doc__ = 'Axuy main loop'
|
||||
__all__ = ['main']
|
||||
|
||||
from .control import CtlConfig, Control
|
||||
|
||||
|
||||
def main():
|
||||
"""Parse arguments and start main loop."""
|
||||
config = CtlConfig()
|
||||
config.parse()
|
||||
with Control(config) as peer: peer.run()
|
||||
|
||||
|
||||
if __name__ == '__main__': main()
|
|
@ -18,9 +18,7 @@
|
|||
|
||||
__doc__ = 'Axuy handling of user control using GLFW'
|
||||
__all__ = ['CtlConfig', 'Control']
|
||||
__version__ = '0.0.7'
|
||||
|
||||
from argparse import ArgumentParser, RawTextHelpFormatter
|
||||
from re import IGNORECASE, match
|
||||
from warnings import warn
|
||||
|
||||
|
@ -52,19 +50,29 @@ class CtlConfig(DispConfig):
|
|||
Zoom speed, in scroll steps per zoom range.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
DispConfig.__init__(self)
|
||||
self.options.add_argument(
|
||||
'--mouse-speed', type=float, dest='mouspeed',
|
||||
help='camera rotational speed (fallback: {:.1f})'.format(
|
||||
self.__mouspeed))
|
||||
self.options.add_argument(
|
||||
'--zoom-speed', type=float, dest='zmspeed',
|
||||
help='zoom speed (fallback: {:.1f})'.format(self.zmspeed))
|
||||
|
||||
@property
|
||||
def mouspeed(self) -> float:
|
||||
"""Relative mouse speed."""
|
||||
# Standard to radians per inch for a 800 DPI mouse, at FOV of 60
|
||||
return self._mouspeed / 800
|
||||
return self.__mouspeed / 800
|
||||
|
||||
@mouspeed.setter
|
||||
def mouspeed(self, value):
|
||||
self._mouspeed = value
|
||||
self.__mouspeed = value
|
||||
|
||||
def parse(self):
|
||||
"""Parse configurations."""
|
||||
DispConfig.parse(self)
|
||||
def fallback(self):
|
||||
"""Parse fallback configurations."""
|
||||
DispConfig.fallback(self)
|
||||
self.mouspeed = self.config.getfloat('Control', 'Mouse speed')
|
||||
self.zmspeed = self.config.getfloat('Control', 'Zoom speed')
|
||||
self.key, self.mouse = {}, {}
|
||||
|
@ -78,9 +86,9 @@ class CtlConfig(DispConfig):
|
|||
except AttributeError:
|
||||
raise ValueError(INVALID_CONTROL_ERR.format(cmd, i))
|
||||
|
||||
def read_args(self, arguments):
|
||||
def read(self, arguments):
|
||||
"""Read and parse a argparse.ArgumentParser.Namespace."""
|
||||
DispConfig.read_args(self, arguments)
|
||||
DispConfig.read(self, arguments)
|
||||
for option in 'fov', 'mouspeed', 'zmspeed':
|
||||
value = getattr(arguments, option)
|
||||
if value is not None: setattr(self, option, value)
|
||||
|
@ -163,49 +171,3 @@ class Control(Display):
|
|||
if self.is_pressed(self.key['left']): right -= 1
|
||||
if self.is_pressed(self.key['right']): right += 1
|
||||
self.camera.update(right, upward, forward)
|
||||
|
||||
|
||||
def main():
|
||||
"""Parse arguments and start main loop."""
|
||||
# Read configuration files
|
||||
config = CtlConfig()
|
||||
config.parse()
|
||||
|
||||
# Parse command-line arguments
|
||||
parser = ArgumentParser(usage='%(prog)s [options]',
|
||||
formatter_class=RawTextHelpFormatter)
|
||||
parser.add_argument('-v', '--version', action='version',
|
||||
version='Axuy {}'.format(__version__))
|
||||
parser.add_argument(
|
||||
'--host',
|
||||
help='host to bind this peer to (fallback: {})'.format(config.host))
|
||||
parser.add_argument(
|
||||
'-p', '--port', type=int,
|
||||
help='port to bind this peer to (fallback: {})'.format(config.port))
|
||||
parser.add_argument('-s', '--seeder',
|
||||
help='address of the peer that created the map')
|
||||
# All these options specific for a graphical peer need to be modularized.
|
||||
parser.add_argument(
|
||||
'--size', type=int, nargs=2, metavar=('X', 'Y'),
|
||||
help='the desired screen size (fallback: {}x{})'.format(*config.size))
|
||||
parser.add_argument(
|
||||
'--vsync', action='store_true', default=None,
|
||||
help='enable vertical synchronization (fallback: {})'.format(
|
||||
config.vsync))
|
||||
parser.add_argument('--no-vsync', action='store_false', dest='vsync',
|
||||
help='disable vertical synchronization')
|
||||
parser.add_argument(
|
||||
'--fov', type=float,
|
||||
help='horizontal field of view (fallback: {:.1f})'.format(config.fov))
|
||||
parser.add_argument(
|
||||
'--mouse-speed', type=float, dest='mouspeed',
|
||||
help='camera rotational speed (fallback: {:.1f})'.format(
|
||||
config._mouspeed))
|
||||
parser.add_argument(
|
||||
'--zoom-speed', type=float, dest='zmspeed',
|
||||
help='zoom speed (fallback: {:.1f})'.format(config.zmspeed))
|
||||
args = parser.parse_args()
|
||||
config.read_args(args)
|
||||
|
||||
with Control(config) as peer:
|
||||
while peer.is_running: peer.update()
|
||||
|
|
|
@ -70,6 +70,24 @@ class DispConfig(PeerConfig):
|
|||
Zoom level.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
PeerConfig.__init__(self)
|
||||
self.options.add_argument(
|
||||
'--size', type=int, nargs=2, metavar=('X', 'Y'),
|
||||
help='the desired screen size (fallback: {}x{})'.format(
|
||||
*self.size))
|
||||
self.options.add_argument(
|
||||
'--vsync', action='store_true', default=None,
|
||||
help='enable vertical synchronization (fallback: {})'.format(
|
||||
self.vsync))
|
||||
self.options.add_argument(
|
||||
'--no-vsync', action='store_false', dest='vsync',
|
||||
help='disable vertical synchronization')
|
||||
self.options.add_argument(
|
||||
'--fov', type=float, metavar='DEGREES',
|
||||
help='horizontal field of view (fallback: {:})'.format(
|
||||
round(self.fov)))
|
||||
|
||||
@property
|
||||
def fov(self) -> float:
|
||||
"""Horizontal field of view in degrees."""
|
||||
|
@ -89,17 +107,17 @@ class DispConfig(PeerConfig):
|
|||
return
|
||||
self.zmlvl = log2(rad)
|
||||
|
||||
def parse(self):
|
||||
"""Parse configurations."""
|
||||
PeerConfig.parse(self)
|
||||
def fallback(self):
|
||||
"""Parse fallback configurations."""
|
||||
PeerConfig.fallback(self)
|
||||
self.size = (self.config.getint('Graphics', 'Screen width'),
|
||||
self.config.getint('Graphics', 'Screen height'))
|
||||
self.vsync = self.config.getboolean('Graphics', 'V-sync')
|
||||
self.fov = self.config.getfloat('Graphics', 'FOV')
|
||||
|
||||
def read_args(self, arguments):
|
||||
def read(self, arguments):
|
||||
"""Read and parse a argparse.ArgumentParser.Namespace."""
|
||||
PeerConfig.read_args(self, arguments)
|
||||
PeerConfig.read(self, arguments)
|
||||
for option in 'size', 'vsync', 'fov':
|
||||
value = getattr(arguments, option)
|
||||
if value is not None: setattr(self, option, value)
|
||||
|
@ -149,8 +167,6 @@ class Display(Peer):
|
|||
Vertex data for final combination of the bloom effect.
|
||||
fb, ping, pong : moderngl.Framebuffer
|
||||
Frame buffers for bloom-effect post-processing.
|
||||
last_time : float
|
||||
timestamp in seconds of the previous frame.
|
||||
fpses : Deque[float]
|
||||
FPS during the last 5 seconds to display the average.
|
||||
"""
|
||||
|
@ -174,7 +190,6 @@ class Display(Peer):
|
|||
glfw.terminate()
|
||||
raise RuntimeError('Failed to create GLFW window')
|
||||
self.fpses = deque()
|
||||
self.last_time = glfw.get_time()
|
||||
|
||||
# Window's rendering and event-handling configuration
|
||||
glfw.set_window_icon(self.window, 1, Image.open(abspath('icon.png')))
|
||||
|
@ -310,16 +325,6 @@ class Display(Peer):
|
|||
"""Camera visibility."""
|
||||
return np.float32(3240 / (self.fov + 240))
|
||||
|
||||
@property
|
||||
def fps(self) -> float:
|
||||
"""Currently rendered frames per second."""
|
||||
return self.camera.fps
|
||||
|
||||
@fps.setter
|
||||
def fps(self, fps):
|
||||
self.camera.fps = fps
|
||||
self.fpses.appendleft(fps)
|
||||
|
||||
@property
|
||||
def fpstr(self) -> str:
|
||||
"""Pretty string for displaying average FPS."""
|
||||
|
@ -327,6 +332,10 @@ class Display(Peer):
|
|||
while len(self.fpses) > mean(self.fpses) * 5 > 0: self.fpses.pop()
|
||||
return '{} fps'.format(round(mean(self.fpses)))
|
||||
|
||||
def get_time(self) -> float:
|
||||
"""Return the current time."""
|
||||
return glfw.get_time()
|
||||
|
||||
def prender(self, obj, va, col, bright):
|
||||
"""Render the obj and its images in bounded 3D space."""
|
||||
rotation = Matrix44.from_matrix33(obj.rot).astype(np.float32).tobytes()
|
||||
|
@ -367,19 +376,16 @@ class Display(Peer):
|
|||
self.prog['visibility'].value = visibility
|
||||
self.prog['camera'].write(self.pos.tobytes())
|
||||
self.prog['vp'].write(vp)
|
||||
picos = list(self.picos.values())
|
||||
for pico in picos:
|
||||
shards = {}
|
||||
for index, shard in pico.shards.items():
|
||||
shard.update(self.fps, picos)
|
||||
if not shard.power: continue
|
||||
self.render_shard(shard)
|
||||
shards[index] = shard
|
||||
pico.shards = shards
|
||||
for pico in self.picos.values():
|
||||
for shard in pico.shards.values(): self.render_shard(shard)
|
||||
if pico is not self.camera: self.render_pico(pico)
|
||||
|
||||
def draw(self):
|
||||
"""Render and post-process."""
|
||||
def update(self):
|
||||
"""Update and render the map."""
|
||||
# Update states
|
||||
Peer.update(self)
|
||||
self.fpses.appendleft(self.fps)
|
||||
|
||||
# Render to framebuffer
|
||||
self.fb.use()
|
||||
self.fb.clear()
|
||||
|
@ -411,15 +417,6 @@ class Display(Peer):
|
|||
self.edge['zoom'].value = (self.zmlvl + 1.0) / 100
|
||||
self.combine.render(moderngl.TRIANGLES)
|
||||
glfw.swap_buffers(self.window)
|
||||
|
||||
def update(self):
|
||||
"""Update and render the map."""
|
||||
next_time = glfw.get_time()
|
||||
self.fps = 1 / (next_time-self.last_time)
|
||||
self.last_time = next_time
|
||||
Peer.update(self)
|
||||
# Render
|
||||
self.draw()
|
||||
glfw.set_window_title(self.window, '{} - axuy@{}:{} ({})'.format(
|
||||
self.postr, *self.addr, self.fpstr))
|
||||
|
||||
|
|
|
@ -17,8 +17,8 @@
|
|||
# along with Axuy. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
__doc__ = 'Axuy miscellaneous functions'
|
||||
__all__ = ['abspath', 'color', 'mapidgen', 'mapgen', 'mirror', 'norm',
|
||||
'normalized', 'sign', 'placeable']
|
||||
__all__ = ['abspath', 'color', 'mapidgen', 'mapgen', 'neighbors', 'mirror',
|
||||
'norm', 'normalized', 'sign', 'twelve', 'nine', 'placeable']
|
||||
|
||||
from itertools import (chain, combinations_with_replacement,
|
||||
permutations, product)
|
||||
|
|
96
axuy/peer.py
96
axuy/peer.py
|
@ -18,13 +18,16 @@
|
|||
|
||||
__doc__ = 'Axuy peer'
|
||||
__all__ = ['PeerConfig', 'Peer']
|
||||
__version__ = '0.0.7'
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from argparse import ArgumentParser, FileType, RawTextHelpFormatter
|
||||
from configparser import ConfigParser
|
||||
from os.path import join as pathjoin, pathsep
|
||||
from pickle import dumps, loads
|
||||
from queue import Empty, Queue
|
||||
from socket import socket, SOCK_DGRAM, SOL_SOCKET, SO_REUSEADDR
|
||||
from sys import stdout
|
||||
from threading import Thread
|
||||
from typing import Iterator, Tuple
|
||||
|
||||
|
@ -33,6 +36,8 @@ from appdirs import AppDirs
|
|||
from .misc import abspath, mapgen, mapidgen
|
||||
from .pico import Pico
|
||||
|
||||
SETTINGS = abspath('settings.ini')
|
||||
|
||||
|
||||
class PeerConfig:
|
||||
"""Networking configurations
|
||||
|
@ -41,6 +46,8 @@ class PeerConfig:
|
|||
----------
|
||||
config : ConfigParser
|
||||
INI configuration file parser.
|
||||
options : ArgumentParser
|
||||
Command-line argument parser.
|
||||
host : str
|
||||
Host to bind the peer to.
|
||||
port : int
|
||||
|
@ -55,9 +62,39 @@ class PeerConfig:
|
|||
parents.append(dirs.user_config_dir)
|
||||
filenames = [pathjoin(parent, 'settings.ini') for parent in parents]
|
||||
|
||||
# Parse configuration files
|
||||
self.config = ConfigParser()
|
||||
self.config.read(abspath('settings.ini')) # default configuration
|
||||
self.config.read(SETTINGS)
|
||||
self.config.read(filenames)
|
||||
self.fallback()
|
||||
|
||||
# Parse command-line arguments
|
||||
self.options = ArgumentParser(usage='%(prog)s [options]',
|
||||
formatter_class=RawTextHelpFormatter)
|
||||
self.options.add_argument('-v', '--version', action='version',
|
||||
version='Axuy {}'.format(__version__))
|
||||
self.options.add_argument(
|
||||
'--write-config', nargs='?', const=stdout, type=FileType('w'),
|
||||
metavar='PATH', dest='cfgout',
|
||||
help='write default config to PATH (fallback: stdout) and exit')
|
||||
self.options.add_argument(
|
||||
'-c', '--config', metavar='PATH',
|
||||
help='location of the configuration file (fallback: {})'.format(
|
||||
pathsep.join(filenames)))
|
||||
self.options.add_argument(
|
||||
'--host',
|
||||
help='host to bind this peer to (fallback: {})'.format(self.host))
|
||||
self.options.add_argument(
|
||||
'-p', '--port', type=int,
|
||||
help='port to bind this peer to (fallback: {})'.format(self.port))
|
||||
self.options.add_argument(
|
||||
'-s', '--seeder', metavar='ADDRESS',
|
||||
help='address of the peer that created the map')
|
||||
|
||||
def fallback(self):
|
||||
"""Parse fallback configurations."""
|
||||
self.host = self.config.get('Peer', 'Host')
|
||||
self.port = self.config.getint('Peer', 'Port')
|
||||
|
||||
# Fallback to None when attribute is missing
|
||||
def __getattr__(self, name): return None
|
||||
|
@ -65,24 +102,31 @@ class PeerConfig:
|
|||
@property
|
||||
def seeder(self) -> Tuple[str, int]:
|
||||
"""Seeder address."""
|
||||
return self._seed
|
||||
return self.__seed
|
||||
|
||||
@seeder.setter
|
||||
def seeder(self, value):
|
||||
host, port = value.split(':')
|
||||
self._seed = host, int(port)
|
||||
self.__seed = host, int(port)
|
||||
|
||||
def parse(self):
|
||||
"""Parse configurations."""
|
||||
self.host = self.config.get('Peer', 'Host')
|
||||
self.port = self.config.getint('Peer', 'Port')
|
||||
|
||||
def read_args(self, arguments):
|
||||
def read(self, arguments):
|
||||
"""Read and parse a argparse.ArgumentParser.Namespace."""
|
||||
for option in 'host', 'port', 'seeder':
|
||||
value = getattr(arguments, option)
|
||||
if value is not None: setattr(self, option, value)
|
||||
|
||||
def parse(self):
|
||||
"""Parse all configurations."""
|
||||
args = self.options.parse_args()
|
||||
if args.cfgout is not None:
|
||||
with open(SETTINGS) as f: args.cfgout.write(f.read())
|
||||
args.cfgout.close()
|
||||
exit()
|
||||
if args.config: # is neither None nor empty
|
||||
self.config.read(args.config)
|
||||
self.fallback()
|
||||
self.read(args)
|
||||
|
||||
|
||||
class Peer(ABC):
|
||||
"""Axuy peer.
|
||||
|
@ -109,8 +153,8 @@ class Peer(ABC):
|
|||
Protagonist.
|
||||
picos : Dict[Tuple[str, int], Pico]
|
||||
All picos present in the map.
|
||||
view : View
|
||||
World representation and renderer.
|
||||
last_time : float
|
||||
Timestamp of the previous update.
|
||||
"""
|
||||
|
||||
def __init__(self, config):
|
||||
|
@ -130,6 +174,7 @@ class Peer(ABC):
|
|||
self.space = mapgen(mapid)
|
||||
self.pico = Pico(self.addr, self.space)
|
||||
self.picos = {self.addr: self.pico}
|
||||
self.last_time = self.get_time()
|
||||
|
||||
Thread(target=self.serve, args=(mapid,), daemon=True).start()
|
||||
Thread(target=self.pull, daemon=True).start()
|
||||
|
@ -152,6 +197,15 @@ class Peer(ABC):
|
|||
else:
|
||||
self.q.task_done()
|
||||
|
||||
@property
|
||||
def fps(self) -> float:
|
||||
"""Current loop rate."""
|
||||
return self.pico.fps
|
||||
|
||||
@fps.setter
|
||||
def fps(self, fps):
|
||||
self.pico.fps = fps
|
||||
|
||||
def serve(self, mapid):
|
||||
"""Initiate other peers."""
|
||||
with socket() as server: # TCP server
|
||||
|
@ -174,6 +228,10 @@ class Peer(ABC):
|
|||
|
||||
def __enter__(self): return self
|
||||
|
||||
@abstractmethod
|
||||
def get_time(self) -> float:
|
||||
"""Return the current time."""
|
||||
|
||||
def sync(self):
|
||||
"""Synchronize states received from other peers."""
|
||||
for data, addr in self.ready:
|
||||
|
@ -193,13 +251,27 @@ class Peer(ABC):
|
|||
def control(self):
|
||||
"""Control the protagonist."""
|
||||
|
||||
@abstractmethod
|
||||
def update(self):
|
||||
"""Update internal states and send them to other peers."""
|
||||
next_time = self.get_time()
|
||||
self.fps = 1 / (next_time-self.last_time)
|
||||
self.last_time = next_time
|
||||
|
||||
self.sync()
|
||||
self.control()
|
||||
picos = list(self.picos.values())
|
||||
for pico in picos:
|
||||
shards = {}
|
||||
for index, shard in pico.shards.items():
|
||||
shard.update(self.fps, picos)
|
||||
if shard.power: shards[index] = shard
|
||||
pico.shards = shards
|
||||
self.push()
|
||||
|
||||
def run(self):
|
||||
"""Start main loop."""
|
||||
while self.is_running: self.update()
|
||||
|
||||
@abstractmethod
|
||||
def close(self):
|
||||
"""Explicitly terminate stuff in subclass
|
||||
|
|
Loading…
Reference in New Issue