· 6 years ago · May 06, 2019, 05:36 AM
1-- hook the necessary libraries
2local component = require( "component" )
3local coroutine = require( "coroutine" )
4local colors = require( "colors" )
5local sides = require( "sides" )
6local computer = require( "computer" )
7local event = require( "event" )
8local keyboard = require( "keyboard" )
9local term = require( "term" )
10local text = require( "text" )
11local unicode = require( "unicode" )
12local ini = require( "ini" )
13
14-- get the current GPU and the dimensions of the screen
15local gpu = term.gpu()
16local gpuWidth, gpuHeight = gpu.getResolution()
17
18-- get proxies of the key components
19local chestBuffer = component.iron -- chest buffer between reactor's inventory and ME Network
20local reactorTank = component.reactor_fluid_port -- reactor's coolant tanks
21local reactorRS = component.reactor_redstone_port -- reactor's redstone port, for heat and EU data
22local reactorInv = component.nuclear_reactor -- reactor's inventory
23local meInterface = component.me_interface -- ME Interface for the component network
24local redstone = component.redstone -- computer's Redstone Card
25
26--------------------------------------------------------------------------------
27-- CONSTANTS --
28--------------------------------------------------------------------------------
29
30local UPDATE_INTERVAL = 0.25 -- time between UI thread (main thread) updates
31local WORK_INTERVAL = 0.05 -- timer used between resumes on the worker coroutine
32
33local LAYOUT_GRID_X = 1 -- x-position of reactor layout grid
34local LAYOUT_GRID_Y = 7 -- y-position of reactor layout grid
35
36local HEAT_METER_X = 65
37local HEAT_METER_Y = 3
38local HEAT_METER_H = 20
39
40local TANK_METER_X = 72
41local TANK_METER_Y = 3
42local TANK_METER_H = 20
43
44local ALIGN_LEFT = 0
45local ALIGN_CENTER = 1
46local ALIGN_RIGHT = 2
47
48-- bundled cable color that controls the reactor's on/off state
49local REACTOR_CONTROL_COLOR = colors.yellow
50-- the side of he computer the bundled cables connect to
51local CABLE_SIDE = sides.back
52local BUFFER_REACTOR_SIDE = "DOWN"
53local BUFFER_TO_INTERFACE_SIDE = "EAST"
54local INTERFACE_TO_BUFFER_SIDE = "WEST"
55
56-- Reactor states
57local STATE_CHECKING = 0 -- 'neutral' checking state
58local STATE_RUNNING = 1 -- running
59local STATE_COOLING_CORE = 2 -- waiting for core to cool
60local STATE_COOLING_COMP = 3 -- waiting for components to cool
61local STATE_WAIT_FLUID = 4 -- waiting for fluid levels to be acceptable
62local STATE_RELOADING = 5 -- changing items in the reactor
63local STATE_WAIT_ITEM = 6 -- waiting for missing items
64
65local STATE_TEXT = {}
66STATE_TEXT[ STATE_CHECKING ] = "Checking"
67STATE_TEXT[ STATE_RUNNING ] = "Running"
68STATE_TEXT[ STATE_COOLING_CORE ] = "Cooling Core"
69STATE_TEXT[ STATE_COOLING_COMP ] = "Cooling Components"
70STATE_TEXT[ STATE_WAIT_FLUID ] = "Waiting on Fluids"
71STATE_TEXT[ STATE_RELOADING ] = "Reloading"
72STATE_TEXT[ STATE_WAIT_ITEM ] = "Waiting on Items"
73
74--------------------------------------------------------------------------------
75-- VARIABLES --
76--------------------------------------------------------------------------------
77
78-- item rule definitions loaded from reactorRules.ini
79local itemRules
80-- layout codes and item raw_names they match to
81local layoutCodes
82-- item class definitions loaded from reactorRules.ini
83local itemClasses
84-- table for reactor's rules and layout. keys are 1-54, values are the raw_name of the items
85local reactorRules
86-- table of items that are missing from the reactor
87local missingItems = {}
88
89-- y-coordinates adjustment used to draw the last frame's indicators for heat and coolant levels
90local lastCoreHeatDisplayY = HEAT_METER_H - 1
91local lastCompHeatDisplayY = HEAT_METER_H - 1
92local lastCoolantDisplayY = TANK_METER_H - 1
93local lastHotCoolantDisplayY = TANK_METER_H - 1
94
95--reactor's current state
96local reactorStatus = STATE_CHECKING
97
98-- coroutine "thread" and timer ID for the main worker function (reactor logic)
99local workerThread
100local workerTimerID
101-- timer ID for the display function
102local visualTimerID
103-- flag for if the program is trying to exit
104local exitFlag = false
105
106-- "declare" the handle_key_down function
107local handle_key_down
108
109--------------------------------------------------------------------------------
110-- FUNCTIONS --
111--------------------------------------------------------------------------------
112
113-- function that can be called from anywhere in the program to tidy up everything before shutdown, whether natural or forced
114local function finalize()
115 gpu.setForeground( colors.white, true )
116 gpu.setBackground( colors.black, true )
117 if visualTimerID ~= nil then event.cancel( visualTimerID ) end
118 if workerTimerID ~= nil then event.cancel( workerTimerID ) end
119 event.ignore( "key_down", handle_key_down )
120 if redstone ~= nil then redstone.setBundledOutput( CABLE_SIDE, REACTOR_CONTROL_COLOR, 0 ) end
121end
122
123-- displays an error message and quits
124local function errorAndDie( message )
125 finalize()
126 io.stderr:write( message .. "\n" )
127 os.exit()
128end
129
130-- draws a string within a specified field size with a specific alignment
131local function drawFieldString( str, size, align )
132 -- check if the string is too big or too small
133 local len = str:len()
134
135 if len >= size then
136 -- print out the first 'size' chars
137 term.write( str:sub( 1, size ) )
138 else
139 local line
140
141 -- pad the string depending on the alignment
142 if align == ALIGN_CENTER then
143 len = ( size - len ) / 2
144 line = string.rep( " ", math.floor( len ) ) .. str .. string.rep( " ", math.ceil( len ) )
145 elseif align == ALIGN_RIGHT then
146 line = string.rep( " ", size - len )
147 else
148 line = str .. string.rep( " ", size - len )
149 end
150
151 term.write( line )
152 end
153end
154
155-- checks if a key exists in a table, displays an error message and quits if not
156local function checkForRequiredKey( t, key, message )
157 if t[ key ] == nil then
158 errorAndDie( string.format( message, key ) )
159 end
160end
161
162-- confirms that all system components are reachable
163local function checkSystemComponents()
164 if chestBuffer == nil then errorAndDie( "chest buffer not found" ) end
165 if reactorTank == nil then errorAndDie( "reactor fluid port not found" ) end
166 if reactorRS == nil then errorAndDie( "reactor redstone port not found" ) end
167 if reactorInv == nil then errorAndDie( "reactor inventory not found" ) end
168 if meInterface == nil then errorAndDie( "ME interface not found" ) end
169 if redstone == nil then errorAndDie( "redstone card not found" ) end
170
171 -- check color depth of GPU -- only supports 4-bit color depth
172 if gpu.getDepth() ~= 4 then errorAndDie( "requires GPU/screen color depth of 4" ) end
173
174 -- make sure there is at least 1 slot free in the chest buffer
175 chestBuffer.condenseItems()
176 local items = chestBuffer.getAllStacks()
177 if #items >= chestBuffer.getInventorySize() then errorAndDie( "chest buffer needs empty space" ) end
178
179 -- ensure the reactor is turned off
180 redstone.setBundledOutput( CABLE_SIDE, REACTOR_CONTROL_COLOR, 0 )
181end
182
183-- loads the reactor's item rules
184local function loadReactorRules ()
185 local reactorIni, result = ini.createTableFromINI( "reactorRules.ini" )
186
187 if reactorIni == nil then
188 errorAndDie( result )
189 end
190
191 -- check that the Items and Classes sections exist
192 checkForRequiredKey( reactorIni, "Items", string.format( "[%%s] section missing" ) )
193 checkForRequiredKey( reactorIni, "Classes", string.format( "[%%s] section missing" ) )
194
195 -- create new tables for the item rules and classes
196 itemRules = {}
197 itemClasses = {}
198 layoutCodes = {}
199
200 -- load in the Classes
201 for k, v in ipairs( reactorIni.Classes ) do
202 -- create the new table
203 itemClasses[ v ] = {}
204
205 -- set the defaults
206 itemClasses[ v ].fuel = false
207 itemClasses[ v ].extract = false
208
209 -- check that the class section exists
210 if reactorIni[ v ] == nil then
211 errorAndDie( string.format( "class section [%s] missing", v ) )
212 end
213
214 -- copy the properties from the INI
215 for propKey, propVal in pairs( reactorIni[ v ] ) do
216 itemClasses[ v ][ propKey ] = propVal
217 end
218
219 -- check that required fields were defined in the INI
220 checkForRequiredKey( itemClasses[ v ], "melts", string.format( "class section [%s] missing '%%s' key", v ) )
221 end
222
223 -- load in the Item rules
224 for k, v in ipairs( reactorIni.Items ) do
225 local key = v:lower()
226
227 -- create the new table
228 itemRules[ key ] = {}
229
230 -- set the defaults
231 itemRules[ key ].layoutCode = nil
232
233 -- check that the item section exists
234 if reactorIni[ v ] == nil then
235 errorAndDie( string.format( "item section [%s] missing", v ) )
236 end
237
238 -- copy the properties from the INI
239 for propKey, propVal in pairs( reactorIni[ v ] ) do
240 itemRules[ key ][ propKey ] = propVal
241 end
242
243 -- check that required fields were defined in the INI
244 checkForRequiredKey( itemRules[ key ], "db_name", string.format( "item section [%s] missing '%%s' key", v ) )
245 checkForRequiredKey( itemRules[ key ], "name", string.format( "item section [%s] missing '%%s' key", v ) )
246 checkForRequiredKey( itemRules[ key ], "icon", string.format( "item section [%s] missing '%%s' key", v ) )
247 checkForRequiredKey( itemRules[ key ], "f_color", string.format( "item section [%s] missing '%%s' key", v ) )
248 checkForRequiredKey( itemRules[ key ], "b_color", string.format( "item section [%s] missing '%%s' key", v ) )
249 checkForRequiredKey( itemRules[ key ], "class", string.format( "item section [%s] missing '%%s' key", v ) )
250 checkForRequiredKey( itemRules[ key ], "max_damage", string.format( "item section [%s] missing '%%s' key", v ) )
251
252 -- check that the color values are valid
253 if colors[ itemRules[ key ].f_color ] == nil then
254 errorAndDie( string.format( "item section [%s] has invalid f_color '%s'", v, itemRules[ key ].f_color ) )
255 end
256 if colors[ itemRules[ key ].b_color ] == nil then
257 errorAndDie( string.format( "item section [%s] has invalid b_color '%s'", v, itemRules[ key ].b_color ) )
258 end
259
260 -- check that the provided 'class' field is a valid class
261 if itemClasses[ itemRules[ key ].class ] == nil then
262 errorAndDie( string.format( "item section [%s] has invalid 'class' key", v ) )
263 end
264
265 -- replace the class key with a pointer to the class table instead of a string
266 itemRules[ key ].class = itemClasses[ itemRules[ key ].class ]
267
268 -- if there is a layout code for this item, add it to the list
269 if itemRules[ key ].layoutCode ~= nil then
270 layoutCodes[ itemRules[ key ].layoutCode ] = v:lower()
271 end
272 end
273
274 -- rules loaded successfully
275end
276
277-- loads a reactor layout from an INI file
278local function loadReactorLayout( file_name )
279 local layoutIni, result = ini.createTableFromINI( file_name )
280
281 if layoutIni == nil then
282 errorAndDie( result )
283 end
284
285 -- check that the Layout and Rules sections exist
286 checkForRequiredKey( layoutIni, "Layout", "[%%s] section missing" )
287 checkForRequiredKey( layoutIni, "Rules", "[%%s] section missing" )
288
289 -- create the new tables
290 reactorRules = {}
291 reactorRules.layout = {}
292
293 -- check that all required keys are preseint in [Rules]
294 checkForRequiredKey( layoutIni.Rules, "maxCoreHeat", "[Rules] section missing '%s' key" )
295 checkForRequiredKey( layoutIni.Rules, "minCoreHeat", "[Rules] section missing '%s' key" )
296 checkForRequiredKey( layoutIni.Rules, "maxCompHeat", "[Rules] section missing '%s' key" )
297 checkForRequiredKey( layoutIni.Rules, "minCompHeat", "[Rules] section missing '%s' key" )
298
299 -- copy the entries from the INI
300 for k, v in pairs( layoutIni.Rules ) do
301 reactorRules[ k ] = v
302 end
303
304 -- check that all required keys are present in [Layout] and begin loading
305 local slot = 1
306 reactorRules.fuelCount = 0
307 for i = 1, 6 do
308 checkForRequiredKey( layoutIni.Layout, i, "[Layout] section missing '%%d' key" )
309 local lineTable = text.tokenize( layoutIni.Layout[ i ] )
310 for j = 1, 9 do
311 -- check that this entry is present
312 if lineTable[ j ] == nil then
313 errorAndDie( string.format( "[Layout] property '%d' is wrong length", i ) )
314 end -- if lineTable[ i ] == nil then
315
316 -- check that the layout code is recognized
317 if layoutCodes[ lineTable[ j ] ] == nil then
318 -- if the code is "--" then it's the special code for empty slots
319 if lineTable[ j ] == "--" then
320 -- allow it
321 reactorRules.layout[ slot ] = "--"
322 else
323 errorAndDie( string.format( "[Layout] property '%d' has unrecognized code '%s'", i, lineTable[ j ] ) )
324 end -- if lineTable[ j ] == "--" then
325 else
326 reactorRules.layout[ slot ] = layoutCodes[ lineTable[ j ] ]
327 end -- if layoutCodes[ lineTable[ j ] ] == nil then
328
329 slot = slot + 1
330 end -- for j = 1, 9 do
331 end -- for i = 1, 6 do
332
333 -- layout loaded successfully
334end
335
336-- draws an item's icon in the specified slot, using the item's raw_name as the key
337local function drawItemCell( slot, raw_name )
338 local cellX = LAYOUT_GRID_X + 1 + ( ( slot - 1 ) % 9 ) * 4
339 local cellY = LAYOUT_GRID_Y + 1 + math.floor( ( slot - 1 ) / 9 ) * 3
340
341 -- handle unknown and special raw_names
342 if raw_name == "--" then
343 gpu.setForeground( colors.gray, true )
344 gpu.setBackground( colors.black, true )
345 -- write the icon
346 term.setCursor( cellX, cellY )
347 term.write( "..." )
348 term.setCursor( cellX, cellY + 1 )
349 term.write( "..." )
350 elseif itemRules[ raw_name ] == nil then
351 gpu.setForeground( colors.white, true )
352 gpu.setBackground( colors.red, true )
353 -- write the icon
354 term.setCursor( cellX, cellY )
355 term.write( "???" )
356 term.setCursor( cellX, cellY + 1 )
357 term.write( "???" )
358 else
359 -- set the colors for the icon, checking first if the colors are already correct (to try and reduce the number of ticks spent on this)
360 if gpu.getForeground() ~= colors[ itemRules[ raw_name ].f_color ] then
361 gpu.setForeground( colors[ itemRules[ raw_name ].f_color ], true )
362 end
363 if gpu.getBackground() ~= colors[ itemRules[ raw_name ].b_color ] then
364 gpu.setBackground( colors[ itemRules[ raw_name ].b_color ], true )
365 end
366
367 -- write the first line
368 term.setCursor( cellX, cellY )
369 term.write( itemRules[ raw_name ].icon:sub( 1, 3 ) )
370 -- write the 2nd line
371 term.setCursor( cellX, cellY + 1)
372 term.write( itemRules[ raw_name ].icon:sub( 4, 6 ) )
373 end
374end
375
376-- draws the reactor layout grid
377local function drawLayoutGrid()
378 gpu.setForeground( colors.black, true )
379 gpu.setBackground( colors.gray, true )
380 term.setCursor( LAYOUT_GRID_X, LAYOUT_GRID_Y )
381
382 -- print it line by line
383 local line = unicode.char( 0x2554 )
384 for i = 0, 7 do
385 line = line .. string.rep( unicode.char( 0x2550 ), 3 ) .. unicode.char( 0x2566 )
386 end
387 line = line .. string.rep( unicode.char( 0x2550 ), 3 ) .. unicode.char( 0x2557 ) .. "\n"
388 -- print the line
389 term.write( line )
390
391 -- generate the middle cells
392 line = ""
393 for i = 0, 1 do
394 line = line .. unicode.char( 0x2551 )
395 for j = 0, 8 do
396 line = line .. string.rep( " ", 3 ) .. unicode.char( 0x2551 )
397 end
398 line = line .. "\n"
399 end
400 line = line .. unicode.char( 0x2560 )
401 for i = 0, 7 do
402 line = line .. string.rep( unicode.char( 0x2550 ), 3 ) .. unicode.char( 0x256C )
403 end
404 line = line .. string.rep( unicode.char( 0x2550 ), 3 ) .. unicode.char( 0x2563 ) .. "\n"
405
406 -- print that a bunch
407 for i = 0, 4 do
408 term.write( line )
409 end
410
411 line = ""
412 for i = 0, 1 do
413 line = line .. unicode.char( 0x2551 )
414 for j = 0, 8 do
415 line = line .. string.rep( " ", 3 ) .. unicode.char( 0x2551 )
416 end
417 line = line .. "\n"
418 end
419 line = line .. unicode.char( 0x255A )
420 for i = 0, 7 do
421 line = line .. string.rep( unicode.char( 0x2550 ), 3 ) .. unicode.char( 0x2569 )
422 end
423 line = line .. string.rep( unicode.char( 0x2550 ), 3 ) .. unicode.char( 0x255D )
424
425 term.write( line )
426end
427
428-- draws the initial display
429local function initDisplay()
430 term.clear()
431
432 -- draw the header
433 drawFieldString( "Nuclear Reactor Manager", gpuWidth, ALIGN_CENTER )
434
435 -- draw the status fields
436 term.setCursor( 1, 3 )
437 term.write( "Status: \n" )
438 term.write( "Refuel In: \n" )
439
440 -- draw the layout grid
441 drawLayoutGrid()
442
443 -- draw the fluid level meters
444 gpu.setForeground( colors.white, true )
445 gpu.setBackground( colors.black, true )
446 -- draw the header
447 term.setCursor( TANK_METER_X, TANK_METER_Y )
448 drawFieldString( "Tanks", 9, ALIGN_CENTER )
449 -- draw the border
450 gpu.setForeground( colors.silver, true )
451 term.setCursor( TANK_METER_X, TANK_METER_Y + 1 )
452 local line = unicode.char( 0x250C ) .. string.rep( unicode.char( 0x2500 ), 3 ) .. unicode.char( 0x252C ) .. string.rep( unicode.char( 0x2500 ), 3 ) .. unicode.char( 0x2510 )
453 term.write( line )
454 line = unicode.char( 0x2502 )
455 for i = 0, 1 do
456 line = line .. string.rep( " ", 3 ) .. unicode.char( 0x2502 )
457 end
458 for i = 0, TANK_METER_H - 1 do
459 term.setCursor( TANK_METER_X, TANK_METER_Y + 2 + i )
460 term.write( line )
461 end
462 line = unicode.char( 0x2514 ) .. string.rep( unicode.char( 0x2500 ), 3 ) .. unicode.char( 0x2534 ) .. string.rep( unicode.char( 0x2500 ), 3 ) .. unicode.char( 0x2518 )
463 term.setCursor( TANK_METER_X, TANK_METER_Y + 2 + TANK_METER_H )
464 term.write( line )
465
466 for i = 0, TANK_METER_H - 1 do
467 local percent = 1 - ( i / TANK_METER_H )
468 local flip = false
469 if percent > 0.75 then
470 line = string.rep( unicode.char( 0x2588 ), 2 )
471 elseif percent > 0.5 then
472 line = string.rep( unicode.char( 0x2591 ), 2 )
473 flip = true
474 elseif percent > 0.25 then
475 line = string.rep( unicode.char( 0x2592 ), 2 )
476 else
477 line = string.rep( unicode.char( 0x2591 ), 2 )
478 end
479
480 if not flip then
481 gpu.setForeground( colors.lightblue, true )
482 gpu.setBackground( colors.black, true )
483 else
484 gpu.setForeground( colors.black, true )
485 gpu.setBackground( colors.lightblue, true )
486 end
487 term.setCursor( TANK_METER_X + 2, TANK_METER_Y + 2 + i )
488 term.write( line )
489
490 if not flip then
491 gpu.setForeground( colors.red, true )
492 else
493 gpu.setBackground( colors.red, true )
494 end
495 term.setCursor( TANK_METER_X + 5, TANK_METER_Y + 2 + i )
496 term.write( line )
497 end
498 -- draw the indicators at 0
499 term.setCursor( TANK_METER_X + 1, TANK_METER_Y + 2 + lastCoreHeatDisplayY )
500 gpu.setForeground( colors.white, true )
501 gpu.setBackground( colors.black, true )
502 term.write( unicode.char( 0x25BA ) )
503 term.setCursor( TANK_METER_X + 7, TANK_METER_Y + 2 + lastCoreHeatDisplayY )
504 term.write( unicode.char( 0x25C4 ) )
505
506 -- draw the core heat meter
507 gpu.setForeground( colors.white, true )
508 gpu.setBackground( colors.black, true )
509 -- draw the header
510 term.setCursor( HEAT_METER_X, HEAT_METER_Y )
511 drawFieldString( "Heat", 6, ALIGN_CENTER )
512 -- draw the border
513 gpu.setForeground( colors.silver, true )
514 term.setCursor( HEAT_METER_X, HEAT_METER_Y + 1 )
515 line = unicode.char( 0x250C ) .. string.rep( unicode.char( 0x2500 ), 4 ) .. unicode.char( 0x2510 )
516 term.write( line )
517 line = unicode.char( 0x2502 ) .. string.rep( " ", 4 ) .. unicode.char( 0x2502 )
518 for i = 0, HEAT_METER_H - 1 do
519 term.setCursor( HEAT_METER_X, HEAT_METER_Y + 2 + i )
520 term.write( line )
521 end
522 line = unicode.char( 0x2514 ) .. string.rep( unicode.char( 0x2500 ), 4 ) .. unicode.char( 0x2518 )
523 term.setCursor( HEAT_METER_X, HEAT_METER_Y + 2 + HEAT_METER_H )
524 term.write( line )
525
526 -- draw the colored portions of the heat meter
527 for i = 0, HEAT_METER_H - 1 do
528 local percent = 1 - ( i / HEAT_METER_H )
529 local f_color = colors.white
530 local b_color = colors.black
531
532 if percent > 0.9 then
533 line = string.rep( unicode.char( 0x2588 ), 2 )
534 f_color = colors.red
535 b_color = colors.red
536 elseif percent > 0.8 then
537 line = string.rep( unicode.char( 0x2592 ), 2 )
538 f_color = colors.red
539 b_color = colors.orange
540 elseif percent > 0.7 then
541 line = string.rep( unicode.char( 0x2588 ), 2 )
542 f_color = colors.orange
543 b_color = colors.orange
544 elseif percent > 0.6 then
545 line = string.rep( unicode.char( 0x2592 ), 2 )
546 f_color = colors.orange
547 b_color = colors.yellow
548 elseif percent > 0.5 then
549 line = string.rep( unicode.char( 0x2588 ), 2 )
550 f_color = colors.yellow
551 b_color = colors.yellow
552 elseif percent > 0.4 then
553 line = string.rep( unicode.char( 0x2592 ), 2 )
554 f_color = colors.yellow
555 b_color = colors.lime
556 elseif percent > 0.31 then
557 line = string.rep( unicode.char( 0x2588 ), 2 )
558 f_color = colors.lime
559 b_color = colors.lime
560 elseif percent > 0.2 then
561 line = string.rep( unicode.char( 0x2592 ), 2 )
562 f_color = colors.lime
563 b_color = colors.green
564 elseif percent > 0.1 then
565 line = string.rep( unicode.char( 0x2588 ), 2 )
566 f_color = colors.green
567 b_color = colors.green
568 else
569 line = string.rep( unicode.char( 0x2592 ), 2 )
570 f_color = colors.green
571 b_color = colors.black
572 end
573
574 gpu.setForeground( f_color, true )
575 gpu.setBackground( b_color, true )
576 term.setCursor( HEAT_METER_X + 2, HEAT_METER_Y + 2 + i )
577 term.write( line )
578 end
579
580 -- draw the indicators at 0
581 term.setCursor( HEAT_METER_X + 1, HEAT_METER_Y + 2 + lastCoreHeatDisplayY )
582 gpu.setForeground( colors.white, true )
583 gpu.setBackground( colors.black, true )
584 term.write( unicode.char( 0x25BA ) )
585 term.setCursor( HEAT_METER_X + 4, HEAT_METER_Y + 2 + lastCoreHeatDisplayY )
586 term.write( unicode.char( 0x25C4 ) )
587end
588
589-- returns a formatted string for a number of seconds
590local function getTimeString( seconds )
591 local s = ""
592
593 if seconds >= 3600 then
594 s = string.format( "%dh%02dm%02ds",
595 math.floor( seconds / 3600 ),
596 math.floor( ( seconds % 3600 ) / 60 ),
597 math.floor( seconds % 60 ) )
598 elseif seconds >= 60 then
599 s = string.format( "%dm%02ds",
600 math.floor( ( seconds % 3600 ) / 60 ),
601 math.floor( seconds % 60 ) )
602 else
603 s = string.format( "%ds", math.floor( seconds % 60 ) )
604 end
605
606
607 return s
608end
609
610-- coroutine that updates the display
611local function visualCoroutine()
612 --while not exitFlag do
613 -- get all the relevant info for the display
614 local reactorItems = reactorInv.getAllStacks()
615 local compHeatPercent = 0
616 local timeToNextCell = 40000 -- time (in seconds) until the next fuel cell is depleted
617 -- iterate through the items in the reactor and get their info
618 for slot, item in pairs( reactorItems ) do
619 reactorItems[ slot ] = reactorItems[ slot ].basic()
620 -- if the item can melt, figure out the hottest component
621 if itemRules[ reactorItems[ slot ].raw_name ].class.melts then
622 if reactorItems[ slot ].dmg / 10000 > compHeatPercent then
623 compHeatPercent = reactorItems[ slot ].dmg / 10000
624 end
625 end
626 -- if the item is a fuel item
627 if itemRules[ reactorItems[ slot ].raw_name ].class.fuel then
628 -- determine this item's time until expiry
629 timeToNextCell = math.min( timeToNextCell, itemRules[ reactorItems[ slot ].raw_name ].max_damage * ( 1 - ( reactorItems[ slot ].dmg / 10000 ) ) )
630 end
631 end -- for slot, item in pairs( reactorItems ) do
632 -- get the reactor's heat
633 local coreHeat = reactorRS.getHeat()
634 local maxHeat = reactorRS.getMaxHeat()
635 local tankInfo = reactorTank.getTankInfo( "UNKNOWN" )
636 if tankInfo[ 1 ].contents ~= nil then
637 coolantLevel = tankInfo[ 1 ].contents.amount
638 else
639 coolantLevel = 0
640 end
641 if tankInfo[ 2 ].contents ~= nil then
642 hotCoolantLevel = tankInfo[ 2 ].contents.amount
643 else
644 hotCoolantLevel = 0
645 end
646
647 gpu.setForeground( colors.white, true )
648 gpu.setBackground( colors.black, true )
649 -- write the state string
650 term.setCursor( 9, 3 )
651 drawFieldString( STATE_TEXT[ reactorStatus ], 20, ALIGN_LEFT )
652 -- write the remaining time, but only if running (otherwise show "N/A")
653 term.setCursor( 12, 4 )
654 if reactorStatus == STATE_RUNNING then
655 drawFieldString( getTimeString( timeToNextCell ), 20, ALIGN_LEFT )
656 else
657 drawFieldString( "N/A", 20, ALIGN_LEFT )
658 end
659 term.setCursor( 1, 5 )
660
661 -- determine where to draw the indicators
662 local heatPos = math.min( HEAT_METER_H - ( coreHeat / maxHeat ) * HEAT_METER_H, HEAT_METER_H - 1 )
663 local compPos = math.min( HEAT_METER_H - compHeatPercent * HEAT_METER_H, HEAT_METER_H - 1 )
664 local coolantPos = math.min( TANK_METER_H - ( coolantLevel / 10000 ) * TANK_METER_H, TANK_METER_H - 1 )
665 local hotCoolantPos = math.min( TANK_METER_H - ( hotCoolantLevel / 10000 ) * TANK_METER_H, TANK_METER_H - 1 )
666
667 if heatPos ~= lastCoreHeatDisplayY then
668 -- erase the old position and draw the new one
669 term.setCursor( HEAT_METER_X + 1, HEAT_METER_Y + lastCoreHeatDisplayY + 2 )
670 term.write( " " )
671 term.setCursor( HEAT_METER_X + 1, HEAT_METER_Y + heatPos + 2 )
672 term.write( unicode.char( 0x25BA ) )
673
674 lastCoreHeatDisplayY = heatPos
675 end -- if heatPos ~= lastCoreHeatDisplayY then
676 if compPos ~= lastCompHeatDisplayY then
677 -- erase the old position and draw the new one
678 term.setCursor( HEAT_METER_X + 4, HEAT_METER_Y + lastCompHeatDisplayY + 2 )
679 term.write( " " )
680 term.setCursor( HEAT_METER_X + 4, HEAT_METER_Y + compPos + 2 )
681 term.write( unicode.char( 0x25C4 ) )
682
683 lastCompHeatDisplayY = compPos
684 end
685 if coolantPos ~= lastCoolantDisplayY then
686 -- erase the old position and draw the new one
687 term.setCursor( TANK_METER_X + 1, TANK_METER_Y + lastCoolantDisplayY + 2 )
688 term.write( " " )
689 term.setCursor( TANK_METER_X + 1, TANK_METER_Y + coolantPos + 2 )
690 term.write( unicode.char( 0x25BA ) )
691
692 lastCoolantDisplayY = coolantPos
693 end -- if coolantPos ~= lastCoolantDisplayY then
694 if hotCoolantPos ~= lastHotCoolantDisplayY then
695 -- erase the old position and draw the new one
696 term.setCursor( TANK_METER_X + 7, TANK_METER_Y + lastHotCoolantDisplayY + 2 )
697 term.write( " " )
698 term.setCursor( TANK_METER_X + 7, TANK_METER_Y + hotCoolantPos + 2 )
699 term.write( unicode.char( 0x25C4 ) )
700
701 lastHotCoolantDisplayY = hotCoolantPos
702 end -- if hotCoolantPos ~= lastHotCoolantDisplayY then
703
704 -- draw all the cells
705 for i = 1, 54 do
706 if reactorItems[ i ] == nil then
707 drawItemCell( i, "--" )
708 else
709 drawItemCell( i, reactorItems[ i ].raw_name )
710 end
711 end -- for i = 1, 54 dd
712
713 -- yield
714 --coroutine.yield()
715 --end
716end
717
718-- common reactor checks (used by CHECKING and RUNNING states)
719-- returns whether the state changed
720local function doCommonChecks()
721 -- check if the contents of the reactor match its layout
722 local reactorItems = reactorInv.getAllStacks()
723 local layoutMatches = true
724 local compHeatPercent = 0
725
726 for slot, item in pairs( reactorItems ) do
727 reactorItems[ slot ] = item.basic()
728 -- if the item can melt, figure out the hottest component
729 if itemRules[ reactorItems[ slot ].raw_name ].class.melts then
730 if reactorItems[ slot ].dmg / 10000 > compHeatPercent then
731 compHeatPercent = reactorItems[ slot ].dmg / 10000
732 end
733 end
734 end
735
736 -- iterate through the layout, comparing it to the reactor's contents
737 for slot, item in ipairs( reactorRules.layout ) do
738 -- check for the special case '--'
739 if item == "--" then
740 -- check if the corresponding slot is nil
741 if reactorItems[ slot ] ~= nil then
742 layoutMatches = false
743 break
744 end -- if reactorItems[ slot ] ~= nil then
745 else
746 -- if there's no item in the reactor slot it doesn't match
747 if reactorItems[ slot ] == nil then
748 layoutMatches = false
749 break
750 end
751 -- check if the raw_names match
752 if reactorItems[ slot ].raw_name ~= reactorRules.layout[ slot ] then
753 layoutMatches = false
754 break
755 end -- if reactorItems[ slot ].raw_name ~= reactorLayout[ slot ] then
756 end -- if item == "--" then
757 end -- for slot, item in ipairs( reactorLayout ) do
758
759 if not layoutMatches then
760 -- reactor layout doesn't match, change to reloading state
761 reactorStatus = STATE_RELOADING
762 return true
763 end
764
765 -- check core temperature
766 if reactorRS.getHeat() / reactorRS.getMaxHeat() * 100 >= reactorRules.maxCoreHeat then
767 reactorStatus = STATE_COOLING_CORE
768 return true
769 end
770
771 -- check component temperature
772 if compHeatPercent * 100 >= reactorRules.maxCompHeat then
773 reactorStatus = STATE_COOLING_COMP
774 return true
775 end
776
777 -- check tank fluid levels
778 local tankInfo = reactorTank.getTankInfo( "UNKNOWN" )
779 -- if there's too little coolant or too much hot coolant, change states
780 for slot, tank in pairs( tankInfo ) do
781 if tank.contents ~= nil then
782 if tank.contents.name == "ic2coolant" and tank.contents.amount <= 2000 then
783 reactorStatus = STATE_WAIT_FLUID
784 return true
785 elseif tank.contents.name == "ic2hotcoolant" and tank.contents.amount >= 8000 then
786 reactorStatus = STATE_WAIT_FLUID
787 return true
788 end
789 end
790 end
791
792 -- if the reactor is not currently running, enter the running state
793 if reactorStatus ~= STATE_RUNNING then
794 reactorStatus = STATE_RUNNING
795 redstone.setBundledOutput( CABLE_SIDE, REACTOR_CONTROL_COLOR, 255 )
796 return true
797 end
798
799 return false
800end
801
802-- main logic worker coroutine
803-- NOTE: It should call coroutine.yield() periodically to ensure the two "threads" work peacefully
804local function workerCoroutine()
805 while not exitFlag do
806 -- act based on the current reactor state
807 if reactorStatus == STATE_CHECKING then
808 -- do the common checks
809 doCommonChecks()
810
811 elseif reactorStatus == STATE_RUNNING then
812 -- turn off the reactor if the state changed from the common checks
813 if doCommonChecks() then
814 redstone.setBundledOutput( CABLE_SIDE, REACTOR_CONTROL_COLOR, 0 )
815 end
816
817 elseif reactorStatus == STATE_COOLING_CORE then
818 -- check if the heat has fallen to the proper threshold
819 if reactorRS.getHeat() / reactorRS.getMaxHeat() * 100 <= reactorRules.minCoreHeat then
820 -- reactor has cooled enough
821 reactorStatus = STATE_CHECKING
822 end
823
824 elseif reactorStatus == STATE_COOLING_COMP then
825 -- check if the hottest comp has cooled enough
826 local reactorItems = reactorInv.getAllStacks()
827 local compHeatPercent = 0
828
829 for slot, item in pairs( reactorItems ) do
830 item = item.basic()
831 if itemRules[ item.raw_name ].class.melts then
832 if item.dmg / 10000 > compHeatPercent then
833 compHeatPercent = item.dmg / 10000
834 end
835 end
836 end
837
838 if compHeatPercent * 100 <= reactorRules.minCompHeat then
839 -- comps have cooled enough
840 reactorStatus = STATE_CHECKING
841 end
842
843 elseif reactorStatus == STATE_WAIT_FLUID then
844 local tankInfo = reactorTank.getTankInfo( "UNKNOWN" )
845 -- resume if there's enough coolant and not too much hot coolant
846 local cool, hot = 0
847 if tankInfo[ 1 ].contents ~= nil then cool = tankInfo[ 1 ].contents.amount end
848 if tankInfo[ 2 ].contents ~= nil then hot = tankInfo[ 2 ].contents.amount end
849 if cool >= 6000 and hot <= 2000 then
850 -- there's enough coolant
851 reactorStatus = STATE_CHECKING
852 end
853
854 elseif reactorStatus == STATE_RELOADING then
855 -- find a free slot in the buffer, ideally the highest free spot
856 local numSlots = chestBuffer.getInventorySize()
857 local bufferInv = chestBuffer.getAllStacks()
858 local freeSlot = 0
859 for i = 1, numSlots do
860 if bufferInv[ i ] == nil then
861 freeSlot = i
862 end
863 end
864 -- TODO: Handle there being no slots free
865
866 -- get the items in the reactor
867 local reactorItems = reactorInv.getAllStacks()
868 for slot, item in pairs( reactorItems ) do
869 reactorItems[ slot ] = item.basic()
870 end
871
872 -- pull out items that don't match, one by one
873 for slot, item in pairs( reactorItems ) do
874 -- compare the raw_name to the layout and pull it if it doesn't match
875
876 if item.raw_name ~= reactorRules.layout[ slot ] then
877 -- pull the item out
878 chestBuffer.pullItemIntoSlot( BUFFER_REACTOR_SIDE, slot, 1, freeSlot )
879 -- then push it into the ME Interface
880 chestBuffer.pushItem( BUFFER_TO_INTERFACE_SIDE, freeSlot )
881 -- erase the entry in the table
882 reactorItems[ slot ] = nil
883 -- yield between each item pulled
884 coroutine.yield()
885 end -- if item.raw_name ~= reactorRules.layout[ slot ] then
886 end -- for slot, item in pairs( reactorItems[ slot ] ) do
887
888 -- get all the items that are available in the ME Network
889 local networkInv = meInterface.getAvailableItems( "ALL" )
890 -- generate a table of fingerprints and quantities of items in the network, using item raw_names as keys
891 local networkItems = {}
892 for key, value in pairs( networkInv ) do
893 -- only proceed if the item is one we care about (in the reactor rules)
894 if itemRules[ value.item.raw_name ] ~= nil then
895 -- create an entry if it doesn't exist
896 if networkItems[ value.item.raw_name ] == nil then
897 networkItems[ value.item.raw_name ] = {}
898 end
899
900 local newTable = {}
901 newTable.fingerprint = value.fingerprint
902 newTable.qty = value.size
903 table.insert( networkItems[ value.item.raw_name ], newTable )
904 end -- if itemRules[ value.item.raw_name ] ~= nil then
905
906 end -- for key, value in pairs( networkInv ) do
907
908 -- iterate through all the empty slots in the reactor, pulling in the appropriate items
909 local itemsAreMissing = false
910
911 for slot = 1, 54 do
912 -- if the slot is empty and the layout doesn't call for it to be empty
913 if reactorItems[ slot ] == nil and reactorRules.layout[ slot ] ~= "--" then
914 -- check if there's an item available to put in
915 if networkItems[ reactorRules.layout[ slot ] ] ~= nil then
916 -- there is, so stick it in
917 -- TODO: Prefer the most damaged fuel items
918
919 -- use the first entry
920 local fingerprint = networkItems[ reactorRules.layout[ slot ] ][ 1 ].fingerprint
921 networkItems[ reactorRules.layout[ slot ] ][ 1 ].qty = networkItems[ reactorRules.layout[ slot ] ][ 1 ].qty - 1
922 if networkItems[ reactorRules.layout[ slot ] ][ 1 ].qty == 0 then
923 table.remove( networkItems[ reactorRules.layout[ slot ] ], 1 )
924 end
925
926 -- if the table is now empty, remove it from the list
927 if #networkItems[ reactorRules.layout[ slot ] ] == 0 then
928 networkItems[ reactorRules.layout[ slot ] ] = nil
929 end
930
931 -- use the fingerprint to grab the item, then push it into the reactor
932 meInterface.exportItem( fingerprint, INTERFACE_TO_BUFFER_SIDE, 1, freeSlot )
933 chestBuffer.pushItemIntoSlot( BUFFER_REACTOR_SIDE, freeSlot, 1, slot )
934
935 -- yield between each item inserted
936 coroutine.yield()
937 else
938 -- there are no instances of this item, increment the counter of missing items
939 itemsAreMissing = true
940 if missingItems[ reactorRules.layout[ slot ] ] == nil then
941 missingItems[ reactorRules.layout[ slot ] ] = 1
942 else
943 missingItems[ reactorRules.layout[ slot ] ] = missingItems[ reactorRules.layout[ slot ] ] + 1
944 end
945 end
946 end -- if reactorItems[ slot ] == nil and reactorRules.layout[ slot ] ~= "--" then
947 end -- for slot = 1, 54 do
948
949 -- if items were missing, change to the appropriate state
950 if itemsAreMissing then
951 reactorStatus = STATE_WAIT_ITEM
952 else
953 -- no items were missing, so return to the Checking state
954 reactorStatus = STATE_CHECKING
955 end
956
957 elseif reactorStatus == STATE_WAIT_ITEM then
958 -- check if any of the missing items are now present in the ME Network
959 local itemsFound = false
960 local networkInv = meInterface.getAvailableItems( "ALL" )
961
962 -- iterate through all the items to see if any of them are one of the missing ones
963 local networkItems = {}
964 for key, value in pairs( networkInv ) do
965 if missingItems[ value.item.raw_name ] ~= nil then
966 -- we found one
967 itemsFound = true
968 break
969 end
970 end -- for key, value in pairs( networkInv ) do
971
972 -- if any of the missing items were found, return to the reloading state
973 if itemsFound then
974 reactorStatus = STATE_RELOADING
975 end
976
977 end -- if reactorStatus == STATE_CHECKING then
978
979 -- yield between each state check/transition
980 coroutine.yield()
981 end
982end
983
984-- timer event for the main UI thread
985local function visualTimerEvent()
986 if not exitFlag then
987 --local ok, message = coroutine.resume( visualThread )
988 --
989 --if not ok then
990 -- exitFlag = true
991 -- term.clear()
992 -- errorAndDie( string.format( "visual coroutine error: %s\n", message ) )
993 --end
994
995 local ok, message = pcall( visualCoroutine )
996
997 if not ok then
998 exitFlag = true
999 term.clear()
1000 errorAndDie( string.format( "visual function error: %s\n", message ) )
1001 end
1002 end
1003end
1004
1005local function workerTimerEvent()
1006 if not exitFlag then
1007 local ok, message = coroutine.resume( workerThread )
1008
1009 if not ok then
1010 exitFlag = true
1011 term.clear()
1012 errorAndDie( string.format( "worker coroutine error: %s\n", message ) )
1013 end
1014 end
1015end
1016
1017-- define handle_key_down
1018handle_key_down = function ( name, keyboardAddress, char, code, playerName )
1019 -- die if the user pressed Space
1020 if code == keyboard.keys.space then
1021 exitFlag = true
1022 end
1023end
1024
1025--------------------------------------------------------------------------------
1026-- MAIN ENTRY POINT --
1027--------------------------------------------------------------------------------
1028
1029-- check system components
1030term.write( "Checking System Components... " )
1031checkSystemComponents()
1032term.write( "OK\n" )
1033-- load the reactor rules
1034term.write( "Loading Reactor Rules... " )
1035loadReactorRules()
1036term.write( "OK\n" )
1037-- load the reactor layout
1038term.write( "Loading Reactor Layout... ")
1039loadReactorLayout( "layout1.ini" )
1040term.write( "OK\n" )
1041
1042-- init the display
1043initDisplay()
1044
1045-- listen for key_down events
1046event.listen( "key_down", handle_key_down )
1047-- create the visual thread and begin its execution
1048visualThread = coroutine.create( visualCoroutine )
1049coroutine.resume( visualThread )
1050-- create the worker thread and begin its execution
1051workerThread = coroutine.create( workerCoroutine )
1052coroutine.resume( workerThread )
1053-- create the timer events and register the functions
1054visualTimerID = event.timer( UPDATE_INTERVAL, visualTimerEvent, math.huge )
1055workerTimerID = event.timer( WORK_INTERVAL, workerTimerEvent, math.huge )
1056
1057-- enter the main loop
1058repeat
1059 event.pull()
1060until exitFlag == true
1061
1062-- finalize
1063finalize()
1064
1065term.clear()
1066--term.setCursor( 1, 1 )