ya2 · news · projects · code · about

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