-
-
Notifications
You must be signed in to change notification settings - Fork 0
/
main.lua
executable file
·210 lines (183 loc) · 6.51 KB
/
main.lua
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
local bit = bit or require "bit32"
local util = require "util"
local shift_register = require "shift"
local ui = require "ui"
local lua_8080 = require "lua-8080"
local cpu = lua_8080.cpu
local bus = lua_8080.bus
local rom = lua_8080.rom
local ram = lua_8080.ram
local sounds = {previous_state = {0, 0}}
local cycles = 0
local interrupts = {1, 2}
local function play_sound(bank)
return function(sound_mask)
local joysticks = love.joystick.getJoysticks()
for i = 0, 4 do
-- Should we play the sound?
local play = bit.band(bit.rshift(sound_mask, i), 1) == 1
-- Is it a rising edge?
local playing = bit.band(bit.rshift(sounds.previous_state[bank], i), 1) == 1
-- Look up sound source
local sound
if bank == 1 and i == 4 then
-- The "extra life" sound file is called 9.wav for some reason
sound = sounds[9]
else
sound = sounds[i + (bank == 2 and 4 or 0)]
end
if sound then
-- Start the sound on a rising edge
if play and not playing then
sound:play()
end
-- Stop it on a falling edge (for looping sounds)
if sound:isLooping() and not play then
sound:stop()
end
end
for _, joystick in ipairs(joysticks) do
if play and not playing then
joystick:setVibration(0.5, 0.5)
elseif playing and not play then
joystick:setVibration()
end
end
end
-- Save the last port output for edge checking
sounds.previous_state[bank] = sound_mask
end
end
function love.load()
-- Load ROM data into ROM chips and connect them to the bus
local rom_files = {
"invaders.h",
"invaders.g",
"invaders.f",
"invaders.e"
}
-- LÖVE automatically searches the save directory first, then the root folder of the .love archive or source folder
-- But if we're running as a fused executable, we're also allowed to mount its base directory
local root_dir = ""
if love.filesystem.isFused() and love.filesystem.mount(love.filesystem.getSourceBaseDirectory(), "fused_dir") then
root_dir = "fused_dir/"
end
local address = 0x0000
local num_files = 0
for _, file in ipairs(rom_files) do
local rom_part
if love.filesystem.getInfo(root_dir .. "assets/" .. file) then
rom_part = util.read_file(root_dir .. "assets/" .. file)
end
if rom_part then
num_files = num_files + 1
-- Read high score from save file and write it to ROM
if file == "invaders.e" then
local savefile = love.filesystem.newFile("hiscore")
local ok = savefile:open("r")
if ok then
for address = 0x03F4, 0x03F5 do
rom_part[address] = string.byte(savefile:read(1))
end
savefile:close()
end
end
local r = rom(rom_part)
bus:connect(address, r)
address = address + r.size
end
end
-- If we didn't load the ROMs correctly, display error screen
if num_files ~= 4 or address ~= 0x2000 then
require "no_rom"
return
end
-- Connect RAM to the bus
local wram = ram(0x0400, 0)
local vram = ram(0x1C00, 0)
-- Expose mirror RAM
for address = 0x2000, 0xE000, 0x2000 do
bus:connect(address, wram)
bus:connect(address + 0x0400, vram)
end
-- Initialize CPU
cpu:init(bus)
-- Set up IO ports with constant values
cpu.ports.internal.input[0] = 0x0E
cpu.ports.internal.input[1] = 0x08
cpu.ports.internal.input[2] = 0x00
-- Set up IO ports connected to the hardware shift register
cpu.ports.internal.output[2] = shift_register.set_offset
cpu.ports.internal.input[3] = shift_register.read
cpu.ports.internal.output[4] = shift_register.shift
-- Sound
cpu.ports.internal.output[3] = play_sound(1)
cpu.ports.internal.output[5] = play_sound(2)
-- Initialize UI
ui.init(cpu, bus)
-- Alternate sound file names
local sound_names = {
"ufo_highpitch",
"shoot",
"explosion",
"invaderkilled",
"fastinvader1",
"fastinvader2",
"fastinvader3",
"fastinvader4",
"ufo_lowpitch",
"extendedplay"
}
-- Load sound files
for i = 0, 9 do
if love.filesystem.getInfo(root_dir .. "assets/" .. i .. ".wav") then
sounds[i] = love.audio.newSource(root_dir .. "assets/" .. i .. ".wav", "static")
elseif love.filesystem.getInfo(root_dir .. "assets/" .. sound_names[i + 1] .. ".wav") then
sounds[i] = love.audio.newSource(root_dir .. "assets/" .. sound_names[i + 1] .. ".wav", "static")
end
if sounds[0] then
sounds[0]:setLooping(true) -- The UFO sound should loop
end
end
end
function love.update(dt)
local num_interrupts = 0
-- Cycle the CPU
-- TODO: Should probably do it by cycles
-- TODO: Use delta time instead of running two frames per frame
while num_interrupts ~= 2 and not cpu.pause do
cycles = cycles + cpu:cycle()
-- Twice per frame, the display logic requests an interrupt.
-- Interrupt 1 (RST 0x08) in the middle of the frame,
-- and interrupt 2 (RST 0x10) at the end. The 8080 runs at
-- 2 MHz, so every 1 MHz we request an interrupt, alternating
-- between 1 and 2.
if cycles >= 1000000 / 30 and cpu.inte then
-- Disable interrupts
cpu.inte = false
-- Consume cycles
cycles = cycles - (1000000 / 30)
-- The display outputs an RST opcode on the data bus
-- which the CPU fetches and executes
cycles =
cycles +
cpu:execute(
{
instruction = "RST",
op1 = interrupts[(num_interrupts % 2) + 1]
}
)
num_interrupts = num_interrupts + 1
end
end
end
function love.quit()
-- Save high score to file
local file = love.filesystem.newFile("hiscore")
file:open("w")
for address = 0x20F4, 0x20F5 do
file:write(string.char(bus[address]), 1)
end
file:close()
return false
end