ya2 · news · projects · code · about

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