From 0f19902a56ef896d621bd601739ad8e1e83f6534 Mon Sep 17 00:00:00 2001 From: Marc Emmanuel Date: Sun, 1 Feb 2026 22:16:04 +0100 Subject: [PATCH] refactor: migrate provinces generator to new module structure (#1295) * refactor: migrate provinces generator to new module structure * fix: after merge fixes of state * refactor: fixed a bug so had to update tests --- public/modules/provinces-generator.js | 257 ------------ src/index.html | 1 - src/modules/index.ts | 1 + src/modules/provinces-generator.ts | 393 ++++++++++++++++++ src/modules/states-generator.ts | 1 + src/types/PackedGraph.ts | 3 + .../e2e/layers.spec.ts-snapshots/borders.html | 2 +- 7 files changed, 399 insertions(+), 259 deletions(-) delete mode 100644 public/modules/provinces-generator.js create mode 100644 src/modules/provinces-generator.ts diff --git a/public/modules/provinces-generator.js b/public/modules/provinces-generator.js deleted file mode 100644 index 3276fdf0..00000000 --- a/public/modules/provinces-generator.js +++ /dev/null @@ -1,257 +0,0 @@ -"use strict"; - -window.Provinces = (function () { - const forms = { - Monarchy: {County: 22, Earldom: 6, Shire: 2, Landgrave: 2, Margrave: 2, Barony: 2, Captaincy: 1, Seneschalty: 1}, - Republic: {Province: 6, Department: 2, Governorate: 2, District: 1, Canton: 1, Prefecture: 1}, - Theocracy: {Parish: 3, Deanery: 1}, - Union: {Province: 1, State: 1, Canton: 1, Republic: 1, County: 1, Council: 1}, - Anarchy: {Council: 1, Commune: 1, Community: 1, Tribe: 1}, - Wild: {Territory: 10, Land: 5, Region: 2, Tribe: 1, Clan: 1, Dependency: 1, Area: 1} - }; - - const generate = (regenerate = false, regenerateLockedStates = false) => { - TIME && console.time("generateProvinces"); - const localSeed = regenerate ? generateSeed() : seed; - Math.random = aleaPRNG(localSeed); - - const {cells, states, burgs} = pack; - const provinces = [0]; // 0 index is reserved for "no province" - const provinceIds = new Uint16Array(cells.i.length); - - const isProvinceLocked = province => province.lock || (!regenerateLockedStates && states[province.state]?.lock); - const isProvinceCellLocked = cell => provinceIds[cell] && isProvinceLocked(provinces[provinceIds[cell]]); - - if (regenerate) { - pack.provinces.forEach(province => { - if (!province.i || province.removed || !isProvinceLocked(province)) return; - - const newId = provinces.length; - for (const i of cells.i) { - if (cells.province[i] === province.i) provinceIds[i] = newId; - } - - province.i = newId; - provinces.push(province); - }); - } - - const provincesRatio = +byId("provincesRatio").value; - const max = provincesRatio == 100 ? 1000 : gauss(20, 5, 5, 100) * provincesRatio ** 0.5; // max growth - - // generate provinces for selected burgs - states.forEach(s => { - s.provinces = []; - if (!s.i || s.removed) return; - if (provinces.length) s.provinces = provinces.filter(p => p.state === s.i).map(p => p.i); // locked provinces ids - if (s.lock && !regenerateLockedStates) return; // don't regenerate provinces of a locked state - - const stateBurgs = burgs - .filter(b => b.state === s.i && !b.removed && !provinceIds[b.cell]) - .sort((a, b) => b.population * gauss(1, 0.2, 0.5, 1.5, 3) - a.population) - .sort((a, b) => b.capital - a.capital); - if (stateBurgs.length < 2) return; // at least 2 provinces are required - - const provincesNumber = Math.max(Math.ceil((stateBurgs.length * provincesRatio) / 100), 2); - const form = Object.assign({}, forms[s.form]); - - for (let i = 0; i < provincesNumber; i++) { - const provinceId = provinces.length; - const center = stateBurgs[i].cell; - const burg = stateBurgs[i]; - const c = stateBurgs[i].culture; - const nameByBurg = P(0.5); - const name = nameByBurg ? stateBurgs[i].name : Names.getState(Names.getCultureShort(c), c); - const formName = rw(form); - form[formName] += 10; - const fullName = name + " " + formName; - const color = getMixedColor(s.color); - const kinship = nameByBurg ? 0.8 : 0.4; - const type = Burgs.getType(center, burg.port); - const coa = COA.generate(stateBurgs[i].coa, kinship, null, type); - coa.shield = COA.getShield(c, s.i); - - s.provinces.push(provinceId); - provinces.push({i: provinceId, state: s.i, center, burg: burg.i, name, formName, fullName, color, coa}); - } - }); - - // expand generated provinces - const queue = new FlatQueue(); - const cost = []; - - provinces.forEach(p => { - if (!p.i || p.removed || isProvinceLocked(p)) return; - provinceIds[p.center] = p.i; - queue.push({e: p.center, province: p.i, state: p.state, p: 0}, 0); - cost[p.center] = 1; - }); - - while (queue.length) { - const {e, p, province, state} = queue.pop(); - - cells.c[e].forEach(e => { - if (isProvinceCellLocked(e)) return; // do not overwrite cell of locked provinces - - const land = cells.h[e] >= 20; - if (!land && !cells.t[e]) return; // cannot pass deep ocean - if (land && cells.state[e] !== state) return; - const evevation = cells.h[e] >= 70 ? 100 : cells.h[e] >= 50 ? 30 : cells.h[e] >= 20 ? 10 : 100; - const totalCost = p + evevation; - - if (totalCost > max) return; - if (!cost[e] || totalCost < cost[e]) { - if (land) provinceIds[e] = province; // assign province to a cell - cost[e] = totalCost; - queue.push({e, province, state, p: totalCost}, totalCost); - } - }); - } - - // justify provinces shapes a bit - for (const i of cells.i) { - if (cells.burg[i]) continue; // do not overwrite burgs - if (isProvinceCellLocked(i)) continue; // do not overwrite cell of locked provinces - - const neibs = cells.c[i] - .filter(c => cells.state[c] === cells.state[i] && !isProvinceCellLocked(c)) - .map(c => provinceIds[c]); - const adversaries = neibs.filter(c => c !== provinceIds[i]); - if (adversaries.length < 2) continue; - - const buddies = neibs.filter(c => c === provinceIds[i]).length; - if (buddies.length > 2) continue; - - const competitors = adversaries.map(p => adversaries.reduce((s, v) => (v === p ? s + 1 : s), 0)); - const max = d3.max(competitors); - if (buddies >= max) continue; - - provinceIds[i] = adversaries[competitors.indexOf(max)]; - } - - // add "wild" provinces if some cells don't have a province assigned - const noProvince = Array.from(cells.i).filter(i => cells.state[i] && !provinceIds[i]); // cells without province assigned - states.forEach(s => { - if (!s.i || s.removed) return; - if (s.lock && !regenerateLockedStates) return; - if (!s.provinces.length) return; - - const coreProvinceNames = s.provinces.map(p => provinces[p]?.name); - const colonyNamePool = [s.name, ...coreProvinceNames].filter(name => name && !/new/i.test(name)); - const getColonyName = () => { - if (colonyNamePool.length < 1) return null; - - const index = rand(colonyNamePool.length - 1); - const spliced = colonyNamePool.splice(index, 1); - return spliced[0] ? `New ${spliced[0]}` : null; - }; - - let stateNoProvince = noProvince.filter(i => cells.state[i] === s.i && !provinceIds[i]); - while (stateNoProvince.length) { - // add new province - const provinceId = provinces.length; - const burgCell = stateNoProvince.find(i => cells.burg[i]); - const center = burgCell ? burgCell : stateNoProvince[0]; - const burg = burgCell ? cells.burg[burgCell] : 0; - provinceIds[center] = provinceId; - - // expand province - const cost = []; - cost[center] = 1; - queue.push({e: center, p: 0}, 0); - while (queue.length) { - const {e, p} = queue.pop(); - - cells.c[e].forEach(nextCellId => { - if (provinceIds[nextCellId]) return; - const land = cells.h[nextCellId] >= 20; - if (cells.state[nextCellId] && cells.state[nextCellId] !== s.i) return; - const ter = land ? (cells.state[nextCellId] === s.i ? 3 : 20) : cells.t[nextCellId] ? 10 : 30; - const totalCost = p + ter; - - if (totalCost > max) return; - if (!cost[nextCellId] || totalCost < cost[nextCellId]) { - if (land && cells.state[nextCellId] === s.i) provinceIds[nextCellId] = provinceId; // assign province to a cell - cost[nextCellId] = totalCost; - queue.push({e: nextCellId, p: totalCost}, totalCost); - } - }); - } - - // generate "wild" province name - const c = cells.culture[center]; - const f = pack.features[cells.f[center]]; - const color = getMixedColor(s.color); - - const provCells = stateNoProvince.filter(i => provinceIds[i] === provinceId); - const singleIsle = provCells.length === f.cells && !provCells.find(i => cells.f[i] !== f.i); - const isleGroup = !singleIsle && !provCells.find(i => pack.features[cells.f[i]].group !== "isle"); - const colony = !singleIsle && !isleGroup && P(0.5) && !isPassable(s.center, center); - - const name = (() => { - const colonyName = colony && P(0.8) && getColonyName(); - if (colonyName) return colonyName; - if (burgCell && P(0.5)) return burgs[burg].name; - return Names.getState(Names.getCultureShort(c), c); - })(); - - const formName = (() => { - if (singleIsle) return "Island"; - if (isleGroup) return "Islands"; - if (colony) return "Colony"; - return rw(forms["Wild"]); - })(); - - const fullName = name + " " + formName; - - const dominion = colony ? P(0.95) : singleIsle || isleGroup ? P(0.7) : P(0.3); - const kinship = dominion ? 0 : 0.4; - const type = Burgs.getType(center, burgs[burg]?.port); - const coa = COA.generate(s.coa, kinship, dominion, type); - coa.shield = COA.getShield(c, s.i); - - provinces.push({i: provinceId, state: s.i, center, burg, name, formName, fullName, color, coa}); - s.provinces.push(provinceId); - - // check if there is a land way within the same state between two cells - function isPassable(from, to) { - if (cells.f[from] !== cells.f[to]) return false; // on different islands - const passableQueue = [from], - used = new Uint8Array(cells.i.length), - state = cells.state[from]; - while (passableQueue.length) { - const current = passableQueue.pop(); - if (current === to) return true; // way is found - cells.c[current].forEach(c => { - if (used[c] || cells.h[c] < 20 || cells.state[c] !== state) return; - passableQueue.push(c); - used[c] = 1; - }); - } - return false; // way is not found - } - - // re-check - stateNoProvince = noProvince.filter(i => cells.state[i] === s.i && !provinceIds[i]); - } - }); - - cells.province = provinceIds; - pack.provinces = provinces; - - TIME && console.timeEnd("generateProvinces"); - }; - - // calculate pole of inaccessibility for each province - const getPoles = () => { - const getType = cellId => pack.cells.province[cellId]; - const poles = getPolesOfInaccessibility(pack, getType); - - pack.provinces.forEach(province => { - if (!province.i || province.removed) return; - province.pole = poles[province.i] || [0, 0]; - }); - }; - - return {generate, getPoles}; -})(); diff --git a/src/index.html b/src/index.html index f4b605b2..3f46d62c 100644 --- a/src/index.html +++ b/src/index.html @@ -8494,7 +8494,6 @@ - diff --git a/src/modules/index.ts b/src/modules/index.ts index a3dbe219..f8fa62ef 100644 --- a/src/modules/index.ts +++ b/src/modules/index.ts @@ -10,3 +10,4 @@ import "./biomes"; import "./cultures-generator"; import "./routes-generator"; import "./states-generator"; +import "./provinces-generator"; diff --git a/src/modules/provinces-generator.ts b/src/modules/provinces-generator.ts new file mode 100644 index 00000000..68d46f33 --- /dev/null +++ b/src/modules/provinces-generator.ts @@ -0,0 +1,393 @@ +import Alea from "alea"; +import { max } from "d3"; +import { + byId, + gauss, + generateSeed, + getMixedColor, + getPolesOfInaccessibility, + P, + rand, + rw, +} from "../utils"; + +declare global { + var Provinces: ProvinceModule; +} + +export interface Province { + i: number; + removed?: boolean; + state: number; + lock?: boolean; + center: number; + burg: number; + name: string; + formName: string; + fullName: string; + color: string; + coa: any; + pole?: [number, number]; +} + +class ProvinceModule { + forms: Record> = { + Monarchy: { + County: 22, + Earldom: 6, + Shire: 2, + Landgrave: 2, + Margrave: 2, + Barony: 2, + Captaincy: 1, + Seneschalty: 1, + }, + Republic: { + Province: 6, + Department: 2, + Governorate: 2, + District: 1, + Canton: 1, + Prefecture: 1, + }, + Theocracy: { Parish: 3, Deanery: 1 }, + Union: { + Province: 1, + State: 1, + Canton: 1, + Republic: 1, + County: 1, + Council: 1, + }, + Anarchy: { Council: 1, Commune: 1, Community: 1, Tribe: 1 }, + Wild: { + Territory: 10, + Land: 5, + Region: 2, + Tribe: 1, + Clan: 1, + Dependency: 1, + Area: 1, + }, + }; + + generate(regenerate = false, regenerateLockedStates = false) { + TIME && console.time("generateProvinces"); + const localSeed = regenerate ? generateSeed() : seed; + Math.random = Alea(localSeed); + + const { cells, states, burgs } = pack; + const provinces: Province[] = [0 as unknown as Province]; // 0 index is reserved for "no province" + const provinceIds = new Uint16Array(cells.i.length); + + const isProvinceLocked = (province: Province) => + province.lock || + (!regenerateLockedStates && states[province.state]?.lock); + const isProvinceCellLocked = (cell: number) => + provinceIds[cell] && isProvinceLocked(provinces[provinceIds[cell]]); + + if (regenerate) { + pack.provinces.forEach((province) => { + if (!province.i || province.removed || !isProvinceLocked(province)) + return; + + const newId = provinces.length; + for (const i of cells.i) { + if (cells.province[i] === province.i) provinceIds[i] = newId; + } + + province.i = newId; + provinces.push(province); + }); + } + + const provincesRatio = (byId("provincesRatio") as HTMLInputElement) + .valueAsNumber; + const maxGrowth = + provincesRatio === 100 + ? 1000 + : gauss(20, 5, 5, 100) * provincesRatio ** 0.5; // max growth + + // generate provinces for selected burgs + states.forEach((s) => { + s.provinces = []; + if (!s.i || s.removed) return; + if (provinces.length) + s.provinces = provinces.filter((p) => p.state === s.i).map((p) => p.i); // locked provinces ids + if (s.lock && !regenerateLockedStates) return; // don't regenerate provinces of a locked state + + const stateBurgs = burgs + .filter((b) => b.state === s.i && !b.removed && !provinceIds[b.cell]) // burgs in this state without province assigned + .sort( + (a, b) => b.population! * gauss(1, 0.2, 0.5, 1.5, 3) - a.population!, + ) // biggest population first + .sort((a, b) => b.capital! - a.capital!); // capitals first + if (stateBurgs.length < 2) return; // at least 2 provinces are required + + const provincesNumber = Math.max( + Math.ceil((stateBurgs.length * provincesRatio) / 100), + 2, + ); + const form = Object.assign({}, this.forms[s.form!]); + + for (let i = 0; i < provincesNumber; i++) { + const provinceId = provinces.length; + const center = stateBurgs[i].cell; + const burg = stateBurgs[i]; + const c = stateBurgs[i].culture!; + const nameByBurg = P(0.5); + const name = nameByBurg + ? stateBurgs[i].name! + : Names.getState(Names.getCultureShort(c), c); + const formName = rw(form); + form[formName] += 10; + const fullName = `${name} ${formName}`; + const color = getMixedColor(s.color!); + const kinship = nameByBurg ? 0.8 : 0.4; + const type = Burgs.getType(center, burg.port); + const coa = COA.generate(stateBurgs[i].coa, kinship, null, type); + coa.shield = COA.getShield(c, s.i); + + s.provinces.push(provinceId); + provinces.push({ + i: provinceId, + state: s.i, + center, + burg: burg.i!, + name, + formName, + fullName, + color, + coa, + }); + } + }); + + // expand generated provinces + const queue = new FlatQueue(); + const cost: number[] = []; + + provinces.forEach((p) => { + if (!p.i || p.removed || isProvinceLocked(p)) return; + provinceIds[p.center] = p.i; + queue.push({ e: p.center, province: p.i, state: p.state, p: 0 }, 0); + cost[p.center] = 1; + }); + + while (queue.length) { + const { e, p, province, state } = queue.pop(); + + cells.c[e].forEach((e) => { + if (isProvinceCellLocked(e)) return; // do not overwrite cell of locked provinces + + const land = cells.h[e] >= 20; + if (!land && !cells.t[e]) return; // cannot pass deep ocean + if (land && cells.state[e] !== state) return; + const evevation = + cells.h[e] >= 70 + ? 100 + : cells.h[e] >= 50 + ? 30 + : cells.h[e] >= 20 + ? 10 + : 100; + const totalCost = p + evevation; + + if (totalCost > maxGrowth) return; + if (!cost[e] || totalCost < cost[e]) { + if (land) provinceIds[e] = province; // assign province to a cell + cost[e] = totalCost; + queue.push({ e, province, state, p: totalCost }, totalCost); + } + }); + } + + // justify provinces shapes a bit + for (const i of cells.i) { + if (cells.burg[i]) continue; // do not overwrite burgs + if (isProvinceCellLocked(i)) continue; // do not overwrite cell of locked provinces + + const neibs = cells.c[i] + .filter( + (c) => cells.state[c] === cells.state[i] && !isProvinceCellLocked(c), + ) + .map((c) => provinceIds[c]); + const adversaries = neibs.filter((c) => c !== provinceIds[i]); + if (adversaries.length < 2) continue; + + const buddies = neibs.filter((c) => c === provinceIds[i]).length; + if (buddies > 2) continue; + + const competitors = adversaries.map((p) => + adversaries.reduce((s, v) => (v === p ? s + 1 : s), 0), + ); + const maxBuddies = max(competitors) as number; + if (buddies >= maxBuddies) continue; + + provinceIds[i] = adversaries[competitors.indexOf(maxBuddies)]; + } + + // add "wild" provinces if some cells don't have a province assigned + const noProvince = Array.from(cells.i).filter( + (i) => cells.state[i] && !provinceIds[i], + ); // cells without province assigned + states.forEach((s) => { + if (!s.i || s.removed) return; + if (s.lock && !regenerateLockedStates) return; + if (!s.provinces?.length) return; + + const coreProvinceNames = s.provinces.map((p) => provinces[p]?.name); + const colonyNamePool = [s.name, ...coreProvinceNames].filter( + (name) => name && !/new/i.test(name), + ); + const getColonyName = () => { + if (colonyNamePool.length < 1) return null; + + const index = rand(colonyNamePool.length - 1); + const spliced = colonyNamePool.splice(index, 1); + return spliced[0] ? `New ${spliced[0]}` : null; + }; + + let stateNoProvince = noProvince.filter( + (i) => cells.state[i] === s.i && !provinceIds[i], + ); + while (stateNoProvince.length) { + // add new province + const provinceId = provinces.length; + const burgCell = stateNoProvince.find((i) => cells.burg[i]); + const center = burgCell ? burgCell : stateNoProvince[0]; + const burg = burgCell ? cells.burg[burgCell] : 0; + provinceIds[center] = provinceId; + + // expand province + const cost: number[] = []; + cost[center] = 1; + queue.push({ e: center, p: 0 }, 0); + while (queue.length) { + const { e, p } = queue.pop(); + + cells.c[e].forEach((nextCellId) => { + if (provinceIds[nextCellId]) return; + const land = cells.h[nextCellId] >= 20; + if (cells.state[nextCellId] && cells.state[nextCellId] !== s.i) + return; + const ter = land + ? cells.state[nextCellId] === s.i + ? 3 + : 20 + : cells.t[nextCellId] + ? 10 + : 30; + const totalCost = p + ter; + + if (totalCost > maxGrowth) return; + if (!cost[nextCellId] || totalCost < cost[nextCellId]) { + if (land && cells.state[nextCellId] === s.i) + provinceIds[nextCellId] = provinceId; // assign province to a cell + cost[nextCellId] = totalCost; + queue.push({ e: nextCellId, p: totalCost }, totalCost); + } + }); + } + + // generate "wild" province name + const c = cells.culture[center]; + const f = pack.features[cells.f[center]]; + const color = getMixedColor(s.color!); + + const provCells = stateNoProvince.filter( + (i) => provinceIds[i] === provinceId, + ); + const singleIsle = + provCells.length === f.cells && + !provCells.find((i) => cells.f[i] !== f.i); + const isleGroup = + !singleIsle && + !provCells.find((i) => pack.features[cells.f[i]].group !== "isle"); + const colony = + !singleIsle && !isleGroup && P(0.5) && !isPassable(s.center, center); + + const name = (() => { + const colonyName = colony && P(0.8) && getColonyName(); + if (colonyName) return colonyName; + if (burgCell && P(0.5)) return burgs[burg].name; + return Names.getState(Names.getCultureShort(c), c); + })(); + + const formName = (() => { + if (singleIsle) return "Island"; + if (isleGroup) return "Islands"; + if (colony) return "Colony"; + return rw(this.forms["Wild"]); + })(); + + const fullName = `${name} ${formName}`; + + const dominion = colony + ? P(0.95) + : singleIsle || isleGroup + ? P(0.7) + : P(0.3); + const kinship = dominion ? 0 : 0.4; + const type = Burgs.getType(center, burgs[burg]?.port); + const coa = COA.generate(s.coa, kinship, dominion, type); + coa.shield = COA.getShield(c, s.i); + + provinces.push({ + i: provinceId, + state: s.i, + center, + burg, + name: name!, + formName, + fullName, + color, + coa, + }); + s.provinces.push(provinceId); + + // check if there is a land way within the same state between two cells + function isPassable(from: number, to: number) { + if (cells.f[from] !== cells.f[to]) return false; // on different islands + const passableQueue = [from], + used = new Uint8Array(cells.i.length), + state = cells.state[from]; + while (passableQueue.length) { + const current = passableQueue.pop() as number; + if (current === to) return true; // way is found + cells.c[current].forEach((c) => { + if (used[c] || cells.h[c] < 20 || cells.state[c] !== state) + return; + passableQueue.push(c); + used[c] = 1; + }); + } + return false; // way is not found + } + + // re-check + stateNoProvince = noProvince.filter( + (i) => cells.state[i] === s.i && !provinceIds[i], + ); + } + }); + + cells.province = provinceIds; + pack.provinces = provinces; + + TIME && console.timeEnd("generateProvinces"); + } + + // calculate pole of inaccessibility for each province + getPoles() { + const getType = (cellId: number) => pack.cells.province[cellId]; + const poles = getPolesOfInaccessibility(pack, getType); + + pack.provinces.forEach((province) => { + if (!province.i || province.removed) return; + province.pole = poles[province.i] || [0, 0]; + }); + } +} + +window.Provinces = new ProvinceModule(); diff --git a/src/modules/states-generator.ts b/src/modules/states-generator.ts index cb13dd76..d577c092 100644 --- a/src/modules/states-generator.ts +++ b/src/modules/states-generator.ts @@ -50,6 +50,7 @@ export interface State { formName?: string; fullName?: string; form?: string; + provinces?: number[]; } class StatesModule { diff --git a/src/types/PackedGraph.ts b/src/types/PackedGraph.ts index d193ead5..33a31bd7 100644 --- a/src/types/PackedGraph.ts +++ b/src/types/PackedGraph.ts @@ -1,6 +1,7 @@ import type { Burg } from "../modules/burgs-generator"; import type { Culture } from "../modules/cultures-generator"; import type { PackedGraphFeature } from "../modules/features"; +import type { Province } from "../modules/provinces-generator"; import type { River } from "../modules/river-generator"; import type { Route } from "../modules/routes-generator"; import type { State } from "../modules/states-generator"; @@ -39,6 +40,7 @@ export interface PackedGraph { religion: TypedArray; // cell religion id state: number[]; // cell state id area: TypedArray; // cell area + province: TypedArray; // cell province id routes: Record>; }; vertices: { @@ -56,4 +58,5 @@ export interface PackedGraph { cultures: Culture[]; routes: Route[]; religions: any[]; + provinces: Province[]; } diff --git a/tests/e2e/layers.spec.ts-snapshots/borders.html b/tests/e2e/layers.spec.ts-snapshots/borders.html index 6e5c5003..47d6122a 100644 --- a/tests/e2e/layers.spec.ts-snapshots/borders.html +++ b/tests/e2e/layers.spec.ts-snapshots/borders.html @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file