Get networking working

This commit is contained in:
Nguyễn Gia Phong 2019-02-26 12:05:47 +07:00
parent 49b8f97d6d
commit 809a577f6d
11 changed files with 367 additions and 162 deletions

View File

@ -1,2 +1,14 @@
# axuy
Mininalist first-person shooter
## Install
The game is under heavy development. For contributors:
```bash
git clone https://github.com/McSinyx/axuy.git
pip3 install -e axuy
axuy --host localhost --port 12345 --width 1234 --height 567
axuy --seeder=localhost:12345 --host localhost --port 8888 --width 1234 --height 567
```

View File

@ -1,96 +0,0 @@
# main.py - start game and 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/>.
from random import shuffle
import glfw
import moderngl
from .misc import color
from .view import View
FOV_MIN = 30
FOV_MAX = 120
FOV_INIT = (FOV_MIN+FOV_MAX) / 2
def main():
"""Create window, OpenGL context and start main loop."""
if not glfw.init():
print('Failed to initialize glfw!')
return
glfw.window_hint(glfw.CONTEXT_VERSION_MAJOR, 3)
glfw.window_hint(glfw.CONTEXT_VERSION_MINOR, 3)
glfw.window_hint(glfw.OPENGL_PROFILE, glfw.OPENGL_CORE_PROFILE)
glfw.window_hint(glfw.OPENGL_FORWARD_COMPAT, True)
window = glfw.create_window(640, 480, 'Axuy', None, None)
if not window:
print('Failed to create glfw window!')
glfw.terminate()
return
glfw.make_context_current(window)
glfw.swap_interval(1)
glfw.set_input_mode(window, glfw.CURSOR, glfw.CURSOR_DISABLED)
glfw.set_input_mode(window, glfw.STICKY_KEYS, True)
context = moderngl.create_context()
context.enable(moderngl.BLEND)
context.enable(moderngl.DEPTH_TEST)
fov = FOV_INIT
def zoom(window, xoffset, yoffset):
"""Adjust FOV according to vertical scroll."""
nonlocal fov
fov += yoffset
if fov < FOV_MIN: fov = FOV_MIN
if fov > FOV_MAX: fov = FOV_MAX
glfw.set_scroll_callback(window, zoom)
mapid = list(range(48))
shuffle(mapid)
view = View(mapid, context)
mypico = view.camera
glfw.set_cursor_pos_callback(window, mypico.look)
last_time = glfw.get_time()
while not glfw.window_should_close(window):
next_time = glfw.get_time()
mypico.fps = 1 / (next_time-last_time)
last_time = next_time
if glfw.get_key(window, glfw.KEY_UP) == glfw.PRESS:
mypico.move(forward=1)
if glfw.get_key(window, glfw.KEY_DOWN) == glfw.PRESS:
mypico.move(forward=-1)
if glfw.get_key(window, glfw.KEY_LEFT) == glfw.PRESS:
mypico.move(right=-1)
if glfw.get_key(window, glfw.KEY_RIGHT) == glfw.PRESS:
mypico.move(right=1)
width, height = glfw.get_window_size(window)
context.viewport = 0, 0, width, height
context.clear(*color('Background'))
view.render(width, height, fov)
glfw.swap_buffers(window)
glfw.set_cursor_pos(window, width/2, height/2)
glfw.poll_events()
glfw.terminate()

View File

@ -17,9 +17,10 @@
# along with Axuy. If not, see <https://www.gnu.org/licenses/>.
from itertools import chain, combinations_with_replacement, permutations
from random import choices, shuffle
import numpy
import pkg_resources
from pkg_resources import resource_filename
TANGO = {'Background': '2e3436',
'Butter': 'fce94f',
@ -30,22 +31,50 @@ TANGO = {'Background': '2e3436',
'Plum': 'ad7fa8',
'Scarlet Red': 'ef2929',
'Aluminium': 'eeeeec'}
NEIGHBORS = set(chain.from_iterable(
map(permutations, combinations_with_replacement((-1, 0, 1), 3))))
# map.npy is generated by ../tools/mapgen
SPACE = numpy.load(resource_filename('axuy', 'map.npy'))
def abspath(resource_name):
"""Return a true filesystem path for the specified resource."""
return resource_filename('axuy', resource_name)
def color(name):
"""Return numpy float32 array of RGB colors from color name."""
"""Return NumPy float32 array of RGB colors from color name."""
return numpy.float32([i / 255 for i in bytes.fromhex(TANGO[name])])
def mapidgen(replacement=False):
"""Return a randomly generated map ID."""
mapid = list(range(48))
if replacement: return choices(mapid, k=48)
shuffle(mapid)
return mapid
def mapgen(mapid):
"""Return the NumPy array of shape (12, 12, 9) of bools
generated from the given ID.
"""
base = numpy.stack([SPACE[i] for i in mapid]).reshape(4, 4, 3, 3, 3, 3)
space = numpy.zeros([12, 12, 9], dtype=bool)
for (i, j, k, x, y, z), occupied in numpy.ndenumerate(base):
if occupied: space[i*3 + x][j*3 + y][k*3 + z] = 1
return space
def neighbors(x, y, z):
"""Return a generator of coordinates of images point (x, y, z)
in neighbor universes.
"""
for i, j, k in set(chain.from_iterable(
map(permutations, combinations_with_replacement((-1, 0, 1), 3)))):
yield x + i*12, y + j*12, z + k*9
for i, j, k in NEIGHBORS: yield x + i*12, y + j*12, z + k*9
def resource_filename(resource_name):
"""Return a true filesystem path for the specified resource."""
return pkg_resources.resource_filename('axuy', resource_name)
def sign(x):
"""Return the sign of number x."""
if x > 0: return 1
if x: return -1
return 0

117
axuy/peer.py Normal file
View File

@ -0,0 +1,117 @@
# peer.py - peer 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'
from argparse import ArgumentParser, RawTextHelpFormatter
from pickle import dumps, loads
from socket import socket, SOCK_DGRAM, SOL_SOCKET, SO_REUSEADDR
from threading import Thread
from .misc import mapgen, mapidgen
from .pico import Picobot
from .view import View
class Peer:
"""Axuy peer.
TODO: Documentation
"""
def __init__(self, args):
if args.seeder is None:
mapid = mapidgen()
self.peers = []
else:
client = socket()
host, port = args.seeder.split(':')
self.peers = [(host, int(port))]
client.connect(*self.peers)
data = loads(client.recv(1024))
mapid = data['mapid']
self.peers.extend(data['peers'])
self.space = mapgen(mapid)
self.pico = Picobot(self.space, (0, 0, 0))
self.view = View(self.pico, args.width, args.height, self.space)
address = args.host, args.port
data_server = Thread(target=self.serve,
args=(address, mapid))
data_server.daemon = True
data_server.start()
self.sock = socket(type=SOCK_DGRAM) # UDP
self.sock.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
self.sock.bind(address)
pusher = Thread(target=self.push)
pusher.daemon = True
pusher.start()
puller = Thread(target=self.pull)
puller.daemon = True
puller.start()
def serve(self, address, mapid):
"""Initiate peers."""
self.server = socket() # TCP server
self.server.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
self.server.bind(address)
self.server.listen(7)
while self.view.is_running:
conn, addr = self.server.accept()
conn.send(dumps({'mapid': mapid, 'peers': self.peers}))
conn.close()
self.server.close()
def push(self):
"""Send own state to peers."""
while self.view.is_running:
for peer in self.peers:
self.sock.sendto(dumps(self.view.camera.state), peer)
def pull(self):
"""Receive peers' state."""
while self.view.is_running:
data, addr = self.sock.recvfrom(1024)
pos, rot = loads(data)
try:
self.view.picos[addr].update(pos, rot)
except KeyError:
self.view.picos[addr] = Picobot(self.space, pos, rot)
self.peers.append(addr)
def __enter__(self): return self
def __exit__(self, exc_type, exc_value, traceback):
self.server.close()
self.sock.close()
self.view.close()
def main():
"""Parse command-line arguments and start main loop."""
parser = ArgumentParser(usage='%(prog)s [options]',
formatter_class=RawTextHelpFormatter)
parser.add_argument('--seeder')
parser.add_argument('--host')
parser.add_argument('--port', type=int)
parser.add_argument('--width', type=int, help='window width')
parser.add_argument('--height', type=int, help='window height')
with Peer(parser.parse_args()) as peer:
while peer.view.is_running: peer.view.update()

View File

@ -26,6 +26,8 @@ import numpy as np
from pyrr import matrix33
BASE = np.float32([[1, 0, 0], [0, 1, 0], [0, 0, -1]])
RP = 1 / 4 # radius of a Picobot
SPEED = 2
MOUSE_SPEED = 1/8
@ -54,15 +56,15 @@ class Picobot:
Currently rendered frames per second.
"""
def __init__(self, space, pos=None, rotation=None):
def __init__(self, space, position=None, rotation=None):
self.space = space
if pos is None:
if position is None:
x, y, z = random()*12, random()*12, random()*9
while not self.empty(x, y, z):
x, y, z = random()*12, random()*12, random()*9
self.x, self.y, self.z = x, y, z
else:
self.x, self.y, self.z = pos
self.x, self.y, self.z = position
if rotation is None:
self.rotation = BASE
@ -72,14 +74,19 @@ class Picobot:
self.fps = 60.0
def update(self, position, rotation):
"""Update state."""
self.pos = position
self.rotation = rotation
def empty(self, x, y, z) -> bool:
"""Return weather a Picobot can be placed at (x, y, z)."""
if self.space[int((x-1/4) % 12)][int(y % 12)][int(z % 9)]: return False
if self.space[int((x+1/4) % 12)][int(y % 12)][int(z % 9)]: return False
if self.space[int(x % 12)][int((y-1/4) % 12)][int(z % 9)]: return False
if self.space[int(x % 12)][int((y+1/4) % 12)][int(z % 9)]: return False
if self.space[int(x % 12)][int(y % 12)][int((z-1/4) % 9)]: return False
if self.space[int(x % 12)][int(y % 12)][int((z+1/4) % 9)]: return False
"""Return whether a Picobot can be placed at (x, y, z)."""
if self.space[int((x-RP) % 12)][int(y % 12)][int(z % 9)]: return False
if self.space[int((x+RP) % 12)][int(y % 12)][int(z % 9)]: return False
if self.space[int(x % 12)][int((y-RP) % 12)][int(z % 9)]: return False
if self.space[int(x % 12)][int((y+RP) % 12)][int(z % 9)]: return False
if self.space[int(x % 12)][int(y % 12)][int((z-RP) % 9)]: return False
if self.space[int(x % 12)][int(y % 12)][int((z+RP) % 9)]: return False
return True
def rotate(self, yaw, pitch):
@ -97,6 +104,20 @@ class Picobot:
if self.empty(self.x, y, self.z): self.y = y % 12
if self.empty(self.x, self.y, z): self.z = z % 9
@property
def pos(self):
"""Position in a NumPy array."""
return np.float32([self.x, self.y, self.z])
@pos.setter
def pos(self, position):
self.x, self.y, self.z = position
@property
def state(self):
"""Position and rotation."""
return self.pos, self.rotation
def look(self, window, xpos, ypos):
"""Look according to cursor position.

10
axuy/shaders/pico.frag Normal file
View File

@ -0,0 +1,10 @@
#version 330
uniform vec3 color;
in float depth;
out vec4 f_color;
void main() {
f_color = vec4(color, 1 - depth);
}

15
axuy/shaders/pico.vert Normal file
View File

@ -0,0 +1,15 @@
#version 330
uniform mat4 vp;
uniform mat4 model;
uniform vec3 camera;
uniform float visibility;
in vec3 in_vert;
out float depth;
void main() {
vec4 vert = model * vec4(in_vert, 1.0);
gl_Position = vp * vert;
depth = distance(camera, vec3(vert)) / visibility;
}

View File

@ -18,18 +18,19 @@
__doc__ = 'Axuy module for map class'
from itertools import product, starmap
from operator import add
from itertools import product
from math import sqrt
import glfw
import moderngl
import numpy as np
from pyrr import Matrix44
from .misc import color, neighbors, resource_filename
from .pico import Picobot
from .misc import abspath, color, neighbors, sign
# map.npy is generated by ../tools/mapgen
SPACE = np.load(resource_filename('map.npy'))
FOV_MIN = 30
FOV_MAX = 120
FOV_INIT = (FOV_MIN+FOV_MAX) / 2
OXY = np.float32([[0, 0, 0], [1, 0, 0], [1, 1, 0],
[1, 1, 0], [0, 1, 0], [0, 0, 0]])
@ -37,25 +38,25 @@ OYZ = np.float32([[0, 0, 0], [0, 1, 0], [0, 1, 1],
[0, 1, 1], [0, 0, 1], [0, 0, 0]])
OZX = np.float32([[0, 0, 0], [1, 0, 0], [1, 0, 1],
[1, 0, 1], [0, 0, 1], [0, 0, 0]])
OCTAHEDRON = np.float32([[+1/4, 0, 0], [0, +1/4, 0], [0, 0, +1/4],
[+1/4, 0 ,0], [0, +1/4, 0], [0, 0, -1/4],
[+1/4, 0 ,0], [0, -1/4, 0], [0, 0, +1/4],
[+1/4, 0 ,0], [0, -1/4, 0], [0, 0, -1/4],
[-1/4, 0 ,0], [0, +1/4, 0], [0, 0, +1/4],
[-1/4, 0 ,0], [0, +1/4, 0], [0, 0, -1/4],
[-1/4, 0 ,0], [0, -1/4, 0], [0, 0, +1/4],
[-1/4, 0 ,0], [0, -1/4, 0], [0, 0, -1/4]])
TETRAHEDRON = np.float32([[+1, +1, +1], [+1, -1, -1], [-1, +1, -1],
[-1, -1, +1], [+1, -1, -1], [-1, +1, -1],
[+1, +1, +1], [-1, -1, +1], [-1, +1, -1],
[+1, +1, +1], [-1, -1, +1], [+1, -1, -1]])
with open(resource_filename('space.vert')) as f: VERTEX_SHADER = f.read()
with open(resource_filename('space.frag')) as f: FRAGMENT_SHADER = f.read()
TETRAVERTICES = np.float32([[0, sqrt(8), -1], [sqrt(6), -sqrt(2), -1],
[0, 0, 3], [-sqrt(6), -sqrt(2), -1]]) / 12
TETRAINDECIES = np.int32([0, 1, 2, 3, 1, 2, 0, 3, 2, 0, 3, 1])
OCTOVERTICES = np.float32([[-1, 0, 0], [0, -1, 0], [0, 0, -1],
[0, 1, 0], [0, 0, 1], [1, 0, 0]]) / 12
OCTOINDECIES = np.int32([0, 1, 2, 0, 1, 4, 3, 0, 2, 3, 0, 4,
2, 1, 5, 4, 1, 5, 2, 5, 3, 4, 5, 3])
with open(abspath('shaders/map.vert')) as f: MAP_VERTEX = f.read()
with open(abspath('shaders/map.frag')) as f: MAP_FRAGMENT = f.read()
with open(abspath('shaders/pico.vert')) as f: PICO_VERTEX = f.read()
with open(abspath('shaders/pico.frag')) as f: PICO_FRAGMENT = f.read()
class View:
"""World map and camera placement.
(Documentation below is not completed.)
Parameters
----------
@ -68,7 +69,7 @@ class View:
----------
space : np.ndarray of shape (12, 12, 9) of bools
3D array of occupied space.
prog : moderngl.Program
maprog : moderngl.Program
Processed executable code in GLSL.
mapva : moderngl.VertexArray
Vertex data of the map.
@ -76,33 +77,70 @@ class View:
Protagonist whose view is the camera.
"""
def __init__(self, mapid, context):
space = np.stack([SPACE[i] for i in mapid]).reshape(4, 4, 3, 3, 3, 3)
self.space = np.zeros([12, 12, 9], dtype=bool)
for (i, j, k, x, y, z), occupied in np.ndenumerate(space):
if occupied: self.space[i*3 + x][j*3 + y][k*3 + z] = 1
def __init__(self, pico, width, height, space):
# Create GLFW window
if not glfw.init(): raise RuntimeError('Failed to initialize GLFW!')
glfw.window_hint(glfw.CONTEXT_VERSION_MAJOR, 3)
glfw.window_hint(glfw.CONTEXT_VERSION_MINOR, 3)
glfw.window_hint(glfw.OPENGL_PROFILE, glfw.OPENGL_CORE_PROFILE)
glfw.window_hint(glfw.OPENGL_FORWARD_COMPAT, True)
self.window = glfw.create_window(width, height, 'Axuy', None, None)
if not self.window:
glfw.terminate()
raise RuntimeError('Failed to create glfw window!')
vertices = []
self.camera = pico
self.picos = {'self': pico}
self.last_time = glfw.get_time() # to keep track of FPS
# Window's rendering and event-handling configuration
glfw.make_context_current(self.window)
glfw.swap_interval(1)
glfw.set_input_mode(self.window, glfw.CURSOR, glfw.CURSOR_DISABLED)
glfw.set_input_mode(self.window, glfw.STICKY_KEYS, True)
glfw.set_cursor_pos_callback(self.window, self.camera.look)
self.fov = FOV_INIT
glfw.set_scroll_callback(self.window, self.zoom)
# Create OpenGL context
self.context = context = moderngl.create_context()
context.enable(moderngl.BLEND)
context.enable(moderngl.DEPTH_TEST)
self.space, vertices = space, []
for (x, y, z), occupied in np.ndenumerate(self.space):
if self.space[x][y][z-1] ^ occupied:
vertices.extend(starmap(add, product(neighbors(x, y, z), OXY)))
vertices.extend(i+j for i,j in product(neighbors(x,y,z), OXY))
if self.space[x-1][y][z] ^ occupied:
vertices.extend(starmap(add, product(neighbors(x, y, z), OYZ)))
vertices.extend(i+j for i,j in product(neighbors(x,y,z), OYZ))
if self.space[x][y-1][z] ^ occupied:
vertices.extend(starmap(add, product(neighbors(x, y, z), OZX)))
vertices.extend(i+j for i,j in product(neighbors(x,y,z), OZX))
self.prog = context.program(vertex_shader=VERTEX_SHADER,
fragment_shader=FRAGMENT_SHADER)
self.prog['bg'].write(color('Background').tobytes())
self.maprog = context.program(vertex_shader=MAP_VERTEX,
fragment_shader=MAP_FRAGMENT)
self.maprog['bg'].write(color('Background').tobytes())
self.maprog['color'].write(color('Aluminium').tobytes())
mapvb = context.buffer(np.stack(vertices).astype(np.float32).tobytes())
self.mapva = context.simple_vertex_array(self.prog, mapvb, 'in_vert')
self.mapva = context.simple_vertex_array(self.maprog, mapvb, 'in_vert')
self.camera = Picobot(self.space)
self.prog = context.program(vertex_shader=PICO_VERTEX,
fragment_shader=PICO_FRAGMENT)
pvb = [(context.buffer(TETRAVERTICES.tobytes()), '3f', 'in_vert')]
pib = context.buffer(TETRAINDECIES.tobytes())
self.pva = context.vertex_array(self.prog, pvb, pib)
self.should_close = None
def zoom(self, window, xoffset, yoffset):
"""Adjust FOV according to vertical scroll."""
self.fov += yoffset
if self.fov < FOV_MIN: self.fov = FOV_MIN
if self.fov > FOV_MAX: self.fov = FOV_MAX
@property
def pos(self):
"""Camera position in a NumPy array."""
return np.float32([self.camera.x, self.camera.y, self.camera.z])
return self.camera.pos
@property
def right(self):
@ -119,17 +157,76 @@ class View:
"""Camera forward direction."""
return self.camera.rotation[2]
def render(self, width, height, fov):
"""Render the map."""
visibility = 360 / fov
self.prog['visibility'].write(np.float32(visibility).tobytes())
self.prog['camera'].write(np.float32(self.pos).tobytes())
@property
def is_running(self):
"""GLFW window status."""
return not glfw.window_should_close(self.window)
projection = Matrix44.perspective_projection(fov, width/height,
9e-6, visibility)
def is_pressed(self, *keys):
"""Return whether given keys are pressed."""
return any(glfw.get_key(self.window, k) == glfw.PRESS for k in keys)
def render(self, obj, va):
"""Render the obj and its images in bounded 3D space."""
rotation = Matrix44.from_matrix33(obj.rotation)
i, j, k = map(sign, self.pos - obj.pos)
for position in product(*zip(obj.pos, obj.pos + [i*12, j*12, k*9])):
model = rotation @ Matrix44.from_translation(position)
self.prog['model'].write(model.astype(np.float32).tobytes())
self.prog['color'].write(color('Background').tobytes())
va.render(moderngl.LINES)
self.prog['color'].write(color('Plum').tobytes())
va.render(moderngl.TRIANGLES)
def render_pico(self, pico):
"""Render the pico and its images in bounded 3D space."""
rotation = Matrix44.from_matrix33(pico.rotation)
i, j, k = map(sign, self.pos - pico.pos)
for position in product(*zip(pico.pos, pico.pos + [i*12, j*12, k*9])):
model = rotation @ Matrix44.from_translation(position)
self.prog['model'].write(model.astype(np.float32).tobytes())
self.prog['color'].write(color('Background').tobytes())
self.pva.render(moderngl.LINES)
self.prog['color'].write(color('Plum').tobytes())
self.pva.render(moderngl.TRIANGLES)
def update(self):
"""Handle input, update GLSL programs and render the map."""
# Character movements
right, upward, forward = 0, 0, 0
if self.is_pressed(glfw.KEY_UP): forward += 1
if self.is_pressed(glfw.KEY_DOWN): forward -= 1
if self.is_pressed(glfw.KEY_LEFT): right -= 1
if self.is_pressed(glfw.KEY_RIGHT): right += 1
self.camera.move(right, upward, forward)
# Renderings
width, height = glfw.get_window_size(self.window)
self.context.viewport = 0, 0, width, height
self.context.clear(*color('Background'))
visibility = sqrt(1800 / self.fov)
projection = Matrix44.perspective_projection(self.fov, width/height,
3E-3, visibility)
view = Matrix44.look_at(self.pos, self.pos + self.forward, self.upward)
vp = view @ projection
vp = (view @ projection).astype(np.float32).tobytes()
self.prog['mvp'].write(vp.astype(np.float32).tobytes())
self.prog['color'].write(color('Aluminium').tobytes())
self.maprog['visibility'].write(np.float32(visibility).tobytes())
self.maprog['camera'].write(self.pos.tobytes())
self.maprog['mvp'].write(vp)
self.mapva.render(moderngl.TRIANGLES)
self.prog['visibility'].write(np.float32(visibility).tobytes())
self.prog['camera'].write(self.pos.tobytes())
self.prog['vp'].write(vp)
for pico in self.picos.copy().values():
if pico is not self.camera: self.render_pico(pico)
glfw.swap_buffers(self.window)
# Resetting cursor position and event queues
glfw.set_cursor_pos(self.window, width/2, height/2)
glfw.poll_events()
def close(self):
"""Close window."""
glfw.terminate()

View File

@ -28,5 +28,5 @@ setup(
keywords='fps opengl glfw',
packages=['axuy'],
install_requires=['numpy', 'pyrr', 'moderngl', 'glfw'],
package_data={'axuy': ['map.npy', 'space.vert', 'space.frag']},
entry_points={'console_scripts': ['axuy = axuy.main:main']})
package_data={'axuy': ['map.npy', 'shaders']},
entry_points={'console_scripts': ['axuy = axuy.peer:main']})