skoolkit 9.2__cp39-cp39-win32.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. skoolkit/__init__.py +236 -0
  2. skoolkit/audio.py +182 -0
  3. skoolkit/basic.py +346 -0
  4. skoolkit/bin2sna.py +127 -0
  5. skoolkit/bin2tap.py +277 -0
  6. skoolkit/ccmiosimulator.cp39-win32.pyd +0 -0
  7. skoolkit/cmiosimulator.py +1004 -0
  8. skoolkit/components.py +60 -0
  9. skoolkit/config.py +191 -0
  10. skoolkit/csimulator.cp39-win32.pyd +0 -0
  11. skoolkit/ctlparser.py +421 -0
  12. skoolkit/defaults.py +718 -0
  13. skoolkit/disassembler.py +1092 -0
  14. skoolkit/graphics.py +428 -0
  15. skoolkit/image.py +332 -0
  16. skoolkit/kbtracer.py +364 -0
  17. skoolkit/loadsample.py +990 -0
  18. skoolkit/loadtracer.py +648 -0
  19. skoolkit/opcodes.py +940 -0
  20. skoolkit/pagingtracer.py +66 -0
  21. skoolkit/pngwriter.py +504 -0
  22. skoolkit/refparser.py +190 -0
  23. skoolkit/resources/128-0.rom +0 -0
  24. skoolkit/resources/128-1.rom +0 -0
  25. skoolkit/resources/48.rom +0 -0
  26. skoolkit/resources/plus2-0.rom +0 -0
  27. skoolkit/resources/plus2-1.rom +0 -0
  28. skoolkit/resources/skoolkit-dark.css +67 -0
  29. skoolkit/resources/skoolkit-green.css +67 -0
  30. skoolkit/resources/skoolkit-plum.css +67 -0
  31. skoolkit/resources/skoolkit-wide.css +6 -0
  32. skoolkit/resources/skoolkit.css +212 -0
  33. skoolkit/rzxinfo.py +182 -0
  34. skoolkit/rzxplay.py +527 -0
  35. skoolkit/simtables.py +281 -0
  36. skoolkit/simulator.py +2842 -0
  37. skoolkit/simutils.py +193 -0
  38. skoolkit/skool2asm.py +143 -0
  39. skoolkit/skool2bin.py +324 -0
  40. skoolkit/skool2ctl.py +79 -0
  41. skoolkit/skool2html.py +409 -0
  42. skoolkit/skoolasm.py +623 -0
  43. skoolkit/skoolctl.py +721 -0
  44. skoolkit/skoolhtml.py +1383 -0
  45. skoolkit/skoolmacro.py +1541 -0
  46. skoolkit/skoolparser.py +964 -0
  47. skoolkit/skoolutils.py +707 -0
  48. skoolkit/sna2ctl.py +87 -0
  49. skoolkit/sna2img.py +174 -0
  50. skoolkit/sna2skool.py +108 -0
  51. skoolkit/snactl.py +436 -0
  52. skoolkit/snapinfo.py +498 -0
  53. skoolkit/snapmod.py +67 -0
  54. skoolkit/snapshot.py +864 -0
  55. skoolkit/snaskool.py +617 -0
  56. skoolkit/tap2sna.py +1142 -0
  57. skoolkit/tapinfo.py +552 -0
  58. skoolkit/textutils.py +64 -0
  59. skoolkit/trace.py +318 -0
  60. skoolkit/traceutils.py +1933 -0
  61. skoolkit/z80.py +1278 -0
  62. skoolkit-9.2.data/scripts/bin2sna.py +26 -0
  63. skoolkit-9.2.data/scripts/bin2tap.py +26 -0
  64. skoolkit-9.2.data/scripts/rzxinfo.py +26 -0
  65. skoolkit-9.2.data/scripts/rzxplay.py +26 -0
  66. skoolkit-9.2.data/scripts/skool2asm.py +26 -0
  67. skoolkit-9.2.data/scripts/skool2bin.py +26 -0
  68. skoolkit-9.2.data/scripts/skool2ctl.py +26 -0
  69. skoolkit-9.2.data/scripts/skool2html.py +26 -0
  70. skoolkit-9.2.data/scripts/sna2ctl.py +26 -0
  71. skoolkit-9.2.data/scripts/sna2img.py +26 -0
  72. skoolkit-9.2.data/scripts/sna2skool.py +26 -0
  73. skoolkit-9.2.data/scripts/snapinfo.py +26 -0
  74. skoolkit-9.2.data/scripts/snapmod.py +26 -0
  75. skoolkit-9.2.data/scripts/tap2sna.py +26 -0
  76. skoolkit-9.2.data/scripts/tapinfo.py +26 -0
  77. skoolkit-9.2.data/scripts/trace.py +26 -0
  78. skoolkit-9.2.dist-info/COPYING +674 -0
  79. skoolkit-9.2.dist-info/METADATA +144 -0
  80. skoolkit-9.2.dist-info/RECORD +82 -0
  81. skoolkit-9.2.dist-info/WHEEL +5 -0
  82. skoolkit-9.2.dist-info/top_level.txt +1 -0
skoolkit/__init__.py ADDED
@@ -0,0 +1,236 @@
1
+ # Copyright 2011-2024 Richard Dymond (rjdymond@gmail.com)
2
+ #
3
+ # This file is part of SkoolKit.
4
+ #
5
+ # SkoolKit is free software: you can redistribute it and/or modify it under the
6
+ # terms of the GNU General Public License as published by the Free Software
7
+ # Foundation, either version 3 of the License, or (at your option) any later
8
+ # version.
9
+ #
10
+ # SkoolKit is distributed in the hope that it will be useful, but WITHOUT ANY
11
+ # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
12
+ # A PARTICULAR PURPOSE. See the GNU General Public License for more details.
13
+ #
14
+ # You should have received a copy of the GNU General Public License along with
15
+ # SkoolKit. If not, see <http://www.gnu.org/licenses/>.
16
+
17
+ import argparse
18
+ import html
19
+ import sys
20
+ import os
21
+ import posixpath
22
+ import textwrap
23
+ import importlib
24
+
25
+ try:
26
+ from skoolkit.csimulator import CSimulator
27
+ except ImportError: # pragma: no cover
28
+ CSimulator = None
29
+
30
+ try:
31
+ from skoolkit.ccmiosimulator import CCMIOSimulator
32
+ except ImportError: # pragma: no cover
33
+ CCMIOSimulator = None
34
+
35
+ VERSION = '9.2'
36
+ PACKAGE_DIR = os.path.dirname(__file__)
37
+
38
+ BASE_10 = 10
39
+ BASE_16 = 16
40
+
41
+ CASE_LOWER = 1
42
+ CASE_UPPER = 2
43
+
44
+ WRAPPER = textwrap.TextWrapper(break_long_words=False, break_on_hyphens=False)
45
+
46
+ AE_CHARS = frozenset(' !=+-*/<>&|^%$ABCDEFabcdef0123456789()')
47
+
48
+ ROM48 = os.path.join(PACKAGE_DIR, 'resources', '48.rom')
49
+
50
+ ROM128 = (
51
+ os.path.join(PACKAGE_DIR, 'resources', '128-0.rom'),
52
+ os.path.join(PACKAGE_DIR, 'resources', '128-1.rom')
53
+ )
54
+
55
+ ROM_PLUS2 = (
56
+ os.path.join(PACKAGE_DIR, 'resources', 'plus2-0.rom'),
57
+ os.path.join(PACKAGE_DIR, 'resources', 'plus2-1.rom')
58
+ )
59
+
60
+ def error(msg):
61
+ sys.stderr.write('ERROR: {0}\n'.format(msg))
62
+ sys.exit(1)
63
+
64
+ def warn(msg):
65
+ sys.stderr.write('WARNING: {0}\n'.format(msg))
66
+
67
+ def info(msg):
68
+ sys.stderr.write('{0}\n'.format(msg))
69
+
70
+ def show_package_dir():
71
+ write_line(PACKAGE_DIR)
72
+ sys.exit(0)
73
+
74
+ def write(text):
75
+ sys.stdout.write(text)
76
+ sys.stdout.flush()
77
+
78
+ def write_line(text):
79
+ sys.stdout.write('{0}\n'.format(text))
80
+
81
+ def write_text(text):
82
+ sys.stdout.write(text)
83
+
84
+ def wrap(text, width):
85
+ WRAPPER.width = width
86
+ return WRAPPER.wrap(text)
87
+
88
+ def get_int_param(num_str, accept0x=False):
89
+ try:
90
+ return int(num_str)
91
+ except ValueError:
92
+ pass
93
+ if num_str.startswith('$'):
94
+ return int(num_str[1:], 16)
95
+ if accept0x and num_str.startswith('0x'):
96
+ return int(num_str[2:], 16)
97
+ if num_str.startswith('%'):
98
+ return int(num_str[1:], 2)
99
+ if num_str.startswith('"') and num_str.endswith('"'):
100
+ try:
101
+ if num_str.startswith('"\\'):
102
+ return ord(num_str[2:-1])
103
+ return ord(num_str[1:-1])
104
+ except TypeError:
105
+ pass
106
+ raise ValueError
107
+
108
+ def parse_int(num_str, default=None):
109
+ try:
110
+ return get_int_param(num_str)
111
+ except ValueError:
112
+ return default
113
+
114
+ def evaluate(param, safe=False):
115
+ try:
116
+ return get_int_param(param)
117
+ except ValueError:
118
+ pass
119
+ param = html.unescape(param)
120
+ if safe or set(param) <= AE_CHARS:
121
+ try:
122
+ return int(eval(param.replace('$', '0x').replace('/', '//').replace('&&', ' and ').replace('||', ' or ')))
123
+ except:
124
+ pass
125
+ raise ValueError
126
+
127
+ def get_address_format(hexadecimal=False, lower=False):
128
+ if hexadecimal:
129
+ if lower:
130
+ return '${:04x}'
131
+ return '${:04X}'
132
+ return '{:05d}'
133
+
134
+ def get_object(name_spec, default_path=''):
135
+ path, sep, name = name_spec.rpartition(':')
136
+ if sep:
137
+ sys.path.insert(0, os.path.expanduser(path) or default_path)
138
+ try:
139
+ return importlib.import_module(name)
140
+ except ImportError:
141
+ pass
142
+ mod_name, sep, attr_name = name.rpartition('.')
143
+ try:
144
+ m = importlib.import_module(mod_name or attr_name)
145
+ except ImportError as e:
146
+ raise SkoolKitError("Failed to import object {}: {}".format(name, e.args[0]))
147
+ try:
148
+ return getattr(m, attr_name)
149
+ except AttributeError:
150
+ raise SkoolKitError("No object named '{}' in module '{}'".format(attr_name, mod_name))
151
+
152
+ def find_file(fname, search_dirs=('',)):
153
+ for f in [os.path.join(d, fname) for d in search_dirs]:
154
+ if os.path.isfile(f):
155
+ return f
156
+
157
+ def open_file(fname, mode='r'):
158
+ if fname == '-':
159
+ if 'w' in mode:
160
+ return sys.stdout.buffer
161
+ return sys.stdin
162
+ try:
163
+ if 'b' in mode:
164
+ return open(fname, mode)
165
+ return open(fname, mode, encoding='utf-8')
166
+ except IOError as e:
167
+ if e.errno == 2:
168
+ raise SkoolKitError('{0}: file not found'.format(fname))
169
+ raise
170
+ except TypeError:
171
+ # Assume this is already a file-like object
172
+ return fname
173
+
174
+ def read_bin_file(fname, size=-1):
175
+ if fname == '-':
176
+ return sys.stdin.buffer.read(size)
177
+ try:
178
+ with open(fname, 'rb') as f:
179
+ return f.read(size)
180
+ except IOError as e:
181
+ if e.errno == 2:
182
+ raise SkoolKitError('{0}: file not found'.format(fname))
183
+ raise
184
+
185
+ def format_template(template__, name__, **fields):
186
+ try:
187
+ return template__.format(**fields)
188
+ except ValueError as e:
189
+ raise SkoolKitError('Failed to format {} template: {}'.format(name__, e.args[0]))
190
+ except KeyError as e:
191
+ raise SkoolKitError("Unknown field '{}' in {} template".format(e.args[0], name__))
192
+
193
+ def normpath(*paths):
194
+ return posixpath.normpath(posixpath.join(*[p.replace('\\', '/') for p in paths]))
195
+
196
+ def get_word(data, index):
197
+ return data[index] + 256 * data[index + 1]
198
+
199
+ def get_word3(data, index):
200
+ return get_word(data, index) + 65536 * data[index + 2]
201
+
202
+ def get_dword(data, index):
203
+ return get_word3(data, index) + 16777216 * data[index + 3]
204
+
205
+ def integer(arg):
206
+ try:
207
+ s_arg = arg.strip()
208
+ if s_arg.startswith('0x'):
209
+ return int(s_arg[2:], 16)
210
+ return int(s_arg)
211
+ except ValueError:
212
+ raise argparse.ArgumentTypeError("invalid integer: '{}'".format(arg))
213
+
214
+ def eval_variable(name, value):
215
+ if name.endswith('$'):
216
+ return value
217
+ return evaluate(value)
218
+
219
+ def variable(arg):
220
+ name, sep, value = arg.partition('=')
221
+ if name and sep:
222
+ try:
223
+ return name, eval_variable(name, value)
224
+ except ValueError:
225
+ raise argparse.ArgumentTypeError("invalid arithmetic expression: '{}'".format(arg))
226
+ elif name:
227
+ raise argparse.ArgumentTypeError("missing variable value: '{}'".format(arg))
228
+ else:
229
+ raise argparse.ArgumentTypeError("missing variable name: '{}'".format(arg))
230
+
231
+ class SkoolKitError(Exception):
232
+ pass
233
+
234
+ # API (ControlDirectiveComposer)
235
+ class SkoolParsingError(SkoolKitError):
236
+ """Raised when an error occurs while parsing a skool file."""
skoolkit/audio.py ADDED
@@ -0,0 +1,182 @@
1
+ # Copyright 2013, 2014, 2022, 2023 Richard Dymond (rjdymond@gmail.com)
2
+ #
3
+ # This file is part of SkoolKit.
4
+ #
5
+ # SkoolKit is free software: you can redistribute it and/or modify it under the
6
+ # terms of the GNU General Public License as published by the Free Software
7
+ # Foundation, either version 3 of the License, or (at your option) any later
8
+ # version.
9
+ #
10
+ # SkoolKit is distributed in the hope that it will be useful, but WITHOUT ANY
11
+ # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
12
+ # A PARTICULAR PURPOSE. See the GNU General Public License for more details.
13
+ #
14
+ # You should have received a copy of the GNU General Public License along with
15
+ # SkoolKit. If not, see <http://www.gnu.org/licenses/>.
16
+
17
+ from math import ceil, floor
18
+
19
+ CLOCK_SPEED = 'ClockSpeed'
20
+ CONTENTION_BEGIN = 'ContentionBegin'
21
+ CONTENTION_END = 'ContentionEnd'
22
+ CONTENTION_FACTOR = 'ContentionFactor'
23
+ FRAME_DURATION = 'FrameDuration'
24
+ INTERRUPT_DELAY = 'InterruptDelay'
25
+ SAMPLE_RATE = 'SampleRate'
26
+
27
+ class AudioWriter:
28
+ def __init__(self, config=None):
29
+ self.options = ({
30
+ CLOCK_SPEED: 3500000,
31
+ CONTENTION_BEGIN: 14334,
32
+ CONTENTION_END: 57248,
33
+ CONTENTION_FACTOR: 51,
34
+ FRAME_DURATION: 69888,
35
+ INTERRUPT_DELAY: 942,
36
+ SAMPLE_RATE: 44100
37
+ }, {
38
+ CLOCK_SPEED: 3546900,
39
+ CONTENTION_BEGIN: 14361,
40
+ CONTENTION_END: 58041,
41
+ CONTENTION_FACTOR: 51,
42
+ FRAME_DURATION: 70908,
43
+ INTERRUPT_DELAY: 1584,
44
+ SAMPLE_RATE: 44100
45
+ })
46
+ if config:
47
+ for k, v in config.items():
48
+ try:
49
+ self.options[0][k] = self.options[1][k] = int(v)
50
+ except ValueError:
51
+ pass
52
+
53
+ def write_audio(self, audio_file, delays, contention=False, interrupts=False, offset=0, ma_filter=False, is128k=False):
54
+ options = self.options[is128k]
55
+ if contention or interrupts:
56
+ self._add_contention(delays, contention, interrupts, offset, options)
57
+ if ma_filter:
58
+ samples = self._moving_average_filter(delays, options)
59
+ else:
60
+ samples = self._delays_to_samples(delays, options)
61
+ self._write_wav(audio_file, samples, options)
62
+
63
+ def formats(self):
64
+ return ('.wav',)
65
+
66
+ def _add_contention(self, delays, contention, interrupts, cycle, options):
67
+ c_begin, c_end = options[CONTENTION_BEGIN], options[CONTENTION_END]
68
+ c_factor = 1 + options[CONTENTION_FACTOR] / 100
69
+ i_delay = options[INTERRUPT_DELAY]
70
+ f_duration = options[FRAME_DURATION]
71
+
72
+ for i in range(len(delays)):
73
+ d_offset = 0
74
+ while 1:
75
+ if interrupts and cycle == 0:
76
+ cycle = i_delay
77
+ if i:
78
+ delays[i] += i_delay
79
+ d_offset += i_delay
80
+ d_remaining = delays[i] - d_offset
81
+ if contention and cycle < c_end:
82
+ if cycle < c_begin:
83
+ gap = c_begin - cycle
84
+ if d_offset + gap >= delays[i]:
85
+ # Delay ends before next contended interval starts
86
+ cycle += d_remaining
87
+ break
88
+ # Delay crosses into contended interval
89
+ d_offset += gap
90
+ cycle = c_begin
91
+ else:
92
+ c_period = c_end - cycle
93
+ d_rem_contended = int(d_remaining * c_factor)
94
+ if d_rem_contended < c_period:
95
+ # Delay ends within contended interval
96
+ delays[i] = d_offset + d_rem_contended
97
+ cycle += d_rem_contended
98
+ break
99
+ # Delay crosses into uncontended interval
100
+ cd_cycles = int(c_period / c_factor)
101
+ delays[i] += c_period - cd_cycles
102
+ d_offset += cd_cycles
103
+ cycle = c_end
104
+ else:
105
+ if d_remaining < f_duration - cycle:
106
+ # Delay ends before frame boundary
107
+ cycle += d_remaining
108
+ break
109
+ # Delay crosses frame boundary
110
+ d_offset += f_duration - cycle
111
+ cycle = 0
112
+
113
+ def _moving_average_filter(self, delays, options):
114
+ sample_delay = options[CLOCK_SPEED] / options[SAMPLE_RATE]
115
+ s0, s1 = 0, sample_delay
116
+ t, t1 = 0, ceil(s1)
117
+ bit = bits = 0
118
+ samples = []
119
+ for d in delays:
120
+ while True:
121
+ if t + d < t1:
122
+ if bit:
123
+ bits += d
124
+ t += d
125
+ break
126
+ i = t1 - t
127
+ if bit:
128
+ bits += i
129
+ d -= i
130
+ samples.append((floor(0xFFFF * bits / (t1 - s0)) - 0x8000) & 0xFFFF)
131
+ s0 = t = t1
132
+ s1 += sample_delay
133
+ t1 = ceil(s1)
134
+ bits = 0
135
+ bit = 1 - bit
136
+ return samples
137
+
138
+ def _delays_to_samples(self, delays, options):
139
+ sample_delay = options[CLOCK_SPEED] / options[SAMPLE_RATE]
140
+ samples = []
141
+ direction = 1
142
+ i = 0
143
+ d = delays[0]
144
+ t = 0
145
+ while 1:
146
+ while t >= d:
147
+ i += 1
148
+ if i >= len(delays):
149
+ break
150
+ d += delays[i]
151
+ direction *= -1
152
+ if i >= len(delays):
153
+ break
154
+ if direction > 0:
155
+ samples.append(32767)
156
+ else:
157
+ samples.append(32768)
158
+ t += sample_delay
159
+ return samples
160
+
161
+ def _to_int32(self, num):
162
+ return (num & 255, (num >> 8) & 255, (num >> 16) & 255, num >> 24)
163
+
164
+ def _write_wav(self, audio_file, samples, options):
165
+ sample_rate = options[SAMPLE_RATE]
166
+ data_length = 2 * len(samples)
167
+ header = bytearray()
168
+ header.extend(b'RIFF')
169
+ header.extend(self._to_int32(36 + data_length))
170
+ header.extend(b'WAVEfmt ')
171
+ header.extend(self._to_int32(16)) # length of fmt chunk
172
+ header.extend((1, 0)) # format (1=PCM)
173
+ header.extend((1, 0)) # channels
174
+ header.extend(self._to_int32(sample_rate)) # sample rate
175
+ header.extend(self._to_int32(sample_rate * 2)) # byte rate
176
+ header.extend((2, 0)) # bytes per sample
177
+ header.extend((16, 0)) # bits per sample
178
+ header.extend(b'data')
179
+ header.extend(self._to_int32(data_length)) # length of data chunk
180
+ audio_file.write(header)
181
+ for sample in samples:
182
+ audio_file.write(bytes((sample & 255, sample // 256)))