ya2 · news · projects · code · about

0fcc1be4422178506504412d2377179ebd5a71b9
[pmachines.git] / pmachines / application / application.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 MusicManager
18 from pmachines.items.background import Background
19 from pmachines.gui.menu import Menu
20 from pmachines.scene.scene import Scene
21 from pmachines.application.persistency import Persistency
22 from ya2.utils.dictfile import DctFile
23 from ya2.utils.logics import LogicsTools
24 from ya2.utils.language import LanguageManager
25 from ya2.utils.log import WindowedLogManager
26 from ya2.utils.functional import FunctionalTest
27 from ya2.utils.asserts import Assert
28 from ya2.utils.gfx import DirectGuiMixin
29 from ya2.utils.gui.gui import GuiTools
30
31
32 class MainFsm(FSM):
33
34 def __init__(self, pmachines):
35 super().__init__('Main FSM')
36 self._pmachines = pmachines
37 self.accept('new_scene', self.__on_new_scene)
38
39 def enterMenu(self):
40 self._pmachines.on_menu_enter()
41
42 def exitMenu(self):
43 self._pmachines.on_menu_exit()
44 self.__do_asserts()
45 DirectGuiMixin.clear_tooltips()
46
47 def enterScene(self, scene_name):
48 self._pmachines.on_scene_enter(scene_name)
49
50 def exitScene(self):
51 self._pmachines.on_scene_exit()
52 self.__do_asserts()
53 DirectGuiMixin.clear_tooltips()
54
55 def __on_new_scene(self):
56 self.demand('Scene', None)
57
58 def __do_asserts(self):
59 args = self._pmachines._args
60 if not LogicsTools.in_build or args.functional_test or args.functional_ref:
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()
68
69 def enterOff(self):
70 self.ignore('new_scene')
71
72
73 class Pmachines:
74
75 @staticmethod
76 def scenes():
77 with open('assets/scenes/index.json') as f:
78 json = loads(f.read())
79 return json['list']
80
81 def __init__(self):
82 info('platform: %s' % platform)
83 info('exists main.py: %s' % exists('main.py'))
84 self._args = args = self._parse_args()
85 self._configure(args)
86 self.base = ShowBase()
87 self._pipeline = None
88 self.is_update_run = args.update
89 self.is_version_run = args.version
90 self.log_mgr = WindowedLogManager.init_cls()
91 self._pos_mgr = {}
92 self._prepare_window(args)
93 GuiTools.init()
94 self._fsm = MainFsm(self)
95 self._fsm.demand('Start') # otherwise it is Off and cleanup in tests won't work
96 if args.update:
97 return
98 if args.functional_test:
99 self._options['settings']['volume'] = 0
100 self._music = MusicManager(self._options['settings']['volume'], 'pmachines')
101 self.lang_mgr = LanguageManager(self._options['settings']['language'],
102 'pmachines',
103 'assets/locale/')
104 if args.functional_test or args.functional_ref:
105 FunctionalTest(args.functional_ref, self._pos_mgr, 'pmachines')
106 if not LogicsTools.in_build 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 = Scene(BulletWorld(), None, True, False, lambda: None, self.scenes(), self._pos_mgr, None, None, None, self._args.screenshot, 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', self._options['development']['auto_start'])
124 else:
125 Scene.scenes_done = self.__persistent.scenes_done
126 self._fsm.demand('Menu')
127
128 def on_menu_enter(self):
129 self._menu_bg = Background()
130 self._menu = Menu(
131 lambda scene_name: self._fsm.demand('Scene', scene_name),
132 self.lang_mgr.set_language, self._options,
133 self._pipeline, self.scenes(), self._args.functional_test or self._args.functional_ref,
134 self._pos_mgr)
135
136 def on_home(self):
137 Scene.scenes_done = self.__persistent.scenes_done
138 self._fsm.demand('Menu')
139
140 def on_menu_exit(self):
141 self._menu_bg.destroy()
142 self._menu.destroy()
143
144 def on_scene_enter(self, scene_name):
145 self._set_physics()
146 self._scene = Scene(
147 self.world, self.on_home,
148 self._options['development']['auto_close_instructions'],
149 self._options['development']['debug_items'],
150 self.reload,
151 self.scenes(),
152 self._pos_mgr,
153 self._args.functional_test or self._args.functional_ref,
154 self._options['development']['mouse_coords'],
155 self.__persistent,
156 scene_name,
157 self._options['development']['editor'],
158 self._options['development']['auto_start_editor'])
159
160 def on_scene_exit(self):
161 self._unset_physics()
162 self._scene.destroy()
163
164 def reload(self, cls):
165 self._fsm.demand('Scene', cls)
166
167 def _configure(self, args):
168 load_prc_file_data('', 'window-title pmachines')
169 load_prc_file_data('', 'framebuffer-srgb true')
170 load_prc_file_data('', 'sync-video true')
171 if args.functional_test or args.functional_ref:
172 load_prc_file_data('', 'win-size 1360 768')
173 # otherwise it is not centered in exwm
174 # load_prc_file_data('', 'threading-model Cull/Draw')
175 # it freezes when you go to the next scene
176 if args.screenshot:
177 load_prc_file_data('', 'window-type offscreen')
178 load_prc_file_data('', 'audio-library-name null')
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')
184 parser.add_argument('--optfile')
185 parser.add_argument('--screenshot')
186 parser.add_argument('--functional-test', action='store_true')
187 parser.add_argument('--functional-ref', action='store_true')
188 cmd_line = [arg for arg in iter(argv[1:]) if not arg.startswith('-psn_')]
189 args = parser.parse_args(cmd_line)
190 return args
191
192 def _prepare_window(self, args):
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)
203 optfile = args.optfile if args.optfile else 'options.ini'
204 info(f'{data_path=}')
205 info('option file: %s' % optfile)
206 info('fixed path: %s' % LogicsTools.platform_specific_path(data_path + '/' + optfile))
207 default_opt = {
208 'settings': {
209 'volume': 1,
210 'language': 'en',
211 'fullscreen': 1,
212 'resolution': '',
213 'antialiasing': 1,
214 'shadows': 1},
215 'save': {
216 'scenes_done': []
217 },
218 'development': {
219 'simplepbr': 1,
220 'verbose_log': 0,
221 'physics_debug': 0,
222 'auto_start': 0,
223 'auto_close_instructions': 0,
224 'show_buffers': 0,
225 'debug_items': 0,
226 'mouse_coords': 0,
227 'fps': 0,
228 'editor': 1,
229 'auto_start_editor': 0}}
230 opt_path = LogicsTools.platform_specific_path(data_path + '/' + optfile) if data_path else optfile
231 opt_exists = exists(opt_path)
232 self._options = DctFile(
233 LogicsTools.platform_specific_path(data_path + '/' + optfile) if data_path else optfile,
234 default_opt)
235 if not opt_exists:
236 self._options.store()
237 self.__persistent = Persistency(self._options['save']['scenes_done'], self._options)
238 Scene.scenes_done = self.__persistent.scenes_done
239 res = self._options['settings']['resolution']
240 if res:
241 res = LVector2i(*[int(_res) for _res in res.split('x')])
242 else:
243 resolutions = []
244 if not self.is_version_run:
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())]
251 res = sorted(resolutions)[-1]
252 fullscreen = self._options['settings']['fullscreen']
253 props = WindowProperties()
254 if args.functional_test or args.functional_ref:
255 fullscreen = False
256 elif not self.is_version_run:
257 props.set_size(res)
258 props.set_fullscreen(fullscreen)
259 props.set_icon_filename('assets/images/icon/pmachines.ico')
260 if not args.screenshot and not self.is_version_run and base.win and not isinstance(base.win, GraphicsBuffer):
261 base.win.request_properties(props)
262 #gltf.patch_loader(base.loader)
263 if self._options['development']['simplepbr'] and not self.is_version_run and base.win:
264 self._pipeline = simplepbr.init(
265 use_normal_maps=True,
266 use_emission_maps=False,
267 use_occlusion_maps=True,
268 msaa_samples=4 if self._options['settings']['antialiasing'] else 1,
269 enable_shadows=int(self._options['settings']['shadows']))
270 debug(f'msaa: {self._pipeline.msaa_samples}')
271 debug(f'shadows: {self._pipeline.enable_shadows}')
272 render.setAntialias(AntialiasAttrib.MAuto)
273 self.base.set_background_color(0, 0, 0, 1)
274 self.base.disable_mouse()
275 if self._options['development']['show_buffers']:
276 base.bufferViewer.toggleEnable()
277 if self._options['development']['fps']:
278 base.set_frame_rate_meter(True)
279 #self.base.accept('window-event', self._on_win_evt)
280 self.base.accept('aspectRatioChanged', self._on_aspect_ratio_changed)
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
292 def _set_physics(self):
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)
299 self._debug_np = render.attach_new_node(debug_node)
300 self._debug_np.show()
301 self.world = BulletWorld()
302 self.world.set_gravity((0, 0, -9.81))
303 if self._options['development']['physics_debug']:
304 self.world.set_debug_node(self._debug_np.node())
305 def update(task):
306 dt = globalClock.get_dt()
307 self.world.do_physics(dt, 10, 1/180)
308 return task.cont
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)
316
317 def _on_aspect_ratio_changed(self):
318 if self._fsm.state == 'Scene':
319 self._scene.on_aspect_ratio_changed()
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:
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
328 return task.again
329
330 def destroy(self):
331 self._fsm.cleanup()
332 self.base.destroy()
333
334 def run(self):
335 self.start()
336 self.base.run()