ya2 · news · projects · code · about

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