ya2 · news · projects · code · about

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