diff --git a/code/modules/admin/verbs/randomverbs.dm b/code/modules/admin/verbs/randomverbs.dm
index 88f639c736..819e520479 100644
--- a/code/modules/admin/verbs/randomverbs.dm
+++ b/code/modules/admin/verbs/randomverbs.dm
@@ -626,19 +626,19 @@ ADMIN_VERB(cmd_admin_add_freeform_ai_law, R_FUN, "Add Custom AI law", "Adds a cu
command_announcement.Announce("Ion storm detected near the [station_name()]. Please check all AI-controlled equipment for errors.", "Anomaly Alert", new_sound = 'sound/AI/ionstorm.ogg')
feedback_add_details("admin_verb","IONC") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
-ADMIN_VERB_AND_CONTEXT_MENU(cmd_admin_rejuvenate, R_ADMIN|R_FUN|R_MOD, "Rejuvenate", "Fully restores the target mob.", ADMIN_CATEGORY_GAME, mob/living/traget_mob as mob in GLOB.mob_list)
- if(!traget_mob)
+ADMIN_VERB_AND_CONTEXT_MENU(cmd_admin_rejuvenate, R_ADMIN|R_FUN|R_MOD, "Rejuvenate", "Fully restores the target mob.", ADMIN_CATEGORY_GAME, mob/living/target_mob as mob in GLOB.mob_list)
+ if(!target_mob)
return
- if(!istype(traget_mob))
+ if(!istype(target_mob))
tgui_alert_async(user, "Cannot revive a ghost")
return
if(CONFIG_GET(flag/allow_admin_rev))
- traget_mob.revive()
+ target_mob.revive()
- log_admin("[key_name(user)] healed / revived [key_name(traget_mob)]")
- var/msg = span_danger("Admin [key_name_admin(user)] healed / revived [ADMIN_LOOKUPFLW(traget_mob)]!")
+ log_admin("[key_name(user)] healed / revived [key_name(target_mob)]")
+ var/msg = span_danger("Admin [key_name_admin(user)] healed / revived [ADMIN_LOOKUPFLW(target_mob)]!")
message_admins(msg)
- admin_ticket_log(traget_mob, msg)
+ admin_ticket_log(target_mob, msg)
else
tgui_alert_async(user, "Admin revive disabled")
feedback_add_details("admin_verb","REJU") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
diff --git a/code/modules/hydroponics/spreading/spreading_growth.dm b/code/modules/hydroponics/spreading/spreading_growth.dm
index 36d5717be7..69efc33395 100644
--- a/code/modules/hydroponics/spreading/spreading_growth.dm
+++ b/code/modules/hydroponics/spreading/spreading_growth.dm
@@ -111,6 +111,8 @@
sleep(rand(3,5))
if(!length(neighbors))
break
+ if(QDELETED(src)) // we sleep, might get deleted!
+ return
spread_to(pick(neighbors))
// We shouldn't have spawned if the controller doesn't exist.
diff --git a/code/modules/player_tips_vr/player_tips_controller_vr.dm b/code/modules/player_tips_vr/player_tips_controller_vr.dm
index a20efefe9f..b3b8991a26 100644
--- a/code/modules/player_tips_vr/player_tips_controller_vr.dm
+++ b/code/modules/player_tips_vr/player_tips_controller_vr.dm
@@ -36,7 +36,7 @@ Controlled by the player_tips subsystem under code/controllers/subsystems/player
if(!(target_mob.client?.prefs?.read_preference(/datum/preference/toggle/player_tips)))
return
if(target_mob.key && !(target_mob.key in HasReceived))
- to_chat(target_mob, span_warning("You have periodic player tips enabled. You may turn them off at any time with the Toggle Receiving Player Tips verb in Preferences, or in character set up under the OOC tab!\n Player tips appear every 45-75 minutes."))
+ to_chat(target_mob, span_warning("You have periodic player tips enabled. You may turn them off at any time with the Toggle Receiving Player Tips verb in Preferences, or in character set up under the OOC tab!\nPlayer tips appear every 45-75 minutes."))
HasReceived.Add(target_mob.key)
to_chat(target_mob, span_notice("[GLOB.is_valid_url.Replace(last_tip, span_linkify("$1"))]"))
diff --git a/tgui/packages/tgui/interfaces/ColorMate/MatrixTabs/ColorMatrixSolver.tsx b/tgui/packages/tgui/interfaces/ColorMate/MatrixTabs/ColorMatrixSolver.tsx
index abf9f83284..9732515323 100644
--- a/tgui/packages/tgui/interfaces/ColorMate/MatrixTabs/ColorMatrixSolver.tsx
+++ b/tgui/packages/tgui/interfaces/ColorMate/MatrixTabs/ColorMatrixSolver.tsx
@@ -2,13 +2,7 @@ import { useBackend } from 'tgui/backend';
import { Box, Button, Input, Section, Stack } from 'tgui-core/components';
import { computeMatrixFromPairs, isValidHex } from '../functions';
import { ColorMatrixColorBox } from '../Helpers/MatrixColorBox';
-import type {
- ColorPair,
- ColorUpdate,
- Data,
- MatrixColors,
- SelectedId,
-} from '../types';
+import type { ColorPair, ColorUpdate, Data, SelectedId } from '../types';
export const ColorMateMatrixSolver = (props: {
activeID: SelectedId;
@@ -51,28 +45,19 @@ export const ColorMateMatrixSolver = (props: {
try {
const matrix = computeMatrixFromPairs(colorPairs);
- const newMatrixcolors: MatrixColors = {
- rr: matrix[0][0],
- rg: matrix[0][1],
- rb: matrix[0][2],
+ const parts: number[] = [];
- gr: matrix[1][0],
- gg: matrix[1][1],
- gb: matrix[1][2],
+ for (let col = 0; col < 3; col++) {
+ for (let row = 0; row < 3; row++) {
+ parts.push(Math.round(matrix[row][col] * 100) / 100);
+ }
+ }
- br: matrix[2][0],
- bg: matrix[2][1],
- bb: matrix[2][2],
-
- cr: matrix[0][3],
- cg: matrix[1][3],
- cb: matrix[2][3],
- };
-
- const ourMatrix = Object.values(newMatrixcolors)
- .map((v) => v.toFixed(2))
- .toString();
+ for (let row = 0; row < 3; row++) {
+ parts.push(Math.round(matrix[row][3] * 100) / 100);
+ }
+ const ourMatrix = parts.toString();
act('set_matrix_string', { value: ourMatrix });
} catch (err) {
console.log(`Matrix computation failed: ${err.message}`);
@@ -122,7 +107,35 @@ export const ColorMateMatrixSolver = (props: {
/>
- {`==>`}
+
+
+ {`==`}
+
+
+
+
+ {`=>`}
+
+
+
Target
@@ -164,7 +177,7 @@ export const ColorMateMatrixSolver = (props: {
onClick={() =>
onColorPairs([
...colorPairs,
- { input: '#ffffff', output: '#000000' },
+ { input: '#ffffff', output: '#ffffff' },
])
}
>
diff --git a/tgui/packages/tgui/interfaces/ColorMate/functions.ts b/tgui/packages/tgui/interfaces/ColorMate/functions.ts
index e6fb81f53f..fb69617639 100644
--- a/tgui/packages/tgui/interfaces/ColorMate/functions.ts
+++ b/tgui/packages/tgui/interfaces/ColorMate/functions.ts
@@ -5,56 +5,23 @@ export function isValidHex(hex: string): boolean {
export function computeMatrixFromPairs(
pairs: { input: string; output: string }[],
): number[][] {
- const identityColors = ['#ff0000', '#00ff00', '#0000ff', '#ffffff'];
+ const rgbIn = pairs.map((p) => hexToRgb255(p.input));
+ const rgbOut = pairs.map((p) => hexToRgb255(p.output));
- const workingPairs = [...pairs];
-
- for (const idColor of identityColors) {
- const alreadyUsed = workingPairs.some(
- (p) => p.input.toLowerCase() === idColor,
- );
- if (!alreadyUsed) {
- workingPairs.push({
- input: idColor,
- output: idColor,
- });
- }
+ if (rgbIn.length === 0) {
+ throw new Error('At least one color pair is required');
}
- const rgbIn = workingPairs.map((p) => hexToRgb(p.input));
- const rgbOut = workingPairs.map((p) => hexToRgb(p.output));
-
const matrix: number[][] = [];
for (let channel = 0; channel < 3; channel++) {
- let attempts = 0;
- let weights: number[] = [];
+ const a = rgbIn.map((rgb) => [rgb[0], rgb[1], rgb[2], 1]);
+ const b = rgbOut.map((rgb) => rgb[channel]);
- while (attempts < 5) {
- try {
- const a = rgbIn.map((rgb) => [rgb[0], rgb[1], rgb[2], 1]);
- const b = rgbOut.map((rgb) => rgb[channel]);
-
- weights = leastSquares(a, b);
-
- if (!weights.every((w) => w >= -10 && w <= 10)) {
- throw new Error('Computed weights out of range');
- }
-
- break;
- } catch (e) {
- const idx = rgbOut.length - 1;
- rgbOut[idx] = rgbOut[idx].map(
- (val) => val + Math.random() * 0.01 - 0.005,
- );
- attempts++;
- }
- }
+ const weights = leastSquares(a, b);
if (weights.length !== 4) {
- throw new Error(
- `Matrix computation failed for channel ${channel} after ${attempts} attempts`,
- );
+ throw new Error(`Matrix computation failed for channel ${channel}`);
}
matrix.push(weights);
@@ -63,66 +30,99 @@ export function computeMatrixFromPairs(
return matrix;
}
-function hexToRgb(hex: string): number[] {
- const clean = hex.replace('#', '').padEnd(6, '0');
- const r = parseInt(clean.slice(0, 2), 16) / 255;
- const g = parseInt(clean.slice(2, 4), 16) / 255;
- const b = parseInt(clean.slice(4, 6), 16) / 255;
+function hexToRgb255(hex: string): number[] {
+ const clean = hex.replace('#', '').padStart(6, '0');
+ const r = parseInt(clean.slice(0, 2), 16);
+ const g = parseInt(clean.slice(2, 4), 16);
+ const b = parseInt(clean.slice(4, 6), 16);
return [r, g, b];
}
-function transpose(matrix: number[][]): number[][] {
- return matrix[0].map((_, i) => matrix.map((row) => row[i]));
-}
+function gaussElim(a: number[][], b: number[]): number[] {
+ const n = a.length;
+ const m = a[0].length;
+ const aug = a.map((row, i) => [...row, b[i]]);
-function multiply(a: number[][], b: number[][]): number[][] {
- const result: number[][] = Array(a.length)
- .fill(0)
- .map(() => Array(b[0].length).fill(0));
- for (let i = 0; i < a.length; i++) {
- for (let j = 0; j < b[0].length; j++) {
- for (let k = 0; k < b.length; k++) {
- result[i][j] += a[i][k] * b[k][j];
+ let rank = 0;
+ for (let col = 0; col < m && rank < n; col++) {
+ let best = rank;
+ for (let row = rank + 1; row < n; row++) {
+ if (Math.abs(aug[row][col]) > Math.abs(aug[best][col])) {
+ best = row;
}
}
- }
- return result;
-}
-function inverse(matrix: number[][]): number[][] {
- const size = matrix.length;
- const augmented = matrix.map((row, i) => [
- ...row,
- ...Array(size)
- .fill(0)
- .map((_, j) => (i === j ? 1 : 0)),
- ]);
-
- for (let i = 0; i < size; i++) {
- const diag = augmented[i][i];
- if (diag === 0) {
- throw new Error('Singular matrix');
+ if (Math.abs(aug[best][col]) < 1e-12) {
+ continue;
}
- for (let j = 0; j < size * 2; j++) augmented[i][j] /= diag;
- for (let k = 0; k < size; k++) {
- if (k === i) continue;
- const factor = augmented[k][i];
- for (let j = 0; j < size * 2; j++) {
- augmented[k][j] -= factor * augmented[i][j];
+
+ [aug[rank], aug[best]] = [aug[best], aug[rank]];
+
+ for (let row = rank + 1; row < n; row++) {
+ const factor = aug[row][col] / aug[rank][col];
+ for (let j = col; j <= m; j++) {
+ aug[row][j] -= factor * aug[rank][j];
}
}
+
+ rank++;
}
- return augmented.map((row) => row.slice(size));
+
+ const x = new Array(m).fill(0);
+ for (let i = Math.min(rank, m) - 1; i >= 0; i--) {
+ if (Math.abs(aug[i][i]) < 1e-12) continue;
+ let sum = aug[i][m];
+ for (let j = i + 1; j < m; j++) {
+ sum -= aug[i][j] * x[j];
+ }
+ x[i] = sum / aug[i][i];
+ }
+
+ return x;
}
function leastSquares(a: number[][], b: number[]): number[] {
- const AT = transpose(a);
- const ATa = multiply(AT, a);
- const ATb = multiply(
- AT,
- b.map((v) => [v]),
+ const rows = a.length;
+ const cols = a[0].length;
+
+ const ATa: number[][] = Array.from({ length: cols }, () =>
+ new Array(cols).fill(0),
);
- const ATainv = inverse(ATa);
- const result = multiply(ATainv, ATb);
- return result.map((r) => r[0]);
+ const ATb = new Array(cols).fill(0);
+
+ for (let i = 0; i < cols; i++) {
+ for (let j = 0; j < cols; j++) {
+ for (let k = 0; k < rows; k++) {
+ ATa[i][j] += a[k][i] * a[k][j];
+ }
+ }
+ for (let k = 0; k < rows; k++) {
+ ATb[i] += a[k][i] * b[k];
+ }
+ }
+
+ return gaussElim(ATa, ATb);
+}
+
+export function transformColor(
+ r: number,
+ g: number,
+ b: number,
+ matrix: number[][],
+): number[] {
+ const outR = clamp(
+ matrix[0][0] * r + matrix[0][1] * g + matrix[0][2] * b + matrix[0][3],
+ );
+ const outG = clamp(
+ matrix[1][0] * r + matrix[1][1] * g + matrix[1][2] * b + matrix[1][3],
+ );
+ const outB = clamp(
+ matrix[2][0] * r + matrix[2][1] * g + matrix[2][2] * b + matrix[2][3],
+ );
+
+ return [outR, outG, outB];
+}
+
+function clamp(v: number): number {
+ return Math.max(0, Math.min(255, Math.round(v)));
}
diff --git a/tgui/packages/tgui/interfaces/ColorMate/index.tsx b/tgui/packages/tgui/interfaces/ColorMate/index.tsx
index acb42a879b..336b95289f 100644
--- a/tgui/packages/tgui/interfaces/ColorMate/index.tsx
+++ b/tgui/packages/tgui/interfaces/ColorMate/index.tsx
@@ -20,7 +20,7 @@ export const ColorMate = (props) => {
const { act, data } = useBackend();
const [colorPairs, setColorPairs] = useState([
- { input: '#ffffff', output: '#000000' },
+ { input: '#ffffff', output: '#ffffff' },
]);
const [activeID, setActiveId] = useState({ id: null, type: null });
@@ -190,7 +190,7 @@ export const ColorMate = (props) => {
Coloring: {item_name}
-
+
{
-
+
{tab[activemode] || Error}