starlit  Check-in [3df08bd5ac]

Overview
Comment:complete (-ish) matter compiler UI (power drain still missing), add printable chemical light
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | trunk
Files: files | file ages | folders
SHA3-256: 3df08bd5acd80fe29755ad29b44b1aa43f2f26154b1dc8ba2785d6ef944b8392
User & Date: lexi on 2024-05-06 16:20:04
Other Links: manifest | tags
Context
2024-05-06
20:58
add storage crate & generic interface for, add LED for print completion, add program tooltips, disfuckulate some longstanding idiot bugs check-in: 4b3aa092f8 user: lexi tags: trunk
16:20
complete (-ish) matter compiler UI (power drain still missing), add printable chemical light check-in: 3df08bd5ac user: lexi tags: trunk
2024-05-05
19:31
better alarm LEDs, continue work on matter compiler UI, hack around gravitational horrorscape (i.e. stop shitting all over the server's `minetest.conf`), better stat interface, tweak some compute stats, be more generous with starting battery loadout, mercilessly squash numberless bugs beneath my jackbooted heel check-in: 953151446f user: lexi tags: trunk
Changes

Modified mods/starlit-electronics/init.lua from [d82ba893dd] to [6d0114ddbf].

   564    564   				'blob'; -- opaque binary blob, so 3d-pty mods can use the
   565    565                       -- file mechanism to store arbirary data.
   566    566   			};
   567    567   			drm = T.u8; -- inhibit copying
   568    568   			name = T.str;
   569    569   			body = T.text;
   570    570   		}, function(file) -- enc
          571  +			assert(E.chip.file[file.kind], string.format('invalid file kind "%s"', file.kind))
   571    572   			local b = E.chip.file[file.kind].enc(file.body)
   572    573   			return {
   573    574   				kind = file.kind;
   574    575   				drm = file.drm;
   575    576   				name = file.name;
   576    577   				body = b;
   577    578   			}
................................................................................
   872    873   	end;
   873    874   	__index = {
   874    875   		read = function(self)
   875    876   			local dat = E.chip.read(self.chip)
   876    877   			return dat.files[self.inode]
   877    878   		end;
   878    879   		write = function(self,data)
   879         -			-- print('writing', self.chip, self.inode)
   880    880   			return E.chip.fileWrite(self.chip, self.inode, data)
   881    881   		end;
   882    882   		erase = function(self)
   883    883   			local dat = E.chip.read(self.chip)
   884    884   			table.remove(dat.files, self.inode)
   885    885   			E.chip.write(self.chip, dat)
   886    886   			self.inode = nil
................................................................................
   909    909   			then
   910    910   				for fl, inode in E.chip.files(e) do
   911    911   					if fl.kind == 'sw' then
   912    912   						local s = starlit.item.sw.db[fl.body.pgmId]
   913    913   						table.insert(sw, {
   914    914   							sw = s, chip = e, chipSlot = i;
   915    915   							file = fl, inode = inode;
          916  +							id = fl.body.pgmId;
   916    917   						})
   917    918   					end
   918    919   				end
   919    920   			end
   920    921   		end
   921    922   	end
   922    923   
   923    924   	for _, s in pairs(sw) do
   924    925   		if s.sw.cost.ram <= comp.ram and pred(s) then
   925    926   			table.insert(r, {
          927  +				id = s.id;
   926    928   				sw = s.sw;
   927    929   				chip = s.chip, chipSlot = s.chipSlot;
   928    930   				file = s.file;
   929    931   				fd = E.chip.fileHandle(s.chip, s.inode);
   930    932   				speed = s.sw.cost.cycles / comp.cycles;
   931    933   				powerCost = s.sw.cost.cycles / comp.powerEfficiency;
   932    934   				comp = comp;

Modified mods/starlit-electronics/sw.lua from [da2672ba3c] to [1f9bdf2a9e].

   133    133   	cost = {
   134    134   		cycles = 100e6;
   135    135   		ram = 500e6;
   136    136   	};
   137    137   	run = shredder{range=3, powerDraw=200};
   138    138   })
   139    139   
   140         -starlit.item.sw.link('starlit_electronics:compile_commune', {
          140  +local function matterCompiler(desc)
          141  +	return {
          142  +		name = desc.name;
          143  +		kind = 'suitPower', powerKind = 'direct';
          144  +		desc = desc.desc;
          145  +		size = desc.size;
          146  +		cost = desc.cost;
          147  +		ui = 'starlit:compile-matter-component';
          148  +		bgProc = function(user, ctx, interval, runState)
          149  +			if runState.flags.compiled == true then return false end
          150  +			-- only so many nanides to go around
          151  +			runState.flags.compiled = true
          152  +
          153  +			local conf = ctx.file.body.conf
          154  +			local job_t = starlit.store.compilerJob
          155  +			local job, jobSlot
          156  +			for i, v in ipairs(conf) do
          157  +				if v.key == 'job' then
          158  +					job = job_t.dec(v.value)
          159  +					jobSlot = i
          160  +					goto found
          161  +				end
          162  +			end
          163  +
          164  +			::notfound:: do
          165  +				return
          166  +			end
          167  +
          168  +			::found::
          169  +			local scm = starlit.item.sw.db[job.schematic]
          170  +			job.cyclesLeft = math.max(0, job.cyclesLeft - ctx.comp.cycles)
          171  +			if job.cyclesLeft == 0 then
          172  +				job.timeLeft = math.max(0, job.timeLeft - interval)
          173  +			end
          174  +			if job.timeLeft == 0 and job.cyclesLeft == 0 then
          175  +				table.remove(conf, jobSlot)
          176  +				user:give(scm.output)
          177  +			else
          178  +				conf[jobSlot].value = job_t.enc(job)
          179  +			end
          180  +
          181  +			ctx.saveConf()
          182  +		end;
          183  +	}
          184  +end
          185  +
          186  +starlit.item.sw.link('starlit_electronics:compile_commune', matterCompiler {
   141    187   	name = 'Compile Matter';
   142         -	kind = 'suitPower', powerKind = 'direct';
   143    188   	desc = "A basic suit matter compiler program. It's rather slow, but it's been ruthlessly optimized for size- and memory-efficiency by some of the Commune's most fanatic coders, to the point where every Commune nanosuit can come with the program preinstalled.";
   144    189   	size = 700e3;
   145    190   	cost = {
   146    191   		cycles = 4e9;
   147    192   		ram = .3e9;
   148    193   	};
   149         -	ui = 'starlit:compile-matter-component';
   150         -	run = function(user, ctx)
   151         -	end;
   152    194   })
   153    195   
   154    196   starlit.item.sw.link('starlit_electronics:compile_block_commune', {
   155    197   	name = 'Compile Block';
   156    198   	kind = 'suitPower', powerKind = 'active';
   157    199   	desc = "An advanced suit matter compiler program, capable of printing complete devices and structure parts directly into the world.";
   158    200   	size = 5e6;
................................................................................
   161    203   		ram = 1e9;
   162    204   	};
   163    205   	ui = 'starlit:compile-matter-block';
   164    206   	run = function(user, ctx)
   165    207   	end;
   166    208   })
   167    209   
   168         -starlit.item.sw.link('starlit_electronics:compile_imperial', {
          210  +starlit.item.sw.link('starlit_electronics:compile_imperial', matterCompiler {
   169    211   	name = 'Genesis Deluxe';
   170         -	kind = 'suitPower', powerKind = 'direct';
   171    212   	desc = "House Bascundir has long dominated the matter compiler market in the Crystal Sea. Their firmware is excessively complex due to mountains of specialized edge-case handling, but the end result is certainly speedier than the competitors'.";
   172    213   	size = 2e4;
   173    214   	cost = {
   174    215   		cycles = 100e6;
   175    216   		ram = 1.5e9;
   176    217   	};
   177         -	ui = 'starlit:compile-matter-component';
   178         -	run = function(user, ctx)
   179         -	end;
   180    218   })
   181    219   
   182    220   do local J = starlit.store.compilerJob
   183    221   	starlit.item.sw.link('starlit_electronics:driver_compiler_commune', {
   184    222   		name = 'Matter Compiler';
   185    223   		kind = 'driver';
   186    224   		desc = "A driver for a standalone matter compiler, suitable for building larger components than your suit alone can handle.";
................................................................................
   188    226   		cost = {
   189    227   			cycles = 400e6;
   190    228   			ram = .2e9;
   191    229   		};
   192    230   		ui = 'starlit:device-compile-matter-component';
   193    231   		run = function(user, ctx)
   194    232   		end;
          233  +		--[[
   195    234   		bgProc = function(user, ctx, interval, runState)
   196    235   			if runState.flags.compiled == true then return false end
   197    236   			-- only so many nanides to go around
   198    237   			runState.flags.compiled = true
   199    238   			local time = minetest.get_gametime()
   200    239   			local cyclesLeft = ctx.comp.cycles * interval
   201    240   
................................................................................
   232    271   					else
   233    272   						e.value = J.enc(t)
   234    273   					end
   235    274   					if not cyclesLeft > 0 then break end
   236    275   				end
   237    276   			end
   238    277   			ctx.saveConf()
   239         -		end;
          278  +		end;]]
   240    279   	})
   241    280   end
   242    281   
   243    282   local function pasv_heal(effect, energy, lvl, pgmId)
   244    283   	return function(user, ctx, interval, runState)
   245    284   		if runState.flags.healed == true then return false end
   246    285   		-- competing nanosurgical programs?? VERY bad idea

Modified mods/starlit-material/elements.lua from [fb5dcf0ef1] to [81b3a1522a].

    40     40   		color = lib.color(1,.8,0.1);
    41     41   	};
    42     42   	calcium = {
    43     43   		name = 'calcium', sym = 'Ca', n = 20; density = 1.55;
    44     44   		metal = true;
    45     45   		color = lib.color(1,1,0.7);
    46     46   	};
           47  +	magnesium = {
           48  +		name = 'magnesium', sym = 'Mg', n = 12, density = 1.738;
           49  +		metal = true;
           50  +		color = lib.color(0.7, 0.7, 0.7);
           51  +	};
    47     52   	aluminum = {
    48     53   		name = 'aluminum', sym = 'Al', n = 13;  density = 2.7;
    49     54   		metal = true;
    50         -		color = lib.color(0.9,.95,1);
           55  +		color = lib.color(0.5,.55,.6);
    51     56   	};
    52     57   	iron = {
    53     58   		name = 'iron', sym = 'Fe', n = 26;  density = 7.874;
    54     59   		metal = true;
    55     60   		color = lib.color(.3,.3,.3);
    56     61   	};
    57     62   	copper = {

Modified mods/starlit-scenario/init.lua from [a805932fc9] to [98f93d94ac].

    42     42   		{'starlit_electronics:shred', 0};
    43     43   		--{'starlit_electronics:compile_empire', 0};
    44     44   		{'starlit_electronics:autodoc_deluxe', 1};
    45     45   		--{'starlit_electronics:driver_compiler_empire', 0};
    46     46   	});
    47     47   	survivalware = makeChip('Emergency Survivalware', {
    48     48   		{'starlit_electronics:battery_chemical_commune_small', 0};
           49  +		{'starlit_tech:chem_lamp', 0};
    49     50   	}, {
    50     51   		{'starlit_electronics:shred', 0};
    51     52   		{'starlit_electronics:compile_commune', 0};
    52     53   		{'starlit_electronics:nanomed', 0};
    53     54   		{'starlit_electronics:driver_compiler_commune', 0};
    54     55   	});
    55     56   	misfortune = makeChip("Sold1er0fMisf0rtune TOP Schematic Crackz REPACK", {

Added mods/starlit-tech/init.lua version [2a09a8ef6e].

            1  +local lib = starlit.mod.lib
            2  +
            3  +
            4  +do -- chemlamp
            5  +	local burnTime = 60*60
            6  +	local maxBright = 12
            7  +	local stages = maxBright
            8  +	local stageTimeout = burnTime / stages
            9  +	local function chemLampID(n)
           10  +		if n == stages then return 'starlit_tech:chem_lamp' end
           11  +		return string.format('starlit_tech:chem_lamp_%s',n)
           12  +	end
           13  +	local fab = starlit.type.fab {
           14  +		element = { carbon = 8, magnesium = 2 };
           15  +		cost = { power = 100 };
           16  +		flag = { print = true };
           17  +		time = { print = 5 };
           18  +		reverseEngineer = {
           19  +			complexity = 1;
           20  +			sw = 'starlit_tech:schematic_chem_lamp';
           21  +		};
           22  +	};
           23  +	for i = stages, 0, -1 do
           24  +		minetest.register_node(chemLampID(i), {
           25  +			short_description = 'Chem Lamp';
           26  +			description = starlit.ui.tooltip {
           27  +				title = 'Chem Lamp';
           28  +				desc = "A simple carbon-frame chemical light source powered by ambient oxygen. Cheap, quick to print, and biodedragable, without any need for an electric grid or complex power storage mechanism. However, the light only lasts a few days, after which the lamp must be recycled or discarded.";
           29  +				color = lib.color(1,.4,.1);
           30  +				props = {
           31  +					{title = 'Burn Remaining', desc=lib.math.timespec(stageTimeout * i), affinity=i > 4 and 'good' or 'bad'};
           32  +					{title = 'Mass', desc='10g', affinity='info'};
           33  +				};
           34  +			};
           35  +			drawtype = 'nodebox';
           36  +			groups = {
           37  +				object = 2;
           38  +				attached_node = 1;
           39  +			};
           40  +			node_box = {
           41  +				type = 'fixed';
           42  +				fixed = {
           43  +					-.4, -.5, -.20;
           44  +					 .4, -.3,  .20;
           45  +				};
           46  +			};
           47  +			tiles = {
           48  +				lib.image 'starlit-tech-lamp-glow.png'
           49  +					:fade(1 - i/stages)
           50  +					:blit(lib.image 'starlit-tech-lamp.png')
           51  +				:render();
           52  +			};
           53  +			paramtype = 'light';
           54  +			paramtype2 = 'wallmounted';
           55  +			wallmounted_rotate_vertical = true;
           56  +			light_source = math.floor(lib.math.lerp(i/stages, 0, maxBright));
           57  +			on_construct = i ~= 0 and function(pos)
           58  +				local t = minetest.get_node_timer(pos)
           59  +				t:start(stageTimeout)
           60  +			end or nil;
           61  +			on_timer = i ~= 0 and function(pos)
           62  +				local me = minetest.get_node(pos)
           63  +				minetest.swap_node(pos, {name=chemLampID(i-1), param2=me.param2})
           64  +				return i > 1
           65  +			end or nil;
           66  +			_starlit = {
           67  +				mass = 10;
           68  +				fab = fab;
           69  +				recover = starlit.type.fab {
           70  +					element = {
           71  +						carbon = 8;
           72  +						magnesium = math.floor(lib.math.lerp(i/stages, 0, 2));
           73  +					};
           74  +					time = {
           75  +						shred = .5;
           76  +						shredPower = 2;
           77  +					};
           78  +				};
           79  +
           80  +			};
           81  +		})
           82  +	end
           83  +	starlit.item.sw.link('starlit_tech:schematic_chem_lamp', {
           84  +		name = 'Chem Lamp Schematic';
           85  +		kind = 'schematic';
           86  +		input = fab;
           87  +		output = chemLampID(stages);
           88  +		size = 32e6;
           89  +		cost = {
           90  +			cycles = 8e9;
           91  +			ram = 16e6;
           92  +		};
           93  +		rarity = 1;
           94  +	})
           95  +end

Added mods/starlit-tech/mod.conf version [4aaf988ed1].

            1  +name = starlit_tech
            2  +title = starlit tech
            3  +depends = vtlib, starlit, starlit_material, starlit_electronics

Added mods/starlit/compile.lua version [8a9b22510e].

            1  +local lib = starlit.mod.lib
            2  +
            3  +local recentJobs do
            4  +	local T,G = lib.marshal.t, lib.marshal.g
            5  +	recentJobs = G.array(8, T.str)
            6  +end
            7  +local function getRecentJobs(state)
            8  +	for i,v in ipairs(state.pgm.file.body.conf) do
            9  +		if v.key == 'recent' then
           10  +			return recentJobs.dec(v.value), i
           11  +		end
           12  +	end
           13  +	return {}
           14  +end
           15  +
           16  +local function buyPrintJob(state, invs, cost, scm)
           17  +	-- consume consumables
           18  +	-- FIXME handle cost.leftovers from partially-consumed items
           19  +	for _, v in pairs(cost.consume) do
           20  +-- 		assert(v.inv == 1, 'impossible condition (wrong inventory ID)')
           21  +		local i = invs[v.inv]
           22  +		i.hnd:set_stack(i.list, v.slot, v.remain)
           23  +	end
           24  +	local output = ItemStack(scm.sw.output):get_definition()
           25  +	local fab = scm.sw.input
           26  +
           27  +	local job_t = starlit.store.compilerJob
           28  +	local conf = state.pgm.file.body.conf
           29  +	table.insert(conf, {
           30  +		key='job', value=job_t.enc {
           31  +			schematic  = scm.id;
           32  +			cyclesLeft = scm.sw.cost and scm.sw.cost.cycles or 0;
           33  +			timeLeft   = (fab.time and fab.time.print or 0);
           34  +		}
           35  +	})
           36  +end
           37  +
           38  +local function compilerCanPrint(user, cpl, scm)
           39  +	local E = starlit.mod.electronics
           40  +	local output = ItemStack(scm.sw.output):get_definition()
           41  +	local fab = scm.sw.input
           42  +	local sw = scm.sw
           43  +	local ok, consume, unsat, leftover, itemSpec = fab:seek {
           44  +		user.entity:get_inventory():get_list 'main';
           45  +	}
           46  +
           47  +	local cost = {
           48  +		fab = fab, output = output;
           49  +		consume = consume, unsat = unsat, leftover = leftover, itemSpec = itemSpec;
           50  +		runtimeEstimate = scm.speed + cpl.speed + (fab.time and fab.time.print or 0);
           51  +		power = cpl.powerCost + scm.powerCost;
           52  +		ram = (cpl.cost and cpl.cost.ram or 0)
           53  +		    + (scm.cost and scm.cost.ram or 0);
           54  +		cycles = (cpl.cost and cpl.cost.cycles or 0)
           55  +		       + (scm.cost and scm.cost.cycles or 0);
           56  +	}
           57  +
           58  +	local userComp = E.chip.sumCompute(
           59  +		user.entity:get_inventory():get_list 'starlit_suit_chips'
           60  +	)
           61  +
           62  +	if ok and cost.power <= user:suitCharge() and cost.ram <= userComp.ram then
           63  +		return true, cost
           64  +	else return false, cost end
           65  +end
           66  +
           67  +starlit.alg.compilerCanPrint = compilerCanPrint
           68  +
           69  +-- TODO destroy suit interfaces when power runs out or suit/chip is otherwise disabled
           70  +starlit.interface.install(starlit.type.ui {
           71  +	id = 'starlit:compile-matter-component';
           72  +	sub = {
           73  +		suit = function(state, user, evt)
           74  +			if evt.kind == 'disrobe' then state:close()
           75  +			elseif evt.kind == 'power' and evt.mode == 'off' then state:close() end
           76  +		end;
           77  +		playerInventory = function(state,user)
           78  +			-- refresh
           79  +		end;
           80  +	};
           81  +	pages = {
           82  +		index = {
           83  +			setupState = function(state, user, ctx)
           84  +				local E = starlit.mod.electronics
           85  +				state.pgm = ctx.program
           86  +				state.ctx = ctx
           87  +				state.select = {}
           88  +				if ctx.context == 'suit' then
           89  +					state.fetch = function()
           90  +						local cst = user.entity:get_inventory():get_list 'starlit_suit_chips'
           91  +						local cl = {order={}, map={}, slot={}}
           92  +						for i, c in ipairs(cst) do
           93  +							if not c:is_empty() then
           94  +								local d = E.chip.read(c)
           95  +								local co = {
           96  +									stack = c;
           97  +									data = d;
           98  +								}
           99  +								table.insert(cl.order, co)
          100  +								cl.map[d.uuid] = co
          101  +								cl.slot[i] = co
          102  +							end
          103  +						end
          104  +
          105  +						-- kill me fam
          106  +						if (   state.select.chip
          107  +							and state.select.chip ~= true
          108  +							and not cl.map[state.select.chip])
          109  +						or (state.select.scm
          110  +						   and not state.select.scms[state.select.scm])
          111  +					   then
          112  +							-- chip or pgm no longer available
          113  +							user:suitSound 'starlit-error'
          114  +							state.select = {}
          115  +						end
          116  +						state.select.chips = cl
          117  +
          118  +						state.select.scms = {}
          119  +						if state.select.chip then
          120  +							state.select.scms = E.chip.usableSoftware(cst,nil, function(s)
          121  +								if state.select.chip ~= true then
          122  +									if cl.slot[s.chipSlot].data.uuid ~= state.select.chip then
          123  +										return false
          124  +									end
          125  +								end
          126  +								return s.sw.kind == 'schematic'
          127  +							end)
          128  +						end
          129  +
          130  +					end
          131  +				end
          132  +			end;
          133  +
          134  +			onClose = function(state, user)
          135  +				user:suitSound 'starlit-quit'
          136  +			end;
          137  +			refresh = true;
          138  +			handle = function(state, user, q)
          139  +				local E = starlit.mod.electronics
          140  +				if not state.ctx.verify() then state.close() end
          141  +				local inv = user.entity:get_inventory()
          142  +
          143  +				local sel = state.select
          144  +				state.fetch()
          145  +				local chips = state.select.chips
          146  +				local function chirp()
          147  +					user:suitSound 'starlit-nav'
          148  +				end
          149  +
          150  +				-- are we signalling a reprint?
          151  +				for k in next, q do
          152  +					local idx = k:match'^recent_(%d+)$'
          153  +					if idx then
          154  +						local jobs = getRecentJobs(state)
          155  +						local scmid = jobs[tonumber(idx)]
          156  +						if not scmid then return false end
          157  +						-- get list of available schematics and make sure this is on it
          158  +						local cst = inv:get_list 'starlit_suit_chips'
          159  +						local all = E.chip.usableSoftware(cst, nil, function(s)
          160  +							return s.sw.kind == 'schematic'
          161  +						end)
          162  +						local scm
          163  +						for i, s in ipairs(all) do
          164  +							if s.id == scmid then scm = s goto found end
          165  +						end
          166  +
          167  +						::fail:: do
          168  +							user:suitSound 'starlit-error'
          169  +							return false
          170  +						end
          171  +
          172  +						::found::
          173  +						local ok,cost = compilerCanPrint(user, state.pgm, scm)
          174  +						if not ok then goto fail end
          175  +						user:suitSound 'starlit-configure'
          176  +						buyPrintJob(state, {{hnd=inv,list='main'}}, cost, scm)
          177  +						state.ctx.saveConf()
          178  +						return true
          179  +					end
          180  +				end
          181  +
          182  +				local function trySelection(id)
          183  +					if sel[id] == nil then
          184  +						for k in next, q do
          185  +							local pat = "^"..id.."_(%d+)$" -- ew
          186  +							local idx = k:match(pat)
          187  +							if idx then
          188  +								local cm = tonumber(idx)
          189  +								if cm then
          190  +									chirp()
          191  +									sel[id] = cm
          192  +									return true
          193  +								end
          194  +							end
          195  +						end
          196  +					end
          197  +				end
          198  +
          199  +				if sel.chip == nil then
          200  +					if q.showAll then
          201  +						chirp()
          202  +						sel.chip = true
          203  +						return true
          204  +					elseif q.find then
          205  +						chirp()
          206  +						-- TODO
          207  +						return true
          208  +					end
          209  +				end
          210  +
          211  +				if trySelection('chip') then
          212  +					return true
          213  +				elseif trySelection('scm') then
          214  +					return true
          215  +				else
          216  +					if q.back then
          217  +						chirp()
          218  +						if sel.input then
          219  +							sel.input = nil
          220  +						elseif sel.scm then
          221  +							sel.scm = nil
          222  +						elseif sel.chip then
          223  +							sel.chip = nil
          224  +						end
          225  +						return true
          226  +					elseif q.commit then
          227  +						if not sel.input then
          228  +							chirp()
          229  +							sel.input = true
          230  +							return true
          231  +						else
          232  +							local scm = sel.scms[sel.scm]
          233  +							local ok, cost = compilerCanPrint(user, state.pgm, scm)
          234  +							if ok then
          235  +								user:suitSound 'starlit-configure'
          236  +
          237  +								buyPrintJob(state, {{hnd=inv,list='main'}}, cost, scm)
          238  +
          239  +								-- add recent print
          240  +								local rctList, pairIdx = getRecentJobs(state)
          241  +								if #rctList >= 30 then
          242  +									table.remove(rctList)
          243  +								end
          244  +
          245  +								if not lib.tbl.has(rctList, scm.id) then
          246  +									table.insert(rctList,1,scm.id)
          247  +								end
          248  +
          249  +								local conf = state.pgm.file.body.conf
          250  +								local encoded = recentJobs.enc(rctList)
          251  +								if pairIdx then
          252  +									conf[pairIdx].value = encoded
          253  +								else
          254  +									table.insert(conf, {key='recent', value=encoded})
          255  +								end
          256  +
          257  +								-- write chip
          258  +								state.ctx.saveConf()
          259  +								-- clear selection
          260  +								state.select = {}
          261  +								return true
          262  +							else
          263  +								user:suitSound 'starlit-error'
          264  +							end
          265  +						end
          266  +					end
          267  +				end
          268  +
          269  +			end;
          270  +
          271  +			render = function(state, user)
          272  +				local sel, pgmSelector = state.select, {}
          273  +				state.fetch()
          274  +				state.ctx.pullConf()
          275  +
          276  +				local function pushSelector(id, item, label, desc, req)
          277  +					local rh = .5
          278  +					local label = {kind = 'text', w = 10-1.5, h=1.5;
          279  +							text = '<global valign=middle>'..lib.str.htsan(label) }
          280  +					if req then
          281  +						label.h = label.h - rh - .2
          282  +
          283  +						local imgs = {}
          284  +						for ci,c in ipairs(req) do
          285  +							for ei, e in ipairs(c.list) do
          286  +								table.insert(imgs, {kind = 'img', w=rh, h=rh,  img=e.img})
          287  +							end
          288  +						end
          289  +						label = {kind = 'vert', w = 10-1.5, h=1.5;
          290  +							label;
          291  +							{kind ='hztl', w=10-1.5, h=rh; unpack(imgs); }
          292  +						}
          293  +					end
          294  +					table.insert(pgmSelector, {kind = 'hztl', w=10,h=1.5;
          295  +						{kind = 'contact', id=id, w=1.5, h=1.5;
          296  +							item = item;
          297  +							color = {hue=220, sat=0, lum=0};
          298  +							desc = desc;
          299  +						};
          300  +						label;
          301  +					})
          302  +				end
          303  +
          304  +				local back = {kind = 'button', id='back', label = '<- Back', w=10,h=1.2}
          305  +				if sel.chips == nil then
          306  +					table.insert(pgmSelector, {kind = 'img', img = 'starlit-ui-alert.png', w=2, h=2})
          307  +				elseif sel.chip == nil then
          308  +					for i, c in ipairs(sel.chips.order) do
          309  +					-- TODO filter out chips without schematics?
          310  +						pushSelector('chip_' .. c.data.uuid, c.stack, c.data.label)
          311  +					end
          312  +					if next(sel.chips.order) then
          313  +						table.insert(pgmSelector, {kind = 'hztl', w=10,h=1.5;
          314  +							{kind = 'button', w=5,h=1.5; id='showAll', label='Show All'};
          315  +							{kind = 'button', w=5,h=1.5; id='find', label='Find'};
          316  +						})
          317  +					end
          318  +				else
          319  +					if sel.scm == nil then
          320  +						for idx, ent in ipairs(sel.scms) do
          321  +							local fab = ItemStack(ent.sw.output):get_definition()._starlit.fab
          322  +							if fab.flag and fab.flag.print then
          323  +								local req = fab:visualize()
          324  +								pushSelector('scm_' .. idx, ent.sw.output, ent.sw.name, nil, req)
          325  +							end
          326  +						end
          327  +						table.insert(pgmSelector, back)
          328  +					else
          329  +						local scm = sel.scms[sel.scm]
          330  +						local output = ItemStack(scm.sw.output):get_definition()
          331  +						local fab = scm.sw.input
          332  +						local sw = scm.sw
          333  +						local function unmet(str)
          334  +							return lib.color(1,.3,.3):fmt(str)
          335  +						end
          336  +						table.insert(pgmSelector, {kind = 'hztl', w=10, h=1.2;
          337  +							{kind = 'img', item = sw.output, w=1.2, h=1.2, desc=output.description};
          338  +							{kind = 'text', text = string.format('<global valign=middle><b>%s</b>', lib.str.htsan(sw.name)), w=10-1.2,h=1.2};
          339  +						})
          340  +						local inputTbl = {kind = 'vert', w=5,h=0;
          341  +							{kind = 'hbar', w=5, h=.5, text=sel.input and 'Input Plan' or 'Input'}};
          342  +						local costTbl = {kind = 'vert', w=5,h=0;
          343  +							{kind = 'hbar', w=5, h=.5, text=sel.input and 'Process Plan' or 'Process'}};
          344  +						local reqPane = {kind = 'pane', id='reqPane', w=10, h=7;
          345  +							{kind = 'hztl', w=10,h=0; inputTbl, costTbl}
          346  +						}
          347  +						local function pushCost(x, t, val)
          348  +							table.insert(costTbl, {kind='label', w=4.5,h=.5,x=x;
          349  +								text=string.format('%s: %s',t,val);
          350  +							})
          351  +						end
          352  +						local function pushComputeCosts(header, p)
          353  +							if p then
          354  +								table.insert(costTbl, {kind = 'label', w=5, h=.5, x=0; text=header});
          355  +								if p.cycles then
          356  +									pushCost(.5, 'Compute', lib.math.siUI({'cycle','cycles'}, p.cycles, true))
          357  +								end
          358  +								if p.power then
          359  +									local str = lib.math.siUI('J', p.power)
          360  +									if p.power > user:suitCharge() then str = unmet(str) end
          361  +									pushCost(.5, 'Power', str)
          362  +								end
          363  +								if p.ram then
          364  +									local str = string.format("%s / %s",
          365  +										lib.math.siUI('B', p.ram),
          366  +										lib.math.siUI('B', state.pgm.comp.ram))
          367  +									if p.ram > state.pgm.comp.ram then str = unmet(str) end
          368  +									pushCost(.5, 'Memory', str)
          369  +								end
          370  +							end
          371  +						end
          372  +
          373  +						local function fabToUI(x, inputTbl, req)
          374  +							for ci,c in ipairs(req) do
          375  +								table.insert(inputTbl, {kind = 'label', w=5-x, h=.5, x=x;
          376  +									text=lib.str.capitalize(c.header)});
          377  +								for ei,e in ipairs(c.list) do
          378  +									table.insert(inputTbl, {kind = 'hztl', w=4.5-x, h=.5, x=x+.5;
          379  +										{kind='img',   w=.5,h=.5, img=e.img};
          380  +										{kind='label', w=3.3,h=.5,x=.2, text=lib.str.capitalize(e.label)};
          381  +									});
          382  +								end
          383  +							end
          384  +						end
          385  +
          386  +						local commitHue, commitLabel = 120
          387  +						if not sel.input then
          388  +							commitLabel = 'Plan'
          389  +							fabToUI(0, inputTbl, fab:visualize())
          390  +							local function pushComputeCostsSw(header, p)
          391  +								if p.sw.cost then
          392  +									pushComputeCosts(header, {
          393  +										cycles = p.sw.cost.cycles;
          394  +										power = p.powerCost;
          395  +										ram = p.sw.cost.ram;
          396  +									})
          397  +								end
          398  +							end
          399  +							pushComputeCostsSw('Schematic', scm)
          400  +							pushComputeCostsSw('Compiler', state.pgm)
          401  +						else
          402  +							commitLabel = 'Commit'
          403  +							pushComputeCosts('Total', {
          404  +								cycles = (scm.sw.cost and scm.sw.cost.cycles or 0)
          405  +								       + (state.pgm.sw.cost and state.pgm.sw.cost.cycles or 0);
          406  +								power = (scm.powerCost or 0)
          407  +								      + (state.pgm.powerCost or 0)
          408  +								      + (fab.cost and fab.cost.power or 0);
          409  +								ram = (scm.sw.cost and scm.sw.cost.ram or 0)
          410  +								    + (state.pgm.sw.cost and state.pgm.sw.cost.ram or 0);
          411  +							})
          412  +							if fab.time and fab.time.print then
          413  +								pushCost(0, 'Job Runtime', lib.math.timespec(fab.time.print + scm.speed))
          414  +								pushCost(.5, 'Print Time', lib.math.timespec(fab.time.print))
          415  +								pushCost(.5, 'CPU Time', lib.math.timespec(scm.speed + state.pgm.speed))
          416  +							end
          417  +							local ok, compileCost = compilerCanPrint(user, state.pgm, scm)
          418  +							fabToUI(0, inputTbl, compileCost.itemSpec:visualize())
          419  +
          420  +							if next(compileCost.unsat) then
          421  +								local vis = compileCost.unsat:visualize()
          422  +								for si, s in ipairs(vis) do
          423  +									s.header = 'Missing ' .. s.header
          424  +									for ei, e in ipairs(s.list) do
          425  +										e.label = lib.color(1,.2,.2):fmt(e.label)
          426  +									end
          427  +								end
          428  +								fabToUI(0, inputTbl, vis)
          429  +							end
          430  +							if not ok then commitHue = 0 end
          431  +						end
          432  +						table.insert(pgmSelector, reqPane)
          433  +						table.insert(pgmSelector, {kind = 'hztl', w=10,h=1.2;
          434  +							{kind = 'button', id='back', label = '← Back', w=5,h=1.2};
          435  +							{kind = 'button', id='commit', label = commitLabel .. ' →', w=5,h=1.2, color={hue=commitHue,sat=0,lum=0}};
          436  +						})
          437  +					end
          438  +				end
          439  +
          440  +				local rec = getRecentJobs(state)
          441  +				local recentList = {kind = 'hwrap', cols=3}
          442  +				if rec then
          443  +					for i,id in ipairs(rec) do
          444  +						local out = ItemStack(starlit.item.sw.db[id].output)
          445  +						table.insert(recentList, {kind='contact',w=1.5,h=1.5,id=string.format('recent_%s',i), item = out})
          446  +					end
          447  +				end
          448  +				local queue = {kind='pane', w=5, h=9.5}
          449  +				for i,v in ipairs(state.pgm.file.body.conf) do
          450  +					if v.key == 'job' then
          451  +						local job = starlit.store.compilerJob.dec(v.value)
          452  +						local scm = starlit.item.sw.db[job.schematic]
          453  +						local cycTotal = (scm.cost and scm.cost.cycles or 0)
          454  +						local secTotal = (scm.input.time and scm.input.time.print or 0)
          455  +						local cycPct = cycTotal == 0 and 1 or (1 - job.cyclesLeft/cycTotal)
          456  +						local secPct = secTotal == 0 and 1 or (1 - job.timeLeft/secTotal)
          457  +						local out = ItemStack(scm.output)
          458  +						local cycBar = {kind = 'hbar', fac = cycPct, w=5-1.5, h = .5;
          459  +							color={hue=150,sat=0,lum=0}; text=string.format("Compute %s%%", math.floor(cycPct*100));
          460  +						}
          461  +						local secBar = {kind = 'hbar', fac = secPct, w=5-1.5, h = .5;
          462  +							color={hue=50,sat=0,lum=0}; text=string.format("Assemble %s%%", math.floor(secPct*100));
          463  +						}
          464  +						table.insert(queue, {kind='hztl';  w=5, h=1.5;
          465  +							{kind = 'contact', w=1.5,h=1.5, id=string.format('cancel_%s', i), item = out};
          466  +							{kind = 'vert', w=5-1.5, h=1.5;
          467  +								{kind = 'label', text=out:get_definition().short_description, w=5-1.5, h=.75};
          468  +								(cycPct < 1 and cycBar or secBar);
          469  +							}
          470  +						})
          471  +					end
          472  +				end
          473  +
          474  +				return starlit.ui.build {
          475  +					kind = 'hztl', padding = 0.5; w = 20, h = 10, mode = 'sw';
          476  +					{kind = 'vert', w = 5, h = 5;
          477  +						{kind = 'hbar', fac=0, w = 5, h = .5, text = '<b><left>Recent Prints</left></b>'};
          478  +						recentList;
          479  +					};
          480  +					{kind = 'vert', w = 10, h = 10;
          481  +						{kind = 'hbar', fac=0, w = 10, h = .5, text = '<b>Program Select</b>'};
          482  +						{kind = 'pane', w = 10, h = 9.5, id='pgmSelect';
          483  +							unpack(pgmSelector)
          484  +						};
          485  +					};
          486  +					{kind = 'vert', w = 5, h = 10;
          487  +						{kind = 'hbar', fac=0, w = 5, h = .5, text = '<b><right>Print Queue</right></b>'};
          488  +						queue;
          489  +					};
          490  +				}
          491  +			end;
          492  +		};
          493  +	};
          494  +})

Modified mods/starlit/fab.lua from [a2443e45a4] to [f85992f960].

   102    102   		end;
   103    103   		image = function(x, n)
   104    104   			return string.format('starlit-element-%s.png', x)
   105    105   		end;
   106    106   		inventory = fRawMat 'element';
   107    107   		op = fQuant;
   108    108   	};
   109         -	metal ={
          109  +	metal = {
   110    110   		name = {"metal", "metals"};
   111    111   		string = function(x, n)
   112    112   			local met = starlit.world.material.metal.db[x]
   113    113   			return lib.math.siUI('g', n) .. ' ' .. met.name
   114    114   		end;
   115    115   		image = function(x, n)
   116    116   			local met = starlit.world.material.metal.db[x]
................................................................................
   333    333   										if not alreadyGot(ii,oi) then
   334    334   											local sv = {
   335    335   												inv=ii, slot=oi;
   336    336   												consume=deduct, remain=st;
   337    337   												satisfy=fab{[cat]={[substance]=avail}}
   338    338   											}
   339    339   											table.insert(stacks, sv)
          340  +											table.insert(consumed, sv)
   340    341   										end
   341    342   										if amtFound >= amt then goto suffice end
   342    343   									end
   343    344   								end
   344    345   							end
   345    346   						end
   346    347   

Modified mods/starlit/init.lua from [aee56c43bb] to [a5bde19f07].

    54     54   	};
    55     55   
    56     56   	interface = lib.registry.mk 'starlit:interface';
    57     57   	item = {
    58     58   		food = lib.registry.mk 'starlit:food';
    59     59   		seed = lib.registry.mk 'starlit:seed';
    60     60   	};
           61  +
           62  +	-- complex algorithms that cut across namespaces or don't belong anywhere else
           63  +	alg = {};
    61     64   
    62     65   	region = {
    63     66   		radiator = {
    64     67   			store = AreaStore();
    65     68   			emitters = {};
    66     69   		};
    67     70   	};
................................................................................
   275    278   
   276    279   starlit.include 'fx/nano'
   277    280   
   278    281   starlit.include 'element'
   279    282   
   280    283   starlit.include 'terrain'
   281    284   starlit.include 'interfaces'
          285  +starlit.include 'compile'
   282    286   starlit.include 'suit'
   283    287   
   284    288   -- minetest.settings:set('movement_gravity', starlit.world.planet.gravity) -- ??? seriously???
   285    289   -- THIS OVERRIDES THE GLOBAL SETTING *AND PERSISTS IT* WHAT IN THE SATANIC FUCK
   286    290   
   287    291   ---------------
   288    292   -- callbacks --
................................................................................
   321    325   		return starlit.ui.userMenuDispatch(user,fields)
   322    326   	end
   323    327   	local ui = starlit.interface.db[formid]
   324    328   	local state = starlit.activeUI[name] or {}
   325    329   	if formid == '__builtin:help_cmds' 
   326    330   	or formid == '__builtin:help_privs' 
   327    331   		then return false end
   328         -	assert(state.form == formid) -- sanity check
          332  +	if not (state.form == formid) then -- sanity check
          333  +		log.warn('user %s attempted to send form %s while %s was active', name, formid, state.form)
          334  +		return false
          335  +	end
   329    336   	user:onRespond(ui, state, fields)
   330    337   	if fields.quit then
   331    338   		starlit.activeUI[name] = nil
   332    339   	end
   333    340   	return true
   334    341   end)
   335    342   
................................................................................
   392    399   })
   393    400   minetest.register_item("starlit:_hand_dig", {
   394    401   	type = "none",
   395    402   	wield_image = "wieldhand.png",
   396    403   	wield_scale = {x=1,y=1,z=2.5},
   397    404   	tool_capabilities = {
   398    405   		groupcaps = {
          406  +			object = {maxlevel=1, times = {.20,.10}};
   399    407   			plant = {maxlevel=1, times = {.50}};
   400    408   
   401    409   			-- sand, dirt, gravel
   402    410   			looseClump = {maxlevel=1, times = {1.5, 2.5}};
   403    411   		};
   404    412   	}
   405    413   })

Modified mods/starlit/interfaces.lua from [c1a1690c3b] to [ddaa1e9478].

    12     12   				img='starlit-ui-icon-nano.png', close=true, color = cmode'nano'};
    13     13   			{kind = 'contact', w=1.5,h=1.5, id = 'mode_weapon',
    14     14   				img='starlit-ui-icon-weapon.png', close=true, color = cmode'weapon'};
    15     15   			{kind = 'contact', w=1.5,h=1.5, id = 'mode_psi',
    16     16   				img='starlit-ui-icon-psi.png', close=true, color = cmode'psi'};
    17     17   		};
    18     18   		{kind = 'hztl';
    19         -			{kind = 'contact', w=1.5,h=1.5, id = 'open_elements',
           19  +			{kind = 'contact', w=1.5,h=1.5, id = 'open_nano',
    20     20   				img='starlit-ui-icon-element.png'};
    21     21   			{kind = 'contact', w=1.5,h=1.5, id = 'open_suit',
    22     22   				img='starlit-item-suit.png^[hsl:200:-.7:0'};
    23     23   			{kind = 'contact', w=1.5,h=1.5, id = 'open_psi',
    24     24   				img='starlit-ui-icon-psi-cfg.png'};
    25     25   			{kind = 'contact', w=1.5,h=1.5, id = 'open_body',
    26     26   				img='starlit-ui-icon-self.png'};
................................................................................
    49     49   			else
    50     50   				setSuitMode(e)
    51     51   			end
    52     52   			return true
    53     53   		end
    54     54   	end
    55     55   
    56         -	if fields.open_elements then
    57         -		user:openUI('starlit:user-menu', 'compiler')
           56  +	if fields.open_nano then
           57  +		user:openUI('starlit:user-menu', 'nano')
    58     58   		return true
    59     59   	elseif fields.open_psi then
    60     60   		user:openUI('starlit:user-menu', 'psi')
    61     61   		return true
    62     62   	elseif fields.open_suit then
    63     63   		if not user:naked() then
    64     64   			user:openUI('starlit:user-menu', 'suit')
................................................................................
   118    118   end
   119    119   
   120    120   local function abilityMenu(a)
   121    121   	-- select primary/secondary abilities or activate ritual abilities
   122    122   	local p = {kind = 'vert'}
   123    123   	for _, o in ipairs(a.order) do
   124    124   		local m = a.menu[o]
   125         -		table.insert(p, {kind='hbar', fac=0.5, text=string.format("<b>%s</b>",m.label), w=a.w, h = .5})
          125  +		table.insert(p, {kind='hbar', fac=0, text=string.format("<b>%s</b>",m.label), w=a.w, h = .5})
   126    126   		table.insert(p, wrapMenu(a.w, a.h, 1.2, 2, m.opts))
   127    127   	end
   128    128   	return p
   129    129   end
   130    130   
   131    131   local function pptrMatch(a,b)
   132    132   	if a == nil or b == nil then return false end
................................................................................
   133    133   	return (a.chipID ~= nil and (a.chipID == b.chipID and a.pgmIndex == b.pgmIndex))
   134    134          or (a.ref ~= nil and a.ref == b.ref)
   135    135   end
   136    136   
   137    137   starlit.interface.install(starlit.type.ui {
   138    138   	id = 'starlit:user-menu';
   139    139   	pages = {
   140         -		compiler = {
          140  +		nano = {
   141    141   			setupState = function(state, user)
   142    142   				-- nanotech/suit software menu
   143    143   				local chips = user.entity:get_inventory():get_list 'starlit_suit_chips' -- FIXME need better subinv api
   144    144   				local sw = starlit.mod.electronics.chip.usableSoftware(chips)
   145    145   				state.suitSW = {}
   146    146   				local dedup = {}
   147    147   				for i, r in ipairs(sw) do if
................................................................................
   168    168   					end
   169    169   				end
   170    170   				if not pgm then return false end -- HAX
   171    171   
   172    172   				-- kind=active programs must be assigned to a command slot
   173    173   				-- kind=direct programs must open their UI
   174    174   				-- kind=passive programs must toggle on and off
          175  +				local inv = user.entity:get_inventory()
   175    176   				local function suitCtx(pgm)
   176         -					local chips = user.entity:get_inventory():get_list 'starlit_suit_chips'
   177         -					local pgmctx = starlit.mod.electronics.chip.usableSoftware(chips, {pgm})[1]
          177  +					local uuid = starlit.mod.electronics.chip.read(pgm.chip).uuid
   178    178   					return {
   179    179   						context = 'suit';
   180         -						program = pgmctx;
          180  +						program = pgm;
          181  +						verify = function() -- ew!!
          182  +							local chipInSlot = inv:get_stack('starlit_suit_chips', pgm.chipSlot)
          183  +							local csd = starlit.mod.electronics.chip.read(chipInSlot)
          184  +							return csd.uuid == uuid
          185  +						end;
          186  +						saveConf = function(cfg) cfg = cfg or pgm.file.body.conf
          187  +							pgm.file.body.conf = cfg
          188  +							pgm.fd:write(pgm.file)
          189  +							inv:set_stack('starlit_suit_chips', pgm.chipSlot, pgm.fd.chip)
          190  +							user:reconfigureSuit()
          191  +						end;
          192  +						pullConf = function()
          193  +							local stack = inv:get_stack('starlit_suit_chips', pgm.chipSlot)
          194  +							pgm.fd.chip=stack
          195  +							pgm.file = pgm.fd:read()
          196  +						end;
   181    197   					}
   182    198   				end
   183    199   
   184    200   				if pgm.sw.powerKind == 'active' then
   185    201   					if cfg then
   186    202   						user:openUI(pgm.sw.ui, 'index', {
   187    203   							context = 'suit';
................................................................................
   495    511   				if     q.powerMode_off  then suitMode = 'off'
   496    512   				elseif q.powerMode_save then suitMode = 'powerSave'
   497    513   				elseif q.powerMode_on   then suitMode = 'on' end
   498    514   				if suitMode then
   499    515   					user:suitPowerStateSet(suitMode)
   500    516   					return true
   501    517   				end
   502         -			end;
   503         -		};
   504         -	};
   505         -})
   506         -
   507         -local function compilerCanPrint(user, cpl, scm)
   508         -	local output = ItemStack(scm.sw.output):get_definition()
   509         -	local fab = output._starlit.fab
   510         -	local sw = scm.sw
   511         -	local ok, consume, unsat, leftover, itemSpec = fab:seek {
   512         -		user.entity:get_inventory():get_list 'main';
   513         -	}
   514         -
   515         -	local cost = {
   516         -		consume = consume, unsat = unsat, leftover = leftover, itemSpec = itemSpec;
   517         -		runtimeEstimate = scm.speed + cpl.speed + (fab.time and fab.time.print or 0);
   518         -		power = cpl.powerCost + scm.powerCost;
   519         -		ram = (cpl.cost and cpl.cost.ram or 0)
   520         -		    + (scm.cost and scm.cost.ram or 0);
   521         -		cycles = (cpl.cost and cpl.cost.cycles or 0)
   522         -		       + (scm.cost and scm.cost.cycles or 0);
   523         -	}
   524         -
   525         -	local userComp = starlit.mod.electronics.chip.sumCompute(
   526         -		user.entity:get_inventory():get_list 'starlit_suit_chips'
   527         -	)
   528         -
   529         -	if ok and cost.power <= user:suitCharge() and cost.ram <= userComp.ram then
   530         -		return true, cost
   531         -	else return false, cost end
   532         -end
   533         -
   534         --- TODO destroy suit interfaces when power runs out or suit/chip is otherwise disabled
   535         -starlit.interface.install(starlit.type.ui {
   536         -	id = 'starlit:compile-matter-component';
   537         -	sub = {
   538         -		suit = function(state, user, evt)
   539         -			if evt.kind == 'disrobe' then state:close()
   540         -			elseif evt.kind == 'power' and evt.mode == 'off' then state:close() end
   541         -		end;
   542         -		playerInventory = function(state,user)
   543         -			-- refresh
   544         -		end;
   545         -	};
   546         -	pages = {
   547         -		index = {
   548         -			setupState = function(state, user, ctx)
   549         -				state.pgm = ctx.program
   550         -				state.select = {}
   551         -				local E = starlit.mod.electronics
   552         -				if ctx.context == 'suit' then
   553         -					state.fetch = function()
   554         -						local cst = user.entity:get_inventory():get_list 'starlit_suit_chips'
   555         -						local cl = {order={}, map={}, slot={}}
   556         -						for i, c in ipairs(cst) do
   557         -							if not c:is_empty() then
   558         -								local d = E.chip.read(c)
   559         -								local co = {
   560         -									stack = c;
   561         -									data = d;
   562         -								}
   563         -								table.insert(cl.order, co)
   564         -								cl.map[d.uuid] = co
   565         -								cl.slot[i] = co
   566         -							end
   567         -						end
   568         -
   569         -						-- kill me fam
   570         -						if (   state.select.chip
   571         -							and state.select.chip ~= true
   572         -							and not cl.map[state.select.chip])
   573         -						or (state.select.scm
   574         -						   and not state.select.scms[state.select.scm])
   575         -					   then
   576         -							-- chip or pgm no longer available
   577         -							user:suitSound 'starlit-error'
   578         -							state.select = {}
   579         -						end
   580         -						state.select.chips = cl
   581         -
   582         -						state.select.scms = {}
   583         -						if state.select.chip then
   584         -							state.select.scms = E.chip.usableSoftware(cst,nil, function(s)
   585         -								if state.select.chip ~= true then
   586         -									if cl.slot[s.chipSlot].data.uuid ~= state.select.chip then
   587         -										return false
   588         -									end
   589         -								end
   590         -								return s.sw.kind == 'schematic'
   591         -							end)
   592         -						end
   593         -
   594         -					end
   595         -				end
   596         -			end;
   597         -
   598         -			onClose = function(state, user)
   599         -				user:suitSound 'starlit-quit'
   600         -			end;
   601         -			handle = function(state, user, q)
   602         -				local sel = state.select
   603         -				state.fetch()
   604         -				local chips = state.select.chips
   605         -				local function chirp()
   606         -					user:suitSound 'starlit-nav'
   607         -				end
   608         -
   609         -				local function trySelection(id)
   610         -					if sel[id] == nil then
   611         -						for k in next, q do
   612         -							local pat = "^"..id.."_(%d+)$" -- ew
   613         -							local idx = k:match(pat)
   614         -							if idx then
   615         -								local cm = tonumber(idx)
   616         -								if cm then
   617         -									chirp()
   618         -									sel[id] = cm
   619         -									return true
   620         -								end
   621         -							end
   622         -						end
   623         -					end
   624         -				end
   625         -
   626         -				if sel.chip == nil then
   627         -					if q.showAll then
   628         -						chirp()
   629         -						sel.chip = true
   630         -						return true
   631         -					elseif q.find then
   632         -						chirp()
   633         -						-- TODO
   634         -						return true
   635         -					end
   636         -				end
   637         -
   638         -				if trySelection('chip') then
   639         -					return true
   640         -				elseif trySelection('scm') then
   641         -					return true
   642         -				else
   643         -					if q.back then
   644         -						chirp()
   645         -						if sel.input then
   646         -							sel.input = nil
   647         -						elseif sel.scm then
   648         -							sel.scm = nil
   649         -						elseif sel.chip then
   650         -							sel.chip = nil
   651         -						end
   652         -						return true
   653         -					elseif q.commit then
   654         -						if not sel.input then
   655         -							chirp()
   656         -							sel.input = true
   657         -							return true
   658         -						else
   659         -							local scm = sel.scms[sel.scm]
   660         -							local ok, cost = compilerCanPrint(user, state.pgm, scm)
   661         -							if ok then
   662         -								user:suitSound 'starlit-configure'
   663         -								-- consume consumables
   664         -								-- add print job
   665         -								state.select = {}
   666         -								return true
   667         -							else
   668         -								user:suitSound 'starlit-error'
   669         -							end
   670         -						end
   671         -					end
   672         -				end
   673         -
   674         -			end;
   675         -
   676         -			render = function(state, user)
   677         -				local sel, pgmSelector = state.select, {}
   678         -				state.fetch()
   679         -
   680         -				local function pushSelector(id, item, label, desc, req)
   681         -					local rh = .5
   682         -					local label = {kind = 'text', w = 10-1.5, h=1.5;
   683         -							text = '<global valign=middle>'..lib.str.htsan(label) }
   684         -					if req then
   685         -						label.h = label.h - rh - .2
   686         -
   687         -						local imgs = {}
   688         -						for ci,c in ipairs(req) do
   689         -							for ei, e in ipairs(c.list) do
   690         -								table.insert(imgs, {kind = 'img', w=rh, h=rh,  img=e.img})
   691         -							end
   692         -						end
   693         -						label = {kind = 'vert', w = 10-1.5, h=1.5;
   694         -							label;
   695         -							{kind ='hztl', w=10-1.5, h=rh; unpack(imgs); }
   696         -						}
   697         -					end
   698         -					table.insert(pgmSelector, {kind = 'hztl', w=10,h=1.5;
   699         -						{kind = 'contact', id=id, w=1.5, h=1.5;
   700         -							item = item;
   701         -							color = {hue=220, sat=0, lum=0};
   702         -							desc = desc;
   703         -						};
   704         -						label;
   705         -					})
   706         -				end
   707         -
   708         -				local back = {kind = 'button', id='back', label = '<- Back', w=10,h=1.2}
   709         -				if sel.chips == nil then
   710         -					table.insert(pgmSelector, {kind = 'img', img = 'starlit-ui-alert.png', w=2, h=2})
   711         -				elseif sel.chip == nil then
   712         -					for i, c in ipairs(sel.chips.order) do
   713         -					-- TODO filter out chips without schematics?
   714         -						pushSelector('chip_' .. c.data.uuid, c.stack, c.data.label)
   715         -					end
   716         -					if next(sel.chips.order) then
   717         -						table.insert(pgmSelector, {kind = 'hztl', w=10,h=1.5;
   718         -							{kind = 'button', w=5,h=1.5; id='showAll', label='Show All'};
   719         -							{kind = 'button', w=5,h=1.5; id='find', label='Find'};
   720         -						})
   721         -					end
   722         -				else
   723         -					if sel.scm == nil then
   724         -						for idx, ent in ipairs(sel.scms) do
   725         -							local fab = ItemStack(ent.sw.output):get_definition()._starlit.fab
   726         -							if fab.flag.print then
   727         -								local req = fab:visualize()
   728         -								pushSelector('scm_' .. idx, ent.sw.output, ent.sw.name, nil, req)
   729         -							end
   730         -						end
   731         -						table.insert(pgmSelector, back)
   732         -					else
   733         -						local scm = sel.scms[sel.scm]
   734         -						local output = ItemStack(scm.sw.output):get_definition()
   735         -						local fab = output._starlit.fab
   736         -						local sw = scm.sw
   737         -						local function unmet(str)
   738         -							return lib.color(1,.3,.3):fmt(str)
   739         -						end
   740         -						table.insert(pgmSelector, {kind = 'hztl', w=10, h=1.2;
   741         -							{kind = 'img', item = sw.output, w=1.2, h=1.2, desc=output.description};
   742         -							{kind = 'text', text = string.format('<global valign=middle><b>%s</b>', lib.str.htsan(sw.name)), w=10-1.2,h=1.2};
   743         -						})
   744         -						local inputTbl = {kind = 'vert', w=5,h=0;
   745         -							{kind = 'hbar', w=5, h=.5, text=sel.input and 'Input Plan' or 'Input'}};
   746         -						local costTbl = {kind = 'vert', w=5,h=0;
   747         -							{kind = 'hbar', w=5, h=.5, text=sel.input and 'Process Plan' or 'Process'}};
   748         -						local reqPane = {kind = 'pane', id='reqPane', w=10, h=7;
   749         -							{kind = 'hztl', w=10,h=0; inputTbl, costTbl}
   750         -						}
   751         -						local function pushCost(x, t, val)
   752         -							table.insert(costTbl, {kind='label', w=4.5,h=.5,x=x;
   753         -								text=string.format('%s: %s',t,val);
   754         -							})
   755         -						end
   756         -						local function pushComputeCosts(header, p)
   757         -							if p then
   758         -								table.insert(costTbl, {kind = 'label', w=5, h=.5, x=0; text=header});
   759         -								if p.cycles then
   760         -									pushCost(.5, 'Compute', lib.math.siUI({'cycle','cycles'}, p.cycles, true))
   761         -								end
   762         -								if p.power then
   763         -									local str = lib.math.siUI('J', p.power)
   764         -									if p.power > user:suitCharge() then str = unmet(str) end
   765         -									pushCost(.5, 'Power', str)
   766         -								end
   767         -								if p.ram then
   768         -									local str = string.format("%s / %s",
   769         -										lib.math.siUI('B', p.ram),
   770         -										lib.math.siUI('B', state.pgm.comp.ram))
   771         -									if p.ram > state.pgm.comp.ram then str = unmet(str) end
   772         -									pushCost(.5, 'Memory', str)
   773         -								end
   774         -							end
   775         -						end
   776         -
   777         -						local function fabToUI(x, inputTbl, req)
   778         -							for ci,c in ipairs(req) do
   779         -								table.insert(inputTbl, {kind = 'label', w=5-x, h=.5, x=x;
   780         -									text=lib.str.capitalize(c.header)});
   781         -								for ei,e in ipairs(c.list) do
   782         -									table.insert(inputTbl, {kind = 'hztl', w=4.5-x, h=.5, x=x+.5;
   783         -										{kind='img',   w=.5,h=.5, img=e.img};
   784         -										{kind='label', w=3.3,h=.5,x=.2, text=lib.str.capitalize(e.label)};
   785         -									});
   786         -								end
   787         -							end
   788         -						end
   789         -
   790         -						local commitHue=120, commitLabel
   791         -						if not sel.input then
   792         -							commitLabel = 'Plan'
   793         -							fabToUI(0, inputTbl, fab:visualize())
   794         -							local function pushComputeCostsSw(header, p)
   795         -								if p.sw.cost then
   796         -									pushComputeCosts(header, {
   797         -										cycles = p.sw.cost.cycles;
   798         -										power = p.powerCost;
   799         -										ram = p.sw.cost.ram;
   800         -									})
   801         -								end
   802         -							end
   803         -							pushComputeCostsSw('Schematic', scm)
   804         -							pushComputeCostsSw('Compiler', state.pgm)
   805         -						else
   806         -							commitLabel = 'Commit'
   807         -							pushComputeCosts('Total', {
   808         -								cycles = (scm.sw.cost and scm.sw.cost.cycles or 0)
   809         -								       + (state.pgm.sw.cost and state.pgm.sw.cost.cycles or 0);
   810         -								power = (scm.powerCost or 0)
   811         -								      + (state.pgm.powerCost or 0)
   812         -								      + (fab.cost and fab.cost.power or 0);
   813         -								ram = (scm.sw.cost and scm.sw.cost.ram or 0)
   814         -								    + (state.pgm.sw.cost and state.pgm.sw.cost.ram or 0);
   815         -							})
   816         -							if fab.time and fab.time.print then
   817         -								pushCost(0, 'Job Runtime', lib.math.timespec(fab.time.print + scm.speed))
   818         -								pushCost(.5, 'Print Time', lib.math.timespec(fab.time.print))
   819         -								pushCost(.5, 'CPU Time', lib.math.timespec(scm.speed + state.pgm.speed))
   820         -							end
   821         -							local ok, compileCost = compilerCanPrint(user, state.pgm, scm)
   822         -							fabToUI(0, inputTbl, compileCost.itemSpec:visualize())
   823         -
   824         -							if next(compileCost.unsat) then
   825         -								local vis = compileCost.unsat:visualize()
   826         -								for si, s in ipairs(vis) do
   827         -									s.header = 'Missing ' .. s.header
   828         -									for ei, e in ipairs(s.list) do
   829         -										e.label = lib.color(1,.2,.2):fmt(e.label)
   830         -									end
   831         -								end
   832         -								fabToUI(0, inputTbl, vis)
   833         -							end
   834         -							if not ok then commitHue = 0 end
   835         -						end
   836         -						table.insert(pgmSelector, reqPane)
   837         -						table.insert(pgmSelector, {kind = 'hztl', w=10,h=1.2;
   838         -							{kind = 'button', id='back', label = '← Back', w=5,h=1.2};
   839         -							{kind = 'button', id='commit', label = commitLabel .. ' →', w=5,h=1.2, color={hue=commitHue,sat=0,lum=0}};
   840         -						})
   841         -					end
   842         -				end
   843         -
   844         -				return starlit.ui.build {
   845         -					kind = 'hztl', padding = 0.5; w = 20, h = 10, mode = 'sw';
   846         -					{kind = 'vert', w = 5, h = 5;
   847         -						{kind = 'hbar', fac=0, w = 5, h = .5, text = '<b><left>Recent Prints</left></b>'};
   848         -					};
   849         -					{kind = 'vert', w = 10, h = 10;
   850         -						{kind = 'hbar', fac=0, w = 10, h = .5, text = '<b>Program Select</b>'};
   851         -						{kind = 'pane', w = 10, h = 9.5, id='pgmSelect';
   852         -							unpack(pgmSelector)
   853         -						};
   854         -					};
   855         -					{kind = 'vert', w = 5, h = 10;
   856         -						{kind = 'hbar', fac=0, w = 5, h = .5, text = '<b><right>Print Queue</right></b>'};
   857         -					};
   858         -				}
   859    518   			end;
   860    519   		};
   861    520   	};
   862    521   })

Modified mods/starlit/store.lua from [9cc49bcaa7] to [ac25f2dec5].

    20     20   for k,v in pairs(starlit.world.stats) do
    21     21   	statStructFields[k] = v.srzType or (
    22     22   		(v.base == true or v.base > 0) and T.s16 or T.u16
    23     23   	)
    24     24   end
    25     25   
    26     26   starlit.store.compilerJob = G.struct {
    27         -	schematic = T.str;
    28         -	progress = T.clamp;
           27  +	schematic  = T.str;
           28  +	cyclesLeft = T.u64;
           29  +	timeLeft   = T.decimal;
    29     30   }
    30     31   
    31     32   starlit.store.persona = G.struct {
    32     33   	name = T.str;
    33     34   	species = T.str;
    34     35   	speciesVariant = T.str;
    35     36   	background = T.str;

Modified mods/starlit/suit.lua from [8324bad06e] to [f9e736d40a].

   427    427   							break
   428    428   						end
   429    429   					end
   430    430   					local fn if s.powerKind == 'passive'
   431    431   						then fn = s.run
   432    432   						else fn = s.bgProc
   433    433   					end
   434         -					function prop.saveConf(cfg) cfg = cfg or conf
          434  +					function prop.saveConf(cfg) cfg = cfg or prop.file
   435    435   						prop.fd:write(cfg)
   436    436   						inv:set_stack('starlit_suit_chips', prop.chipSlot, prop.fd.chip)
   437    437   						reconfSuit = true
   438    438   					end
          439  +					function prop.pullConf()
          440  +						local stack = inv:get_stack('starlit_suit_chips', suitprog.chipSlot)
          441  +						prop.fd.chip=stack
          442  +						prop.file = prop.fd:read()
          443  +					end
   439    444   					function prop.giveItem(st)
   440    445   						u:thrustUpon(st)
   441    446   					end
   442    447   					
   443    448   					if enabled and fn(u, prop, suitInterval, runState) then
   444    449   						runState.pgmsRun[s] = true
   445    450   					end

Modified mods/starlit/terrain.lua from [4e1556f916] to [e1d816e7a5].

   240    240   	tiles = {'starlit-terrain-feldspar.png'};
   241    241   	excludeOre = true;
   242    242   	recover = starlit.type.fab {
   243    243   		time = { shred = 3; };
   244    244   		cost = { shredPower = 3; };
   245    245   	};
   246    246   	recover_vary = function(rng, ctx)
   247         -		-- print('vary!', rng:int(), rng:int(0,10))
   248    247   		return starlit.type.fab {
   249    248   			element = {
   250    249   				aluminum  = rng:int(0,4);
   251    250   				potassium = rng:int(0,2);
   252    251   				calcium   = rng:int(0,2);
   253    252   			}
   254    253   		};

Modified mods/starlit/ui.lua from [08cb8bbbbd] to [16bf595c8c].

   150    150   			widget('container[%s,%s]%scontainer_end[]', state.x, state.y, src)
   151    151   			-- TODO alignments
   152    152   			state.x=state.x + state.spacing + st.w
   153    153   			state.h = math.max(state.h, st.h)
   154    154   		end
   155    155   		state.h = state.h + state.padding
   156    156   		state.w = state.x + state.padding/2
          157  +	elseif def.kind == 'hwrap' then
          158  +		local idx, rowH, totalH = 0,0,0
          159  +		local cols = def.cols or math.huge
          160  +		local startX = state.x
          161  +		local maxX = startX
          162  +		for _, w in ipairs(def) do idx = idx + 1
          163  +			local src, st = starlit.ui.build(w, state)
          164  +			-- TODO alignments
          165  +			rowH = math.max(rowH, st.h)
          166  +			if idx > cols or def.w and (state.x + state.spacing + st.w > def.w) then
          167  +				totalH = totalH + rowH
          168  +				state.x = startX
          169  +				rowH,idx = 0,0
          170  +				state.y = state.y + state.spacing + st.h
          171  +			end
          172  +			widget('container[%s,%s]%scontainer_end[]', state.x, state.y, src)
          173  +			state.x=state.x + state.spacing + st.w
          174  +			maxX = math.max(state.x, maxX)
          175  +		end
          176  +		totalH = totalH + rowH
          177  +		state.h = math.max(state.h, totalH) + state.padding
          178  +		state.w = state.x + state.padding/2
          179  +		state.x = maxX
   157    180   	elseif def.kind == 'pane' then
   158    181   		widget('scroll_container[%s,%s;%s,%s;%s;vertical]',
   159    182   			state.x, state.y, state.w, state.h,
   160    183   			def.id)
   161    184   		local y = 0
   162    185   		for _, w in ipairs(def) do
   163    186   			local src, st = starlit.ui.build(w, state)

Modified mods/starlit/user.lua from [c928629a64] to [aa3a95c242].

  1010   1010   			local ul = self.hud.led.map[kind]
  1011   1011   			if ul then
  1012   1012   				if time - ul.origin > minFreq then
  1013   1013   					ul.origin = time
  1014   1014   				else return end
  1015   1015   			end
  1016   1016   
  1017         -			if urgency > 0 then
         1017  +			if urgency ~= 0 then
  1018   1018   				local urgencies = {
         1019  +					[-2] = {sound = 'starlit-success'};
         1020  +					[-1] = {sound = 'starlit-nav'};
  1019   1021   					[1] = {sound = 'starlit-alarm'};
  1020   1022   					[2] = {sound = 'starlit-alarm-urgent'};
  1021   1023   				}
  1022   1024   			   local urg = urgencies[urgency] or urgencies[#urgencies]
  1023   1025   
  1024   1026   			   if time - self.cooldownTimes.alarm > 1.5 then
  1025   1027   				   self.cooldownTimes.alarm = time
................................................................................
  1085   1087   			return true; -- TODO
  1086   1088   		end;
  1087   1089   
  1088   1090   		---------------
  1089   1091   		-- inventory --
  1090   1092   		---------------
  1091   1093   		give = function(self, item)
         1094  +			item = ItemStack(item)
  1092   1095   			local inv = self.entity:get_inventory()
  1093   1096   			local function is(grp)
  1094   1097   				return minetest.get_item_group(item:get_name(), grp) ~= 0
  1095   1098   			end
  1096   1099   			-- TODO notif popups
  1097   1100   			if is 'specialInventory' then
  1098   1101   			--[[
................................................................................
  1130   1133   }
  1131   1134   
  1132   1135   local clockInterval = 1.0
  1133   1136   starlit.startJob('starlit:clock', clockInterval, function(delta)
  1134   1137   	for id, u in pairs(starlit.activeUsers) do
  1135   1138   		u.hud.elt.time:update()
  1136   1139   		u:updateLEDs()
         1140  +		local ui = starlit.activeUI[u.name]
         1141  +		if ui and (ui.self.refresh or ui.self.pages[ui.page].refresh) then
         1142  +			ui.self:show(u)
         1143  +		end
  1137   1144   	end
  1138   1145   end)
  1139   1146   
  1140   1147   -- performs a general HUD refresh, mainly to update the HUD backlight brightness
  1141   1148   local hudInterval = 10
  1142   1149   starlit.startJob('starlit:hud-refresh', hudInterval, function(delta)
  1143   1150   	for id, u in pairs(starlit.activeUsers) do

Modified mods/starlit/world.lua from [822a964373] to [d1f4916ac1].

   115    115   				attached_node = 3;
   116    116   			};
   117    117   			drop = st.drop;
   118    118   			_starlit = {
   119    119   				plant = {
   120    120   					id = id, stage = n;
   121    121   				};
   122         -				recover = starlit.type.fab {
          122  +				recover = b.recover or starlit.type.fab {
   123    123   					time = { shred = .3; };
   124    124   					cost = { shredPower = 1; };
   125    125   				};
   126         -				recover_vary = function(rng, ctx)
          126  +				recover_vary = b.recover_vary or function(rng, ctx)
   127    127   					return starlit.type.fab {
   128    128   						element = {
   129         -							carbon    = rng:int(0,1);
          129  +							carbon    = rng:int(0,2);
   130    130   							potassium = rng:int(0,1);
          131  +							magnesium = rng:int(0,b.biolum and 2 or 1);
   131    132   						}
   132    133   					};
   133    134   				end;
   134    135   			};
   135    136   		}
   136    137   		if st.swap then
   137    138   			base.node_dig_prediction = ""

Modified mods/vtlib/math.lua from [2d47a7e2f1] to [557fe13815].

   158    158   function fn.timespec(n)
   159    159   	if n == 0 then return '0s' end
   160    160   	if n < 0 then return '-' .. fn.timespec(n*-1) end
   161    161   
   162    162   	local sec = math.floor(n % 60)
   163    163   	local min = math.floor(n / 60)
   164    164   	local hr = math.floor(min / 60)
          165  +	min = min % 60
   165    166   	local spec = {}
   166    167   
   167    168   	if hr  ~= 0 then table.insert(spec, string.format("%shr", hr))  end
   168    169   	if min  ~= 0 then table.insert(spec, string.format("%sm", min))  end
   169    170   	if sec ~= 0 then table.insert(spec, string.format("%ss",  sec)) end
   170    171   	return table.concat(spec, ' ')
   171    172   end
   172    173   return fn