ya2 · news · projects · code · about

housekeeping: ya2 module
[pmachines.git] / pmachines / app.py
CommitLineData
8ee66edd 1import argparse
4894bb48 2import simplepbr
420ce99a 3#import gltf
012cf969 4from json import loads
807c4e10 5from sys import platform, exit, argv
9209a23b 6from platform import node
94a18c21 7from logging import info, debug
8ee66edd
FC
8from os.path import exists
9from os import makedirs
df4f85fc 10from multiprocessing import cpu_count
6fff1464 11from panda3d.core import Filename, load_prc_file_data, AntialiasAttrib, \
bf77b5d5 12 WindowProperties, LVector2i, TextNode, GraphicsBuffer
1be87278 13from panda3d.bullet import BulletWorld, BulletDebugNode
8ee66edd 14from direct.showbase.ShowBase import ShowBase
882c058d 15from direct.gui.OnscreenText import OnscreenText
5964572b 16from direct.fsm.FSM import FSM
4586cbf6
FC
17from pmachines.audio.music import MusicMgr
18from pmachines.items.background import Background
19from pmachines.gui.menu import Menu
20from pmachines.scene import Scene
4586cbf6 21from pmachines.posmgr import PositionMgr
92c29685 22from pmachines.persistent import Persistent
b35b1f62 23from ya2.utils.dictfile import DctFile
bf77b5d5 24from ya2.utils.logics import LogicsTools
b35b1f62
FC
25from ya2.utils.lang import LangMgr
26from ya2.utils.log import LogMgr
27from ya2.utils.functional import FunctionalTest
bf77b5d5 28from ya2.utils.asserts import Assert
8ee66edd
FC
29
30
5964572b
FC
31class MainFsm(FSM):
32
33 def __init__(self, pmachines):
34 super().__init__('Main FSM')
35 self._pmachines = pmachines
3e3c4caf 36 self.accept('new_scene', self.__on_new_scene)
5964572b
FC
37
38 def enterMenu(self):
39 self._pmachines.on_menu_enter()
40
41 def exitMenu(self):
42 self._pmachines.on_menu_exit()
1bfdf72a 43 self.__do_asserts()
5964572b 44
8c9bf90e
FC
45 def enterScene(self, cls):
46 self._pmachines.on_scene_enter(cls)
5964572b
FC
47
48 def exitScene(self):
49 self._pmachines.on_scene_exit()
1bfdf72a
FC
50 self.__do_asserts()
51
3e3c4caf
FC
52 def __on_new_scene(self):
53 self.demand('Scene', None)
54
1bfdf72a
FC
55 def __do_asserts(self):
56 args = self._pmachines._args
bf77b5d5 57 if not LogicsTools.runtime() or args.functional_test or args.functional_ref:
19eae267
FC
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()
5964572b 65
bf77b5d5
FC
66 def enterOff(self):
67 self.ignore('new_scene')
68
5964572b 69
aad6ced2 70class Pmachines:
8ee66edd 71
012cf969
FC
72 @staticmethod
73 def scenes():
74 with open('assets/scenes/index.json') as f:
75 json = loads(f.read())
76 return json['list']
6168d0c2 77
8ee66edd 78 def __init__(self):
8ee66edd
FC
79 info('platform: %s' % platform)
80 info('exists main.py: %s' % exists('main.py'))
e982cdde 81 self._args = args = self._parse_args()
d18f757d
FC
82 self._configure(args)
83 self.base = ShowBase()
8ce16d6c 84 self._pipeline = None
aad6ced2
FC
85 self.is_update_run = args.update
86 self.is_version_run = args.version
aed9737a 87 self.log_mgr = LogMgr.init_cls()()
2d1773b1 88 self._pos_mgr = PositionMgr()
8ce16d6c 89 self._prepare_window(args)
bf77b5d5
FC
90 self._fsm = MainFsm(self)
91 self._fsm.demand('Start') # otherwise it is Off and cleanup in tests won't work
8ee66edd
FC
92 if args.update:
93 return
361d3942 94 if args.functional_test:
edeef6f9 95 self._options['settings']['volume'] = 0
e1e44d5c 96 self._music = MusicMgr(self._options['settings']['volume'])
a0b33e12 97 self.lang_mgr = LangMgr(self._options['settings']['language'],
2aaa10d3
FC
98 'pmachines',
99 'assets/locale/')
d6c157a0
FC
100 if args.functional_test or args.functional_ref:
101 FunctionalTest(args.functional_ref, self._pos_mgr)
bf77b5d5 102 if not LogicsTools.runtime() or args.functional_test or args.functional_ref:
d6c157a0
FC
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:
f7eade0f 108 #cls = [cls for cls in self.scenes if cls.__name__ == self._args.screenshot][0]
2aeb9f68 109 scene = Scene(BulletWorld(), None, True, False, lambda: None, self.scenes(), self._pos_mgr, None, None, None, self._args.screenshot, None, None)
a747111f
FC
110 scene.screenshot()
111 scene.destroy()
63e7aeb2
FC
112 exit()
113 elif self._options['development']['auto_start']:
d3a2e50a
FC
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'])
a2a89363 120 else:
107a889e 121 Scene.scenes_done = self.__persistent.scenes_done
a2a89363 122 self._fsm.demand('Menu')
5964572b
FC
123
124 def on_menu_enter(self):
125 self._menu_bg = Background()
a9aba267
FC
126 self._menu = Menu(
127 self._fsm, self.lang_mgr, self._options, self._music,
012cf969 128 self._pipeline, self.scenes(), self._args.functional_test or self._args.functional_ref,
2d1773b1 129 self._pos_mgr)
5964572b
FC
130
131 def on_home(self):
107a889e 132 Scene.scenes_done = self.__persistent.scenes_done
5964572b
FC
133 self._fsm.demand('Menu')
134
135 def on_menu_exit(self):
136 self._menu_bg.destroy()
4071c6d8 137 self._menu.destroy()
5964572b 138
f7eade0f 139 def on_scene_enter(self, scene_name):
1be87278 140 self._set_physics()
f7eade0f 141 self._scene = Scene(
e669403e 142 self.world, self.on_home,
31237524 143 self._options['development']['auto_close_instructions'],
9914cfc9 144 self._options['development']['debug_items'],
6168d0c2 145 self.reload,
012cf969 146 self.scenes(),
ce302b41 147 self._pos_mgr,
7fa58640 148 self._args.functional_test or self._args.functional_ref,
92c29685 149 self._options['development']['mouse_coords'],
f7eade0f 150 self.__persistent,
d3a2e50a
FC
151 scene_name,
152 self._options['development']['editor'],
153 self._options['development']['auto_start_editor'])
5964572b
FC
154
155 def on_scene_exit(self):
156 self._unset_physics()
157 self._scene.destroy()
8ee66edd 158
9914cfc9
FC
159 def reload(self, cls):
160 self._fsm.demand('Scene', cls)
161
d18f757d 162 def _configure(self, args):
8ee66edd 163 load_prc_file_data('', 'window-title pmachines')
4894bb48 164 load_prc_file_data('', 'framebuffer-srgb true')
a9e8696e 165 load_prc_file_data('', 'sync-video true')
d982c0a5 166 if args.functional_test or args.functional_ref:
addec9c9 167 load_prc_file_data('', 'win-size 1360 768')
d982c0a5 168 # otherwise it is not centered in exwm
9914cfc9
FC
169 # load_prc_file_data('', 'threading-model Cull/Draw')
170 # it freezes when you go to the next scene
a747111f 171 if args.screenshot:
d18f757d
FC
172 load_prc_file_data('', 'window-type offscreen')
173 load_prc_file_data('', 'audio-library-name null')
8ee66edd
FC
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')
9ba5488b 179 parser.add_argument('--optfile')
a747111f 180 parser.add_argument('--screenshot')
361d3942 181 parser.add_argument('--functional-test', action='store_true')
edeef6f9 182 parser.add_argument('--functional-ref', action='store_true')
807c4e10 183 cmd_line = [arg for arg in iter(argv[1:]) if not arg.startswith('-psn_')]
8ee66edd
FC
184 args = parser.parse_args(cmd_line)
185 return args
186
9ba5488b 187 def _prepare_window(self, args):
8ee66edd
FC
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)
9ba5488b
FC
198 optfile = args.optfile if args.optfile else 'options.ini'
199 info('data path: %s' % data_path)
200 info('option file: %s' % optfile)
bf77b5d5 201 info('fixed path: %s' % LogicsTools.fixpath(data_path + '/' + optfile))
9ba5488b
FC
202 default_opt = {
203 'settings': {
a0b33e12 204 'volume': 1,
6fff1464
FC
205 'language': 'en',
206 'fullscreen': 1,
a9aba267 207 'resolution': '',
5fdf77d0
FC
208 'antialiasing': 1,
209 'shadows': 1},
92c29685
FC
210 'save': {
211 'scenes_done': []
212 },
9ba5488b 213 'development': {
a5dc83f4 214 'simplepbr': 1,
9ba5488b 215 'verbose_log': 0,
e669403e
FC
216 'physics_debug': 0,
217 'auto_start': 0,
218 'auto_close_instructions': 0,
31237524 219 'show_buffers': 0,
b41381b2 220 'debug_items': 0,
882c058d 221 'mouse_coords': 0,
d3a2e50a
FC
222 'fps': 0,
223 'editor': 0,
224 'auto_start_editor': 0}}
bf77b5d5 225 opt_path = LogicsTools.fixpath(data_path + '/' + optfile) if data_path else optfile
9ba5488b
FC
226 opt_exists = exists(opt_path)
227 self._options = DctFile(
bf77b5d5 228 LogicsTools.fixpath(data_path + '/' + optfile) if data_path else optfile,
9ba5488b
FC
229 default_opt)
230 if not opt_exists:
231 self._options.store()
92c29685
FC
232 self.__persistent = Persistent(self._options['save']['scenes_done'], self._options)
233 Scene.scenes_done = self.__persistent.scenes_done
6fff1464
FC
234 res = self._options['settings']['resolution']
235 if res:
236 res = LVector2i(*[int(_res) for _res in res.split('x')])
237 else:
dd32d640 238 resolutions = []
aad6ced2 239 if not self.is_version_run:
dd32d640
FC
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())]
6e6ca4ad 246 res = sorted(resolutions)[-1]
edeef6f9 247 fullscreen = self._options['settings']['fullscreen']
d982c0a5 248 props = WindowProperties()
edeef6f9 249 if args.functional_test or args.functional_ref:
edeef6f9 250 fullscreen = False
aad6ced2 251 elif not self.is_version_run:
d982c0a5 252 props.set_size(res)
edeef6f9 253 props.set_fullscreen(fullscreen)
7e487769 254 props.set_icon_filename('assets/images/icon/pmachines.ico')
bf77b5d5 255 if not args.screenshot and not self.is_version_run and base.win and not isinstance(base.win, GraphicsBuffer):
d18f757d 256 base.win.request_properties(props)
420ce99a 257 #gltf.patch_loader(base.loader)
66b856f5 258 if self._options['development']['simplepbr'] and not self.is_version_run and base.win:
a9aba267 259 self._pipeline = simplepbr.init(
a5dc83f4
FC
260 use_normal_maps=True,
261 use_emission_maps=False,
a9aba267 262 use_occlusion_maps=True,
5fdf77d0
FC
263 msaa_samples=4 if self._options['settings']['antialiasing'] else 1,
264 enable_shadows=int(self._options['settings']['shadows']))
94a18c21
FC
265 debug(f'msaa: {self._pipeline.msaa_samples}')
266 debug(f'shadows: {self._pipeline.enable_shadows}')
4894bb48 267 render.setAntialias(AntialiasAttrib.MAuto)
1be87278
FC
268 self.base.set_background_color(0, 0, 0, 1)
269 self.base.disable_mouse()
e669403e
FC
270 if self._options['development']['show_buffers']:
271 base.bufferViewer.toggleEnable()
b41381b2
FC
272 if self._options['development']['fps']:
273 base.set_frame_rate_meter(True)
651713a9
FC
274 #self.base.accept('window-event', self._on_win_evt)
275 self.base.accept('aspectRatioChanged', self._on_aspect_ratio_changed)
882c058d
FC
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
1be87278 287 def _set_physics(self):
9ba5488b
FC
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)
5964572b
FC
294 self._debug_np = render.attach_new_node(debug_node)
295 self._debug_np.show()
1be87278
FC
296 self.world = BulletWorld()
297 self.world.set_gravity((0, 0, -9.81))
9ba5488b 298 if self._options['development']['physics_debug']:
5964572b 299 self.world.set_debug_node(self._debug_np.node())
1be87278
FC
300 def update(task):
301 dt = globalClock.get_dt()
0625cf49 302 self.world.do_physics(dt, 10, 1/180)
1be87278 303 return task.cont
5964572b
FC
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)
651713a9
FC
311
312 def _on_aspect_ratio_changed(self):
5964572b
FC
313 if self._fsm.state == 'Scene':
314 self._scene.on_aspect_ratio_changed()
2ef21fc3
FC
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:
9209a23b
FC
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
2ef21fc3 323 return task.again
aad6ced2 324
bf77b5d5
FC
325 def destroy(self):
326 self._fsm.cleanup()
327 self.base.destroy()
328
aad6ced2 329 def run(self):
d6c157a0 330 self.start()
aad6ced2 331 self.base.run()