skoolkit 9.5__cp313-cp313-win_amd64.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 (85) hide show
  1. skoolkit/__init__.py +239 -0
  2. skoolkit/audio.py +211 -0
  3. skoolkit/basic.py +350 -0
  4. skoolkit/bin2sna.py +127 -0
  5. skoolkit/bin2tap.py +280 -0
  6. skoolkit/ccmiosimulator.cp313-win_amd64.pyd +0 -0
  7. skoolkit/cmiosimulator.py +1459 -0
  8. skoolkit/comment.py +606 -0
  9. skoolkit/components.py +66 -0
  10. skoolkit/config.py +209 -0
  11. skoolkit/csimulator.cp313-win_amd64.pyd +0 -0
  12. skoolkit/ctlparser.py +421 -0
  13. skoolkit/defaults.py +723 -0
  14. skoolkit/disassembler.py +1136 -0
  15. skoolkit/graphics.py +428 -0
  16. skoolkit/image.py +332 -0
  17. skoolkit/kbtracer.py +364 -0
  18. skoolkit/loadsample.py +990 -0
  19. skoolkit/loadtracer.py +699 -0
  20. skoolkit/opcodes.py +940 -0
  21. skoolkit/pagingtracer.py +66 -0
  22. skoolkit/pngwriter.py +504 -0
  23. skoolkit/refparser.py +190 -0
  24. skoolkit/resources/128-0.rom +0 -0
  25. skoolkit/resources/128-1.rom +0 -0
  26. skoolkit/resources/48.rom +0 -0
  27. skoolkit/resources/plus2-0.rom +0 -0
  28. skoolkit/resources/plus2-1.rom +0 -0
  29. skoolkit/resources/skoolkit-dark.css +67 -0
  30. skoolkit/resources/skoolkit-green.css +67 -0
  31. skoolkit/resources/skoolkit-plum.css +67 -0
  32. skoolkit/resources/skoolkit-wide.css +6 -0
  33. skoolkit/resources/skoolkit.css +212 -0
  34. skoolkit/rzxinfo.py +182 -0
  35. skoolkit/rzxplay.py +443 -0
  36. skoolkit/screen.py +189 -0
  37. skoolkit/simtables.py +281 -0
  38. skoolkit/simulator.py +2841 -0
  39. skoolkit/simutils.py +195 -0
  40. skoolkit/skool2asm.py +143 -0
  41. skoolkit/skool2bin.py +352 -0
  42. skoolkit/skool2ctl.py +79 -0
  43. skoolkit/skool2html.py +410 -0
  44. skoolkit/skoolasm.py +634 -0
  45. skoolkit/skoolctl.py +727 -0
  46. skoolkit/skoolhtml.py +1393 -0
  47. skoolkit/skoolmacro.py +1691 -0
  48. skoolkit/skoolparser.py +974 -0
  49. skoolkit/skoolutils.py +714 -0
  50. skoolkit/sna2ctl.py +89 -0
  51. skoolkit/sna2img.py +174 -0
  52. skoolkit/sna2skool.py +110 -0
  53. skoolkit/snactl.py +472 -0
  54. skoolkit/snapinfo.py +524 -0
  55. skoolkit/snapmod.py +71 -0
  56. skoolkit/snapshot.py +886 -0
  57. skoolkit/snaskool.py +638 -0
  58. skoolkit/tap2sna.py +975 -0
  59. skoolkit/tape.py +787 -0
  60. skoolkit/tapinfo.py +142 -0
  61. skoolkit/textutils.py +64 -0
  62. skoolkit/trace.py +415 -0
  63. skoolkit/traceutils.py +1943 -0
  64. skoolkit/z80.py +1514 -0
  65. skoolkit-9.5.data/scripts/bin2sna.py +26 -0
  66. skoolkit-9.5.data/scripts/bin2tap.py +26 -0
  67. skoolkit-9.5.data/scripts/rzxinfo.py +26 -0
  68. skoolkit-9.5.data/scripts/rzxplay.py +26 -0
  69. skoolkit-9.5.data/scripts/skool2asm.py +26 -0
  70. skoolkit-9.5.data/scripts/skool2bin.py +26 -0
  71. skoolkit-9.5.data/scripts/skool2ctl.py +26 -0
  72. skoolkit-9.5.data/scripts/skool2html.py +26 -0
  73. skoolkit-9.5.data/scripts/sna2ctl.py +26 -0
  74. skoolkit-9.5.data/scripts/sna2img.py +26 -0
  75. skoolkit-9.5.data/scripts/sna2skool.py +26 -0
  76. skoolkit-9.5.data/scripts/snapinfo.py +26 -0
  77. skoolkit-9.5.data/scripts/snapmod.py +26 -0
  78. skoolkit-9.5.data/scripts/tap2sna.py +26 -0
  79. skoolkit-9.5.data/scripts/tapinfo.py +26 -0
  80. skoolkit-9.5.data/scripts/trace.py +26 -0
  81. skoolkit-9.5.dist-info/COPYING +674 -0
  82. skoolkit-9.5.dist-info/METADATA +145 -0
  83. skoolkit-9.5.dist-info/RECORD +85 -0
  84. skoolkit-9.5.dist-info/WHEEL +5 -0
  85. skoolkit-9.5.dist-info/top_level.txt +1 -0
skoolkit/__init__.py ADDED
@@ -0,0 +1,239 @@
1
+ # Copyright 2011-2025 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.5'
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 as_dword(num):
203
+ return (num % 256, (num >> 8) % 256, (num >> 16) % 256, (num >> 24) % 256)
204
+
205
+ def get_dword(data, index):
206
+ return get_word3(data, index) + 16777216 * data[index + 3]
207
+
208
+ def integer(arg):
209
+ try:
210
+ s_arg = arg.strip()
211
+ if s_arg.startswith('0x'):
212
+ return int(s_arg[2:], 16)
213
+ return int(s_arg)
214
+ except ValueError:
215
+ raise argparse.ArgumentTypeError("invalid integer: '{}'".format(arg))
216
+
217
+ def eval_variable(name, value):
218
+ if name.endswith('$'):
219
+ return value
220
+ return evaluate(value)
221
+
222
+ def variable(arg):
223
+ name, sep, value = arg.partition('=')
224
+ if name and sep:
225
+ try:
226
+ return name, eval_variable(name, value)
227
+ except ValueError:
228
+ raise argparse.ArgumentTypeError("invalid arithmetic expression: '{}'".format(arg))
229
+ elif name:
230
+ raise argparse.ArgumentTypeError("missing variable value: '{}'".format(arg))
231
+ else:
232
+ raise argparse.ArgumentTypeError("missing variable name: '{}'".format(arg))
233
+
234
+ class SkoolKitError(Exception):
235
+ pass
236
+
237
+ # API (ControlDirectiveComposer)
238
+ class SkoolParsingError(SkoolKitError):
239
+ """Raised when an error occurs while parsing a skool file."""
skoolkit/audio.py ADDED
@@ -0,0 +1,211 @@
1
+ # Copyright 2013, 2014, 2022-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
+ 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
+ """
29
+ Initialise the audio writer.
30
+
31
+ :param config: A dictionary constructed from the contents of the
32
+ :ref:`ref-AudioWriter` section of the ref file.
33
+ """
34
+ # Component API
35
+ def __init__(self, config=None):
36
+ self.options = ({
37
+ CLOCK_SPEED: 3500000,
38
+ CONTENTION_BEGIN: 14334,
39
+ CONTENTION_END: 57248,
40
+ CONTENTION_FACTOR: 51,
41
+ FRAME_DURATION: 69888,
42
+ INTERRUPT_DELAY: 942,
43
+ SAMPLE_RATE: 44100
44
+ }, {
45
+ CLOCK_SPEED: 3546900,
46
+ CONTENTION_BEGIN: 14361,
47
+ CONTENTION_END: 58041,
48
+ CONTENTION_FACTOR: 51,
49
+ FRAME_DURATION: 70908,
50
+ INTERRUPT_DELAY: 1584,
51
+ SAMPLE_RATE: 44100
52
+ })
53
+ if config:
54
+ for k, v in config.items():
55
+ try:
56
+ self.options[0][k] = self.options[1][k] = int(v)
57
+ except ValueError:
58
+ pass
59
+
60
+ # Component API
61
+ def write_audio(self, audio_file, delays, contention=False, interrupts=False, offset=0, ma_filter=False, is128k=False):
62
+ """
63
+ Write an audio file.
64
+
65
+ :param audio_file: The file object to write the audio to.
66
+ :param delays: A sequence of integers representing the intervals (in
67
+ T-states) between speaker flips.
68
+ :param contention: Whether to simulate additional delays due to memory
69
+ and I/O contention.
70
+ :param interrupts: Whether to simulate additional delays due to
71
+ interrupts.
72
+ :param offset: The offset (in T-states) of the first speaker flip from
73
+ the start of the frame. Used when simulating delays due
74
+ to contention and interrupts.
75
+ :param ma_filter: Whether to apply a moving average filter.
76
+ :param is128k: Whether to use 128K timings.
77
+ """
78
+ options = self.options[is128k]
79
+ if contention or interrupts:
80
+ self._add_contention(delays, contention, interrupts, offset, options)
81
+ if ma_filter:
82
+ samples = self._moving_average_filter(delays, options)
83
+ else:
84
+ samples = self._delays_to_samples(delays, options)
85
+ self._write_wav(audio_file, samples, options)
86
+
87
+ # Component API
88
+ def formats(self):
89
+ """
90
+ Return a sequence of filename extensions (including the '.')
91
+ corresponding to the audio formats supported by this writer.
92
+ """
93
+ return ('.wav',)
94
+
95
+ def _add_contention(self, delays, contention, interrupts, cycle, options):
96
+ c_begin, c_end = options[CONTENTION_BEGIN], options[CONTENTION_END]
97
+ c_factor = 1 + options[CONTENTION_FACTOR] / 100
98
+ i_delay = options[INTERRUPT_DELAY]
99
+ f_duration = options[FRAME_DURATION]
100
+
101
+ for i in range(len(delays)):
102
+ d_offset = 0
103
+ while 1:
104
+ if interrupts and cycle == 0:
105
+ cycle = i_delay
106
+ if i:
107
+ delays[i] += i_delay
108
+ d_offset += i_delay
109
+ d_remaining = delays[i] - d_offset
110
+ if contention and cycle < c_end:
111
+ if cycle < c_begin:
112
+ gap = c_begin - cycle
113
+ if d_offset + gap >= delays[i]:
114
+ # Delay ends before next contended interval starts
115
+ cycle += d_remaining
116
+ break
117
+ # Delay crosses into contended interval
118
+ d_offset += gap
119
+ cycle = c_begin
120
+ else:
121
+ c_period = c_end - cycle
122
+ d_rem_contended = int(d_remaining * c_factor)
123
+ if d_rem_contended < c_period:
124
+ # Delay ends within contended interval
125
+ delays[i] = d_offset + d_rem_contended
126
+ cycle += d_rem_contended
127
+ break
128
+ # Delay crosses into uncontended interval
129
+ cd_cycles = int(c_period / c_factor)
130
+ delays[i] += c_period - cd_cycles
131
+ d_offset += cd_cycles
132
+ cycle = c_end
133
+ else:
134
+ if d_remaining < f_duration - cycle:
135
+ # Delay ends before frame boundary
136
+ cycle += d_remaining
137
+ break
138
+ # Delay crosses frame boundary
139
+ d_offset += f_duration - cycle
140
+ cycle = 0
141
+
142
+ def _moving_average_filter(self, delays, options):
143
+ sample_delay = options[CLOCK_SPEED] / options[SAMPLE_RATE]
144
+ s0, s1 = 0, sample_delay
145
+ t, t1 = 0, ceil(s1)
146
+ bit = bits = 0
147
+ samples = []
148
+ for d in delays:
149
+ while True:
150
+ if t + d < t1:
151
+ if bit:
152
+ bits += d
153
+ t += d
154
+ break
155
+ i = t1 - t
156
+ if bit:
157
+ bits += i
158
+ d -= i
159
+ samples.append((floor(0xFFFF * bits / (t1 - s0)) - 0x8000) & 0xFFFF)
160
+ s0 = t = t1
161
+ s1 += sample_delay
162
+ t1 = ceil(s1)
163
+ bits = 0
164
+ bit = 1 - bit
165
+ return samples
166
+
167
+ def _delays_to_samples(self, delays, options):
168
+ sample_delay = options[CLOCK_SPEED] / options[SAMPLE_RATE]
169
+ samples = []
170
+ direction = 1
171
+ i = 0
172
+ d = delays[0]
173
+ t = 0
174
+ while 1:
175
+ while t >= d:
176
+ i += 1
177
+ if i >= len(delays):
178
+ break
179
+ d += delays[i]
180
+ direction *= -1
181
+ if i >= len(delays):
182
+ break
183
+ if direction > 0:
184
+ samples.append(32767)
185
+ else:
186
+ samples.append(32768)
187
+ t += sample_delay
188
+ return samples
189
+
190
+ def _to_int32(self, num):
191
+ return (num & 255, (num >> 8) & 255, (num >> 16) & 255, num >> 24)
192
+
193
+ def _write_wav(self, audio_file, samples, options):
194
+ sample_rate = options[SAMPLE_RATE]
195
+ data_length = 2 * len(samples)
196
+ header = bytearray()
197
+ header.extend(b'RIFF')
198
+ header.extend(self._to_int32(36 + data_length))
199
+ header.extend(b'WAVEfmt ')
200
+ header.extend(self._to_int32(16)) # length of fmt chunk
201
+ header.extend((1, 0)) # format (1=PCM)
202
+ header.extend((1, 0)) # channels
203
+ header.extend(self._to_int32(sample_rate)) # sample rate
204
+ header.extend(self._to_int32(sample_rate * 2)) # byte rate
205
+ header.extend((2, 0)) # bytes per sample
206
+ header.extend((16, 0)) # bits per sample
207
+ header.extend(b'data')
208
+ header.extend(self._to_int32(data_length)) # length of data chunk
209
+ audio_file.write(header)
210
+ for sample in samples:
211
+ audio_file.write(bytes((sample & 255, sample // 256)))