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")))