ya2 · news · projects · code · about

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