ya2 · news · projects · code · about

housekeeping: ya2 module
[pmachines.git] / pmachines / app.py
1 import argparse
2 import simplepbr
3 #import gltf
4 from json import loads
5 from sys import platform, exit, argv
6 from platform import node
7 from logging import info, debug
8 from os.path import exists
9 from os import makedirs
10 from multiprocessing import cpu_count
11 from panda3d.core import Filename, load_prc_file_data, AntialiasAttrib, \
12 WindowProperties, LVector2i, TextNode, GraphicsBuffer
13 from panda3d.bullet import BulletWorld, BulletDebugNode
14 from direct.showbase.ShowBase import ShowBase
15 from direct.gui.OnscreenText import OnscreenText
16 from direct.fsm.FSM import FSM
17 from pmachines.audio.music import MusicMgr
18 from pmachines.items.background import Background
19 from pmachines.gui.menu import Menu
20 from pmachines.scene import Scene
21 from pmachines.posmgr import PositionMgr
22 from pmachines.persistent import Persistent
23 from ya2.utils.dictfile import DctFile
24 from ya2.utils.logics import LogicsTools
25 from ya2.utils.lang import LangMgr
26 from ya2.utils.log import LogMgr
27 from ya2.utils.functional import FunctionalTest
28 from ya2.utils.asserts import Assert
29
30
31 class MainFsm(FSM):
32
33 def __init__(self, pmachines):
34 super().__init__('Main FSM')
35 self._pmachines = pmachines
36 self.accept('new_scene', self.__on_new_scene)
37
38 def enterMenu(self):
39 self._pmachines.on_menu_enter()
40
41 def exitMenu(self):
42 self._pmachines.on_menu_exit()
43 self.__do_asserts()
44
45 def enterScene(self, cls):
46 self._pmachines.on_scene_enter(cls)
47
48 def exitScene(self):
49 self._pmachines.on_scene_exit()
50 self.__do_asserts()
51
52 def __on_new_scene(self):
53 self.demand('Scene', None)
54
55 def __do_asserts(self):
56 args = self._pmachines._args
57 if not LogicsTools.runtime() or args.functional_test or args.functional_ref:
58 Assert.assert_threads()
59 Assert.assert_tasks()
60 Assert.assert_render3d()
61 Assert.assert_render2d()
62 Assert.assert_aspect2d()
63 Assert.assert_events()
64 Assert.assert_buffers()
65
66 def enterOff(self):
67 self.ignore('new_scene')
68
69
70 class Pmachines:
71
72 @staticmethod
73 def scenes():
74 with open('assets/scenes/index.json') as f:
75 json = loads(f.read())
76 return json['list']
77
78 def __init__(self):
79 info('platform: %s' % platform)
80 info('exists main.py: %s' % exists('main.py'))
81 self._args = args = self._parse_args()
82 self._configure(args)
83 self.base = ShowBase()
84 self._pipeline = None
85 self.is_update_run = args.update
86 self.is_version_run = args.version
87 self.log_mgr = LogMgr.init_cls()()
88 self._pos_mgr = PositionMgr()
89 self._prepare_window(args)
90 self._fsm = MainFsm(self)
91 self._fsm.demand('Start') # otherwise it is Off and cleanup in tests won't work
92 if args.update:
93 return
94 if args.functional_test:
95 self._options['settings']['volume'] = 0
96 self._music = MusicMgr(self._options['settings']['volume'])
97 self.lang_mgr = LangMgr(self._options['settings']['language'],
98 'pmachines',
99 'assets/locale/')
100 if args.functional_test or args.functional_ref:
101 FunctionalTest(args.functional_ref, self._pos_mgr)
102 if not LogicsTools.runtime() or args.functional_test or args.functional_ref:
103 self.__fps_lst = []
104 taskMgr.do_method_later(1.0, self.__assert_fps, 'assert_fps')
105
106 def start(self):
107 if self._args.screenshot:
108 #cls = [cls for cls in self.scenes if cls.__name__ == self._args.screenshot][0]
109 scene = Scene(BulletWorld(), None, True, False, lambda: None, self.scenes(), self._pos_mgr, None, None, None, self._args.screenshot, None, None)
110 scene.screenshot()
111 scene.destroy()
112 exit()
113 elif self._options['development']['auto_start']:
114 # mod_name = 'pmachines.scenes.scene_' + self._options['development']['auto_start']
115 # for member in import_module(mod_name).__dict__.values():
116 # if isclass(member) and issubclass(member, Scene) and \
117 # member != Scene:
118 # cls = member
119 self._fsm.demand('Scene', self._options['development']['auto_start'])
120 else:
121 Scene.scenes_done = self.__persistent.scenes_done
122 self._fsm.demand('Menu')
123
124 def on_menu_enter(self):
125 self._menu_bg = Background()
126 self._menu = Menu(
127 self._fsm, self.lang_mgr, self._options, self._music,
128 self._pipeline, self.scenes(), self._args.functional_test or self._args.functional_ref,
129 self._pos_mgr)
130
131 def on_home(self):
132 Scene.scenes_done = self.__persistent.scenes_done
133 self._fsm.demand('Menu')
134
135 def on_menu_exit(self):
136 self._menu_bg.destroy()
137 self._menu.destroy()
138
139 def on_scene_enter(self, scene_name):
140 self._set_physics()
141 self._scene = Scene(
142 self.world, self.on_home,
143 self._options['development']['auto_close_instructions'],
144 self._options['development']['debug_items'],
145 self.reload,
146 self.scenes(),
147 self._pos_mgr,
148 self._args.functional_test or self._args.functional_ref,
149 self._options['development']['mouse_coords'],
150 self.__persistent,
151 scene_name,
152 self._options['development']['editor'],
153 self._options['development']['auto_start_editor'])
154
155 def on_scene_exit(self):
156 self._unset_physics()
157 self._scene.destroy()
158
159 def reload(self, cls):
160 self._fsm.demand('Scene', cls)
161
162 def _configure(self, args):
163 load_prc_file_data('', 'window-title pmachines')
164 load_prc_file_data('', 'framebuffer-srgb true')
165 load_prc_file_data('', 'sync-video true')
166 if args.functional_test or args.functional_ref:
167 load_prc_file_data('', 'win-size 1360 768')
168 # otherwise it is not centered in exwm
169 # load_prc_file_data('', 'threading-model Cull/Draw')
170 # it freezes when you go to the next scene
171 if args.screenshot:
172 load_prc_file_data('', 'window-type offscreen')
173 load_prc_file_data('', 'audio-library-name null')
174
175 def _parse_args(self):
176 parser = argparse.ArgumentParser()
177 parser.add_argument('--update', action='store_true')
178 parser.add_argument('--version', action='store_true')
179 parser.add_argument('--optfile')
180 parser.add_argument('--screenshot')
181 parser.add_argument('--functional-test', action='store_true')
182 parser.add_argument('--functional-ref', action='store_true')
183 cmd_line = [arg for arg in iter(argv[1:]) if not arg.startswith('-psn_')]
184 args = parser.parse_args(cmd_line)
185 return args
186
187 def _prepare_window(self, args):
188 data_path = ''
189 if (platform.startswith('win') or platform.startswith('linux')) and (
190 not exists('main.py') or __file__.startswith('/app/bin/')):
191 # it is the deployed version for windows
192 data_path = str(Filename.get_user_appdata_directory()) + '/pmachines'
193 home = '/home/flavio' # we must force this for wine
194 if data_path.startswith('/c/users/') and exists(home + '/.wine/'):
195 data_path = home + '/.wine/drive_' + data_path[1:]
196 info('creating dirs: %s' % data_path)
197 makedirs(data_path, exist_ok=True)
198 optfile = args.optfile if args.optfile else 'options.ini'
199 info('data path: %s' % data_path)
200 info('option file: %s' % optfile)
201 info('fixed path: %s' % LogicsTools.fixpath(data_path + '/' + optfile))
202 default_opt = {
203 'settings': {
204 'volume': 1,
205 'language': 'en',
206 'fullscreen': 1,
207 'resolution': '',
208 'antialiasing': 1,
209 'shadows': 1},
210 'save': {
211 'scenes_done': []
212 },
213 'development': {
214 'simplepbr': 1,
215 'verbose_log': 0,
216 'physics_debug': 0,
217 'auto_start': 0,
218 'auto_close_instructions': 0,
219 'show_buffers': 0,
220 'debug_items': 0,
221 'mouse_coords': 0,
222 'fps': 0,
223 'editor': 0,
224 'auto_start_editor': 0}}
225 opt_path = LogicsTools.fixpath(data_path + '/' + optfile) if data_path else optfile
226 opt_exists = exists(opt_path)
227 self._options = DctFile(
228 LogicsTools.fixpath(data_path + '/' + optfile) if data_path else optfile,
229 default_opt)
230 if not opt_exists:
231 self._options.store()
232 self.__persistent = Persistent(self._options['save']['scenes_done'], self._options)
233 Scene.scenes_done = self.__persistent.scenes_done
234 res = self._options['settings']['resolution']
235 if res:
236 res = LVector2i(*[int(_res) for _res in res.split('x')])
237 else:
238 resolutions = []
239 if not self.is_version_run:
240 d_i = base.pipe.get_display_information()
241 def _res(idx):
242 return d_i.get_display_mode_width(idx), \
243 d_i.get_display_mode_height(idx)
244 resolutions = [
245 _res(idx) for idx in range(d_i.get_total_display_modes())]
246 res = sorted(resolutions)[-1]
247 fullscreen = self._options['settings']['fullscreen']
248 props = WindowProperties()
249 if args.functional_test or args.functional_ref:
250 fullscreen = False
251 elif not self.is_version_run:
252 props.set_size(res)
253 props.set_fullscreen(fullscreen)
254 props.set_icon_filename('assets/images/icon/pmachines.ico')
255 if not args.screenshot and not self.is_version_run and base.win and not isinstance(base.win, GraphicsBuffer):
256 base.win.request_properties(props)
257 #gltf.patch_loader(base.loader)
258 if self._options['development']['simplepbr'] and not self.is_version_run and base.win:
259 self._pipeline = simplepbr.init(
260 use_normal_maps=True,
261 use_emission_maps=False,
262 use_occlusion_maps=True,
263 msaa_samples=4 if self._options['settings']['antialiasing'] else 1,
264 enable_shadows=int(self._options['settings']['shadows']))
265 debug(f'msaa: {self._pipeline.msaa_samples}')
266 debug(f'shadows: {self._pipeline.enable_shadows}')
267 render.setAntialias(AntialiasAttrib.MAuto)
268 self.base.set_background_color(0, 0, 0, 1)
269 self.base.disable_mouse()
270 if self._options['development']['show_buffers']:
271 base.bufferViewer.toggleEnable()
272 if self._options['development']['fps']:
273 base.set_frame_rate_meter(True)
274 #self.base.accept('window-event', self._on_win_evt)
275 self.base.accept('aspectRatioChanged', self._on_aspect_ratio_changed)
276 if self._options['development']['mouse_coords']:
277 coords_txt = OnscreenText(
278 '', parent=base.a2dTopRight, scale=0.04,
279 pos=(-.03, -.06), fg=(.9, .9, .9, 1), align=TextNode.A_right)
280 def update_coords(task):
281 txt = '%s %s' % (int(base.win.get_pointer(0).x),
282 int(base.win.get_pointer(0).y))
283 coords_txt['text'] = txt
284 return task.cont
285 taskMgr.add(update_coords, 'update_coords')
286
287 def _set_physics(self):
288 if self._options['development']['physics_debug']:
289 debug_node = BulletDebugNode('Debug')
290 debug_node.show_wireframe(True)
291 debug_node.show_constraints(True)
292 debug_node.show_bounding_boxes(True)
293 debug_node.show_normals(True)
294 self._debug_np = render.attach_new_node(debug_node)
295 self._debug_np.show()
296 self.world = BulletWorld()
297 self.world.set_gravity((0, 0, -9.81))
298 if self._options['development']['physics_debug']:
299 self.world.set_debug_node(self._debug_np.node())
300 def update(task):
301 dt = globalClock.get_dt()
302 self.world.do_physics(dt, 10, 1/180)
303 return task.cont
304 self._phys_tsk = taskMgr.add(update, 'update')
305
306 def _unset_physics(self):
307 if self._options['development']['physics_debug']:
308 self._debug_np.remove_node()
309 self.world = None
310 taskMgr.remove(self._phys_tsk)
311
312 def _on_aspect_ratio_changed(self):
313 if self._fsm.state == 'Scene':
314 self._scene.on_aspect_ratio_changed()
315
316 def __assert_fps(self, task):
317 if len(self.__fps_lst) > 3:
318 self.__fps_lst.pop(0)
319 self.__fps_lst += [globalClock.average_frame_rate]
320 if len(self.__fps_lst) == 4:
321 fps_threshold = 55 if cpu_count() >= 4 and node() != 'localhost.localdomain' else 10 # i.e. it is the builder machine
322 assert not all(fps < fps_threshold for fps in self.__fps_lst), 'low fps %s' % self.__fps_lst
323 return task.again
324
325 def destroy(self):
326 self._fsm.cleanup()
327 self.base.destroy()
328
329 def run(self):
330 self.start()
331 self.base.run()