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