src/platform/python/mgba/core.py (view raw)
1# Copyright (c) 2013-2016 Jeffrey Pfau
2#
3# This Source Code Form is subject to the terms of the Mozilla Public
4# License, v. 2.0. If a copy of the MPL was not distributed with this
5# file, You can obtain one at http://mozilla.org/MPL/2.0/.
6from ._pylib import ffi, lib # pylint: disable=no-name-in-module
7from . import tile, audio
8from cached_property import cached_property
9from functools import wraps
10
11
12def find(path):
13 core = lib.mCoreFind(path.encode('UTF-8'))
14 if core == ffi.NULL:
15 return None
16 return Core._init(core)
17
18
19def find_vf(vfile):
20 core = lib.mCoreFindVF(vfile.handle)
21 if core == ffi.NULL:
22 return None
23 return Core._init(core)
24
25
26def load_path(path):
27 core = find(path)
28 if not core or not core.load_file(path):
29 return None
30 return core
31
32
33def load_vf(vfile):
34 core = find_vf(vfile)
35 if not core or not core.load_rom(vfile):
36 return None
37 return core
38
39
40def needs_reset(func):
41 @wraps(func)
42 def wrapper(self, *args, **kwargs):
43 if not self._was_reset:
44 raise RuntimeError("Core must be reset first")
45 return func(self, *args, **kwargs)
46 return wrapper
47
48
49def protected(func):
50 @wraps(func)
51 def wrapper(self, *args, **kwargs):
52 if self._protected:
53 raise RuntimeError("Core is protected")
54 return func(self, *args, **kwargs)
55 return wrapper
56
57
58@ffi.def_extern()
59def _mCorePythonCallbacksVideoFrameStarted(user): # pylint: disable=invalid-name
60 context = ffi.from_handle(user)
61 context._video_frame_started()
62
63
64@ffi.def_extern()
65def _mCorePythonCallbacksVideoFrameEnded(user): # pylint: disable=invalid-name
66 context = ffi.from_handle(user)
67 context._video_frame_ended()
68
69
70@ffi.def_extern()
71def _mCorePythonCallbacksCoreCrashed(user): # pylint: disable=invalid-name
72 context = ffi.from_handle(user)
73 context._core_crashed()
74
75
76@ffi.def_extern()
77def _mCorePythonCallbacksSleep(user): # pylint: disable=invalid-name
78 context = ffi.from_handle(user)
79 context._sleep()
80
81
82@ffi.def_extern()
83def _mCorePythonCallbacksKeysRead(user): # pylint: disable=invalid-name
84 context = ffi.from_handle(user)
85 context._keys_read()
86
87
88class CoreCallbacks(object):
89 def __init__(self):
90 self._handle = ffi.new_handle(self)
91 self.video_frame_started = []
92 self.video_frame_ended = []
93 self.core_crashed = []
94 self.sleep = []
95 self.keys_read = []
96 self.context = lib.mCorePythonCallbackCreate(self._handle)
97
98 def _video_frame_started(self):
99 for callback in self.video_frame_started:
100 callback()
101
102 def _video_frame_ended(self):
103 for callback in self.video_frame_ended:
104 callback()
105
106 def _core_crashed(self):
107 for callback in self.core_crashed:
108 callback()
109
110 def _sleep(self):
111 for callback in self.sleep:
112 callback()
113
114 def _keys_read(self):
115 for callback in self.keys_read:
116 callback()
117
118
119class Core(object):
120 if hasattr(lib, 'PLATFORM_GBA'):
121 PLATFORM_GBA = lib.PLATFORM_GBA
122
123 if hasattr(lib, 'PLATFORM_GB'):
124 PLATFORM_GB = lib.PLATFORM_GB
125
126 def __init__(self, native):
127 self._core = native
128 self._was_reset = False
129 self._protected = False
130 self._callbacks = CoreCallbacks()
131 self._core.addCoreCallbacks(self._core, self._callbacks.context)
132 self.config = Config(ffi.addressof(native.config))
133
134 def __del__(self):
135 self._was_reset = False
136
137 @cached_property
138 def graphics_cache(self):
139 if not self._was_reset:
140 raise RuntimeError("Core must be reset first")
141 return tile.CacheSet(self)
142
143 @cached_property
144 def tiles(self):
145 tiles = []
146 native_tiles = ffi.addressof(self.graphics_cache.cache.tiles)
147 for i in range(lib.mTileCacheSetSize(native_tiles)):
148 tiles.append(tile.TileView(lib.mTileCacheSetGetPointer(native_tiles, i)))
149 return tiles
150
151 @cached_property
152 def maps(self):
153 maps = []
154 native_maps = ffi.addressof(self.graphics_cache.cache.maps)
155 for i in range(lib.mMapCacheSetSize(native_maps)):
156 maps.append(tile.MapView(lib.mMapCacheSetGetPointer(native_maps, i)))
157 return maps
158
159 @classmethod
160 def _init(cls, native):
161 core = ffi.gc(native, native.deinit)
162 success = bool(core.init(core))
163 lib.mCoreInitConfig(core, ffi.NULL)
164 if not success:
165 raise RuntimeError("Failed to initialize core")
166 return cls._detect(core)
167
168 @classmethod
169 def _detect(cls, core):
170 if hasattr(cls, 'PLATFORM_GBA') and core.platform(core) == cls.PLATFORM_GBA:
171 from .gba import GBA
172 return GBA(core)
173 if hasattr(cls, 'PLATFORM_GB') and core.platform(core) == cls.PLATFORM_GB:
174 from .gb import GB
175 return GB(core)
176 return Core(core)
177
178 def _load(self):
179 self._was_reset = True
180
181 @protected
182 def load_file(self, path):
183 return bool(lib.mCoreLoadFile(self._core, path.encode('UTF-8')))
184
185 def is_rom(self, vfile):
186 return bool(self._core.isROM(vfile.handle))
187
188 @protected
189 def load_rom(self, vfile):
190 return bool(self._core.loadROM(self._core, vfile.handle))
191
192 @protected
193 def load_bios(self, vfile, id=0):
194 res = bool(self._core.loadBIOS(self._core, vfile.handle, id))
195 if res:
196 vfile._claimed = True
197 return res
198
199 @protected
200 def load_save(self, vfile):
201 res = bool(self._core.loadSave(self._core, vfile.handle))
202 if res:
203 vfile._claimed = True
204 return res
205
206 @protected
207 def load_temporary_save(self, vfile):
208 return bool(self._core.loadTemporarySave(self._core, vfile.handle))
209
210 @protected
211 def load_patch(self, vfile):
212 return bool(self._core.loadPatch(self._core, vfile.handle))
213
214 @protected
215 def load_config(self, config):
216 lib.mCoreLoadForeignConfig(self._core, config._native)
217
218 @protected
219 def autoload_save(self):
220 return bool(lib.mCoreAutoloadSave(self._core))
221
222 @protected
223 def autoload_patch(self):
224 return bool(lib.mCoreAutoloadPatch(self._core))
225
226 @protected
227 def autoload_cheats(self):
228 return bool(lib.mCoreAutoloadCheats(self._core))
229
230 @property
231 def platform(self):
232 return self._core.platform(self._core)
233
234 @protected
235 def desired_video_dimensions(self):
236 width = ffi.new("unsigned*")
237 height = ffi.new("unsigned*")
238 self._core.desiredVideoDimensions(self._core, width, height)
239 return width[0], height[0]
240
241 @protected
242 def set_video_buffer(self, image):
243 self._core.setVideoBuffer(self._core, image.buffer, image.stride)
244
245 @protected
246 def set_audio_buffer_size(self, size):
247 self._core.setAudioBufferSize(self._core, size)
248
249 @property
250 def audio_buffer_size(self):
251 return self._core.getAudioBufferSize(self._core)
252
253 @protected
254 def get_audio_channels(self):
255 return audio.StereoBuffer(self.get_audio_channel(0), self.get_audio_channel(1));
256
257 @protected
258 def get_audio_channel(self, channel):
259 return audio.Buffer(self._core.getAudioChannel(self._core, channel), self.frequency)
260
261 @protected
262 def reset(self):
263 self._core.reset(self._core)
264 self._load()
265
266 @needs_reset
267 @protected
268 def run_frame(self):
269 self._core.runFrame(self._core)
270
271 @needs_reset
272 @protected
273 def run_loop(self):
274 self._core.runLoop(self._core)
275
276 @needs_reset
277 @protected
278 def step(self):
279 self._core.step(self._core)
280
281 @needs_reset
282 @protected
283 def load_raw_state(self, state):
284 if len(state) < self._core.stateSize(self._core):
285 return False
286 return self._core.loadState(self._core, state)
287
288 @needs_reset
289 @protected
290 def save_raw_state(self):
291 state = ffi.new('unsigned char[%i]' % self._core.stateSize(self._core))
292 if self._core.saveState(self._core, state):
293 return state
294 return None
295
296 @staticmethod
297 def _keys_to_int(*args, **kwargs):
298 keys = 0
299 if 'raw' in kwargs:
300 keys = kwargs['raw']
301 for key in args:
302 keys |= 1 << key
303 return keys
304
305 @protected
306 def set_keys(self, *args, **kwargs):
307 self._core.setKeys(self._core, self._keys_to_int(*args, **kwargs))
308
309 @protected
310 def add_keys(self, *args, **kwargs):
311 self._core.addKeys(self._core, self._keys_to_int(*args, **kwargs))
312
313 @protected
314 def clear_keys(self, *args, **kwargs):
315 self._core.clearKeys(self._core, self._keys_to_int(*args, **kwargs))
316
317 @property
318 @needs_reset
319 def frame_counter(self):
320 return self._core.frameCounter(self._core)
321
322 @property
323 def frame_cycles(self):
324 return self._core.frameCycles(self._core)
325
326 @property
327 def frequency(self):
328 return self._core.frequency(self._core)
329
330 @property
331 def game_title(self):
332 title = ffi.new("char[16]")
333 self._core.getGameTitle(self._core, title)
334 return ffi.string(title, 16).decode("ascii")
335
336 @property
337 def game_code(self):
338 code = ffi.new("char[12]")
339 self._core.getGameCode(self._core, code)
340 return ffi.string(code, 12).decode("ascii")
341
342 def add_frame_callback(self, callback):
343 self._callbacks.video_frame_ended.append(callback)
344
345 @property
346 def crc32(self):
347 return self._native.romCrc32
348
349
350class ICoreOwner(object):
351 def claim(self):
352 raise NotImplementedError
353
354 def release(self):
355 raise NotImplementedError
356
357 def __enter__(self):
358 self.core = self.claim()
359 self.core._protected = True
360 return self.core
361
362 def __exit__(self, type, value, traceback):
363 self.core._protected = False
364 self.release()
365
366
367class IRunner(object):
368 def pause(self):
369 raise NotImplementedError
370
371 def unpause(self):
372 raise NotImplementedError
373
374 def use_core(self):
375 raise NotImplementedError
376
377 @property
378 def running(self):
379 raise NotImplementedError
380
381 @property
382 def paused(self):
383 raise NotImplementedError
384
385
386class Config(object):
387 def __init__(self, native=None, port=None, defaults={}):
388 if not native:
389 self._port = ffi.NULL
390 if port:
391 self._port = ffi.new("char[]", port.encode("UTF-8"))
392 native = ffi.gc(ffi.new("struct mCoreConfig*"), lib.mCoreConfigDeinit)
393 lib.mCoreConfigInit(native, self._port)
394 self._native = native
395 for key, value in defaults.items():
396 if isinstance(value, bool):
397 value = int(value)
398 lib.mCoreConfigSetDefaultValue(self._native, ffi.new("char[]", key.encode("UTF-8")), ffi.new("char[]", str(value).encode("UTF-8")))
399
400 def __getitem__(self, key):
401 string = lib.mCoreConfigGetValue(self._native, ffi.new("char[]", key.encode("UTF-8")))
402 if not string:
403 return None
404 return ffi.string(string)
405
406 def __setitem__(self, key, value):
407 if isinstance(value, bool):
408 value = int(value)
409 lib.mCoreConfigSetValue(self._native, ffi.new("char[]", key.encode("UTF-8")), ffi.new("char[]", str(value).encode("UTF-8")))