all repos — mgba @ f03389bfca8e6ec52815ae2d9941ec7f0a9e691c

mGBA Game Boy Advance Emulator

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    if hasattr(lib, 'PLATFORM_DS'):
127        PLATFORM_GB = lib.PLATFORM_DS
128
129    def __init__(self, native):
130        self._core = native
131        self._was_reset = False
132        self._protected = False
133        self._callbacks = CoreCallbacks()
134        self._core.addCoreCallbacks(self._core, self._callbacks.context)
135        self.config = Config(ffi.addressof(native.config))
136
137    def __del__(self):
138        self._was_reset = False
139
140    @cached_property
141    def graphics_cache(self):
142        if not self._was_reset:
143            raise RuntimeError("Core must be reset first")
144        return tile.CacheSet(self)
145
146    @cached_property
147    def tiles(self):
148        tiles = []
149        native_tiles = ffi.addressof(self.graphics_cache.cache.tiles)
150        for i in range(lib.mTileCacheSetSize(native_tiles)):
151            tiles.append(tile.TileView(lib.mTileCacheSetGetPointer(native_tiles, i)))
152        return tiles
153
154    @cached_property
155    def maps(self):
156        maps = []
157        native_maps = ffi.addressof(self.graphics_cache.cache.maps)
158        for i in range(lib.mMapCacheSetSize(native_maps)):
159            maps.append(tile.MapView(lib.mMapCacheSetGetPointer(native_maps, i)))
160        return maps
161
162    @classmethod
163    def _init(cls, native):
164        core = ffi.gc(native, native.deinit)
165        success = bool(core.init(core))
166        lib.mCoreInitConfig(core, ffi.NULL)
167        if not success:
168            raise RuntimeError("Failed to initialize core")
169        return cls._detect(core)
170
171    @classmethod
172    def _detect(cls, core):
173        if hasattr(cls, 'PLATFORM_GBA') and core.platform(core) == cls.PLATFORM_GBA:
174            from .gba import GBA
175            return GBA(core)
176        if hasattr(cls, 'PLATFORM_GB') and core.platform(core) == cls.PLATFORM_GB:
177            from .gb import GB
178            return GB(core)
179        if hasattr(cls, 'PLATFORM_DS') and core.platform(core) == cls.PLATFORM_DS:
180            from .ds import DS
181            return DS(core)
182        return Core(core)
183
184    def _load(self):
185        self._was_reset = True
186
187    @protected
188    def load_file(self, path):
189        return bool(lib.mCoreLoadFile(self._core, path.encode('UTF-8')))
190
191    def is_rom(self, vfile):
192        return bool(self._core.isROM(vfile.handle))
193
194    @protected
195    def load_rom(self, vfile):
196        return bool(self._core.loadROM(self._core, vfile.handle))
197
198    @protected
199    def load_bios(self, vfile, id=0):
200        res = bool(self._core.loadBIOS(self._core, vfile.handle, id))
201        if res:
202            vfile._claimed = True
203        return res
204
205    @protected
206    def load_save(self, vfile):
207        res = bool(self._core.loadSave(self._core, vfile.handle))
208        if res:
209            vfile._claimed = True
210        return res
211
212    @protected
213    def load_temporary_save(self, vfile):
214        return bool(self._core.loadTemporarySave(self._core, vfile.handle))
215
216    @protected
217    def load_patch(self, vfile):
218        return bool(self._core.loadPatch(self._core, vfile.handle))
219
220    @protected
221    def load_config(self, config):
222        lib.mCoreLoadForeignConfig(self._core, config._native)
223
224    @protected
225    def autoload_save(self):
226        return bool(lib.mCoreAutoloadSave(self._core))
227
228    @protected
229    def autoload_patch(self):
230        return bool(lib.mCoreAutoloadPatch(self._core))
231
232    @protected
233    def autoload_cheats(self):
234        return bool(lib.mCoreAutoloadCheats(self._core))
235
236    @property
237    def platform(self):
238        return self._core.platform(self._core)
239
240    @protected
241    def desired_video_dimensions(self):
242        width = ffi.new("unsigned*")
243        height = ffi.new("unsigned*")
244        self._core.desiredVideoDimensions(self._core, width, height)
245        return width[0], height[0]
246
247    @protected
248    def set_video_buffer(self, image):
249        self._core.setVideoBuffer(self._core, image.buffer, image.stride)
250
251    @protected
252    def set_audio_buffer_size(self, size):
253        self._core.setAudioBufferSize(self._core, size)
254
255    @property
256    def audio_buffer_size(self):
257        return self._core.getAudioBufferSize(self._core)
258
259    @protected
260    def get_audio_channels(self):
261        return audio.StereoBuffer(self.get_audio_channel(0), self.get_audio_channel(1));
262
263    @protected
264    def get_audio_channel(self, channel):
265        return audio.Buffer(self._core.getAudioChannel(self._core, channel), self.frequency)
266
267    @protected
268    def reset(self):
269        self._core.reset(self._core)
270        self._load()
271
272    @needs_reset
273    @protected
274    def run_frame(self):
275        self._core.runFrame(self._core)
276
277    @needs_reset
278    @protected
279    def run_loop(self):
280        self._core.runLoop(self._core)
281
282    @needs_reset
283    @protected
284    def step(self):
285        self._core.step(self._core)
286
287    @needs_reset
288    @protected
289    def load_raw_state(self, state):
290        if len(state) < self._core.stateSize(self._core):
291            return False
292        return self._core.loadState(self._core, state)
293
294    @needs_reset
295    @protected
296    def save_raw_state(self):
297        state = ffi.new('unsigned char[%i]' % self._core.stateSize(self._core))
298        if self._core.saveState(self._core, state):
299            return state
300        return None
301
302    @staticmethod
303    def _keys_to_int(*args, **kwargs):
304        keys = 0
305        if 'raw' in kwargs:
306            keys = kwargs['raw']
307        for key in args:
308            keys |= 1 << key
309        return keys
310
311    @protected
312    def set_keys(self, *args, **kwargs):
313        self._core.setKeys(self._core, self._keys_to_int(*args, **kwargs))
314
315    @protected
316    def add_keys(self, *args, **kwargs):
317        self._core.addKeys(self._core, self._keys_to_int(*args, **kwargs))
318
319    @protected
320    def clear_keys(self, *args, **kwargs):
321        self._core.clearKeys(self._core, self._keys_to_int(*args, **kwargs))
322
323    @property
324    @needs_reset
325    def frame_counter(self):
326        return self._core.frameCounter(self._core)
327
328    @property
329    def frame_cycles(self):
330        return self._core.frameCycles(self._core)
331
332    @property
333    def frequency(self):
334        return self._core.frequency(self._core)
335
336    @property
337    def game_title(self):
338        title = ffi.new("char[16]")
339        self._core.getGameTitle(self._core, title)
340        return ffi.string(title, 16).decode("ascii")
341
342    @property
343    def game_code(self):
344        code = ffi.new("char[12]")
345        self._core.getGameCode(self._core, code)
346        return ffi.string(code, 12).decode("ascii")
347
348    def add_frame_callback(self, callback):
349        self._callbacks.video_frame_ended.append(callback)
350
351    @property
352    def crc32(self):
353        return self._native.romCrc32
354
355
356class ICoreOwner(object):
357    def claim(self):
358        raise NotImplementedError
359
360    def release(self):
361        raise NotImplementedError
362
363    def __enter__(self):
364        self.core = self.claim()
365        self.core._protected = True
366        return self.core
367
368    def __exit__(self, type, value, traceback):
369        self.core._protected = False
370        self.release()
371
372
373class IRunner(object):
374    def pause(self):
375        raise NotImplementedError
376
377    def unpause(self):
378        raise NotImplementedError
379
380    def use_core(self):
381        raise NotImplementedError
382
383    @property
384    def running(self):
385        raise NotImplementedError
386
387    @property
388    def paused(self):
389        raise NotImplementedError
390
391
392class Config(object):
393    def __init__(self, native=None, port=None, defaults={}):
394        if not native:
395            self._port = ffi.NULL
396            if port:
397                self._port = ffi.new("char[]", port.encode("UTF-8"))
398            native = ffi.gc(ffi.new("struct mCoreConfig*"), lib.mCoreConfigDeinit)
399            lib.mCoreConfigInit(native, self._port)
400        self._native = native
401        for key, value in defaults.items():
402            if isinstance(value, bool):
403                value = int(value)
404            lib.mCoreConfigSetDefaultValue(self._native, ffi.new("char[]", key.encode("UTF-8")), ffi.new("char[]", str(value).encode("UTF-8")))
405
406    def __getitem__(self, key):
407        string = lib.mCoreConfigGetValue(self._native, ffi.new("char[]", key.encode("UTF-8")))
408        if not string:
409            return None
410        return ffi.string(string)
411
412    def __setitem__(self, key, value):
413        if isinstance(value, bool):
414            value = int(value)
415        lib.mCoreConfigSetValue(self._native, ffi.new("char[]", key.encode("UTF-8")), ffi.new("char[]", str(value).encode("UTF-8")))