starlit  compile.lua at [52a4f364ac]

File mods/starlit/compile.lua artifact d92d0ca2de part of check-in 52a4f364ac


local lib = starlit.mod.lib

local recentJobs do
	local T,G = lib.marshal.t, lib.marshal.g
	recentJobs = G.array(8, T.str)
end
local function getRecentJobs(state)
	for i,v in ipairs(state.pgm.file.body.conf) do
		if v.key == 'recent' then
			return recentJobs.dec(v.value), i
		end
	end
	return {}
end

local function buyPrintJob(state, invs, cost, scm)
	-- consume consumables
	-- FIXME handle cost.leftovers from partially-consumed items
	for _, v in pairs(cost.consume) do
-- 		assert(v.inv == 1, 'impossible condition (wrong inventory ID)')
		local i = invs[v.inv]
		i.hnd:set_stack(i.list, v.slot, v.remain)
	end
	local output = ItemStack(scm.sw.output):get_definition()
	local fab = scm.sw.input

	local job_t = starlit.store.compilerJob
	local conf = state.pgm.file.body.conf
	table.insert(conf, {
		key='job', value=job_t.enc {
			schematic  = scm.id;
			cyclesLeft = scm.sw.cost and scm.sw.cost.cycles or 0;
			timeLeft   = (fab.time and fab.time.print or 0);
			powerLeft  = cost.power;
			numinaLeft = cost.numina;
		}
	})
end

local function compilerCanPrint(user, cpl, scm)
	local E = starlit.mod.electronics
	local output = ItemStack(scm.sw.output):get_definition()
	local fab = scm.sw.input
	local sw = scm.sw
	local ok, consume, unsat, leftover, itemSpec = fab:seek {
		user.entity:get_inventory():get_list 'main';
	}

	local cost = {
		fab = fab, output = output;
		consume = consume, unsat = unsat, leftover = leftover, itemSpec = itemSpec;
		runtimeEstimate = scm.speed + cpl.speed + (fab.time and fab.time.print or 0);
		power = cpl.powerCost + scm.powerCost
		      + (sw.input.cost and sw.input.cost.power);
		ram = (cpl.cost and cpl.cost.ram or 0)
		    + (scm.cost and scm.cost.ram or 0);
		cycles = (cpl.cost and cpl.cost.cycles or 0)
		       + (scm.cost and scm.cost.cycles or 0);
		numina = fab.cost and fab.cost.numina or 0;
	}

	local userComp = E.chip.sumCompute(
		user.entity:get_inventory():get_list 'starlit_suit_chips'
	)

	if ok and cost.power <= user:suitCharge() and cost.ram <= userComp.ram then
		return true, cost
	else return false, cost end
end

starlit.alg.compilerCanPrint = compilerCanPrint

-- TODO destroy suit interfaces when power runs out or suit/chip is otherwise disabled
starlit.interface.install(starlit.type.ui {
	id = 'starlit:compile-matter-component';
	sub = {
		suit = function(state, user, evt)
			if evt.kind == 'disrobe' then state:close()
			elseif evt.kind == 'power' and evt.mode == 'off' then state:close() end
		end;
		playerInventory = function(state,user)
			-- refresh
		end;
	};
	pages = {
		index = {
			setupState = function(state, user, ctx)
				local E = starlit.mod.electronics
				state.pgm = ctx.program
				state.ctx = ctx
				state.select = {}
				if ctx.context == 'suit' then
					state.fetch = function()
						local cst = user.entity:get_inventory():get_list 'starlit_suit_chips'
						local cl = {order={}, map={}, slot={}}
						for i, c in ipairs(cst) do
							if not c:is_empty() then
								local d = E.chip.read(c)
								local co = {
									stack = c;
									data = d;
								}
								table.insert(cl.order, co)
								cl.map[d.uuid] = co
								cl.slot[i] = co
							end
						end

						-- kill me fam
						if (   state.select.chip
							and state.select.chip ~= true
							and not cl.map[state.select.chip])
						or (state.select.scm
						   and not state.select.scms[state.select.scm])
					   then
							-- chip or pgm no longer available
							user:suitSound 'starlit-error'
							state.select = {}
						end
						state.select.chips = cl

						state.select.scms = {}
						if state.select.chip then
							state.select.scms = E.chip.usableSoftware(cst,nil, function(s)
								if state.select.chip ~= true then
									if cl.slot[s.chipSlot].data.uuid ~= state.select.chip then
										return false
									end
								end
								return s.sw.kind == 'schematic'
							end)
						end

					end
				end
			end;

			onClose = function(state, user)
				user:suitSound 'starlit-quit'
			end;
			refresh = true;
			handle = function(state, user, q)
				local E = starlit.mod.electronics
				if not state.ctx.verify() then state.close() end
				local inv = user.entity:get_inventory()

				local sel = state.select
				state.fetch()
				local chips = state.select.chips
				local function chirp()
					user:suitSound 'starlit-nav'
				end

				-- are we signalling a reprint?
				for k in next, q do
					local idx = k:match'^recent_(%d+)$'
					if idx then
						local jobs = getRecentJobs(state)
						local scmid = jobs[tonumber(idx)]
						if not scmid then return false end
						-- get list of available schematics and make sure this is on it
						local cst = inv:get_list 'starlit_suit_chips'
						local all = E.chip.usableSoftware(cst, nil, function(s)
							return s.sw.kind == 'schematic'
						end)
						local scm
						for i, s in ipairs(all) do
							if s.id == scmid then scm = s goto found end
						end

						::fail:: do
							user:suitSound 'starlit-error'
							return false
						end

						::found::
						local ok,cost = compilerCanPrint(user, state.pgm, scm)
						if not ok then goto fail end
						user:suitSound 'starlit-configure'
						buyPrintJob(state, {{hnd=inv,list='main'}}, cost, scm)
						state.ctx.saveConf()
						return true
					end
				end

				local function trySelection(id)
					if sel[id] == nil then
						for k in next, q do
							local pat = "^"..id.."_(%d+)$" -- ew
							local idx = k:match(pat)
							if idx then
								local cm = tonumber(idx)
								if cm then
									chirp()
									sel[id] = cm
									return true
								end
							end
						end
					end
				end

				if sel.chip == nil then
					if q.showAll then
						chirp()
						sel.chip = true
						return true
					elseif q.find then
						chirp()
						-- TODO
						return true
					end
				end

				if trySelection('chip') then
					return true
				elseif trySelection('scm') then
					return true
				else
					if q.back then
						chirp()
						if sel.input then
							sel.input = nil
						elseif sel.scm then
							sel.scm = nil
						elseif sel.chip then
							sel.chip = nil
						end
						return true
					elseif q.commit then
						if not sel.input then
							chirp()
							sel.input = true
							return true
						else
							local scm = sel.scms[sel.scm]
							local ok, cost = compilerCanPrint(user, state.pgm, scm)
							if ok then
								user:suitSound 'starlit-configure'

								buyPrintJob(state, {{hnd=inv,list='main'}}, cost, scm)

								-- add recent print
								local rctList, pairIdx = getRecentJobs(state)
								if #rctList >= 30 then
									table.remove(rctList)
								end

								if not lib.tbl.has(rctList, scm.id) then
									table.insert(rctList,1,scm.id)
								end

								local conf = state.pgm.file.body.conf
								local encoded = recentJobs.enc(rctList)
								if pairIdx then
									conf[pairIdx].value = encoded
								else
									table.insert(conf, {key='recent', value=encoded})
								end

								-- write chip
								state.ctx.saveConf()
								-- clear selection
								state.select = {}
								return true
							else
								user:suitSound 'starlit-error'
							end
						end
					end
				end

			end;

			render = function(state, user)
				local sel, pgmSelector = state.select, {kind = 'vert'}
				state.fetch()
				state.ctx.pullConf()

				local function pushSelector(tbl, id, item, label, desc, req, w)
					w = w or 12
					local rh = .5
					local label = {kind = 'text', w = w-1.5, h=1.6;
						text = '<global valign=middle>'..lib.str.htsan(label) }
					if req then
						label.h = label.h - rh - .2

						local imgs = {}
						for ci,c in ipairs(req) do
							for ei, e in ipairs(c.list) do
								table.insert(imgs, {kind = 'img', w=rh, h=rh,  img=e.img})
							end
						end
						label = {kind = 'vert', w = w-1.5, h=1.8;
							label;
							{kind ='hztl', w=w-1.5, h=rh; unpack(imgs); }
						}
					end
					table.insert(tbl, {kind = 'hztl', w=w,h=1.8;
						{kind = 'contact', id=id, w=1.5, h=1.5;
							item = item;
							color = {hue=220, sat=0, lum=0};
							desc = desc;
						};
						label;
					})
				end

				local back = {kind = 'button', id='back', label = '<- Back', w=12,h=1.2}
				if sel.chips == nil then
					table.insert(pgmSelector, {kind = 'img', img = 'starlit-ui-alert.png', w=2, h=2})
				elseif sel.chip == nil then
					for i, c in ipairs(sel.chips.order) do
					-- TODO filter out chips without schematics?
						pushSelector(pgmSelector, 'chip_' .. c.data.uuid, c.stack, c.data.label)
					end
					if next(sel.chips.order) then
						table.insert(pgmSelector, {kind = 'hztl', w=12,h=1.5;
							{kind = 'button', w=6,h=1.5; id='showAll', label='Show All'};
							{kind = 'button', w=6,h=1.5; id='find', label='Find'};
						})
					end
				else
					if sel.scm == nil then
						-- l m a o
-- 						pgmSelector.kind = 'hwrap'
-- 						pgmSelector.cols = 2
						local wrap = {kind='hwrap',cols=2,w=12}
						for idx, ent in ipairs(sel.scms) do
							local fab = ent.sw.input
							if fab.flag and fab.flag.print then
								local req = fab:visualize()
								pushSelector(wrap,'scm_' .. idx, ent.sw.output, ent.sw.name, nil, req,6)
							end
						end
						table.insert(pgmSelector, wrap)
						table.insert(pgmSelector, back)
					else
						local scm = sel.scms[sel.scm]
						local output = ItemStack(scm.sw.output):get_definition()
						local fab = scm.sw.input
						local sw = scm.sw
						local function unmet(str)
							return lib.color(1,.3,.3):fmt(str)
						end
						table.insert(pgmSelector, {kind = 'hztl', w=12, h=1.2;
							{kind = 'img', item = sw.output, w=1.2, h=1.2, desc=output.description};
							{kind = 'text', text = string.format('<global valign=middle><b>%s</b>', lib.str.htsan(sw.name)), w=12-1.2,h=1.2};
						})
						local inputTbl = {kind = 'vert', w=6,h=0;
							{kind = 'hbar', w=6, h=.5, text=sel.input and 'Input Plan' or 'Input'}};
						local costTbl = {kind = 'vert', w=6,h=0;
							{kind = 'hbar', w=6, h=.5, text=sel.input and 'Process Plan' or 'Process'}};
						local reqPane = {kind = 'pane', id='reqPane', w=12, h=8;
							{kind = 'hztl', w=12,h=0; inputTbl, costTbl}
						}
						local function pushCost(x, t, val)
							table.insert(costTbl, {kind='label', w=5.5,h=.5,x=x;
								text=string.format('%s: %s',t,val);
							})
						end
						local function pushComputeCosts(header, p)
							if p then
								table.insert(costTbl, {kind = 'label', w=6, h=.5, x=0; text=header});
								if p.cycles then
									pushCost(.5, 'Compute', lib.math.siUI({'cycle','cycles'}, p.cycles, true))
								end
								if p.power then
									local str = lib.math.siUI('J', p.power)
									if p.power > user:suitCharge() then str = unmet(str) end
									pushCost(.5, 'Power', str)
								end
								if p.ram then
									local str = string.format("%s / %s",
										lib.math.siUI('B', p.ram),
										lib.math.siUI('B', state.pgm.comp.ram))
									if p.ram > state.pgm.comp.ram then str = unmet(str) end
									pushCost(.5, 'Memory', str)
								end
							end
						end

						local function fabToUI(x, inputTbl, req)
							for ci,c in ipairs(req) do
								table.insert(inputTbl, {kind = 'label', w=6-x, h=.5, x=x;
									text=lib.str.capitalize(c.header)});
								for ei,e in ipairs(c.list) do
									table.insert(inputTbl, {kind = 'hztl', w=5.5-x, h=.5, x=x+.5;
										{kind='img',   w=.5,h=.5, img=e.img};
										{kind='label', w=4.3,h=.5,x=.2, text=lib.str.capitalize(e.label)};
									});
								end
							end
						end

						local commitHue, commitLabel = 120
						if not sel.input then
							commitLabel = 'Plan'
							fabToUI(0, inputTbl, fab:visualize())
							local function pushComputeCostsSw(header, p)
								if p.sw.cost then
									pushComputeCosts(header, {
										cycles = p.sw.cost.cycles;
										power = p.powerCost;
										ram = p.sw.cost.ram;
									})
								end
							end
							pushComputeCostsSw('Schematic', scm)
							pushComputeCostsSw('Compiler', state.pgm)
						else
							commitLabel = 'Commit'
							pushComputeCosts('Total', {
								cycles = (scm.sw.cost and scm.sw.cost.cycles or 0)
								       + (state.pgm.sw.cost and state.pgm.sw.cost.cycles or 0);
								power = (scm.powerCost or 0)
								      + (state.pgm.powerCost or 0)
								      + (fab.cost and fab.cost.power or 0);
								ram = (scm.sw.cost and scm.sw.cost.ram or 0)
								    + (state.pgm.sw.cost and state.pgm.sw.cost.ram or 0);
							})
							if fab.time and fab.time.print then
								pushCost(0, 'Job Runtime', lib.math.timespec(fab.time.print + scm.speed))
								pushCost(.5, 'Print Time', lib.math.timespec(fab.time.print))
								pushCost(.5, 'CPU Time', lib.math.timespec(scm.speed + state.pgm.speed))
							end
							local ok, compileCost = compilerCanPrint(user, state.pgm, scm)
							fabToUI(0, inputTbl, compileCost.itemSpec:visualize())

							if next(compileCost.unsat) then
								local vis = compileCost.unsat:visualize()
								for si, s in ipairs(vis) do
									s.header = 'Missing ' .. s.header
									for ei, e in ipairs(s.list) do
										e.label = lib.color(1,.2,.2):fmt(e.label)
									end
								end
								fabToUI(0, inputTbl, vis)
							end
							if not ok then commitHue = 0 end
						end
						table.insert(pgmSelector, reqPane)
						table.insert(pgmSelector, {kind = 'hztl', w=10,h=1.2;
							{kind = 'button', id='back', label = '← Back', w=6,h=1.2};
							{kind = 'button', id='commit', label = commitLabel .. ' →', w=6,h=1.2, color={hue=commitHue,sat=0,lum=0}};
						})
					end
				end

				local rec = getRecentJobs(state)
				local recentList = {kind = 'hwrap', cols=3}
				if rec then
					for i,id in ipairs(rec) do
						local out = ItemStack(starlit.item.sw.db[id].output)
						table.insert(recentList, {kind='contact',w=1.5,h=1.5,id=string.format('recent_%s',i), item = out})
					end
				end
				local queue = {kind='pane', w=5, h=9.5}
				local cst = state.ctx.availableChips()
				for i,v in ipairs(state.pgm.file.body.conf) do
					if v.key == 'job' then
						local job = starlit.store.compilerJob.dec(v.value)
						local scm = starlit.mod.electronics.chip.usableSoftware(cst, {{
							id = job.schematic;
							sw = starlit.item.sw.db[job.schematic];
						}})[1]
						if scm == nil then goto badJob end
						local steps = {
							{label = 'Compute', n = job.cyclesLeft, color=lib.color(0,1,0);
								total = scm.sw.cost and scm.sw.cost.cycles or 0};
							{label = 'Energize', n = job.powerLeft, color=lib.color(0,.5,1);
								total = (scm.powerCost or 0)
								      + (state.pgm.powerCost or 0)
								      + (scm.sw.input.cost and scm.sw.input.cost.power or 0)};
							{label = 'Assemble', n = job.timeLeft, color=lib.color(1,.2,0);
								total = scm.sw.input.time and scm.sw.input.time.print or 0};
							{label = 'Numenize', n = job.numinaLeft, color=lib.color(1,0,1);
								total = scm.sw.cost and scm.sw.cost.numina or 0};
						}
						local bar = {kind = 'hbar', text = 'Complete', fac=1, w=5-1.5, h = .5}
						for i, s in ipairs(steps) do
							if s.n > 0 then
								local pct = 1 - s.n/s.total
								bar.color = s.color
								bar.fac = pct
								bar.text = string.format("%s %s%%", s.label, math.floor(pct*100))
								break
							end
						end

						local out = ItemStack(scm.sw.output)
						table.insert(queue, {kind='hztl';  w=6, h=1.5;
							{kind = 'contact', w=1.5,h=1.5, id=string.format('cancel_%s', i), item = out};
							{kind = 'vert', w=6-1.5, h=1.5;
								{kind = 'label', text=out:get_definition().short_description, w=6-1.5, h=.75};
								bar;
							}
						})
					end
					::badJob::
				end

				return starlit.ui.build {
					kind = 'hztl', padding = 0.5; w = 22, h = 11, mode = 'sw';
					{kind = 'vert', w = 5, h = 15;
						{kind = 'hbar', fac=0, w = 5, h = .5, text = '<b><left>Recent Prints</left></b>'};
						recentList;
					};
					{kind = 'vert', w = 12, h = 11;
						{kind = 'hbar', fac=0, w = 12, h = .5, text = '<b>Program Select</b>'};
						{kind = 'pane', w = 12, h = 10.5, id='pgmSelect';
							pgmSelector
						};
					};
					{kind = 'vert', w = 5, h = 11;
						{kind = 'hbar', fac=0, w = 5, h = .5, text = '<b><right>Print Queue</right></b>'};
						queue;
					};
				}
			end;
		};
	};
})