teek 0.1.2 → 0.1.4
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.
- checksums.yaml +4 -4
- data/README.md +21 -0
- data/Rakefile +121 -22
- data/ext/teek/extconf.rb +19 -1
- data/ext/teek/tcltkbridge.c +44 -2
- data/ext/teek/tcltkbridge.h +3 -0
- data/ext/teek/tkdrop.c +66 -0
- data/ext/teek/tkdrop.h +26 -0
- data/ext/teek/tkdrop_macos.m +141 -0
- data/ext/teek/tkdrop_win.c +232 -0
- data/ext/teek/tkdrop_x11.c +337 -0
- data/ext/teek/tkwin.c +42 -0
- data/lib/teek/platform.rb +29 -0
- data/lib/teek/version.rb +1 -1
- data/lib/teek.rb +248 -7
- data/teek.gemspec +3 -2
- metadata +7 -45
- data/sample/calculator.rb +0 -255
- data/sample/debug_demo.rb +0 -43
- data/sample/goldberg.rb +0 -1803
- data/sample/goldberg_helpers.rb +0 -170
- data/sample/minesweeper/assets/MINESWEEPER_0.png +0 -0
- data/sample/minesweeper/assets/MINESWEEPER_1.png +0 -0
- data/sample/minesweeper/assets/MINESWEEPER_2.png +0 -0
- data/sample/minesweeper/assets/MINESWEEPER_3.png +0 -0
- data/sample/minesweeper/assets/MINESWEEPER_4.png +0 -0
- data/sample/minesweeper/assets/MINESWEEPER_5.png +0 -0
- data/sample/minesweeper/assets/MINESWEEPER_6.png +0 -0
- data/sample/minesweeper/assets/MINESWEEPER_7.png +0 -0
- data/sample/minesweeper/assets/MINESWEEPER_8.png +0 -0
- data/sample/minesweeper/assets/MINESWEEPER_F.png +0 -0
- data/sample/minesweeper/assets/MINESWEEPER_M.png +0 -0
- data/sample/minesweeper/assets/MINESWEEPER_X.png +0 -0
- data/sample/minesweeper/minesweeper.rb +0 -452
- data/sample/optcarrot/vendor/optcarrot/apu.rb +0 -856
- data/sample/optcarrot/vendor/optcarrot/config.rb +0 -257
- data/sample/optcarrot/vendor/optcarrot/cpu.rb +0 -1162
- data/sample/optcarrot/vendor/optcarrot/driver.rb +0 -144
- data/sample/optcarrot/vendor/optcarrot/mapper/cnrom.rb +0 -14
- data/sample/optcarrot/vendor/optcarrot/mapper/mmc1.rb +0 -105
- data/sample/optcarrot/vendor/optcarrot/mapper/mmc3.rb +0 -153
- data/sample/optcarrot/vendor/optcarrot/mapper/uxrom.rb +0 -14
- data/sample/optcarrot/vendor/optcarrot/nes.rb +0 -105
- data/sample/optcarrot/vendor/optcarrot/opt.rb +0 -168
- data/sample/optcarrot/vendor/optcarrot/pad.rb +0 -92
- data/sample/optcarrot/vendor/optcarrot/palette.rb +0 -65
- data/sample/optcarrot/vendor/optcarrot/ppu.rb +0 -1468
- data/sample/optcarrot/vendor/optcarrot/rom.rb +0 -143
- data/sample/optcarrot/vendor/optcarrot.rb +0 -14
- data/sample/optcarrot.rb +0 -354
- data/sample/paint/assets/bucket.png +0 -0
- data/sample/paint/assets/cursor.png +0 -0
- data/sample/paint/assets/eraser.png +0 -0
- data/sample/paint/assets/pencil.png +0 -0
- data/sample/paint/assets/spray.png +0 -0
- data/sample/paint/layer.rb +0 -255
- data/sample/paint/layer_manager.rb +0 -179
- data/sample/paint/paint_demo.rb +0 -837
- data/sample/paint/sparse_pixel_buffer.rb +0 -202
- data/sample/sdl2_demo.rb +0 -318
- data/sample/threading_demo.rb +0 -494
|
@@ -1,143 +0,0 @@
|
|
|
1
|
-
module Optcarrot
|
|
2
|
-
# Cartridge class (with NROM mapper implemented)
|
|
3
|
-
class ROM
|
|
4
|
-
MAPPER_DB = { 0x00 => self }
|
|
5
|
-
|
|
6
|
-
# These are optional
|
|
7
|
-
require_relative "mapper/mmc1"
|
|
8
|
-
require_relative "mapper/uxrom"
|
|
9
|
-
require_relative "mapper/cnrom"
|
|
10
|
-
require_relative "mapper/mmc3"
|
|
11
|
-
|
|
12
|
-
def self.zip_extract(filename)
|
|
13
|
-
require "zlib"
|
|
14
|
-
bin = File.binread(filename)
|
|
15
|
-
loop do
|
|
16
|
-
sig, _, flags, comp, _, _, _, data_len, _, fn_len, ext_len = bin.slice!(0, 30).unpack("a4v5V3v2")
|
|
17
|
-
break if sig != "PK\3\4".b
|
|
18
|
-
fn = bin.slice!(0, fn_len)
|
|
19
|
-
bin.slice!(0, ext_len)
|
|
20
|
-
data = bin.slice!(0, data_len)
|
|
21
|
-
next if File.extname(fn).downcase != ".nes"
|
|
22
|
-
next if flags & 0x11 != 0
|
|
23
|
-
next if comp != 0 && comp != 8
|
|
24
|
-
if comp == 8
|
|
25
|
-
zs = Zlib::Inflate.new(-15)
|
|
26
|
-
data = zs.inflate(data)
|
|
27
|
-
zs.finish
|
|
28
|
-
zs.close
|
|
29
|
-
end
|
|
30
|
-
return data
|
|
31
|
-
end
|
|
32
|
-
raise "failed to extract ROM file from `#{ filename }'"
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
def self.load(conf, cpu, ppu)
|
|
36
|
-
filename = conf.romfile
|
|
37
|
-
basename = File.basename(filename)
|
|
38
|
-
|
|
39
|
-
blob = (File.extname(filename) == ".zip" ? zip_extract(filename) : File.binread(filename)).bytes
|
|
40
|
-
|
|
41
|
-
# parse mapper
|
|
42
|
-
mapper = (blob[6] >> 4) | (blob[7] & 0xf0)
|
|
43
|
-
|
|
44
|
-
klass = MAPPER_DB[mapper]
|
|
45
|
-
raise NotImplementedError, "Unsupported mapper type 0x%02x" % mapper unless klass
|
|
46
|
-
klass.new(conf, cpu, ppu, basename, blob)
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
class InvalidROM < StandardError
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
def parse_header(buf)
|
|
53
|
-
raise InvalidROM, "Missing 16-byte header" if buf.size < 16
|
|
54
|
-
raise InvalidROM, "Missing 'NES' constant in header" if buf[0, 4] != "NES\x1a".bytes
|
|
55
|
-
raise NotImplementedError, "trainer not supported" if buf[6][2] == 1
|
|
56
|
-
raise NotImplementedError, "VS cart not supported" if buf[7][0] == 1
|
|
57
|
-
raise NotImplementedError, "PAL not supported" unless buf[9][0] == 0
|
|
58
|
-
|
|
59
|
-
prg_banks = buf[4]
|
|
60
|
-
chr_banks = buf[5]
|
|
61
|
-
@mirroring = buf[6][0] == 0 ? :horizontal : :vertical
|
|
62
|
-
@mirroring = :four_screen if buf[6][3] == 1
|
|
63
|
-
@battery = buf[6][1] == 1
|
|
64
|
-
@mapper = (buf[6] >> 4) | (buf[7] & 0xf0)
|
|
65
|
-
ram_banks = [1, buf[8]].max
|
|
66
|
-
|
|
67
|
-
return prg_banks, chr_banks, ram_banks
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
def initialize(conf, cpu, ppu, basename, buf)
|
|
71
|
-
@conf = conf
|
|
72
|
-
@cpu = cpu
|
|
73
|
-
@ppu = ppu
|
|
74
|
-
@basename = basename
|
|
75
|
-
|
|
76
|
-
prg_count, chr_count, wrk_count = parse_header(buf.slice!(0, 16))
|
|
77
|
-
|
|
78
|
-
raise InvalidROM, "EOF in ROM bank data" if buf.size < 0x4000 * prg_count
|
|
79
|
-
@prg_banks = (0...prg_count).map { buf.slice!(0, 0x4000) }
|
|
80
|
-
|
|
81
|
-
raise InvalidROM, "EOF in CHR bank data" if buf.size < 0x2000 * chr_count
|
|
82
|
-
@chr_banks = (0...chr_count).map { buf.slice!(0, 0x2000) }
|
|
83
|
-
|
|
84
|
-
@prg_ref = [nil] * 0x10000
|
|
85
|
-
@prg_ref[0x8000, 0x4000] = @prg_banks.first
|
|
86
|
-
@prg_ref[0xc000, 0x4000] = @prg_banks.last
|
|
87
|
-
|
|
88
|
-
@chr_ram = chr_count == 0 # No CHR bank implies CHR-RAM (writable CHR bank)
|
|
89
|
-
@chr_ref = @chr_ram ? [0] * 0x2000 : @chr_banks[0].dup
|
|
90
|
-
|
|
91
|
-
@wrk_readable = wrk_count > 0
|
|
92
|
-
@wrk_writable = false
|
|
93
|
-
@wrk = wrk_count > 0 ? (0x6000..0x7fff).map {|addr| addr >> 8 } : nil
|
|
94
|
-
|
|
95
|
-
init
|
|
96
|
-
|
|
97
|
-
@ppu.nametables = @mirroring
|
|
98
|
-
@ppu.set_chr_mem(@chr_ref, @chr_ram)
|
|
99
|
-
end
|
|
100
|
-
|
|
101
|
-
def init
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
def reset
|
|
105
|
-
@cpu.add_mappings(0x8000..0xffff, @prg_ref, nil)
|
|
106
|
-
end
|
|
107
|
-
|
|
108
|
-
def inspect
|
|
109
|
-
[
|
|
110
|
-
"Mapper: #{ @mapper } (#{ self.class.to_s.split("::").last })",
|
|
111
|
-
"PRG Banks: #{ @prg_banks.size }",
|
|
112
|
-
"CHR Banks: #{ @chr_banks.size }",
|
|
113
|
-
"Mirroring: #{ @mirroring }",
|
|
114
|
-
].join("\n")
|
|
115
|
-
end
|
|
116
|
-
|
|
117
|
-
def peek_6000(addr)
|
|
118
|
-
@wrk_readable ? @wrk[addr - 0x6000] : (addr >> 8)
|
|
119
|
-
end
|
|
120
|
-
|
|
121
|
-
def poke_6000(addr, data)
|
|
122
|
-
@wrk[addr - 0x6000] = data if @wrk_writable
|
|
123
|
-
end
|
|
124
|
-
|
|
125
|
-
def vsync
|
|
126
|
-
end
|
|
127
|
-
|
|
128
|
-
def load_battery
|
|
129
|
-
return unless @battery
|
|
130
|
-
sav = @basename + ".sav"
|
|
131
|
-
return unless File.readable?(sav)
|
|
132
|
-
sav = File.binread(sav)
|
|
133
|
-
@wrk.replace(sav.bytes)
|
|
134
|
-
end
|
|
135
|
-
|
|
136
|
-
def save_battery
|
|
137
|
-
return unless @battery
|
|
138
|
-
sav = @basename + ".sav"
|
|
139
|
-
puts "Saving: " + sav
|
|
140
|
-
File.binwrite(sav, @wrk.pack("C*"))
|
|
141
|
-
end
|
|
142
|
-
end
|
|
143
|
-
end
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
# Optcarrot namespace
|
|
2
|
-
module Optcarrot
|
|
3
|
-
VERSION = "0.9.0"
|
|
4
|
-
end
|
|
5
|
-
|
|
6
|
-
require_relative "optcarrot/nes"
|
|
7
|
-
require_relative "optcarrot/rom"
|
|
8
|
-
require_relative "optcarrot/pad"
|
|
9
|
-
require_relative "optcarrot/cpu"
|
|
10
|
-
require_relative "optcarrot/apu"
|
|
11
|
-
require_relative "optcarrot/ppu"
|
|
12
|
-
require_relative "optcarrot/palette"
|
|
13
|
-
require_relative "optcarrot/driver"
|
|
14
|
-
require_relative "optcarrot/config"
|
data/sample/optcarrot.rb
DELETED
|
@@ -1,354 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env ruby
|
|
2
|
-
# frozen_string_literal: true
|
|
3
|
-
# teek-record: title=Optcarrot NES (Thwaite)
|
|
4
|
-
|
|
5
|
-
# Optcarrot NES Emulator running on teek-sdl2
|
|
6
|
-
#
|
|
7
|
-
# Uses teek for the window and teek-sdl2 for GPU-accelerated rendering.
|
|
8
|
-
# Optcarrot is installed automatically via bundler/inline.
|
|
9
|
-
#
|
|
10
|
-
# Usage:
|
|
11
|
-
# ruby -Ilib -Iteek-sdl2/lib sample/optcarrot.rb path/to/rom.nes
|
|
12
|
-
#
|
|
13
|
-
# Controls:
|
|
14
|
-
# Arrow keys - D-pad
|
|
15
|
-
# Z - A button
|
|
16
|
-
# X - B button
|
|
17
|
-
# Return - Start
|
|
18
|
-
# Space - Select
|
|
19
|
-
# Q / Escape - Quit
|
|
20
|
-
|
|
21
|
-
FALLBACK_ROMS = [
|
|
22
|
-
File.join(__dir__, 'optcarrot', 'thwaite.nes'),
|
|
23
|
-
].freeze
|
|
24
|
-
|
|
25
|
-
rom_path = ARGV.find { |a| !a.start_with?('--') }
|
|
26
|
-
unless rom_path
|
|
27
|
-
rom_path = FALLBACK_ROMS.find { |p| File.exist?(p) }
|
|
28
|
-
unless rom_path
|
|
29
|
-
abort <<~USAGE
|
|
30
|
-
Usage: ruby sample/optcarrot.rb [options] <rom.nes>
|
|
31
|
-
|
|
32
|
-
Options are passed through to optcarrot.
|
|
33
|
-
ROM file must be provided — it is not bundled.
|
|
34
|
-
Fallback locations checked:
|
|
35
|
-
#{FALLBACK_ROMS.join("\n ")}
|
|
36
|
-
USAGE
|
|
37
|
-
end
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
unless File.exist?(rom_path)
|
|
41
|
-
abort "ROM not found: #{rom_path}"
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
$LOAD_PATH.unshift File.expand_path('../lib', __dir__)
|
|
45
|
-
$LOAD_PATH.unshift File.expand_path('../teek-sdl2/lib', __dir__)
|
|
46
|
-
$LOAD_PATH.unshift File.join(__dir__, 'optcarrot', 'vendor')
|
|
47
|
-
require 'teek'
|
|
48
|
-
require 'teek/sdl2'
|
|
49
|
-
require 'optcarrot'
|
|
50
|
-
|
|
51
|
-
# --- Video Driver -----------------------------------------------------------
|
|
52
|
-
|
|
53
|
-
class TeekVideo < Optcarrot::Video
|
|
54
|
-
NES_WIDTH = 256
|
|
55
|
-
NES_HEIGHT = 224
|
|
56
|
-
|
|
57
|
-
def init
|
|
58
|
-
super
|
|
59
|
-
|
|
60
|
-
@app = Teek::App.new
|
|
61
|
-
@app.show
|
|
62
|
-
@app.set_window_title('Optcarrot NES — teek-sdl2')
|
|
63
|
-
@app.set_window_geometry("#{NES_WIDTH * 2}x#{NES_HEIGHT * 2}")
|
|
64
|
-
@app.update
|
|
65
|
-
|
|
66
|
-
@viewport = Teek::SDL2::Viewport.new(@app, width: NES_WIDTH * 2, height: NES_HEIGHT * 2)
|
|
67
|
-
@viewport.pack(fill: :both, expand: true)
|
|
68
|
-
|
|
69
|
-
@texture = @viewport.renderer.create_texture(NES_WIDTH, NES_HEIGHT, :streaming)
|
|
70
|
-
|
|
71
|
-
# Build palette: optcarrot palette_rgb is [[r,g,b], ...] → ARGB8888
|
|
72
|
-
@palette = @palette_rgb.map do |r, g, b|
|
|
73
|
-
0xFF000000 | (r << 16) | (g << 8) | b
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
# Load font for status overlay
|
|
77
|
-
font_path = File.join(__dir__, '..', 'teek-sdl2', 'assets', 'JetBrainsMonoNL-Regular.ttf')
|
|
78
|
-
@status_font = @viewport.renderer.load_font(font_path, 13)
|
|
79
|
-
|
|
80
|
-
# Auto-focus viewport for keyboard input
|
|
81
|
-
@app.tcl_eval("focus -force #{@viewport.frame.path}")
|
|
82
|
-
@app.update
|
|
83
|
-
|
|
84
|
-
# FPS tracking
|
|
85
|
-
@fps_count = 0
|
|
86
|
-
@fps_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
87
|
-
@frame_num = 0
|
|
88
|
-
@fps_text = '0.0 fps'
|
|
89
|
-
end
|
|
90
|
-
|
|
91
|
-
def tick(colors)
|
|
92
|
-
return super if @disposed
|
|
93
|
-
|
|
94
|
-
# colors is already palette-mapped uint32 ARGB values from the PPU.
|
|
95
|
-
# Pack to byte string for texture update.
|
|
96
|
-
@texture.update(Teek::SDL2::Pixels.pack_uint32(colors, NES_WIDTH, NES_HEIGHT))
|
|
97
|
-
|
|
98
|
-
@viewport.render do |r|
|
|
99
|
-
r.clear(0, 0, 0)
|
|
100
|
-
r.copy(@texture)
|
|
101
|
-
|
|
102
|
-
# Status overlay at bottom of game surface (outline + green text)
|
|
103
|
-
right_text = "Hello from Teek! Ruby #{RUBY_VERSION}"
|
|
104
|
-
lx, ly = 6, NES_HEIGHT * 2 - 18
|
|
105
|
-
rw, = @status_font.measure(right_text)
|
|
106
|
-
rx, ry = NES_WIDTH * 2 - rw - 6, NES_HEIGHT * 2 - 18
|
|
107
|
-
# Black outline (draw offset in 4 directions)
|
|
108
|
-
[[-1,0],[1,0],[0,-1],[0,1]].each do |dx, dy|
|
|
109
|
-
r.draw_text(lx+dx, ly+dy, @fps_text, font: @status_font, r: 0, g: 0, b: 0)
|
|
110
|
-
r.draw_text(rx+dx, ry+dy, right_text, font: @status_font, r: 0, g: 0, b: 0)
|
|
111
|
-
end
|
|
112
|
-
r.draw_text(lx, ly, @fps_text, font: @status_font, r: 0, g: 255, b: 0)
|
|
113
|
-
r.draw_text(rx, ry, right_text, font: @status_font, r: 0, g: 255, b: 0)
|
|
114
|
-
end
|
|
115
|
-
|
|
116
|
-
# FPS display
|
|
117
|
-
@fps_count += 1
|
|
118
|
-
@frame_num += 1
|
|
119
|
-
now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
120
|
-
elapsed = now - @fps_time
|
|
121
|
-
if elapsed >= 1.0
|
|
122
|
-
fps = @fps_count / elapsed
|
|
123
|
-
@fps_text = "#{fps.round(1)} fps | frame #{@frame_num}"
|
|
124
|
-
@app.set_window_title("Optcarrot — #{fps.round(1)} fps")
|
|
125
|
-
@fps_count = 0
|
|
126
|
-
@fps_time = now
|
|
127
|
-
end
|
|
128
|
-
|
|
129
|
-
# Process Tk events so window stays responsive
|
|
130
|
-
@app.update
|
|
131
|
-
|
|
132
|
-
super
|
|
133
|
-
end
|
|
134
|
-
|
|
135
|
-
def dispose
|
|
136
|
-
@disposed = true
|
|
137
|
-
@texture&.destroy
|
|
138
|
-
@viewport&.destroy
|
|
139
|
-
# Don't destroy app here — TeekDemo.finish needs it for the recording
|
|
140
|
-
# harness signal. Process exit handles cleanup for interactive mode.
|
|
141
|
-
end
|
|
142
|
-
|
|
143
|
-
def app
|
|
144
|
-
@app
|
|
145
|
-
end
|
|
146
|
-
|
|
147
|
-
def viewport
|
|
148
|
-
@viewport
|
|
149
|
-
end
|
|
150
|
-
end
|
|
151
|
-
|
|
152
|
-
# --- Input Driver ------------------------------------------------------------
|
|
153
|
-
|
|
154
|
-
class TeekInput < Optcarrot::Input
|
|
155
|
-
KEY_MAP = {
|
|
156
|
-
'left' => :left,
|
|
157
|
-
'right' => :right,
|
|
158
|
-
'up' => :up,
|
|
159
|
-
'down' => :down,
|
|
160
|
-
'z' => :a,
|
|
161
|
-
'x' => :b,
|
|
162
|
-
'return' => :start,
|
|
163
|
-
'space' => :select,
|
|
164
|
-
}.freeze
|
|
165
|
-
|
|
166
|
-
# Demo script: [{frame:, button:, action: :keydown/:keyup}, ...]
|
|
167
|
-
# Thwaite (missile command): title → 1 player → shoot missiles
|
|
168
|
-
DEMO_SCRIPT = [
|
|
169
|
-
# Title screen — mash Start quickly
|
|
170
|
-
{ frame: 60, button: :start, action: :keydown },
|
|
171
|
-
{ frame: 65, button: :start, action: :keyup },
|
|
172
|
-
# 1 player select — A
|
|
173
|
-
{ frame: 80, button: :a, action: :keydown },
|
|
174
|
-
{ frame: 85, button: :a, action: :keyup },
|
|
175
|
-
# Skip any text — A again
|
|
176
|
-
{ frame: 100, button: :a, action: :keydown },
|
|
177
|
-
{ frame: 105, button: :a, action: :keyup },
|
|
178
|
-
{ frame: 120, button: :a, action: :keydown },
|
|
179
|
-
{ frame: 125, button: :a, action: :keyup },
|
|
180
|
-
# Dismiss text screens
|
|
181
|
-
{ frame: 185, button: :a, action: :keydown },
|
|
182
|
-
{ frame: 190, button: :a, action: :keyup },
|
|
183
|
-
{ frame: 230, button: :a, action: :keydown },
|
|
184
|
-
{ frame: 235, button: :a, action: :keyup },
|
|
185
|
-
# Game should be starting — move and shoot aggressively
|
|
186
|
-
# Move crosshair right+up, shoot
|
|
187
|
-
{ frame: 250, button: :right, action: :keydown },
|
|
188
|
-
{ frame: 250, button: :up, action: :keydown },
|
|
189
|
-
{ frame: 280, button: :up, action: :keyup },
|
|
190
|
-
{ frame: 290, button: :right, action: :keyup },
|
|
191
|
-
{ frame: 295, button: :b, action: :keydown },
|
|
192
|
-
{ frame: 300, button: :b, action: :keyup },
|
|
193
|
-
# Sweep left, shoot
|
|
194
|
-
{ frame: 320, button: :left, action: :keydown },
|
|
195
|
-
{ frame: 320, button: :up, action: :keydown },
|
|
196
|
-
{ frame: 350, button: :up, action: :keyup },
|
|
197
|
-
{ frame: 380, button: :left, action: :keyup },
|
|
198
|
-
{ frame: 385, button: :b, action: :keydown },
|
|
199
|
-
{ frame: 390, button: :b, action: :keyup },
|
|
200
|
-
# Sweep right+down, shoot
|
|
201
|
-
{ frame: 410, button: :right, action: :keydown },
|
|
202
|
-
{ frame: 410, button: :down, action: :keydown },
|
|
203
|
-
{ frame: 440, button: :down, action: :keyup },
|
|
204
|
-
{ frame: 470, button: :right, action: :keyup },
|
|
205
|
-
{ frame: 475, button: :b, action: :keydown },
|
|
206
|
-
{ frame: 480, button: :b, action: :keyup },
|
|
207
|
-
# Quick left, shoot
|
|
208
|
-
{ frame: 500, button: :left, action: :keydown },
|
|
209
|
-
{ frame: 530, button: :left, action: :keyup },
|
|
210
|
-
{ frame: 535, button: :b, action: :keydown },
|
|
211
|
-
{ frame: 540, button: :b, action: :keyup },
|
|
212
|
-
# Up+right, shoot
|
|
213
|
-
{ frame: 560, button: :right, action: :keydown },
|
|
214
|
-
{ frame: 560, button: :up, action: :keydown },
|
|
215
|
-
{ frame: 590, button: :up, action: :keyup },
|
|
216
|
-
{ frame: 600, button: :right, action: :keyup },
|
|
217
|
-
{ frame: 605, button: :b, action: :keydown },
|
|
218
|
-
{ frame: 610, button: :b, action: :keyup },
|
|
219
|
-
# Sweep left, shoot twice
|
|
220
|
-
{ frame: 630, button: :left, action: :keydown },
|
|
221
|
-
{ frame: 680, button: :left, action: :keyup },
|
|
222
|
-
{ frame: 685, button: :b, action: :keydown },
|
|
223
|
-
{ frame: 690, button: :b, action: :keyup },
|
|
224
|
-
{ frame: 710, button: :up, action: :keydown },
|
|
225
|
-
{ frame: 730, button: :up, action: :keyup },
|
|
226
|
-
{ frame: 735, button: :b, action: :keydown },
|
|
227
|
-
{ frame: 740, button: :b, action: :keyup },
|
|
228
|
-
# Down+right, shoot
|
|
229
|
-
{ frame: 760, button: :right, action: :keydown },
|
|
230
|
-
{ frame: 760, button: :down, action: :keydown },
|
|
231
|
-
{ frame: 790, button: :down, action: :keyup },
|
|
232
|
-
{ frame: 810, button: :right, action: :keyup },
|
|
233
|
-
{ frame: 815, button: :b, action: :keydown },
|
|
234
|
-
{ frame: 820, button: :b, action: :keyup },
|
|
235
|
-
# Final flurry — left, shoot, right, shoot
|
|
236
|
-
{ frame: 840, button: :left, action: :keydown },
|
|
237
|
-
{ frame: 870, button: :left, action: :keyup },
|
|
238
|
-
{ frame: 875, button: :b, action: :keydown },
|
|
239
|
-
{ frame: 880, button: :b, action: :keyup },
|
|
240
|
-
{ frame: 900, button: :right, action: :keydown },
|
|
241
|
-
{ frame: 900, button: :up, action: :keydown },
|
|
242
|
-
{ frame: 925, button: :up, action: :keyup },
|
|
243
|
-
{ frame: 940, button: :right, action: :keyup },
|
|
244
|
-
{ frame: 945, button: :b, action: :keydown },
|
|
245
|
-
{ frame: 950, button: :b, action: :keyup },
|
|
246
|
-
].freeze
|
|
247
|
-
|
|
248
|
-
def init
|
|
249
|
-
@viewport = @video.viewport
|
|
250
|
-
@prev_state = {}
|
|
251
|
-
@demo_mode = defined?(TeekDemo) && TeekDemo.active?
|
|
252
|
-
@demo_idx = 0
|
|
253
|
-
end
|
|
254
|
-
|
|
255
|
-
def tick(frame, pads)
|
|
256
|
-
# Real keyboard input
|
|
257
|
-
KEY_MAP.each do |key, button|
|
|
258
|
-
down = @viewport.key_down?(key)
|
|
259
|
-
was_down = @prev_state[key]
|
|
260
|
-
|
|
261
|
-
if down && !was_down
|
|
262
|
-
event(pads, :keydown, button, 0)
|
|
263
|
-
elsif !down && was_down
|
|
264
|
-
event(pads, :keyup, button, 0)
|
|
265
|
-
end
|
|
266
|
-
|
|
267
|
-
@prev_state[key] = down
|
|
268
|
-
end
|
|
269
|
-
|
|
270
|
-
# Demo script — virtual button presses by frame count
|
|
271
|
-
if @demo_mode
|
|
272
|
-
while @demo_idx < DEMO_SCRIPT.size && DEMO_SCRIPT[@demo_idx][:frame] <= frame
|
|
273
|
-
cmd = DEMO_SCRIPT[@demo_idx]
|
|
274
|
-
event(pads, cmd[:action], cmd[:button], 0)
|
|
275
|
-
@demo_idx += 1
|
|
276
|
-
end
|
|
277
|
-
end
|
|
278
|
-
|
|
279
|
-
# Quit on Q or Escape
|
|
280
|
-
if @viewport.key_down?('q') || @viewport.key_down?('escape')
|
|
281
|
-
throw :quit_nes
|
|
282
|
-
end
|
|
283
|
-
end
|
|
284
|
-
|
|
285
|
-
def dispose
|
|
286
|
-
end
|
|
287
|
-
end
|
|
288
|
-
|
|
289
|
-
# --- Register drivers and run ------------------------------------------------
|
|
290
|
-
|
|
291
|
-
# Inject our drivers into optcarrot's driver database
|
|
292
|
-
Optcarrot::Driver::DRIVER_DB[:video][:teek] = :TeekVideo
|
|
293
|
-
Optcarrot::Driver::DRIVER_DB[:input][:teek] = :TeekInput
|
|
294
|
-
Optcarrot.const_set(:TeekVideo, TeekVideo)
|
|
295
|
-
Optcarrot.const_set(:TeekInput, TeekInput)
|
|
296
|
-
|
|
297
|
-
# Prevent Driver.load_each from trying to require_relative our inline drivers
|
|
298
|
-
original_load_each = Optcarrot::Driver.method(:load_each)
|
|
299
|
-
Optcarrot::Driver.define_singleton_method(:load_each) do |conf, type, name|
|
|
300
|
-
if name == :teek
|
|
301
|
-
klass_name = Optcarrot::Driver::DRIVER_DB[type][name]
|
|
302
|
-
return Optcarrot.const_get(klass_name)
|
|
303
|
-
end
|
|
304
|
-
original_load_each.call(conf, type, name)
|
|
305
|
-
end
|
|
306
|
-
|
|
307
|
-
# Patch Config's option candidates to accept our driver names
|
|
308
|
-
# (candidates are snapshot at class load from DRIVER_DB.keys)
|
|
309
|
-
Optcarrot::Config.const_get(:OPTIONS).each do |_section, opts|
|
|
310
|
-
opts.each do |_id, opt|
|
|
311
|
-
next unless opt[:type] == :driver && opt[:candidates]
|
|
312
|
-
opt[:candidates] << :teek unless opt[:candidates].include?(:teek)
|
|
313
|
-
end
|
|
314
|
-
end
|
|
315
|
-
|
|
316
|
-
# Automated demo support (testing and recording)
|
|
317
|
-
require_relative '../lib/teek/demo_support'
|
|
318
|
-
|
|
319
|
-
# Build argv for optcarrot
|
|
320
|
-
optcarrot_argv = ['--video=teek', '--input=teek']
|
|
321
|
-
|
|
322
|
-
if TeekDemo.active?
|
|
323
|
-
# Run for ~960 frames (~16s) in demo mode
|
|
324
|
-
optcarrot_argv << '--frames=960'
|
|
325
|
-
end
|
|
326
|
-
|
|
327
|
-
ARGV.each { |a| optcarrot_argv << a if a.start_with?('--') }
|
|
328
|
-
optcarrot_argv << rom_path
|
|
329
|
-
|
|
330
|
-
nes = Optcarrot::NES.new(optcarrot_argv)
|
|
331
|
-
video = nes.instance_variable_get(:@video)
|
|
332
|
-
TeekDemo.app = video.app
|
|
333
|
-
|
|
334
|
-
if TeekDemo.recording?
|
|
335
|
-
video.app.set_window_geometry('+0+0')
|
|
336
|
-
video.app.tcl_eval('. configure -cursor none')
|
|
337
|
-
TeekDemo.signal_recording_ready
|
|
338
|
-
end
|
|
339
|
-
|
|
340
|
-
catch(:quit_nes) { nes.run }
|
|
341
|
-
|
|
342
|
-
if TeekDemo.active?
|
|
343
|
-
# Signal recording harness directly (can't use TeekDemo.finish because
|
|
344
|
-
# there's no Tk event loop running after the NES exits)
|
|
345
|
-
if (port = ENV['TK_STOP_PORT'])
|
|
346
|
-
begin
|
|
347
|
-
sock = TCPSocket.new('127.0.0.1', port.to_i)
|
|
348
|
-
sock.read(1) # wait for harness to stop ffmpeg
|
|
349
|
-
sock.close
|
|
350
|
-
rescue StandardError
|
|
351
|
-
end
|
|
352
|
-
end
|
|
353
|
-
video.app.destroy('.') rescue nil
|
|
354
|
-
end
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|