starlit  Check-in [953151446f]

Overview
Comment: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
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | trunk
Files: files | file ages | folders
SHA3-256: 953151446f688fc9771fdc5fe3b2278a6a3981b9ac6e05dc595388f31d0ca482
User & Date: lexi on 2024-05-05 19:31:39
Other Links: manifest | tags
Context
2024-05-06
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
2024-05-04
22:41
add beginnings of matter compiler UI, check in missing files check-in: 0e7832a24c user: lexi tags: trunk
Changes

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

   275    275   -- firstborn ("god-tier"): exceptional
   276    276   
   277    277   local batteryTiers = {
   278    278   	makeshift = {
   279    279   		name = 'Makeshift'; capacity = .5, decay = 3, leak = 2, dischargeRate = 1,
   280    280   		fab = starlit.type.fab {
   281    281   			element = {copper=10};
          282  +			cost = {power = 0.3};
          283  +			time = {print = .25};
   282    284   		};
   283    285   		desc = "Every attosecond this electrical abomination doesn't explode in your face is but the unearned grace of the Wild Gods.";
   284    286   		complexity = 1;
   285    287   		sw = {rarity = 1};
   286    288   	};
   287    289   	imperial  = {
   288    290   		name = 'Imperial'; capacity = 2, decay = 2, leak = 2, dischargeRate = 2; 
   289    291   		fab = starlit.type.fab {
   290    292   			element = {copper=15, iron = 20};
   291    293   			size = { print = 0.1 };
          294  +			cost = {power = 2};
          295  +			time = {print = .5};
   292    296   		};
   293    297   		desc = "The Empire's native technology is a lumbering titan: bulky, inefficient, unreliable, ugly, and awesomely powerful. Their batteries are no exception, with raw capacity and throughput that exceed even Usukinwya designs.";
   294    298   		drm = 1;
   295    299   		complexity = 2;
   296    300   		sw = {rarity = 2};
   297    301   	};
   298    302   	commune   = {
   299    303   		name = 'Commune'; capacity = 1, decay = .5, leak = .2, dischargeRate = 1; 
   300    304   		fab = starlit.type.fab {
   301    305   			element = {vanadium = 50};
   302    306   			metal = {steel=10};
   303    307   			size = { print = 0.05 };
          308  +			cost = {power = 1};
   304    309   		};
   305    310   		desc = "The Commune's proprietary battery designs prioritize reliability, compactness, and maintenance concerns above raw throughput, with an elegance of engineering and design that would make a Su'ikuri cry.";
   306    311   		complexity = 5;
   307    312   		sw = {rarity = 3};
   308    313   	};
   309    314   	usukwinya = {
   310    315   		name = 'Usukwinya'; capacity = 2, decay = 1, leak = 1, dischargeRate = 1.5,
   311    316   		fab = starlit.type.fab {
   312    317   			element = {argon=10};
   313    318   			metal = {vanadium=30};
   314    319   			size = { print = 0.07 };
          320  +			cost = {power = .8};
   315    321   		};
   316    322   		desc = "A race of consummate value engineers, the Usukwinya have spent thousands of years refining their tech to be as cheap to build as possible, without compromising much on quality. The Tradebirds drive an infamously hard bargain, but their batteries are more than worth their meagre cost.";
   317    323   		drm = 2;
   318    324   		sw = {rarity = 10};
   319    325   		complexity = 15;
   320    326   	};
   321    327   	eluthrai  = {
   322    328   		name = 'Eluthrai'; capacity = 3, decay = .4, leak = .1, dischargeRate = 1.5,
   323    329   		fab = starlit.type.fab {
   324    330   			element = {beryllium=20, platinum=20, technetium = 1};
   325    331   			metal = {cinderstone = 10};
   326    332   			size = { print = 0.03 };
          333  +			cost = {power = 10};
          334  +			time = {print = 2};
   327    335   		};
   328    336   		desc = "The uncompromising Eluthrai are never satisfied until every quantifiable characteristic of their tech is maximally optimised down to the picoscale. Their batteries are some of the best in the Reach, and unquestionably the most expensive -- especially for those lesser races trying to copy the designs without the benefit of the sublime autofabricator ecosystem of the Eluthrai themselves.";
   329    337   		complexity = 200;
   330    338   		sw = {rarity = 0}; -- you think you're gonna buy eluthran schematics on SuperDiscountNanoWare.space??
   331    339   	};
   332    340   	firstborn = {
   333    341   		name = 'Firstborn'; capacity = 5, decay = 0.1, leak = 0, dischargeRate = 3;
   334    342   		fab = starlit.type.fab {
   335    343   			element = {neodymium=20, xenon=150, technetium=5};
   336    344   			metal = {sunsteel = 10};
   337    345   			crystal = {astrite = 1};
   338    346   			size = { print = 0.05 };
          347  +			cost = {power = 50};
          348  +			time = {print = 4};
   339    349   		};
   340    350   		desc = "Firstborn engineering seamlessly merges psionic effects with a mastery of the physical universe unattained by even the greatest of the living Starsouls. Their batteries reach levels of performance that strongly imply Quantum Gravity Theory -- and several major holy books -- need to be rewritten. From the ground up.";
   341    351   		complexity = 1000;
   342    352   		sw = {rarity = 0}; -- lol no
   343    353   	};
   344    354   }
   345    355   
   346    356   local batterySizes = {
   347         -	small = {name = 'Small', capacity = .5, dischargeRate =  .5, complexity = 1, matMult = .5, fab = starlit.type.fab {size={print=0.1}}};
   348         -	mid   = {                capacity =  1, dischargeRate =   1, complexity = 1, matMult = 1, fab = starlit.type.fab {size={print=0.3}}};
   349         -	large = {name = 'Large', capacity =  2, dischargeRate = 1.5, complexity = 1, matMult = 1.5, fab = starlit.type.fab {size={print=0.5}}};
   350         -	huge  = {name = 'Huge',  capacity =  3, dischargeRate =   2, complexity = 1, matMult = 2, fab = starlit.type.fab {size={print=0.8}}};
          357  +	small = {name = 'Small', capacity = .5, dischargeRate =  .5, complexity = 1, matMult = .5,
          358  +		fab = starlit.type.fab {size={print=0.1},cost={power=.5},time={print=25}}};
          359  +	mid   = {                capacity =  1, dischargeRate =   1, complexity = 1, matMult = 1,
          360  +		fab = starlit.type.fab {size={print=0.3},cost={power=1},time={print=40}}};
          361  +	large = {name = 'Large', capacity =  2, dischargeRate = 1.5, complexity = 1, matMult = 1.5,
          362  +		fab = starlit.type.fab {size={print=0.5},cost={power=2},time={print=60}}};
          363  +	huge  = {name = 'Huge',  capacity =  3, dischargeRate =   2, complexity = 1, matMult = 2,
          364  +		fab = starlit.type.fab {size={print=0.8},cost={power=8},time={print=120}}};
   351    365   }
   352    366   
   353    367   local batteryTypes = {
   354    368   	supercapacitor = {
   355    369   		name = 'Supercapacitor';
   356    370   		desc = 'Room-temperature superconductors make for very reliable, high-dischargeRate, but low-capacity batteries.';
   357    371   		fab = starlit.type.fab {
   358    372   			metal = { enodium = 5 };
   359    373   			size = {print=0.8};
          374  +			cost = {power = 1e3};
   360    375   		};
   361    376   		sw = {
   362    377   			cost = {
   363         -				cycles = 5e9; -- 5 bil cycles
   364         -				ram = 10e9; -- 10GB
          378  +				cycles = 48e9; -- 48 bil cycles
          379  +				ram = 4e9; -- 10GB
   365    380   			};
   366    381   			pgmSize = 2e9; -- 2GB
   367    382   			rarity = 5;
   368    383   		};
   369    384   		capacity = 50e3, dischargeRate = 1000;
   370    385   		leak = 0, decay = 1e-6;
   371    386   
................................................................................
   373    388   	};
   374    389   	chemical = {
   375    390   		name = 'Chemical';
   376    391   		desc = '';
   377    392   		fab = starlit.type.fab {
   378    393   			element = { lithium = 3 };
   379    394   			size = {print=1.0};
          395  +			cost = {power = .5e3};
   380    396   		};
   381    397   		sw = {
   382    398   			cost = {
   383         -				cycles = 1e9; -- 1 bil cycles
   384         -				ram = 2e9; -- 2GB
          399  +				cycles = 16e9; -- 16 bil cycles
          400  +				ram = 1e9; -- 1GB
   385    401   			};
   386    402   			pgmSize = 512e6; -- 512MB
   387    403   			rarity = 2;
   388    404   		};
   389    405   		capacity = 200e3, dischargeRate = 200;
   390    406   		leak = 0.2, decay = 1e-2;
   391    407   		complexity = 1;
................................................................................
   393    409   	carbon = {
   394    410   		name = 'Carbon';
   395    411   		desc = 'Carbon nanotubes form the basis of many important metamaterials, chief among them power-polymer.';
   396    412   		capacity = 1;
   397    413   		fab = starlit.type.fab {
   398    414   			element = { carbon = 40 };
   399    415   			size = {print=0.5};
          416  +			cost = {power = 2.5e3};
   400    417   		};
   401    418   		sw = {
   402    419   			cost = {
   403         -				cycles = 50e9; -- 50 bil cycles
   404         -				ram = 64e9; -- 64GB
          420  +				cycles = 256e9; -- 256 bil cycles
          421  +				ram = 16e9; -- 64GB
   405    422   			};
   406         -			pgmSize = 1e9; -- 1GB
          423  +			pgmSize = 4e9; -- 4GB
   407    424   			rarity = 10;
   408    425   		};
   409    426   		capacity = 100e3, dischargeRate = 500;
   410    427   		leak = 0.1, decay = 1e-3;
   411    428   		complexity = 10;
   412    429   	};
   413    430   	hybrid = {
................................................................................
   416    433   		capacity = 1;
   417    434   		fab = starlit.type.fab {
   418    435   			element = {
   419    436   				lithium = 10;
   420    437   				carbon = 20;
   421    438   			};
   422    439   			size = {print=1.5};
          440  +			cost = {power = 10e3};
   423    441   		};
   424    442   		sw = {
   425    443   			cost = {
   426         -				cycles = 65e9; -- 65 bil cycles
   427         -				ram = 96e9; -- 96GB
          444  +				cycles = 512e9; -- 512 bil cycles
          445  +				ram = 24e9; -- 96GB
   428    446   			};
   429         -			pgmSize = 5e9; -- 5GB
          447  +			pgmSize = 7e9; -- 7GB
   430    448   			rarity = 15;
   431    449   		};
   432    450   		capacity = 300e3, dischargeRate = 350;
   433    451   		leak = 0.3, decay = 1e-5;
   434    452   		complexity = 30;
   435    453   	};
   436    454   }
   437    455   
          456  +--[[
   438    457   local function elemath(dest, src, mult)
   439    458   	dest = dest or {}
   440    459   	for k,v in pairs(src) do
   441    460   		if not dest[k] then dest[k] = 0 end
   442    461   		dest[k] = dest[k] + v*mult
   443    462   	end
   444    463   	return dest
   445         -end
          464  +end]]
   446    465   
   447    466   for bTypeName, bType in pairs(batteryTypes) do
   448    467   for bTierName, bTier in pairs(batteryTiers) do
   449    468   for bSizeName, bSize in pairs(batterySizes) do
   450    469   	-- elemath(elementCost, bType.fab.element or {}, bSize.matMult)
   451    470   	-- elemath(elementCost, bTier.fab.element or {}, bSize.matMult)
   452    471   	-- elemath(metalCost, bType.fab.metal or {}, bSize.matMult)
................................................................................
   764    783   	})
   765    784   end)
   766    785   
   767    786   -- in case other mods want to define their own tiers
   768    787   E.chip.tiers = lib.registry.mk 'starlit_electronics:chipTiers'
   769    788   E.chip.tiers.meld {
   770    789   	-- GP chips
   771         -	tiny    = {name = 'Tiny Chip', clockRate = 512e3, flash = 4096, ram = 1024, powerEfficiency = 1e9, size = 1};
   772         -	small   = {name = 'Small Chip', clockRate = 128e6, flash = 512e6, ram = 512e6, powerEfficiency = 1e8, size = 3};
   773         -	med     = {name = 'Chip', clockRate = 1e9, flash = 4e9, ram = 4e9, powerEfficiency = 1e7, size = 6};
   774         -	large   = {name = 'Large Chip', clockRate = 2e9, flash = 8e9, ram = 8e9, powerEfficiency = 1e6, size = 8};
          790  +	tiny    = {name = 'Tiny Chip', clockRate = 512e3, flash = 4096, ram = 1024, powerEfficiency = 1e10, size = 1};
          791  +	small   = {name = 'Small Chip', clockRate = 128e6, flash = 512e6, ram = 512e6, powerEfficiency = 1e9, size = 3};
          792  +	med     = {name = 'Chip', clockRate = 1e9, flash = 4e9, ram = 4e9, powerEfficiency = 1e8, size = 6};
          793  +	large   = {name = 'Large Chip', clockRate = 2e9, flash = 8e9, ram = 8e9, powerEfficiency = 1e7, size = 8};
   775    794   	-- specialized chips
   776         -	compute = {name = 'Compute Chip', clockRate = 4e9, flash = 24e6, ram = 64e9, powerEfficiency = 1e8, size = 4};
   777         -	data    = {name = 'Data Chip', clockRate = 128e3, flash = 2e12, ram = 32e3, powerEfficiency = 1e5, size = 4};
   778         -	lp      = {name = 'Low-Power Chip', clockRate = 128e6, flash = 64e6, ram = 1e9, powerEfficiency = 1e10, size = 4};
   779         -	carbon  = {name = 'Carbon Chip', clockRate = 64e6, flash = 32e6, ram = 2e6, powerEfficiency = 2e9, size = 2, circ='carbon'};
          795  +	compute = {name = 'Compute Chip', clockRate = 4e9, flash = 24e6, ram = 64e9, powerEfficiency = 1e9, size = 4};
          796  +	data    = {name = 'Data Chip', clockRate = 128e3, flash = 2e12, ram = 32e3, powerEfficiency = 1e6, size = 4};
          797  +	lp      = {name = 'Low-Power Chip', clockRate = 128e6, flash = 64e6, ram = 1e9, powerEfficiency = 1e11, size = 4};
          798  +	carbon  = {name = 'Carbon Chip', clockRate = 64e6, flash = 32e6, ram = 2e6, powerEfficiency = 2e10, size = 2, circ='carbon'};
   780    799   }
   781    800   
   782    801   E.chip.tiers.foreach('starlit_electronics:genChips', {}, function(id, t)
   783    802   	id = t.id or string.format('%s:chip_%s', minetest.get_current_modname(), id)
   784    803   	local circMat = t.circ or 'silicon';
   785    804   	starlit.item.chip.link(id, {
   786    805   		name = t.name;
................................................................................
   792    811   			flag = {
   793    812   				silicompile = true;
   794    813   			};
   795    814   			time = {
   796    815   				silicompile = t.size * 24*60;
   797    816   			};
   798    817   			cost = {
   799         -				energy = 50e3 + t.size * 15e2;
          818  +				power = 50e3 + t.size * 15e2;
   800    819   			};
   801    820   			element = {
   802    821   				[circMat] = 50 * t.size;
   803    822   				copper = 30;
   804    823   				gold = 15;
   805    824   			};
   806    825   		};

Modified mods/starlit-electronics/sw.lua from [430c447d0b] to [da2672ba3c].

   136    136   	};
   137    137   	run = shredder{range=3, powerDraw=200};
   138    138   })
   139    139   
   140    140   starlit.item.sw.link('starlit_electronics:compile_commune', {
   141    141   	name = 'Compile Matter';
   142    142   	kind = 'suitPower', powerKind = 'direct';
   143         -	desc = "A basic suit matter compiler program, rather slow but ruthlessly optimized for power- and memory-efficiency by some of the Commune's most fanatic coders.";
          143  +	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    144   	size = 700e3;
   145    145   	cost = {
   146         -		cycles = 300e6;
   147         -		ram = 2e9;
          146  +		cycles = 4e9;
          147  +		ram = .3e9;
   148    148   	};
   149    149   	ui = 'starlit:compile-matter-component';
   150    150   	run = function(user, ctx)
   151    151   	end;
   152    152   })
   153    153   
   154    154   starlit.item.sw.link('starlit_electronics:compile_block_commune', {
   155    155   	name = 'Compile Block';
   156    156   	kind = 'suitPower', powerKind = 'active';
   157    157   	desc = "An advanced suit matter compiler program, capable of printing complete devices and structure parts directly into the world.";
   158    158   	size = 5e6;
   159    159   	cost = {
   160         -		cycles = 700e6;
   161         -		ram = 4e9;
          160  +		cycles = 8e9;
          161  +		ram = 1e9;
   162    162   	};
   163    163   	ui = 'starlit:compile-matter-block';
   164    164   	run = function(user, ctx)
   165    165   	end;
   166    166   })
          167  +
          168  +starlit.item.sw.link('starlit_electronics:compile_imperial', {
          169  +	name = 'Genesis Deluxe';
          170  +	kind = 'suitPower', powerKind = 'direct';
          171  +	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  +	size = 2e4;
          173  +	cost = {
          174  +		cycles = 100e6;
          175  +		ram = 1.5e9;
          176  +	};
          177  +	ui = 'starlit:compile-matter-component';
          178  +	run = function(user, ctx)
          179  +	end;
          180  +})
   167    181   
   168    182   do local J = starlit.store.compilerJob
   169    183   	starlit.item.sw.link('starlit_electronics:driver_compiler_commune', {
   170    184   		name = 'Matter Compiler';
   171    185   		kind = 'driver';
   172    186   		desc = "A driver for a standalone matter compiler, suitable for building larger components than your suit alone can handle.";
   173    187   		size = 850e3;
   174    188   		cost = {
   175    189   			cycles = 400e6;
   176         -			ram = 2e9;
          190  +			ram = .2e9;
   177    191   		};
   178    192   		ui = 'starlit:device-compile-matter-component';
   179    193   		run = function(user, ctx)
   180    194   		end;
   181    195   		bgProc = function(user, ctx, interval, runState)
   182    196   			if runState.flags.compiled == true then return false end
   183    197   			-- only so many nanides to go around

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

   106    106   	social = {
   107    107   		empire = 'workingClass';
   108    108   		commune = 'metic';
   109    109   	};
   110    110   
   111    111   	startingItems = {
   112    112   		suit = ItemStack('starlit_suit:suit_survival_commune');
   113         -		suitBatteries = {battery 'starlit_electronics:battery_carbon_commune_small'};
          113  +		suitBatteries = {battery 'starlit_electronics:battery_carbon_commune_mid'};
   114    114   		suitChips = {
   115    115   			chipLibrary.survivalware;
   116    116   			-- you didn't notice it earlier, but your Commune environment suit
   117    117   			-- came with this chip already plugged in. it's apparently true
   118    118   			-- what they say: the Commune is always prepared for everything.
   119    119   			-- E V E R Y T H I N G.
   120    120   		};
................................................................................
   121    121   		suitGuns = {};
   122    122   		suitAmmo = {};
   123    123   		suitCans = {
   124    124   -- 			ItemStack('starlit_material:canister_small');
   125    125   			volume('liquid', 'water', 5);
   126    126   		};
   127    127   		carry = {
          128  +			battery 'starlit_electronics:battery_carbon_commune_small';
   128    129   			chipLibrary.compendium;
   129    130   			-- you bought this on a whim before you left the Empire, and
   130    131   			-- just happened to still have it on your person when everything
   131    132   			-- went straight to the Wild Gods' privy
   132    133   		};
   133    134   	};
   134    135   })

Modified mods/starlit/element.lua from [25b10aa9d6] to [f0333e36f4].

     3      3   local M = W.material
     4      4   
     5      5   M.element.foreach('starlit:sort', {}, function(id, m)
     6      6   	if m.metal then
     7      7   		M.metal.link(id, {
     8      8   			name = m.name;
     9      9   			composition = starlit.type.fab{element = {[id] = 1}};
    10         -			color = m.color;
    11     10   			-- n.b. this is a RATIO: it will be appropriately multiplied
    12         -			-- for the object in question; e.g a normal chunk will be
    13         -			-- 100 $element, an ingot will be 1000 $element
           11  +			-- for the object in question
           12  +			color = m.color;
           13  +			elemental = m.name;
    14     14   		})
    15     15   	elseif m.gas then
    16     16   		M.gas.link(id, {
    17     17   			name = m.name;
    18     18   			composition = starlit.type.fab{element = {[id] = 1}};
    19     19   			density = m.density;
           20  +			elemental = m.name;
    20     21   		})
    21     22   	elseif m.liquid then
    22     23   		M.liquid.link(id, {
    23     24   			name = m.name;
    24     25   			composition = starlit.type.fab{element = {[id] = 1}};
    25     26   			density = m.density;
           27  +			elemental = m.name;
    26     28   		})
    27     29   	end
    28     30   end)
    29     31   
    30     32   local F = string.format
    31     33   
    32     34   local function mkEltIndicator(composition)
................................................................................
    53     55   			indsz, indsz,
    54     56   			indicator);
    55     57   	end
    56     58   end
    57     59   
    58     60   M.element.foreach('starlit:gen-forms', {}, function(id, m)
    59     61   	local eltID = F('%s:element_%s', minetest.get_current_modname(), id)
    60         -	local eltName = F('Elemental %s', lib.str.capitalize(m.name))
           62  +-- 	local eltName = F('Elemental %s', lib.str.capitalize(m.name))
    61     63   	local tt = function(t, d, g)
    62     64   		return starlit.ui.tooltip {
    63     65   			title = t, desc = d;
    64     66   			color = lib.color(0.1,0.2,0.1);
    65     67   			props = {
    66     68   				{title = 'Mass', desc = lib.math.si('g', g), affinity='info'}
    67     69   			}
................................................................................
    90     92   			_starlit = {
    91     93   				mass = 1;
    92     94   				material = {
    93     95   					kind = 'element';
    94     96   					element = id;
    95     97   				};
    96     98   				fab = starlit.type.fab {
           99  +					flag = {smelt = m.metal and true or nil};
    97    100   					element = comp;
    98    101   				};
    99    102   			};
   100    103   		});
   101    104   	end
   102    105   
   103    106   	--[[
................................................................................
   121    124   		};
   122    125   	});
   123    126   	]]
   124    127   end)
   125    128   
   126    129   
   127    130   M.metal.foreach('starlit:gen-forms', {}, function(id, m)
   128         -	local baseID = F('%s:metal_%s_', minetest.get_current_modname(), id)
   129         -	local ingotID = baseID .. 'ingot'
   130         -	local ingotName = F('%s Ingot', lib.str.capitalize(m.name))
   131         -	m.form = m.form or {}
   132         -	m.form.ingot = ingotID
   133         -	local tt = function(t, d, g)
   134         -		return starlit.ui.tooltip {
   135         -			title = t, desc = d;
   136         -			color = lib.color(0.1,0.1,0.1);
   137         -			props = {
   138         -				{title = 'Mass', desc = lib.math.si('g', g), affinity='info'}
          131  +	if m.elemental then -- avoid multiple forms for same material
          132  +		m.form = M.element.db[m.elemental].form;
          133  +	else
          134  +		local baseID = F('%s:metal_%s_', minetest.get_current_modname(), id)
          135  +		local brickID = baseID .. 'brick'
          136  +		local brickName = F('%s Brick', lib.str.capitalize(m.name))
          137  +		m.form = m.form or {}
          138  +		m.form.brick = brickID
          139  +		local tt = function(t, d, g)
          140  +			return starlit.ui.tooltip {
          141  +				title = t, desc = d;
          142  +				color = lib.color(0.1,0.1,0.1);
          143  +				props = {
          144  +					{title = 'Mass', desc = lib.math.si('g', g), affinity='info'}
          145  +				}
   139    146   			}
   140         -		}
   141         -	end
   142         -	local mcomp = m.composition:elementalize().element
   143         -	local function comp(n)
   144         -		local t = {}
   145         -		for id, amt in pairs(mcomp) do
   146         -			t[id] = amt * n
          147  +		end
          148  +		local mcomp = m.composition:elementalize().element
          149  +		local function comp(n)
          150  +			local t = {}
          151  +			for id, amt in pairs(mcomp) do
          152  +				t[id] = amt * n
          153  +			end
          154  +			return t
   147    155   		end
   148         -		return t
   149         -	end
   150         -	local iblit = mkEltIndicator(mcomp)
   151         -	local function img(s)
   152         -		return iblit(s:colorize(m.color):render())
   153         -	end
          156  +		local iblit = mkEltIndicator(mcomp)
          157  +		local function img(s)
          158  +			return iblit(s:colorize(m.color):render())
          159  +		end
   154    160   
   155         -	minetest.register_craftitem(ingotID, {
   156         -		short_description = ingotName;
   157         -		description = tt(ingotName, F('A solid ingot of %s, ready to be worked by a large matter compiler', m.name), 1e3);
   158         -		inventory_image = img(lib.image('starlit-item-ingot.png'));
   159         -		wield_image = lib.image 'starlit-item-ingot.png':colorize(m.color):render();
   160         -		groups = {metal = 1, ingot = 1};
   161         -		stack_max = 5;
   162         -		_starlit = {
   163         -			mass = 1e3;
   164         -			material = {
   165         -				kind = 'metal';
   166         -				metal = id;
          161  +		local mass = 1
          162  +		minetest.register_craftitem(brickID, {
          163  +			short_description = brickName;
          164  +			description = tt(brickName, F('A small brick of %s, ready to be worked by a matter compiler', m.name), mass);
          165  +			inventory_image = img(lib.image('starlit-item-brick.png'));
          166  +			wield_image = lib.image 'starlit-item-brick.png':colorize(m.color):render();
          167  +			groups = {metal = 1, brick = 1};
          168  +			stack_max = 500;
          169  +			_starlit = {
          170  +				mass = mass;
          171  +				material = {
          172  +					kind = 'metal';
          173  +					metal = id;
          174  +				};
          175  +				fab = starlit.type.fab {
          176  +					flag = {smelt=true};
          177  +					element = comp(1e3);
          178  +				};
   167    179   			};
   168         -			fab = starlit.type.fab {
   169         -				flag = {smelt=true};
   170         -				element = comp(1e3);
   171         -			};
   172         -		};
   173         -	});
   174         -
          180  +		});
          181  +	end
   175    182   
   176    183   end)
   177    184   
   178    185   local canisterMeta = lib.marshal.metaStore {
   179    186   	contents = {key = 'starlit:canister_contents', type = starlit.store.volume};
   180    187   }
   181    188   
................................................................................
   227    234   		short_description = c.name;
   228    235   		description = canisterDesc(nil, c);
   229    236   		inventory_image = c.image or 'starlit-item-element-canister.png';
   230    237   		groups = {canister = 1};
   231    238   		stack_max = 1;
   232    239   		_starlit = {
   233    240   			canister = c;
   234         -			container = {
   235         -				handle = function(stack, oldstack)
   236         -					stack:get_meta():set_string('description', canisterDesc(stack))
   237         -					return stack
   238         -				end;
   239         -				list = {
   240         -					elem = {
   241         -						key = 'starlit:canister_elem';
   242         -						accept = 'powder';
   243         -						sz = c.slots;
   244         -					};
   245         -				};
   246         -			};
   247    241   		};
   248    242   	})
   249    243   end)
   250    244   
   251    245   function starlit.item.canister.contents(st)
   252    246   	local m = canisterMeta(st)
   253    247   	return m.read 'contents'

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

    12     12   --    * used for determining quantities. that is,
    13     13   --			f*x = spec to make x instances of f
    14     14   --
    15     15   --    new fab fields must be defined in starlit.type.fab.fields.
    16     16   --    this maps a name to fn(a,b,n) -> quant, where a is the first
    17     17   --    argument, b is a compounding amount, and n is a quantity of
    18     18   --    items to produce. fields that are unnamed will be underwritten
           19  +
           20  +local fab
    19     21   
    20     22   local function fQuant(a,b,n) return ((a or 0)+(b or 0))*n end
    21     23   local function fFac  (a,b,n)
    22     24   	if a == nil and b == nil then return nil end
    23     25   	local f if a == nil or b == nil then
    24     26   		f = a or b
    25     27   	else
................................................................................
    29     31   end
    30     32   local function fReq  (a,b,n) return a or b         end
    31     33   local function fFlag (a,b,n) return a and b        end
    32     34   local function fSize (a,b,n) return math.max(a,b)  end
    33     35   
    34     36   local F = string.format
    35     37   local lib = starlit.mod.lib
           38  +
           39  +local function fRawMat(class)
           40  +	return function(x,n,stack)
           41  +		local def = stack:get_definition()._starlit
           42  +		if not def.material then return 0 end
           43  +		local mf = fab {[def.material.kind] = {[def.material[def.material.kind]] = def.mass}}
           44  +
           45  +--		this is bugged: the same item can satisfy both e.g. metal.steel and element.fe
           46  +-- 		if not (mf[class] and mf[class][x]) then
           47  +-- 			mf = mf:elementalize()
           48  +			if not (mf[class] and mf[class][x]) then return 0 end
           49  +-- 		end
           50  +
           51  +		local perItem = mf[class][x]
           52  +		local wholeStack = perItem * stack:get_count()
           53  +
           54  +		local deduct = ItemStack()
           55  +		local taken = 0 repeat
           56  +			taken = taken + perItem
           57  +			deduct:add_item(stack:take_item(1))
           58  +		until taken >= n or stack:is_empty()
           59  +		return taken, deduct
           60  +
           61  +		--[[  outsmarted myself with this one :/
           62  +		local fab = def.recover or def.fab
           63  +		-- we ignore recover_vary bc this needs to be deterministic
           64  +		local function tryFab(fab)
           65  +			if not fab then return 0 end
           66  +			if fab[class] and fab[class][x] then
           67  +				local perItem = fab[class][x]
           68  +				local wholeStack = perItem * stack:get_count()
           69  +				print('fab has substance', n, perItem, wholeStack)
           70  +				local deduct = ItemStack()
           71  +				local taken = 0 repeat
           72  +					taken = taken + perItem
           73  +					deduct:add_item(stack:take_item(1))
           74  +				until taken >= n
           75  +				return taken, deduct
           76  +			end
           77  +			return 0
           78  +		end
           79  +		local z,c = tryFab(fab)
           80  +		if z == 0 then -- does it work if we break down the constituent compounds?
           81  +			z,c = tryFab(fab:elementalize())
           82  +		end]]
           83  +	end
           84  +end
           85  +local function fCanister(class)
           86  +	return function(x, n, stack)
           87  +		local amt, deduct = 0
           88  +		return amt, deduct
           89  +	end
           90  +end
    36     91   
    37     92   local fields = {
    38     93   	-- fabrication eligibility will be determined by which kinds
    39     94   	-- of input a particular fabricator can introduce. e.g. a
    40     95   	-- printer with a  but no cache can only print items whose
    41     96   	-- recipe only names elements as ingredients
    42     97   	element = {
    43     98   		name = {"element", "elements"};
    44     99   		string = function(x, n, long)
    45    100   			local el = starlit.world.material.element.db[x]
    46         -			return lib.math.si('g', n) .. ' ' .. ((not long and el.sym) or el.name)
          101  +			return lib.math.siUI('g', n) .. ' ' .. ((not long and el.sym) or el.name)
    47    102   		end;
    48    103   		image = function(x, n)
    49    104   			return string.format('starlit-element-%s.png', x)
    50    105   		end;
          106  +		inventory = fRawMat 'element';
    51    107   		op = fQuant;
    52    108   	};
    53    109   	metal ={
    54    110   		name = {"metal", "metals"};
    55    111   		string = function(x, n)
    56    112   			local met = starlit.world.material.metal.db[x]
    57         -			return lib.math.si('g', n) .. ' ' .. met.name
          113  +			return lib.math.siUI('g', n) .. ' ' .. met.name
    58    114   		end;
    59    115   		image = function(x, n)
    60    116   			local met = starlit.world.material.metal.db[x]
    61         -			return ItemStack(met.form.ingot):get_definition().inventory_image
          117  +			return ItemStack(met.form.brick):get_definition().inventory_image
    62    118   		end;
          119  +		inventory = fRawMat 'metal';
    63    120   		op = fQuant;
    64    121   	};
    65    122   	liquid = {
    66    123   		name = {"liquid", "liquids"};
    67    124   		string = function(x, n)
    68    125   			local liq = starlit.world.material.liquid.db[x]
    69         -			return lib.math.si('L', n) .. ' ' .. liq.name
          126  +			return lib.math.siUI('L', n) .. ' ' .. liq.name
    70    127   		end;
          128  +		inventory = fCanister 'liquid';
    71    129   		op = fQuant;
    72    130   	};
    73    131   	gas = {
    74    132   		name = {"gas", "gasses"};
    75    133   		string = function(x, n)
    76    134   			local gas = starlit.world.material.gas.db[x]
    77         -			return lib.math.si('g', n) .. ' ' .. gas.name
          135  +			return lib.math.siUI('g', n) .. ' ' .. gas.name
    78    136   		end;
          137  +		inventory = fCanister 'gas';
    79    138   		op = fQuant;
    80    139   	};
    81    140   -- 	crystal = {
    82    141   -- 		op = fQuant;
    83    142   -- 	};
    84    143   	item = {
    85    144   		name = {"item", "items"};
    86    145   		string = function(x, n)
    87    146   			local i = minetest.registered_items[x]
    88    147   			return tostring(n) .. 'x ' .. i.short_description
    89    148   		end;
          149  +		image = function(x, n)
          150  +			return ItemStack(x):get_definition().inventory_image
          151  +		end;
          152  +		inventory = function(x, n, stack)
          153  +			x = ItemStack(x)
          154  +			if not x:equals(stack) then return nil end
          155  +			local deduct = stack:take_item(x:get_count() * n)
          156  +			return deduct:get_count(), deduct
          157  +		end;
    90    158   	};
    91    159   
    92    160   	-- factors
    93    161   
    94         -	cost = {op=fFac}; -- units vary
          162  +	cost = {
          163  +		name = {"cost", "costs"};
          164  +		op=fFac; -- units vary
          165  +		string = function(x,n)
          166  +			local units = {
          167  +				power = 'J';
          168  +			}
          169  +			local s
          170  +			if units[x] then
          171  +				s = lib.math.siUI(units[x], n)
          172  +			elseif starlit.world.stats[x] then
          173  +				s = starlit.world.stats[x].desc(n)
          174  +			else
          175  +				s = tostring(n)
          176  +			end
          177  +			return string.format('%s: %s',x,s)
          178  +		end;
          179  +		image = function(x,n)
          180  +			local icons = {
          181  +				power = 'starlit-ui-icon-stat-power.png';
          182  +				numina = 'starlit-ui-icon-stat-numina.png'
          183  +			}
          184  +			return icons[x]
          185  +		end;
          186  +	};
    95    187   	time = {op=fFac}; -- (s)
    96    188   		-- print: base printing time
    97    189   	size = {op=fSize};
    98    190   		-- printBay: size of the printer bay necessary to produce the item
    99    191   	req  = {op=fReq};
   100    192   	flag = {op=fFlag}; -- means that can be used to produce the item & misc flags
   101    193   		-- print: allow production with a printer
   102    194   		-- smelt: allow production with a smelter
   103    195   	-- all else defaults to underwrite
   104    196   }
   105    197   
   106    198   local order = {
   107         -	'element', 'metal', 'liquid', 'gas', 'item'
          199  +	'element', 'metal', 'liquid', 'gas', 'item',
          200  +	'cost'
   108    201   }
   109    202   
   110    203   local lib = starlit.mod.lib
   111    204   
   112         -local fab fab = lib.class {
          205  +fab = lib.class {
   113    206   	__name = 'starlit:fab';
   114    207   	
   115    208   	fields = fields;
   116    209   	order = order;
   117    210   	construct = function(q) return q end;
   118    211   	__index = {
   119    212   		elementalize = function(self)
   120    213   			local e = fab {element = self.element or {}}
   121    214   			for _, kind in pairs {'metal', 'gas', 'liquid'} do
   122    215   				for m,mass in pairs(self[kind] or {}) do
   123         -					local mc = starlit.world.material[kind][m].composition
          216  +					local mc = starlit.world.material[kind].db[m].composition
   124    217   					e = e + mc:elementalize()*mass
   125    218   				end
   126    219   			end
   127    220   			return e
   128    221   		end;
   129    222   
   130    223   		elementSeq = function(self)
................................................................................
   204    297   				if next(t) then table.insert(all, {
   205    298   					id=o, list=t;
   206    299   					header=fields[o].name[t[2] and 2 or 1];
   207    300   				}) end
   208    301   			end
   209    302   			return all
   210    303   		end;
          304  +		seek = function(self, invs)
          305  +			local consumed = {}
          306  +			local spec = fab{item={}} -- used to generate a convenient visualization
          307  +			local unsatisfied = fab{}
          308  +			local cache = {}
          309  +			local leftover = fab{}
          310  +			local function alreadyGot(inv,slot)
          311  +				local already = cache[inv] and cache[inv][slot] and true
          312  +				if cache[inv] == nil then cache[inv] = {} end
          313  +				cache[inv][slot] = true
          314  +				return already
          315  +			end
          316  +			for ci, cat in ipairs(order) do
          317  +				local scan = fields[cat].inventory
          318  +				if scan and self[cat] then
          319  +					for substance, amt in pairs(self[cat]) do
          320  +-- 						print('check substance', substance, amt, dump(self[cat]))
          321  +						local amtFound = 0
          322  +						local stacks = {}
          323  +						for ii, inv in ipairs(invs) do
          324  +-- 							print('  - check inventory',ii,inv,'for',cat,substance,amt)
          325  +							for oi, o in ipairs(inv) do
          326  +-- 								print('    - check stack', oi, o)
          327  +								local st = ItemStack(o)
          328  +								if not st:is_empty() then
          329  +									local avail, deduct = scan(substance,amt,st)
          330  +									if avail > 0 then
          331  +										amtFound = amtFound + avail
          332  +-- 										print('       - found amt', amtFound,ii,oi)
          333  +										if not alreadyGot(ii,oi) then
          334  +											local sv = {
          335  +												inv=ii, slot=oi;
          336  +												consume=deduct, remain=st;
          337  +												satisfy=fab{[cat]={[substance]=avail}}
          338  +											}
          339  +											table.insert(stacks, sv)
          340  +										end
          341  +										if amtFound >= amt then goto suffice end
          342  +									end
          343  +								end
          344  +							end
          345  +						end
          346  +
          347  +						::insufficient:: do -- record the failure and move on
          348  +							if unsatisfied[cat] == nil then unsatisfied[cat] = {} end
          349  +							unsatisfied[cat][substance] = amt-amtFound
          350  +						end
          351  +
          352  +						::suffice:: -- commit the stack diff
          353  +						for si,sv in ipairs(stacks) do
          354  +-- 							table.insert(consumed, sv)
          355  +							local di = ItemStack(sv.consume)
          356  +							local din = ItemStack(sv.consume):get_name()
          357  +							if not spec.item[din] then spec.item[din] = 0 end
          358  +							spec.item[din] = spec.item[din] + di:get_count()
          359  +							local lo = amtFound-amt if lo > 0 then
          360  +								leftover = leftover + fab{[cat]={[substance]=lo}}
          361  +							end
          362  +						end
          363  +
          364  +					end
          365  +				end
          366  +			end
          367  +			return (next(unsatisfied) == nil), consumed, unsatisfied, leftover, spec
          368  +		end;
   211    369   	};
   212    370   
   213    371   	__tostring = function(self)
   214    372   		local t = {}
   215    373   		for i,o in ipairs(order) do
   216    374   			if self[o] and fields[o].string then
   217    375   				for mat,amt in pairs(self[o]) do

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

    32     32   			safe = 4;
    33     33   			overheat = 32;
    34     34   			boiling = 100;
    35     35   			thermalConductivity = 0.05; -- κ
    36     36   		};
    37     37   		rad = {
    38     38   		};
           39  +
           40  +		phys = {
           41  +			--- HACK HACK HAAAAAAAAAAACK
           42  +			engineGravity = minetest.settings:get('movement_gravity') or 9.81
           43  +		};
    39     44   	};
    40     45   
    41     46   	activeUsers = {
    42     47   		-- map of username -> user object
    43     48   	};
    44     49   	activeUI = {
    45     50   		-- map of username -> UI context
................................................................................
   272    277   
   273    278   starlit.include 'element'
   274    279   
   275    280   starlit.include 'terrain'
   276    281   starlit.include 'interfaces'
   277    282   starlit.include 'suit'
   278    283   
   279         -minetest.settings:set('movement_gravity', starlit.world.planet.gravity) -- ??? seriously???
          284  +-- minetest.settings:set('movement_gravity', starlit.world.planet.gravity) -- ??? seriously???
          285  +-- THIS OVERRIDES THE GLOBAL SETTING *AND PERSISTS IT* WHAT IN THE SATANIC FUCK
   280    286   
   281    287   ---------------
   282    288   -- callbacks --
   283    289   ---------------
   284    290   -- here we connect our types up to the minetest API
   285    291   
   286    292   local function userCB(fn)

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

   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 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]
          178  +					return {
          179  +						context = 'suit';
          180  +						program = pgmctx;
          181  +					}
          182  +				end
          183  +
   175    184   				if pgm.sw.powerKind == 'active' then
   176    185   					if cfg then
   177    186   						user:openUI(pgm.sw.ui, 'index', {
   178    187   							context = 'suit';
   179    188   							program = pgm;
   180    189   						})
   181    190   						return false
................................................................................
   189    198   					elseif pptrMatch(ptr, pnan.secondary) then
   190    199   						pnan.secondary = nil
   191    200   					else
   192    201   						pnan.secondary = ptr
   193    202   					end
   194    203   					user:suitSound 'starlit-configure'
   195    204   				elseif pgm.sw.powerKind == 'direct' then
   196         -					local ctx = {
   197         -						context = 'suit';
   198         -						program = pgm;
   199         -					}
          205  +					local ctx = suitCtx(pgm)
   200    206   					if pgm.sw.ui then
   201    207   						user:openUI(pgm.sw.ui, 'index', ctx)
   202    208   						return false
   203    209   					else
   204    210   						pgm.sw.run(user, ctx)
   205    211   					end
   206    212   				elseif pgm.sw.powerKind == 'passive' then
   207    213   					if cfg then
   208         -						user:openUI(pgm.sw.ui, 'index', {
   209         -							context = 'suit';
   210         -							program = pgm;
   211         -						})
          214  +						user:openUI(pgm.sw.ui, 'index', suitCtx(pgm))
   212    215   						return false
   213    216   					end
   214    217   
   215    218   					local addDisableRec = true
   216    219   					for i, e in ipairs(pgm.file.body.conf) do
   217    220   						if e.key == 'disable' and e.value == 'yes' then
   218    221   							addDisableRec = false
................................................................................
   326    329   						w=2, h=2;
   327    330   					})
   328    331   					menu.padding = 1;
   329    332   				end
   330    333   				return starlit.ui.build(menu)
   331    334   			end;
   332    335   		};
   333         -		compilerListRecipes = {
   334         -		};
   335    336   		psi = {
   336    337   			render = function(state, user)
   337    338   				return starlit.ui.build {
   338    339   					kind = 'vert', mode = 'sw';
   339    340   					padding = 0.5;
   340    341   				}
   341    342   			end;
................................................................................
   346    347   				local tb = {
   347    348   					kind = 'vert', mode = 'sw';
   348    349   					padding = 0.5, 
   349    350   					{kind = 'hztl', padding = 0.25;
   350    351   						{kind = 'label', text = 'Name', w = 2, h = barh};
   351    352   						{kind = 'label', text = user.persona.name, w = 4, h = barh}};
   352    353   				}
   353         -				local statBars = {'nutrition', 'hydration', 'fatigue', 'morale', 'irradiation', 'illness'}
   354         -				for idx, id in ipairs(statBars) do
   355         -					local s = starlit.world.stats[id]
   356         -					local amt, sv = user:effectiveStat(id)
   357         -					local min, max = starlit.world.species.statRange(user.persona.species, user.persona.speciesVariant, id)
          354  +				local statBars = {'stamina', 'numina', 'nutrition', 'hydration', 'fatigue', 'morale', 'irradiation', 'illness'}
          355  +				local function wrapElts(n, l)
          356  +					local all = {kind='vert'}
          357  +					local ct, row
          358  +					local function flush()
          359  +						if row then
          360  +							table.insert(all, row)
          361  +						end
          362  +						row = {kind='hztl', spacing = 0.2}
          363  +						ct = 0
          364  +					end
          365  +					flush()
          366  +					for i, e in ipairs(l) do
          367  +						ct = ct + 1
          368  +						table.insert(row, e)
          369  +						if ct >= n then flush() end
          370  +					end
          371  +					flush()
          372  +					return all
          373  +				end
          374  +				local bars = {}
          375  +				local function pushBar(s, amt, sv, min, max)
   358    376   					local st = string.format('%s / %s', s.desc(amt, true), s.desc(max))
   359         -					table.insert(tb, {kind = 'hztl', padding = 0.25;
          377  +					table.insert(bars, {kind = 'hztl', padding = 0.25;
   360    378   						{kind = 'label', w=2, h=barh, text = lib.str.capitalize(s.name)};
   361    379   						{kind = 'hbar',  w=4, h=barh, fac = sv, text = st, color=s.color};
   362    380   					})
   363    381   				end
          382  +				do local hp, hpf = user:effectiveStat 'health'
          383  +					local desc = {
          384  +						name='health';
          385  +						desc = function(hp) return tostring(hp) end;
          386  +						color = {hue=10,sat=1,lum=.5};
          387  +					}
          388  +					pushBar(desc, hp, hpf, starlit.world.species.statRange(user.persona.species, user.persona.speciesVariant, 'health'))
          389  +				end
          390  +				do local ep, ex = user:suitCharge(), user:suitPowerCapacity()
          391  +					local desc = {
          392  +						name = 'power';
          393  +						desc = function(j) return lib.math.siUI('J', j) end;
          394  +						color = {hue=190,sat=1,lum=.5};
          395  +					}
          396  +					pushBar(desc, ep, ep/ex, 0, ex)
          397  +				end
          398  +				for idx, id in ipairs(statBars) do
          399  +					local s = starlit.world.stats[id]
          400  +					local amt, sv = user:effectiveStat(id)
          401  +					local min, max = starlit.world.species.statRange(user.persona.species, user.persona.speciesVariant, id)
          402  +					pushBar(s, amt, sv, min, max)
          403  +				end
          404  +				table.insert(tb, wrapElts(2, bars))
   364    405   				local abilities = {
   365    406   					maneuver = {};
   366    407   					direct = {};
   367    408   					passive = {};
   368    409   				}
   369    410   				state.abilityMap = {}
   370    411   				for i, a in pairs(user:species().abilities) do
................................................................................
   458    499   					user:suitPowerStateSet(suitMode)
   459    500   					return true
   460    501   				end
   461    502   			end;
   462    503   		};
   463    504   	};
   464    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
   465    533   
   466    534   -- TODO destroy suit interfaces when power runs out or suit/chip is otherwise disabled
   467    535   starlit.interface.install(starlit.type.ui {
   468    536   	id = 'starlit:compile-matter-component';
   469    537   	sub = {
   470    538   		suit = function(state, user, evt)
   471    539   			if evt.kind == 'disrobe' then state:close()
................................................................................
   480    548   			setupState = function(state, user, ctx)
   481    549   				state.pgm = ctx.program
   482    550   				state.select = {}
   483    551   				local E = starlit.mod.electronics
   484    552   				if ctx.context == 'suit' then
   485    553   					state.fetch = function()
   486    554   						local cst = user.entity:get_inventory():get_list 'starlit_suit_chips'
   487         -						local cl = {order={}, map={}}
          555  +						local cl = {order={}, map={}, slot={}}
   488    556   						for i, c in ipairs(cst) do
   489    557   							if not c:is_empty() then
   490    558   								local d = E.chip.read(c)
   491    559   								local co = {
   492    560   									stack = c;
   493    561   									data = d;
   494    562   								}
   495    563   								table.insert(cl.order, co)
   496    564   								cl.map[d.uuid] = co
          565  +								cl.slot[i] = co
   497    566   							end
   498    567   						end
   499         -						if state.select.chip and not cl.map[state.select.chip.data.uuid] then
   500         -							-- chip no longer available
          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
   501    577   							user:suitSound 'starlit-error'
   502    578   							state.select = {}
   503    579   						end
   504    580   						state.select.chips = cl
   505    581   
   506    582   						state.select.scms = {}
   507    583   						if state.select.chip then
   508         -							state.select.scms = E.chip.usableSoftware({state.select.chip.stack},nil,
   509         -								function(s) return s.sw.kind == 'schematic' end)
          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)
   510    592   						end
          593  +
   511    594   					end
   512    595   				end
   513    596   			end;
   514    597   
   515    598   			onClose = function(state, user)
   516    599   				user:suitSound 'starlit-quit'
   517    600   			end;
................................................................................
   518    601   			handle = function(state, user, q)
   519    602   				local sel = state.select
   520    603   				state.fetch()
   521    604   				local chips = state.select.chips
   522    605   				local function chirp()
   523    606   					user:suitSound 'starlit-nav'
   524    607   				end
   525         -				local function onPickChip(chip)
   526         -					chirp()
   527         -					sel.chip = chip
   528         -					return true
   529         -				end
   530         -				local function onPickScm(scm)
   531         -					chirp()
   532         -					sel.scm = scm
   533         -					return true
          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
   534    624   				end
   535    625   
   536    626   				if sel.chip == nil then
   537         -					for k in next, q do
   538         -						local id = k:match "^chip_(%d+)$"
   539         -						if id then
   540         -							local cm = chips.map[tonumber(id)]
   541         -							if cm then return onPickChip(cm) end
   542         -						end
          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
   543    635   					end
   544         -				elseif sel.scm == nil then
   545         -					if q.back then chirp() sel.chip = nil return true end
   546         -					for k in next, q do
   547         -						local id = k:match "^scm_(%d+)$"
   548         -						if id then
   549         -							local cm = state.select.scms[tonumber(id)]
   550         -							if cm then return onPickScm(cm) 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
   551    670   						end
   552    671   					end
   553         -				else
   554         -					if q.back then chirp() sel.scm = nil return true end
   555    672   				end
          673  +
   556    674   			end;
   557    675   
   558    676   			render = function(state, user)
   559    677   				local sel, pgmSelector = state.select, {}
   560    678   				state.fetch()
   561    679   
   562    680   				local function pushSelector(id, item, label, desc, req)
   563    681   					local rh = .5
   564    682   					local label = {kind = 'text', w = 10-1.5, h=1.5;
   565         -							text = '<global valign=middle>'..label }
          683  +							text = '<global valign=middle>'..lib.str.htsan(label) }
   566    684   					if req then
   567    685   						label.h = label.h - rh - .2
   568    686   
   569    687   						local imgs = {}
   570    688   						for ci,c in ipairs(req) do
   571    689   							for ei, e in ipairs(c.list) do
   572    690   								table.insert(imgs, {kind = 'img', w=rh, h=rh,  img=e.img})
................................................................................
   591    709   				if sel.chips == nil then
   592    710   					table.insert(pgmSelector, {kind = 'img', img = 'starlit-ui-alert.png', w=2, h=2})
   593    711   				elseif sel.chip == nil then
   594    712   					for i, c in ipairs(sel.chips.order) do
   595    713   					-- TODO filter out chips without schematics?
   596    714   						pushSelector('chip_' .. c.data.uuid, c.stack, c.data.label)
   597    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
   598    722   				else
   599    723   					if sel.scm == nil then
   600    724   						for idx, ent in ipairs(sel.scms) do
   601    725   							local fab = ItemStack(ent.sw.output):get_definition()._starlit.fab
   602    726   							if fab.flag.print then
   603    727   								local req = fab:visualize()
   604    728   								pushSelector('scm_' .. idx, ent.sw.output, ent.sw.name, nil, req)
   605    729   							end
   606    730   						end
   607    731   						table.insert(pgmSelector, back)
   608    732   					else
   609         -						local output = ItemStack(sel.scm.sw.output):get_definition()
          733  +						local scm = sel.scms[sel.scm]
          734  +						local output = ItemStack(scm.sw.output):get_definition()
   610    735   						local fab = output._starlit.fab
   611         -						local sw = sel.scm.sw
          736  +						local sw = scm.sw
          737  +						local function unmet(str)
          738  +							return lib.color(1,.3,.3):fmt(str)
          739  +						end
   612    740   						table.insert(pgmSelector, {kind = 'hztl', w=10, h=1.2;
   613    741   							{kind = 'img', item = sw.output, w=1.2, h=1.2, desc=output.description};
   614         -							{kind = 'text', text = string.format('<global valign=middle><b>%s</b>', sw.name), w=10-1.2,h=1.2};
          742  +							{kind = 'text', text = string.format('<global valign=middle><b>%s</b>', lib.str.htsan(sw.name)), w=10-1.2,h=1.2};
   615    743   						})
   616    744   						local inputTbl = {kind = 'vert', w=5,h=0;
   617         -							{kind = 'hbar', w=5, h=.5, text='Input'}};
   618         -						local costTbl = {kind = 'vert', w=5,h=0; spacing=.25;
   619         -							{kind = 'hbar', w=5, h=.5, text='Process'}};
          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'}};
   620    748   						local reqPane = {kind = 'pane', id='reqPane', w=10, h=7;
   621    749   							{kind = 'hztl', w=10,h=0; inputTbl, costTbl}
   622    750   						}
   623         -						local req = fab:visualize()
   624         -						for ci,c in ipairs(req) do
   625         -							table.insert(inputTbl, {kind = 'label', w=4.5, h=1, x=.5;
   626         -								text=lib.str.capitalize(c.header)});
   627         -							for ei,e in ipairs(c.list) do
   628         -								table.insert(inputTbl, {kind = 'hztl', w=4, h=.5, x=1;
   629         -									{kind='img',   w=.5,h=.5, img=e.img};
   630         -									{kind='label', w=3.3,h=.5,x=.2, text=e.label};
   631         -								});
          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
   632    787   							end
   633    788   						end
   634         -						if sw.cost then
   635         -							local function pushCost(t, val)
   636         -								table.insert(costTbl, {kind='text', w=4.5,h=.5,x=.5;
   637         -									text=string.format('<b>%s</b>: %s',t,val);
   638         -								})
          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)
   639    833   							end
   640         -							if sw.cost.cycles then
   641         -								pushCost('Energy', lib.math.siUI('J', sel.scm.powerCost))
   642         -								pushCost('Compute', lib.math.siUI({'cycle','cycles'}, sw.cost.cycles, true))
   643         -							end
          834  +							if not ok then commitHue = 0 end
   644    835   						end
   645    836   						table.insert(pgmSelector, reqPane)
   646    837   						table.insert(pgmSelector, {kind = 'hztl', w=10,h=1.2;
   647         -							{kind = 'button', id='back', label = '<- Back', w=5,h=1.2};
   648         -							{kind = 'button', id='print', label = 'Print ->', w=5,h=1.2, color={hue=120,sat=0,lum=0}};
          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}};
   649    840   						})
   650    841   					end
   651    842   				end
   652    843   
   653    844   				return starlit.ui.build {
   654    845   					kind = 'hztl', padding = 0.5; w = 20, h = 10, mode = 'sw';
   655    846   					{kind = 'vert', w = 5, h = 5;

Modified mods/starlit/species.lua from [34f8558850] to [e0959a01e2].

    90     90   					local invis = lib.image '[fill:1x1:0,0:#00000000'
    91     91   					local plate = adorn.suit and adorn.suit.plate or invis
    92     92   					local lining = adorn.suit and adorn.suit.lining or invis
    93     93   
    94     94   					return {lining, plate, skin, skin, eye, hair}
    95     95   				end;
    96     96   				stats = {
    97         -					psi = 1.2;
           97  +					numina = 1.2;
    98     98   					nutrition = .8; -- women have smaller stomachs
    99     99   					hydration = .8;
   100    100   					morale = 0.8; -- you are not She-Bear Grylls
   101    101   					irradiation = 0.8; -- you are smaller, so it takes less rads to kill ya
   102    102   				};
   103    103   				traits = {
   104    104   					health = 400;
................................................................................
   105    105   					lungCapacity = .6;
   106    106   					sturdiness = 0; -- women are more fragile and thus susceptible to blunt force trauma
   107    107   					metabolism = .150; -- kCal/s
   108    108   					painTolerance = 0.4;
   109    109   					dehydration = 10e-4; -- L/s
   110    110   					speed = 1.1;
   111    111   					staminaRegen = 10.0;
   112         -					psiRegen = 0.05; -- ψ/s
   113         -					psiPower = 1.2;
          112  +					numinaRegen = 0.05; -- ψ/s
          113  +					psi = 1.2;
   114    114   				};
   115    115   			};
   116    116   			male = {
   117    117   				name = 'Human Male';
   118    118   				eyeHeight = 1.6;
   119    119   				stats = {
   120         -					psi = 1.0;
          120  +					numina = 1.0;
   121    121   					nutrition = 1.0;
   122    122   					hydration = 1.0;
   123    123   					staminaRegen = 7; -- men are strong but have inferior endurance
   124    124   				};
   125    125   				traits = {
   126    126   					health = 500;
   127    127   					painTolerance = 1.0;
   128    128   					lungCapacity = 1.0;
   129    129   					sturdiness = 0.3;
   130    130   					metabolism = .150; -- kCal/s
   131    131   					dehydration = 15e-4; -- L/s
   132    132   					speed = 1.0;
   133         -					psiRegen = 0.025;
   134         -					psiPower = 1.0;
          133  +					numinaRegen = 0.025;
          134  +					psi = 1.0;
   135    135   				};
   136    136   			};
   137    137   		};
   138    138   		traits = {};
   139    139   		abilities = {bioAbilities.sprint};
   140    140   	};
   141    141   }
................................................................................
   232    232   		local min, max = starlit.world.species.statRange(pSpecies, pVariant, st)
   233    233   		local delta = max - min
   234    234   		return min + delta*p
   235    235   	end
   236    236   	local ps = starlit.world.species.mkPersonaFor(pSpecies,pVariant)
   237    237   	local startingHP = pct('health', 1.0)
   238    238   	if circumstances.injured    then startingHP = pct('health', circumstances.injured) end
   239         -	if circumstances.psiCharged then ps.statDeltas.psi = pct('psi', circumstances.psiCharged) end
          239  +	if circumstances.numinaCharged then ps.statDeltas.numina = pct('numina', circumstances.numinaCharged) end
   240    240   	for k,v in pairs(starlit.world.stats) do ps.statDeltas[k] = 0 end
   241    241   	ps.statDeltas.warmth = 20 -- don't instantly start dying of frostbite
   242    242   	ps.statDeltas.nutrition = 2000 -- shoulda packed more MRE :c
   243    243   	ps.statDeltas.hydration = 3 -- stay hydrated uwu
   244    244   
   245    245   	entity:set_properties{hp_max = var.traits.health or sp.traits.health}
   246    246   	entity:set_hp(startingHP, 'initial hp')

Modified mods/starlit/stats.lua from [0688acc2a6] to [9558da3e0c].

    18     18   	end
    19     19   end
    20     20   
    21     21   local function C(h, s, l)
    22     22   	return lib.color {hue = h, sat = s or 1, lum = l or .7}
    23     23   end
    24     24   starlit.world.stats = {
    25         -	psi        = {min = 0, max = 500, base = 0, desc = U('ψ', 1), color = C(320), name = 'numina', srzType = T.decimal};
           25  +	numina     = {min = 0, max = 500, base = 0, desc = U('ψ', 1), color = C(320), name = 'numina', srzType = T.decimal};
    26     26   	-- numina is measured in ψ
    27     27   	warmth     = {min = -1000, max = 1000, base = 0, desc = U('°C', 10, true), color = C(5), name = 'warmth'};
    28     28   	-- warmth in measured in d°C
    29         -	fatigue    = {min = 0, max = 76 * 60, base = 0, desc = U('hr', 60, true), color = C(288,.3,.5), name = 'fatigue', harm=true, srzType = T.decimal};
           29  +	fatigue    = {min = 0, max = 76 * 60, base = 0, desc = U('hr', 60, true), color = C(288,1,.8), name = 'fatigue', harm=true, srzType = T.decimal};
    30     30   	-- fatigue is measured in minutes one needs to sleep to cure it
    31         -	stamina    = {min = 0, max = 10 * 20, base = true, desc = U('m', 100), color = C(88), name = 'stamina'};
           31  +	stamina    = {min = 0, max = 10 * 20, base = true, desc = U('m', 10), color = C(88), name = 'stamina'};
    32     32   	-- stamina is measured in how many 10th-nodes (== cm) one can sprint
    33         -	nutrition  = {min = 0, max = 8000, base = 0, desc = U('kCal', 1, true), color = C(43,.5,.4), name = 'nutrition', srzType = T.decimal};
           33  +	nutrition  = {min = 0, max = 8000, base = 0, desc = U('kCal', 1, true), color = C(43,1,.8), name = 'nutrition', srzType = T.decimal};
    34     34   	-- hunger is measured in kcalories one must consume to cure it. at 0, you start dying
    35         -	hydration  = {min = 0, max = 4, base = 0, desc = U('L', 1), color = C(217, .25,.4), name = 'hydration', srzType = T.decimal};
           35  +	hydration  = {min = 0, max = 4, base = 0, desc = U('L', 1), color = C(217), name = 'hydration', srzType = T.decimal};
    36     36   	-- thirst is measured in L of H²O required to cure it
    37     37   	morale     = {min = 0, max = 10 * 24 * 60, base = true, color = C(0,0,.8), name = 'morale', srzType = T.decimal;
    38     38   		desc = function(amt, excU) return lib.math.timespec(amt) end};
    39     39   	-- morale is measured in minutes. e.g. at base rate morale degrades by
    40     40   	-- 60 points every hour. morale can last up to 10 earthdays
    41     41   	irradiation = {min = 0, max = 10, base = 0, desc = U('Gy', 1), color = C(141,1,.5), name = 'irradiation', harm=true, srzType = T.decimal};
    42     42   	-- irrad is measured is milligreys
................................................................................
    50     50   	-- illness is increased by certain conditions, and decreases on its own as your
    51     51   	-- body heals when those conditions wear off. some drugs can lower accumulated illness
    52     52   	-- but illness-causing conditions require specific cures
    53     53   	-- illness also causes thirst and fatigue to increase proportionately
    54     54   }
    55     55   
    56     56   starlit.world.statOrder = {
    57         -	'health', 'stamina', 'psi', 'warmth';
           57  +	'health', 'stamina', 'numina', 'warmth';
    58     58   
    59     59   	'nutrition', 'hydration', 'irradiation';
    60     60   	'illness', 'morale', 'fatigue';
    61     61   }
    62     62   
    63     63   local impactStruct = G.struct {
    64     64   	base = G.array(8, G.struct {id = T.str, val = T.decimal});

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

   341    341   		if rst.itemClass and not grp(item, rst.itemClass) then
   342    342   			return false
   343    343   		end
   344    344   		if rst.maintenanceNode then return false end
   345    345   		-- FIXME figure out best way to identify when the player is using a maintenance node
   346    346   
   347    347   		if grp(item, 'specialInventory') then
   348         -			if grp(item, 'powder') and list ~= 'starlit_suit_elem' then return false end
   349         -			-- FIXME handle containers
   350         -			if grp(item, 'psi') and list ~= 'starlit_psi' then return false end
   351    348   		end
   352    349   
   353    350   		return true
   354    351   	end
   355    352   	local function itemCanLeave(item, list)
   356    353   		local rst, ok = checkBaseRestrictions(list)
   357    354   		if not ok then return false end

Modified mods/starlit/ui.lua from [81aedb85b1] to [08cb8bbbbd].

   249    249   		if def.kind == 'hbar'
   250    250   			then wfac = wfac * clamp
   251    251   			else hfac = hfac * clamp
   252    252   		end
   253    253   		local x,y, w,h = state.x, state.y, def.w, def.h
   254    254   		widget('box[%s,%s;%s,%s;%s]',
   255    255   			x,y, w,h, cl:brighten(0.2):hex())
   256         -		widget('box[%s,%s;%s,%s;%s]',
   257         -			x, y + (h*(1-hfac)), w * wfac, h * hfac, cl:hex())
          256  +		if clamp > 0 then
          257  +			widget('box[%s,%s;%s,%s;%s]',
          258  +				x, y + (h*(1-hfac)), w * wfac, h * hfac, cl:hex())
          259  +		end
   258    260   		if def.text then
   259    261   			widget('hypertext[%s,%s;%s,%s;;%s]',
   260    262   				state.x, state.y, def.w, def.h,
   261    263   				string.format('<global halign=center valign=middle color=%s>%s', fg:hex(), E(def.text)))
   262    264   		end
   263    265   	end
   264    266   

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

    19     19   	persona = {
    20     20   		key  = 'starlit:persona';
    21     21   		type = starlit.store.persona;
    22     22   	};
    23     23   }
    24     24   
    25     25   local suitStore = starlit.store.suitMeta
           26  +
           27  +local leds = {
           28  +	freeze = {
           29  +		icon = lib.image('starlit-ui-alert-temp-cold.png');
           30  +		bg = lib.image('starlit-ui-alert-bg-temp-cold.png');
           31  +		side = 'left';
           32  +	};
           33  +	overheat = {
           34  +		icon = lib.image('starlit-ui-alert-temp-hot.png');
           35  +		bg = lib.image('starlit-ui-alert-bg-temp-hot.png');
           36  +		side = 'left';
           37  +	};
           38  +	hydration = {
           39  +		icon = lib.image('starlit-ui-alert-hydration.png');
           40  +		bg = lib.image('starlit-ui-alert-bg-hydration.png');
           41  +		side = 'left';
           42  +	};
           43  +	nutrition = {
           44  +		icon = lib.image('starlit-ui-alert-nutrition.png');
           45  +		bg = lib.image('starlit-ui-alert-bg-nutrition.png');
           46  +		side = 'left';
           47  +	};
           48  +
           49  +	radiation = {
           50  +		icon = lib.image('starlit-ui-alert-rad.png');
           51  +		bg = lib.image('starlit-ui-alert-bg-rad.png');
           52  +		side = 'right';
           53  +	};
           54  +	fatigue = {
           55  +		icon = lib.image('starlit-ui-alert-fatigue.png');
           56  +		bg = lib.image('starlit-ui-alert-bg-fatigue.png');
           57  +		side = 'right';
           58  +	};
           59  +}
    26     60   
    27     61   starlit.type.user = lib.class {
    28     62   	name = 'starlit:user';
           63  +	leds = leds;
    29     64   	construct = function(ident)
    30     65   		local name, luser
    31     66   		if type(ident) == 'string' then
    32     67   			name = ident
    33     68   			luser = minetest.get_player_by_name(name)
    34     69   		else
    35     70   			luser = ident
................................................................................
    38     73   		return {
    39     74   			entity = luser;
    40     75   			name = name;
    41     76   			hud = {
    42     77   				elt = {};
    43     78   				bar = {};
    44     79   				alarm = {};
           80  +				led = { left={}, right={}, map={} };
    45     81   			};
    46     82   			tree = {};
    47     83   			action = {
    48     84   				bits = 0; -- for control deltas
    49     85   				prog = {}; -- for recording action progress on a node; reset on refocus
    50     86   				tgt = {type='nothing'};
    51     87   				sfx = {};
................................................................................
    69    105   		}
    70    106   	end;
    71    107   	__index = {
    72    108   		--------------
    73    109   		-- overlays --
    74    110   		--------------
    75    111   		updateOverlays = function(self)
          112  +			-- minetest: because fuck you, that's why
          113  +			local engineGravity = starlit.constant.phys.engineGravity
          114  +			local targetGravity = starlit.world.planet.gravity
    76    115   			local phys = {
    77    116   				speed = self.pheno:trait('speed',1);
    78    117   				jump = self.pheno:trait('jump',1);
    79         -				gravity = 1;
          118  +				gravity = targetGravity / engineGravity;
    80    119   				speed_climb = 1;
    81    120   				speed_crouch = 1;
    82    121   				speed_walk = 1;
    83    122   				acceleration_default = 1;
    84    123   				acceleration_air = 1;
    85    124   			}
    86    125   			for i, o in ipairs(self.overlays) do o(phys) end
................................................................................
   372    411   			self.hud.elt.bat = self:attachStatBar {
   373    412   				name = 'battery', stat = batteryLookup;
   374    413   				color = C(190,0,.2), size = 100;
   375    414   				pos = {x=0.5, y=1}, ofs = {x = hbofs - 4, y=-48 - bpad};
   376    415   				dir = 0;
   377    416   				align = {x=1, y=-1};
   378    417   			}
   379         -			self.hud.elt.psi = attachBasicStat {
   380         -				name = 'psi', stat = 'psi';
          418  +			self.hud.elt.numina = attachBasicStat {
          419  +				name = 'numina', stat = 'numina';
   381    420   				color = C(320,0,.2), size = 100;
   382    421   				pos = {x=0.5, y=1}, ofs = {x = hbofs - 4, y=-24 - bpad};
   383    422   				dir = 0;
   384    423   				align = {x=1, y=-1};
   385    424   			}
   386    425   			self.hud.elt.time = self:attachTextBox {
   387    426   				name = 'time';
................................................................................
   477    516   				self.entity:hud_remove(e.id)
   478    517   			end
   479    518   		end;
   480    519   		updateHUD = function(self)
   481    520   			for name, e in pairs(self.hud.elt) do
   482    521   				if e.update then e.update() end
   483    522   			end
          523  +			self:updateLEDs()
          524  +		end;
          525  +		updateLEDs = function(self)
          526  +			local time = minetest.get_gametime()
          527  +			local function updateSide(name, ofs, tx)
          528  +				local del = {}
          529  +				for i, l in ipairs(self.hud.led[name]) do
          530  +					local idx = 0
          531  +					if time - l.origin > 3 then
          532  +						if l.elt then self.entity:hud_remove(l.elt.id) end
          533  +						self.hud.led.map[l.kind] = nil
          534  +						table.insert(del, i)
          535  +					else
          536  +						local xc = (idx*48 + 400)*ofs
          537  +						if l.elt and next(del) then
          538  +							l.elt:update('offset', {x=xc, y=1})
          539  +						else
          540  +							local tex = leds[l.kind].icon:blit(hudAdjustBacklight(leds[l.kind].bg))
          541  +							if tx then tex = tex:transform(tx) end
          542  +							if not l.elt then
          543  +								l.elt = self:attachImage {
          544  +									tex = tex:render();
          545  +									align = {x=ofs, y=-1};
          546  +									pos = {x=.5, y=1};
          547  +									scale = {x=1,y=1};
          548  +									ofs = {x=xc, y=0};
          549  +								}
          550  +							end
          551  +						end
          552  +						idx = idx + 1
          553  +					end
          554  +				end
          555  +				for _, i in ipairs(del) do
          556  +					table.remove(self.hud.led[name], i)
          557  +				end
          558  +
          559  +			end
          560  +			updateSide('left', -1)
          561  +			updateSide('right', 1, 'FX')
   484    562   		end;
   485    563   
   486    564   		---------------------
   487    565   		-- actions & modes --
   488    566   		---------------------
   489    567   		onModeChange = function(self, oldMode, silent)
   490    568   			self.hud.elt.crosshair.update()
................................................................................
   920    998   			if run then
   921    999   				run(self, ctx)
   922   1000   				return true
   923   1001   			end
   924   1002   			return false
   925   1003   		end;
   926   1004   
   927         -		alarm = function(self, urgency, kind, freq, where)
         1005  +		alarm = function(self, urgency, kind, minFreq)
         1006  +			minFreq = minFreq or 1.5
         1007  +			local time = minetest.get_gametime()
         1008  +			local led = leds[kind]
         1009  +
         1010  +			local ul = self.hud.led.map[kind]
         1011  +			if ul then
         1012  +				if time - ul.origin > minFreq then
         1013  +					ul.origin = time
         1014  +				else return end
         1015  +			end
         1016  +
         1017  +			if urgency > 0 then
         1018  +				local urgencies = {
         1019  +					[1] = {sound = 'starlit-alarm'};
         1020  +					[2] = {sound = 'starlit-alarm-urgent'};
         1021  +				}
         1022  +			   local urg = urgencies[urgency] or urgencies[#urgencies]
         1023  +
         1024  +			   if time - self.cooldownTimes.alarm > 1.5 then
         1025  +				   self.cooldownTimes.alarm = time
         1026  +				   self:suitSound(urg.sound)
         1027  +			   end
         1028  +		   end
         1029  +
         1030  +
         1031  +			local newLed = {
         1032  +				kind = kind;
         1033  +				origin = time;
         1034  +			}
         1035  +			self.hud.led.map[kind] = newLed
         1036  +			table.insert(self.hud.led[led.side], newLed)
         1037  +
         1038  +
         1039  +		   self:updateLEDs()
         1040  +
         1041  +		--[[
   928   1042   			freq = freq or 3
   929   1043   			local urgencies = {
   930   1044   				[1] = {sound = 'starlit-alarm'};
   931   1045   				[2] = {sound = 'starlit-alarm-urgent'};
   932   1046   			}
   933   1047   		   local gt = minetest.get_gametime()
   934   1048   		   local urg = urgencies[urgency] or urgencies[#urgencies]
................................................................................
   954   1068   			   -- HATE. HATE. HAAAAAAAAAAATE
   955   1069   			   minetest.after(freq/2, function()
   956   1070   				   for k,v in pairs(self.hud.alarm) do
   957   1071   					   self.entity:hud_remove(v.id)
   958   1072   				   end
   959   1073   				   self.hud.alarm={}
   960   1074   			   end)
   961         -		   end
         1075  +		   end]]
   962   1076   	   end;
   963   1077   
   964   1078   		-------------
   965   1079   		-- weather --
   966   1080   		-------------
   967   1081   		updateWeather = function(self)
   968   1082   		end;
................................................................................
  1015   1129   	};
  1016   1130   }
  1017   1131   
  1018   1132   local clockInterval = 1.0
  1019   1133   starlit.startJob('starlit:clock', clockInterval, function(delta)
  1020   1134   	for id, u in pairs(starlit.activeUsers) do
  1021   1135   		u.hud.elt.time:update()
         1136  +		u:updateLEDs()
  1022   1137   	end
  1023   1138   end)
  1024   1139   
  1025   1140   -- performs a general HUD refresh, mainly to update the HUD backlight brightness
  1026   1141   local hudInterval = 10
  1027   1142   starlit.startJob('starlit:hud-refresh', hudInterval, function(delta)
  1028         -	for id, u in pairs(starlit.activeUsers) do u:updateHUD() end
         1143  +	for id, u in pairs(starlit.activeUsers) do
         1144  +	u:updateHUD() end
  1029   1145   end)
  1030   1146   
  1031   1147   local biointerval = 1.0
  1032   1148   starlit.startJob('starlit:bio', biointerval, function(delta)
  1033   1149   	for id, u in pairs(starlit.activeUsers) do
  1034   1150   		if u:effectiveStat 'health' ~= 0 then
  1035   1151   			local bmr = u:phenoTrait 'metabolism' * biointerval
................................................................................
  1092   1208   
  1093   1209   			if sp < 1.0 and minetest.get_gametime() - u.cooldownTimes.stamina > 5.0 then
  1094   1210   				u:statDelta('stamina', (u:phenoTrait('staminaRegen',1) * penaltyFromFatigue) / heatPenalty)
  1095   1211   -- 				print('stam', u:effectiveStat 'stamina', u:phenoTrait('staminaRegen',1) / heatPenalty, heatPenalty)
  1096   1212   			end
  1097   1213   
  1098   1214   			local morale, mp = u:effectiveStat 'morale'
  1099         -			local pr = u:phenoTrait 'psiRegen'
  1100         -			u:statDelta('psi', pr * penaltyFromFatigue * mp)
         1215  +			local pr = u:phenoTrait 'numinaRegen'
         1216  +			u:statDelta('numina', pr * penaltyFromFatigue * mp)
  1101   1217   		end
  1102   1218   	end
  1103   1219   end)
  1104   1220   
  1105   1221   local cbit = {
  1106   1222   	up   = 0x001;
  1107   1223   	down = 0x002;

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

   181    181   	for name,user in pairs(starlit.activeUsers) do
   182    182   		local tr = user:species().tempRange
   183    183   		local t = starlit.world.climate.temp(user.entity:get_pos())
   184    184   
   185    185   		do -- this bit probably belongs in starlit:bio but we do it here in order
   186    186   		   -- to spare ourselves another call into the dark swamp of climate.temp
   187    187   		   local urg = 1
   188         -		   local function alarm(kind)
   189         -			   user:alarm(urg, kind, nil, {
   190         -				   elt = user.hud.elt.temp, ofs = {x=100,y=0};
   191         -				   tex = 'starlit-ui-alert-'..kind..'.png';
   192         -			   })
   193         -		   end
   194    188   		   local hz = user:tempHazard(t)
   195    189   			local tr = user:species().tempRange.survivable
   196    190   		   if hz == 'cold' then
   197    191   			   if tr[1] - t > 7 then urg = 2 end
   198         -			   alarm 'temp-cold'
          192  +			   user:alarm(urg, 'freeze', 3)
   199    193   		   elseif hz == 'hot' then
   200    194   			   if t - tr[2] > 7 then urg = 2 end
   201         -			   alarm 'temp-hot'
          195  +			   user:alarm(urg, 'overheat', 3)
   202    196   		   end
   203    197   		end
   204    198   
   205    199   		local insul = 0
   206    200   		local naked = user:naked()
   207    201   		local suitDef
   208    202   		if not naked then

Modified mods/vtlib/math.lua from [43eaf4251e] to [2d47a7e2f1].

   156    156   -- function fn.vlerp
   157    157   
   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         -	local hr = math.floor(n / 60)
          163  +	local min = math.floor(n / 60)
          164  +	local hr = math.floor(min / 60)
   164    165   	local spec = {}
   165    166   
   166    167   	if hr  ~= 0 then table.insert(spec, string.format("%shr", hr))  end
          168  +	if min  ~= 0 then table.insert(spec, string.format("%sm", min))  end
   167    169   	if sec ~= 0 then table.insert(spec, string.format("%ss",  sec)) end
   168    170   	return table.concat(spec, ' ')
   169    171   end
   170    172   return fn

Modified mods/vtlib/str.lua from [01c1839f00] to [ded7121fc6].

   161    161   			else
   162    162   				tbl[#tbl+1] = string.sub(ss,1,d-1)
   163    163   			end
   164    164   		until i > string.len(str)
   165    165   		return tbl
   166    166   	end;
   167    167   
   168         -	rand = function(min,max)
          168  +	rand = function(rng, min,max)
   169    169   		if not min then min = 16  end
   170    170   		if not max then max = min end
   171    171   		local str = ''
   172    172   		local r_int   =            0x39 - 0x30
   173    173   		local r_upper = r_int   + (0x5a - 0x41)
   174    174   		local r_lower = r_upper + (0x7a - 0x61)
   175         -		for i = 1,math.random(max - min) + min do
          175  +		for i = 1,rng:int(min,max) do
   176    176   			-- 0x30 -- 0x39
   177    177   			-- 0x41 -- 0x5A
   178    178   			-- 0x61 -- 0x71
   179         -			local codepoint = math.random(r_lower)
          179  +			local codepoint = rng:int(r_lower)
   180    180   			if codepoint > r_upper then
   181    181   				codepoint = (codepoint - r_upper) + 0x61
   182    182   			elseif codepoint > r_int then
   183    183   				codepoint = (codepoint - r_int) + 0x41
   184    184   			else
   185    185   				codepoint = codepoint + 0x30
   186    186   			end
   187    187   			str = str .. string.char(codepoint)
   188    188   		end
   189    189   		return str
   190    190   	end;
          191  +
          192  +	htsan = function(str)
          193  +		return str:gsub('([<\\])', '\\%1')
          194  +	end;
   191    195   
   192    196   	chop = function(str)
   193    197   		if string.sub(str, 1,1) == ' ' then
   194    198   			str = string.sub(str, 2)
   195    199   		end
   196    200   		if string.sub(str, #str,#str) == ' ' then
   197    201   			str = string.sub(str, 1, #str - 1)

Modified starlit.ct from [8d6b5074b6] to [7cb4085b3b].

    37     37   
    38     38   	p11143: https://github.com/minetest/minetest/pull/11143.diff
    39     39   
    40     40   ### shadows
    41     41   i was delighted to see dynamic shadows land in minetest, and i hope the implementation will eventually mature. however, as it stands, there are severe issues with shadows that make them essentially incompatible with complex meshes like the Starlit player character meshes. for the sake of those who don't mind these glitches, Starlit does enable shadows, but i unfortunately have to recommend that you disable them until the minetest devs get their act together on this feature.
    42     42   
    43     43   ## gameplay
    44         -starlit is somewhat unusual in how it uses the minetest engine. it's a voxel game but not of the minecraft variety.
           44  +starlit is somewhat unusual in how it uses the minetest engine. it's a voxel game but not of the minecraft variety. you do have some control over your environment, but it's limited and exerting it is much more expensive than you might be used to -- the focus of the game is figuring out how to work with nature, not against it. Farthest Shadow has little patience for those who do not show her the respect a living world is due, and she is unconcerned with human virtues like "mercy" or "fairness" or "proportionate retribution".
    45     45   
    46         -the most important thing to understand about starlit is that is is [*mean], by design.
           46  +this is to say, starlit is [*mean], by design.
    47     47   
    48     48   * chance plays an important role. your escape pod might land in the midst of a lush, temperate forest with plenty of nearby shipwrecks to scavenge. or it might land in the exact geographic center of a vast, harsh desert that your suit's cooling systems can't protect you from, ten klicks from anything of value. "unfair", you say? tough. Farthest Shadow doesn't care about your feelings.
    49     49   * death is much worse than a slap on the wrist. when you die, you drop your possessions and your suit, and respawn naked at your spawn point. this is a serious danger, as you might be kilometers away from your spawn point -- and there's no guarantee someone else won't take your suit before you can find your way back to it. good luck crossing long distances without climate control! if you haven't carefully prepared for this eventuality by keeping a spare suit by your spawn point, death can be devastating, to the point of making the game unsurvivable without another player's help. 
    50     50   
    51     51   ### scenarios
    52         -your starting character configuration depends on the scenario you select. (right now this is configured in minetest settings, which is intensely awkward, but i don't have a better solution). the scenario controls your species, sex, and starting inventory. [*neither species nor sex is cosmetic]; e.g. human females are physically weaker but psionically stronged than males. the current playable scenarios are:
           52  +your starting character configuration depends on the scenario you select. (right now this is configured in minetest settings, which is intensely awkward, but i don't have a better solution). the scenario controls your species, sex, and starting inventory. [*neither species nor sex is cosmetic]; e.g. human females are physically weaker but psionically stronger than males. the current playable scenarios are:
    53     53   
    54     54   #### Imperial Expat
    55     55   [*phenotype]: human female
    56     56   [*starting gear]: Commune survival kit
    57     57   > Hoping to escape a miserable life deep in the grinding gears of the capitalist machine for the bracing freedom of the frontier, you sought entry as a colonist to the new Commune world of Thousand Petal. Fate -- which is to say, terrorists -- intervened, and you wound up stranded on Farthest Shadow with little more than the nanosuit on your back, ship blown to tatters and your soul thoroughly mauled by the explosion of a twisted alien artifact -- which SOMEONE neglected to inform you your ride would be carrying.
    58     58   > At least you got some handy psionic powers out of this whole clusterfuck. Hopefully they're safe to use.
    59     59   
    60     60   #### Gentleman Adventurer [!(unimplemented)]
    61     61   [*phenotype]: human male
    62     62   [*starting gear]: Imperial survival kit
    63         -> Tired of the same-old-same-old, sick of your idiot contemporaries, exasperated with the shallow soul-rotting luxury of life as landless lordling, and earnestly eager to enrage your father, you resolved to see the Reach in all her splendor. Deftly evading the usual tourist traps, you finagled your way into the confidence of the Commune ambassador with a few modest infusions of Father's money -- now [!that] should pop his monocle -- and secured yourself a seat on a ride to their brand-new colony at Thousand Petal. How exciting -- a genuine frontier outing!
           63  +> Tired of the same-old-same-old, sick of your idiot contemporaries, exasperated with the shallow soul-rotting luxury of life as landless lordling, and earnestly eager to enrage your father, you resolved to see the Reach in all her splendor. Deftly evading the usual tourist traps, you finagled your way into the confidence of the Commune ambassador with a few modest infusions of Father's money -- now [!that] should pop his monocle! -- and secured yourself a seat on a ride to their brand-new colony at Thousand Petal.
           64  +> How exciting -- a genuine frontier outing!
    64     65   
    65     66   #### Terrorist Tagalong
    66     67   [*phenotype]: human female
    67     68   [*starting gear]: star merc combat kit
    68     69   > It turns out there's a *reason* Crown jobs pay so well.
    69     70   
    70     71   #### Tradebird Bodyguard [!(unimplemented)]