ya2 · news · projects · code · about

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