Mod to increase city size

Gonzo8927

Chieftain
Joined
Feb 18, 2025
Messages
7
Hey everyone,

So to start off, I have no idea what I am doing and am heavily using ChatGPT. I was able to fix another mod here with chatGPT then was like "hur dur, I can do one from scratch"

I'm working on a mod to expand city territory in my game from 3 tiles to 5, but I'm running into issues. I have zero coding experience, so I need someone to help troubleshoot why this isn't working.

What I’m Trying to Do:

  • Expand the city working radius from 3 to 5 tiles.
  • Allow cities to claim and expand into territory up to 5 tiles away.
  • Ensure UI updates properly to reflect the expanded borders.

What I Have So Far:

I updated two files:

  1. City Radius Expansion Mod (Expands city territory in logic).
  2. UI Adjustments (To reflect the changes visually).
Despite making these changes, the mod doesn’t seem to work at all. I’ve tried checking if it loads but I don’t see any impact in-game.

What I Need Help With:

  • Checking if the mod is even loading.
  • Making sure the radius expansion actually applies.
  • Fixing any issues preventing UI from displaying properly.
  • Any general advice for someone with zero coding experience.
I’ve attached the two modified files below. If anyone could take a look and guide me in the right direction, I’d really appreciate it!

====================File 1=========================

class CityExpansionMod {
constructor() {
this.newCityRadius = 5; // Expanding from 3 to 5 tiles
this.init();
}

init() {
engine.on('GameStarted', this.applyCityExpansion.bind(this));
engine.on('GameLoaded', this.applyCityExpansion.bind(this));
engine.on('TileClaimed', this.updateTileOwnership.bind(this));
engine.on('CityGrowth', this.expandTerritory.bind(this));
}

applyCityExpansion() {
if (typeof City !== 'undefined' && City.prototype) {
City.prototype.getMaxWorkingRadius = function () {
return 5; // Expanding from 3 to 5 tiles
};

City.prototype.canAcquireTile = function (tile) {
let distance = this.getDistanceFromCityCenter(tile);
let isOwned = LandClaimFlagManager.isTileOwned(tile, this.owner);
return distance <= 5 && isOwned; // Ensure tile is within range and owned
};
}
console.log(`CityExpansionMod: City radius and tile acquisition expanded to 5 tiles.`);
}

updateTileOwnership(tile, owner) {
if (typeof City !== 'undefined' && City.prototype) {
City.prototype.canAcquireTile = function (tile) {
let distance = this.getDistanceFromCityCenter(tile);
let isOwned = LandClaimFlagManager.isTileOwned(tile, owner);
return distance <= 5 && isOwned; // Ensure tile is within range and owned
};
}
}

expandTerritory(city) {
let tiles = city.getNearbyTiles(5);
tiles.forEach(tile => {
if (!tile.isOwned && city.canAcquireTile(tile)) {
LandClaimFlagManager.claimTile(tile, city.owner);
console.log(`CityExpansionMod: Tile at ${tile.x}, ${tile.y} claimed.`);
}
});
}

cleanup() {
engine.off('GameStarted', this.applyCityExpansion.bind(this));
engine.off('GameLoaded', this.applyCityExpansion.bind(this));
engine.off('TileClaimed', this.updateTileOwnership.bind(this));
engine.off('CityGrowth', this.expandTerritory.bind(this));
delete window.CityExpansionMod;
}
}

window.CityExpansionMod = new CityExpansionMod();

====================File 2=========================

<?xml version="1.0" encoding="utf-8"?>
<Mod id="CityExpansionMod" version="1.0"
xmlns="ModInfo">
<Properties>
<Name>City Radius Expansion</Name>
<Description>Expands the city working radius from 3 tiles to 5 tiles.</Description>
<Authors>YourName</Authors>
<Package>Mod</Package>
<AffectsSavedGames>1</AffectsSavedGames>
</Properties>
<Dependencies>
<Mod id="base-standard" title="LOC_MODULE_BASE_STANDARD_NAME" />
</Dependencies>
<ActionCriteria>
<Criteria id="always">
<AlwaysMet/>
</Criteria>
</ActionCriteria>
<ActionGroups>
<ActionGroup id="city-expansion-actions" scope="game" criteria="always">
<Actions>
<UIScripts>
<Item>ui/CityExpansion.js</Item>
</UIScripts>
</Actions>
</ActionGroup>
</ActionGroups>
</Mod>



Thanks in advance!
 
As far as I know and in my testing, city max buy plot range doesn't work.

To try and answer the op's question. The main reasons it's not working is your javascript file is....let's just say incomplete. Your modinfo file seems fine to me.


To start, you are invoking LandClaimFlagManager and trying to use a function isTileOwned. First, that's not how you invoke a function from a separate file in js.

import { LandClaimFlagManager } from 'base-standard/ui/land-claim-flag/land-claim-flag-manager.js';

This line would need to be before your class definition. Secondly LandClaimFlagManager has no function that is called 'isTileOwned'. If you read the file it doesn't even call them tiles, it calls them plots.
Let's move on to the first function. applyCityExpansion(). You start the function with an if statement. I'll get to the illogical if in a moment, you call typeof (another function) with an undeclared object City. You then correctly use one boolean operator and incorrectly use another boolean operator. Let me explain.

Boolean operators work with boolean logic. If you don't know boolean logic there are many videos online. Boolean logic is similar to algebra so you read it left to right and parenthesis matter. Since your first check (strict inequality, !==) is false (typeof City = 'undefined' because you never defined it.) the second boolean operator (logical AND, &&) is now checking a false value with an uninitialized object, (f|City.prototype) so it stays false. Sorry, that would be for & operator. The && operator would just quit at the first false and therefore not even evaluate the second operand. [Side note, you could be using this as a way to first check it's not undefined and then make sure City.prototype exists, in which case it would work as is. Just with the other logical errors it's hard to tell. But honestly, that seems like AI slop to me, as simply checking if it's defined would tell you if it exists. The second boolean check is redundant.]

Anyway, after that check you are trying to set a nonexistent variable (from an uninitialized object) [City.prototype.getMaxWorkingRadius].
Which you could just, you know, set. City.prototype.getMaxWorkingRadius = 5; (If any of these structures existed.) But instead you are defining a runtime function and assigning it to a nested class variable, which you seemingly don't have access to. (IE. private class) In other words you are trying to assign a function to an undeclared variable.

You are setting a variable called distance to this.getDistanceFromCityCenter(). Let's break that one down. this is a keyword in javascript. It is a reference to the object you are currently in. In this case, this would be the instantiated object created from your class CityExpansionMod. So does CityExpansionMod have a function called getDistanceFromCityCenter? Nope. So distance = null.
Let's move on to the value(s) you are trying to return. You end this function with return distance <= 5 && isOwned. return is another javascript keyword. It is the output of your function. You are asking your function to output the variable distance <= 5 && isOwned . I wouldn't even know what to do with that request and I'm a human. I would suggest taking a coding class because this isn't even in the realm of being compile-able. So yeah, the rest of the file is pretty much the same-same.


TL;DR : As to why your file isn't working? It didn't compile due to the errors in the code. [Side - side note: AI is useful for somethings. But unless you already know how to code and how to fix broken code, AI will never give you something usable. EVER]

My understanding of the situation is that the code for workable plot lengths is packed into either the game's executable or another packed file. So unless someone finds a way to reverse engineer the compression or they release a modding kit, it isn't doable. Maybe I'm wrong (I would love to be wrong.) but with what I've already read in the js files I just don't see the code necessary. All I could find was references and invoked functions from private objects. Maybe there is a way to do it now, I'll have to think on it. That being said there are a lot of files so who knows?
 
Last edited:
Try:

<Database>
<GlobalParameters>
<Update>
<Set Value="5" />
<Where Name="CITY_MAX_BUY_PLOT_RANGE" />
</Update>
</GlobalParameters>
</Database>
Pretty sure that's just a civ 6 artifact

Not sure what these parameters do, but you could experiment. They're not referenced in any JS though.

LAND_CLAIM_MAX_RANGE -1
LAND_CLAIM_RANGE_PER_STEP 2
LAND_CLAIM_STEPS 1
 
I've also tested those. Setting LAND_CLAIM_MAX_RANGE to anything greater than -1 seems to make it impossible to place down a city or town at all. I believe these have to do with CITY_MIN_RANGE and in setting the amount of land a city takes up not the amount of land that is workable by a city. The 3 tile range seems to be hard-coded into a packed file.

From what I've seen in the code I believe it works like this. The game has a base map that is 3 tiles long by 3 tiles wide centered on a tile (city or town).[If they are smart they would use a 3x3 and simply rotate it and they do center everything around a point so they probably do.] When I say map I mean it in the associative array way, not like minimap. This map is generated by a private class called Game This is how the game handles when a new tile is acquired.

From : modules/base-standard/ui/place-population/model-place-population.js
-------------------------------------------------------------------------------------------------------
updateExpandPlots(id) {
this.expandPlots = [];
const result = Game.CityCommands.canStart(id, CityCommandTypes.EXPAND, {}, false);
if (!result.Plots) {
console.error("model-place-population: updateExpandPlots() failed to get any Plots for Expand command");
return;
}
for (const [index, plotIndex] of result.Plots.entries()) {
....
-------------------------------------------------------------------------------------------------------
The game stores the mapping in a const it calls result and then checks if the plots are valid. However this validation of the plots happens in the private class Game and we can only see the results via instantiating CityCommands with the proper type and storing the results (With a private non-documented function canStart()). So unless you decide to implement your own custom mappings for all cities, it appears we have to wait for a proper modding toolkit. Although it may be possible to copy the base map into a local map and simply expand it. I might play around with that tomorrow.

I do also know that the game is using hashed arrays to store resource objects so it's not that unexpected them to use the same technique for workable plots.
 
Last edited:
It might be possible by editing this snippet I found in, base-standard/ui/plot-workers/plot-workers-manager.js


this._allWorkerPlots = city.Workers.GetAllPlacementInfo();
this._allWorkerPlots.forEach(info => {
this._allWorkerPlotIndexes.push(info.PlotIndex);
if (info.IsBlocked) {
this._blockedPlots.push(info);
this._blockedPlotIndexes.push(info.PlotIndex);
}
else {
this._workablePlots.push(info);
this._workablePlotIndexes.push(info.PlotIndex);
}
});
}

I'll have to print out vars / objects to see what everything is and what can be changed but it might be possible to simply overwrite this with an import and push extra tiles into __workablePlots. (Probably have to use a custom binding as doing it the other way might mess with game logic that would be a headache to fix but maybe not idk too tired.) There are functions that can be called to find each city and its position there is also a function to step in a circle around a point that is used to enlarge lakes in the map code. I'll dig into it more after some sleep but that might be possible...?
 
Last edited:
As a side note, it is possible to make each Urban district take more buildings (vanilla is 2). This indirectly gives cities "more tiles". That is pretty easy to do, but may have other side effects (ie is it still a Quarter?)
 
As a side note, it is possible to make each Urban district take more buildings (vanilla is 2). This indirectly gives cities "more tiles". That is pretty easy to do, but may have other side effects (ie is it still a Quarter?)

I didn't try it, how does the game handle it visually?
 
Im glad I could leave this matter in more capable hands. As far as removing the building limit, Im not looking to get more production out of the city. I want the city to fill out more of the land more and have many clusters of cities vs one giant mega city.
 
So as I thought, cities are just hashed objects like resources. Nice part is they are simply named LOC_CITY_NAME_*CIV*[#] , so for greece it would be: LOC_CITY_NAME_GREECE1, LOC_CITY_NAME_GREECE2, etc.

Here is the code I used and here is the output.

...
JavaScript:
const myCity = Cities.get(this._cityID);

var myPlot = myCity.Workers.GetAllPlacementInfo();

console.error('------------------------------------------------------------------------------------------------------------');

        console.error(myCity);

        console.error(JSON.stringify(myCity));

        console.error(myPlot);

        myPlot.forEach(myInfo => {

                console.error(JSON.stringify(myInfo));

        });

        console.error(JSON.stringify(myPlot));

        console.error('City name: ' + myCity.name + '| City ID: ' + myCity.id + '| City Population: ' + myCity.population + '| IsTown: ' + myCity.isTown);

        console.error('------------------------------------------------------------------------------------------------------------');
...

[2025-02-19 17:02:19] ------------------------------------------------------------------------------------------------------------
While executing JS :15:23
[2025-02-19 17:02:19] [object Object]
While executing JS :15:23
[2025-02-19 17:02:19] {"Workers":{},"Yields":{},"TurnManager":{},"Trade":{"routes":[],"waterTradeRouteRange":{"value":45,"type":1,"id":186225367,"description":"LOC_ATTR_WATER_TRADE_ROUTE_RA
NGE","steps":[{"value":45,"type":0,"id":-665120508,"description":"Base Value"}]},"landTradeRouteRange":{"value":15,"type":1,"id":1976422831,"description":"LOC_ATTR_LAND_TRADE_ROUTE_RANGE","st
eps":[{"value":15,"type":0,"id":-665120508,"description":"Base Value"}]},"numRoutes":{"value":0,"type":1,"id":-2028113185,"description":"LOC_ATTR_TOTAL_TRADE_ROUTES"}},"Resources":{},"Religio
n":{"isHolyCity":false,"numExtraPromotions":0,"ruralReligion":-1,"urbanReligion":-1,"majorityReligion":-1},"Production":{"productionYieldSource":-1155936758},"Happiness":{"turnsOfUnrest":-1,"
hasWarWeariness":false,"hasUnrest":false,"netHappinessPerTurn":5},"Growth":{"isReadyToPlacePopulation":true,"isReadyToExpand":true,"turnsUntilStarvation":-1,"turnsUntilGrowth":45,"projectType
":-1,"migrationCityID":{"owner":0,"id":65536,"type":1},"growthType":-
[2025-02-19 17:02:19] [object Object]
While executing JS :15:23
[2025-02-19 17:02:19] {"PlotIndex":2019,"CityID":{"owner":0,"id":65536,"type":1},"NumWorkers":0,"IsBlocked":true,"CurrentYields":[0,0,0,0,0,0,0],"CurrentMaintenance":[0,0,0,0,0,0,0],"NextYi
elds":[0,0,0,2,2,0,0],"NextMaintenance":[2,0,0,0,0,2,0]}
While executing JS :15:23
[2025-02-19 17:02:19] [{"PlotIndex":2019,"CityID":{"owner":0,"id":65536,"type":1},"NumWorkers":0,"IsBlocked":true,"CurrentYields":[0,0,0,0,0,0,0],"CurrentMaintenance":[0,0,0,0,0,0,0],"NextY
ields":[0,0,0,2,2,0,0],"NextMaintenance":[2,0,0,0,0,2,0]}]
While executing JS :15:23
[2025-02-19 17:02:19] City name: LOC_CITY_NAME_GREECE1| City ID: [object Object]| City Population: 4| IsTown: false
While executing JS :15:23
[2025-02-19 17:02:19] ------------------------------------------------------------------------------------------------------------


I was hoping that Workers.GetAllPlacementInfo() would give me the map or atleast an object with the map as a struct but no luck. (On second thought, GetAllPlacementInfo() does exactly what it says, it gets all the associated info from all placements in the city workable area and localizes it to a single plot (a city or town), duh.) I'll keep tinkering around. I do know they have a Map() function so I'll go look at that for a while.






*Snippet I found that confirms the game is using hash maps for some things. However since I am planning to simply increase all city sizes it won't be an issue.*

JavaScript:
const specialistYieldMap = this.createBlankMap();

            const allWorkerPlacementInfo = cityWorkers.GetAllPlacementInfo();

            for (const workerPlacementInfo of allWorkerPlacementInfo) {

                for (let i = 0; i < workerPlacementInfo.CurrentMaintenance.length; i++) {

                    specialistYieldMap[Game.getHash(GameInfo.Yields.YieldType)] -= workerPlacementInfo.CurrentMaintenance;

                }

                for (let i = 0; i < workerPlacementInfo.CurrentYields.length; i++) {

                    specialistYieldMap[Game.getHash(GameInfo.Yields.YieldType)] += workerPlacementInfo.CurrentYields;

                }

            }
 
Last edited:
Okay so I think it will be possible, maybe even kinda easy. I'm able to get the map of plots available for a city to expand to. When an edge of what I assume is the default city map is reached the city expand function (Game.CityCommands.canStart / CityCommandTypes.EXPAND) doesn't give new plots, as expected (hard-coded limit). However, using the data of the previous plots the math is trivial to find out what the next plot would be if we could place one. So really all that needs to be done is implementing the code and then fixing any UI related / graphical errors. The # of indices is the amount of expandable plots NOT the claimable plots, which is why it errors out after the third plot. There are still claimable plots but NOT expandable plots at that edge. [read Index] If I had caught my error there would still be a list of plots able to be expanded to but not in that direction [read Index]. (Turns out when there are no plots left it just means the play can no longer select a new tile, nothing to do with hitting the edge. Also the # of indices is the amount of workable tiles that city owns, not the expandable plots.)

Here is the main body of my js code used for testing and the results of a single city claiming 3 plots in one turn. (The population goes up 3, from 1 -> 4 instead of 1 -> 2 because I added an effect to my civ that gives the capital 2 bonus claims if founded before the first 4 turns for testing the edge.) Also on the final plot my ui bugged out. After taking the third plot the screen didn't change (The claim plot lens layer stayed open). I was able to simply click on the screen to lower it and resolve the issue. Turns out it was because I removed a break in the code I used to test for a lengthy reason but sufficed to say I forgot to use a try catch on the rest of the code and thus it soft errored out at the end. The game was still functional and ran, it was just a UI glitch due to the uncaught error.


JavaScript:
const myCity = Cities.get(this._cityID);
        const myPlot = myCity.Workers.GetAllPlacementInfo();
        const resultMap = Game.CityCommands.canStart(this._cityID, CityCommandTypes.EXPAND, {}, false);
        console.error('------------------------------------------------------------------------------------------------------------');
        if (!resultMap.Plots) {
            console.error("No base game plots left!");
        }
        for (const [index, plotIndex] of resultMap.Plots.entries()) {
            if (!resultMap.ConstructibleTypes) {
                console.error(`No ConstructibleTypes for plotIndex ${plotIndex}`);
            }
            console.error('Index: ' + index + ' | PlotIndex: ' + plotIndex);
        }
        console.error(JSON.stringify(resultMap));
        console.error(JSON.stringify(myCity));
        console.error(JSON.stringify(myPlot));
        console.error('City name: ' + myCity.name + ' | City Population: ' + myCity.population + ' | IsTown: ' + myCity.isTown);
        console.error('------------------------------------------------------------------------------------------------------------');


[2025-02-19 18:19:11] ------------------------------------------------------------------------------------------------------------
While executing JS :15:23
[2025-02-19 18:19:11] Index: 0 | PlotIndex: 8336
While executing JS :15:23
[2025-02-19 18:19:11] Index: 1 | PlotIndex: 8237
While executing JS :15:23
[2025-02-19 18:19:11] Index: 2 | PlotIndex: 8136
While executing JS :15:23
[2025-02-19 18:19:11] Index: 3 | PlotIndex: 8135
While executing JS :15:23
[2025-02-19 18:19:11] Index: 4 | PlotIndex: 8235
While executing JS :15:23
[2025-02-19 18:19:11] Index: 5 | PlotIndex: 8335
While executing JS :15:23
[2025-02-19 18:19:11] {"Success":true,"Plots":[8336,8237,8136,8135,8235,8335],"ConstructibleTypes":[8,8,10,10,10,8]}
While executing JS :15:23
[2025-02-19 18:19:11] {"Workers":{},"Yields":{},"TurnManager":{},"Trade":{"routes":[],"waterTradeRouteRange":{"value":45,"type":1,"id":186225367,"description":"LOC_ATTR_WATER_TRADE_ROUTE_RA
NGE","steps":[{"value":45,"type":0,"id":-665120508,"description":"Base Value"}]},"landTradeRouteRange":{"value":15,"type":1,"id":1976422831,"description":"LOC_ATTR_LAND_TRADE_ROUTE_RANGE","st
eps":[{"value":15,"type":0,"id":-665120508,"description":"Base Value"}]},"numRoutes":{"value":0,"type":1,"id":-2028113185,"description":"LOC_ATTR_TOTAL_TRADE_ROUTES"}},"Resources":{},"Religio
n":{"isHolyCity":false,"numExtraPromotions":0,"ruralReligion":-1,"urbanReligion":-1,"majorityReligion":-1},"Production":{"productionYieldSource":-1155936758},"Happiness":{"turnsOfUnrest":-1,"
hasWarWeariness":false,"hasUnrest":false,"netHappinessPerTurn":5},"Growth":{"isReadyToPlacePopulation":true,"isReadyToExpand":true,"turnsUntilStarvation":-1,"turnsUntilGrowth":45,"projectType
":-1,"migrationCityID":{"owner":0,"id":65536,"type":1},"growthType":-
[2025-02-19 18:19:11] [{"PlotIndex":8236,"CityID":{"owner":0,"id":65536,"type":1},"NumWorkers":0,"IsBlocked":true,"CurrentYields":[0,0,0,0,0,0,0],"CurrentMaintenance":[0,0,0,0,0,0,0],"NextY
ields":[0,0,0,2,2,0,0],"NextMaintenance":[2,0,0,0,0,2,0]}]
While executing JS :15:23
[2025-02-19 18:19:11] City name: LOC_CITY_NAME_GREECE1 | City Population: 4 | IsTown: false
While executing JS :15:23
[2025-02-19 18:19:11] ------------------------------------------------------------------------------------------------------------

[2025-02-19 18:19:17] ------------------------------------------------------------------------------------------------------------
While executing JS :15:23
[2025-02-19 18:19:17] Index: 0 | PlotIndex: 8336
While executing JS :15:23
[2025-02-19 18:19:17] Index: 1 | PlotIndex: 8237
While executing JS :15:23
[2025-02-19 18:19:17] Index: 2 | PlotIndex: 8136
While executing JS :15:23
[2025-02-19 18:19:17] Index: 3 | PlotIndex: 8135
While executing JS :15:23
[2025-02-19 18:19:17] Index: 4 | PlotIndex: 8335
While executing JS :15:23
[2025-02-19 18:19:17] Index: 5 | PlotIndex: 8134
While executing JS :15:23
[2025-02-19 18:19:17] Index: 6 | PlotIndex: 8234
While executing JS :15:23
[2025-02-19 18:19:17] Index: 7 | PlotIndex: 8334
While executing JS :15:23
[2025-02-19 18:19:17] {"Success":true,"Plots":[8336,8237,8136,8135,8335,8134,8234,8334],"ConstructibleTypes":[8,8,10,10,8,7,4,10]}
While executing JS :15:23
[2025-02-19 18:19:17] {"Workers":{},"Yields":{},"TurnManager":{},"Trade":{"routes":[],"waterTradeRouteRange":{"value":45,"type":1,"id":186225367,"description":"LOC_ATTR_WATER_TRADE_ROUTE_RA
NGE","steps":[{"value":45,"type":0,"id":-665120508,"description":"Base Value"}]},"landTradeRouteRange":{"value":15,"type":1,"id":1976422831,"description":"LOC_ATTR_LAND_TRADE_ROUTE_RANGE","st
eps":[{"value":15,"type":0,"id":-665120508,"description":"Base Value"}]},"numRoutes":{"value":0,"type":1,"id":-2028113185,"description":"LOC_ATTR_TOTAL_TRADE_ROUTES"}},"Resources":{},"Religio
n":{"isHolyCity":false,"numExtraPromotions":0,"ruralReligion":-1,"urbanReligion":-1,"majorityReligion":-1},"Production":{"productionYieldSource":-1155936758},"Happiness":{"turnsOfUnrest":-1,"
hasWarWeariness":false,"hasUnrest":false,"netHappinessPerTurn":6},"Growth":{"isReadyToPlacePopulation":true,"isReadyToExpand":true,"turnsUntilStarvation":-1,"turnsUntilGrowth":37,"projectType
":-1,"migrationCityID":{"owner":0,"id":65536,"type":1},"growthType":-
[2025-02-19 18:19:17] [{"PlotIndex":8236,"CityID":{"owner":0,"id":65536,"type":1},"NumWorkers":0,"IsBlocked":true,"CurrentYields":[0,0,0,0,0,0,0],"CurrentMaintenance":[0,0,0,0,0,0,0],"NextY
ields":[0,0,0,2,2,0,0],"NextMaintenance":[2,0,0,0,0,2,0]}]
While executing JS :15:23
[2025-02-19 18:19:17] City name: LOC_CITY_NAME_GREECE1 | City Population: 4 | IsTown: false
While executing JS :15:23
[2025-02-19 18:19:17] ------------------------------------------------------------------------------------------------------------

[2025-02-19 18:19:18] ------------------------------------------------------------------------------------------------------------
While executing JS :15:23
[2025-02-19 18:19:18] Index: 0 | PlotIndex: 8336
While executing JS :15:23
[2025-02-19 18:19:18] Index: 1 | PlotIndex: 8237
While executing JS :15:23
[2025-02-19 18:19:18] Index: 2 | PlotIndex: 8136
While executing JS :15:23
[2025-02-19 18:19:18] Index: 3 | PlotIndex: 8135
While executing JS :15:23
[2025-02-19 18:19:18] Index: 4 | PlotIndex: 8335
While executing JS :15:23
[2025-02-19 18:19:18] Index: 5 | PlotIndex: 8134
While executing JS :15:23
[2025-02-19 18:19:18] Index: 6 | PlotIndex: 8334
While executing JS :15:23
[2025-02-19 18:19:18] Index: 7 | PlotIndex: 8133
While executing JS :15:23
[2025-02-19 18:19:18] Index: 8 | PlotIndex: 8233
While executing JS :15:23
[2025-02-19 18:19:18] Index: 9 | PlotIndex: 8333
While executing JS :15:23
[2025-02-19 18:19:18] {"Success":true,"Plots":[8336,8237,8136,8135,8335,8134,8334,8133,8233,8333],"ConstructibleTypes":[8,8,10,10,8,7,10,8,10,10]}
While executing JS :15:23
[2025-02-19 18:19:18] {"Workers":{},"Yields":{},"TurnManager":{},"Trade":{"routes":[],"waterTradeRouteRange":{"value":45,"type":1,"id":186225367,"description":"LOC_ATTR_WATER_TRADE_ROUTE_RA
NGE","steps":[{"value":45,"type":0,"id":-665120508,"description":"Base Value"}]},"landTradeRouteRange":{"value":15,"type":1,"id":1976422831,"description":"LOC_ATTR_LAND_TRADE_ROUTE_RANGE","st
eps":[{"value":15,"type":0,"id":-665120508,"description":"Base Value"}]},"numRoutes":{"value":0,"type":1,"id":-2028113185,"description":"LOC_ATTR_TOTAL_TRADE_ROUTES"}},"Resources":{},"Religio
n":{"isHolyCity":false,"numExtraPromotions":0,"ruralReligion":-1,"urbanReligion":-1,"majorityReligion":-1},"Production":{"productionYieldSource":-1155936758},"Happiness":{"turnsOfUnrest":-1,"
hasWarWeariness":false,"hasUnrest":false,"netHappinessPerTurn":7},"Growth":{"isReadyToPlacePopulation":true,"isReadyToExpand":true,"turnsUntilStarvation":-1,"turnsUntilGrowth":28,"projectType
":-1,"migrationCityID":{"owner":0,"id":65536,"type":1},"growthType":-
[2025-02-19 18:19:18] [{"PlotIndex":8236,"CityID":{"owner":0,"id":65536,"type":1},"NumWorkers":0,"IsBlocked":true,"CurrentYields":[0,0,0,0,0,0,0],"CurrentMaintenance":[0,0,0,0,0,0,0],"NextY
ields":[0,0,0,2,2,0,0],"NextMaintenance":[2,0,0,0,0,2,0]}]
While executing JS :15:23
[2025-02-19 18:19:18] City name: LOC_CITY_NAME_GREECE1 | City Population: 4 | IsTown: false
While executing JS :15:23
[2025-02-19 18:19:18] ------------------------------------------------------------------------------------------------------------

[2025-02-19 18:19:19] ------------------------------------------------------------------------------------------------------------
While executing JS :15:23
[2025-02-19 18:19:19] No base game plots left!
While executing JS :15:23
[2025-02-19 18:19:19] JS Error: fs://game/base-standard/ui/plot-workers/plot-workers-manager.js:130: TypeError: Cannot read properties of undefined (reading 'entries')
[2025-02-19 18:19:19] JS Error (Line): for (const [index, plotIndex] of resultMap.Plots.entries()) {
 
Last edited:
Slowly but surely. Still a lot of coding needs to be done. Currently the tile isn't selectable but I think that's an easy fix if I'm correct. Also I forced the construction type to be a farm. I'm pretty sure the construction type is calculated in the same internal function that sets the range so it will all need to be redone. Although if that's all that needs to be done it shouldn't be too bad. Also I haven't started on the logic for when to allow these tiles to be selected but again, once things are actually working it shouldn't be that much work.
test.png
 
Last edited:
Slowly but surely. Still a lot of coding needs to be done. Currently the tile isn't selectable but I think that's an easy fix if I'm correct. Also I forced the construction type to be a farm. I'm pretty sure the construction type is calculated in the same internal function that sets the range so it will all need to be redone. Although if that's all that needs to be done it shouldn't be too bad. Also I haven't started on the logic for when to allow these tiles to be selected but again, once things are actually working it shouldn't be that much work. View attachment 720901
Not going to lie, I cant really follow what you are saying in the chat, but its so friggen cool that you are continuing to work on this. If a mod comes from it please, take all the credit. Looking forward to see what you can come up with!
 
It's not looking too good. I'll keep working on it in the morning but so far I have only been able to update the city border layer and the expandable plots. If I try to claim the extra plots it plays the sound but then nothing. If I click it again nothing happens but I can see in the logging that it's trying to propose a new plot for the city and thus errors out with "Error: A plot is already being proposed." So if I take a look at the code that is used to propose and commit plots that's where it becomes a problem. The city border is simply an overlay which is how I was able to manipulate it. Also the expansionTiles have to be calculated on the fly which means they required a local instance. I was able to add new entries to the local instance and thus I was able to edit the expansion plot map. However the 'valid' plots are generated through a private function and the local code only receives a local map, so no editing can be done. There are four ways I see of how to get the game to allow you to own a new tile. They are all undocumented private functions. Some of them have getter functions but I don't think any have setter functions so........inaccessible unfortunately.

*RESETTLE*
const args = {};
args.X = plot.x;
args.Y = plot.y;
if (context.UnitID) {
Game.UnitCommands.sendRequest(context.UnitID, 'UNITCOMMAND_RESETTLE', args);

*ASSIGN_WORKER*
const workerArgs = {
Location: plotIndex,
Amount: 1
};
Game.PlayerOperations.sendRequest(GameContext.localPlayerID, PlayerOperationTypes.ASSIGN_WORKER, workerArgs);

*SUPPORT_DIPLOMATIC_ACTION*
const supportArgs = {
ID: ongoingActions[0].uniqueID
Type: DiplomacyTokenTypes.DIPLOMACY_TOKEN_GLOBAL,
Amount: 1,
SubType: true
};

Game.PlayerOperations.sendRequest(GameContext.localPlayerID, PlayerOperationTypes.SUPPORT_DIPLOMATIC_ACTION, supportArgs);

*LAND_CLAIM*
let args = {
Type: 2,
X: myLoc.x,
Y: myLoc.y,
};
Game.PlayerOperations.sendRequest(GameContext.localPlayerID, PlayerOperationTypes.LAND_CLAIM, args);


Using support_diplomatic_action requries a uniqueID (which I can't generate) and a type (which looks to be a diplomacy token which I don't know how to give a player).
Using assign_worker usually just fails but sometimes it crashed my game so...?
Using resettle just fails with a nondescript error.
Using Land Claim might work? It fails with a message saying I had 120 out of 189 influence. I'll test it tomorrow with enough influence and see if anything happens. But as I suspected it appears the relevant functions are in Game.PlayerOperations / Game.CityOperations which are packed into the exe. (I did a string search and sure enough they both come up.)

Without a way to actually link the plot to the parent city object, a mod like this will never work sadly. But I'll try again tomorrow, hopefully I missed something. If I had to hazard a guess I assume the game is sending a valid request to claim a tile but the request is being made against a base map. Since my plot would lie beyond the bounds of the map it either fails in an uncaught function (thus the hanging response) or just isn't programmed for such an edge case and bugs out (hence the sometimes crashing.) Since there is seemingly no way to edit the base map that is applied to all cites, since all cities are just derived from a single blueprint and the base map is initialized with the blueprint (standard OOP), there is seemingly no way to add extra plots to cities without writing your own functions to handle everything (and I do mean everything) about their interactions with cities. (How much yield they contribute, if they have resources or not, if there is a trade route going through the tile, if there is a unit in the tile, if the tile changes ownership, if an event happens in the tile, etc).



[2025-02-20 10:45:13] {"FailureReasons":["You need 189 but have 120[icon:YIELD_DIPLOMACY] Influence."],"Success":false}
While executing JS :15:23
[2025-02-20 10:45:13] {"Success":false}


[2025-02-20 10:45:15] Which plot was choosen:
While executing JS :15:23
[2025-02-20 10:45:15] {"x":23,"y":20}
While executing JS :15:23
[2025-02-20 10:45:15] {"owner":0,"id":65536,"type":1}
[2025-02-20 10:45:15] {"Location":2023,"Amount":1}
While executing JS :15:23
[2025-02-20 10:45:23] Which plot was choosen:
While executing JS :15:23
[2025-02-20 10:45:23] {"x":23,"y":20}
While executing JS :15:23
[2025-02-20 10:45:23] {"owner":0,"id":65536,"type":1}
While executing JS :15:23
[2025-02-20 10:45:23] JS Error: fs://game/base-standard/ui/interface-modes/interface-mode-acquire-tile.js:97: Error: A plot is already being proposed.
[2025-02-20 10:45:23] JS Error (Line): throw new Error("A plot is already being proposed.");
[2025-02-20 10:45:23] JS Error (Stack): Error: A plot is already being proposed.
at AcquireTileInterfaceMode.selectPlot (fs://game/base-standard/ui/interface-modes/interface-mode-acquire-tile.js:97:19)
at WorldInputSingleton.ChoosePlotInterfaceMode.plotSelectionHandler (fs://game/base-standard/ui/interface-modes/interface-mode-choose-plot.js:23:75)
at WorldInputSingleton.selectPlot (fs://game/base-standard/ui/world-input/world-input.js:240:14)
at WorldInputSingleton.trySelectPlot (fs://game/base-standard/ui/world-input/world-input.js:77:14)
at WorldInputSingleton.actionActivate (fs://game/base-standard/ui/world-input/world-input.js:88:21)
at WorldInputSingleton.handleInput (fs://game/base-standard/ui/world-input/world-input.js:48:29)
at fs://game/core/ui/context-manager/context-manager.js:575:29
at Array.some (<anonymous>)
at ContextManagerSingleton.handleInput (fs://game/core/ui/context-manager/context-manager.js:574:47)
at ActionHandlerSingleton.onEngineInput (fs://




test.png
test2.png
 
Last edited:
Well I got a bit farther and then hit another wall. I was able to get to change the ownership of the plot and it now properly displays in the expand panel overlay. (It has a deep red and white border and has the correct transparency.) However the problem I ran into is an internal error about not being able to find a certain village resource. Villages are what the code calls all towns and cities. So I'm pretty sure it's using some function that either calls the city map, and offsets and rotates it to find the owning city or some other kind of distance mapping. Since the added plot will always lay beyond this mapping space, it will always fail. Maybe I'm just missing something else, some flag or struct component. I'll keep checking.

[2025-02-21 05:20:02] Which plot was chosen:
While executing JS :15:23
[2025-02-21 05:20:02] {"x":23,"y":20}
While executing JS :15:23
[2025-02-21 05:20:02] [2119,1919,1918,2018,2118,2120,2121,1821,2023]
While executing JS :15:23
[2025-02-21 05:20:02] {"Location":2023,"Amount":1}
While executing JS :15:23
[2025-02-21 05:20:02] {"x":23,"y":20}
While executing JS :15:23
[2025-02-21 05:20:02] Plot Accepted!
While executing JS :15:23
[2025-02-21 05:20:03] Failed to open file - impicon_village
[2025-02-21 05:20:03] ResourceRequestJob | Failed loading resource: blp:impicon_village
 
I did find something interesting. I found a way to give ownership of tiles to any player at any location. It should function as normal, it gives vision (probably prevents settlement / building of other players) however they are not expandable plots. So they don't towards city growth or districts / improvements. They also don't count towards yields. So it's slightly more than cosmetic but only slightly. The view is a bit buggy when you have the expand plot lens open. It shows you own the tiles but they are disconnected from your border. I might know how to fix that.
 

Attachments

  • test.jpg
    test.jpg
    563.7 KB · Views: 28
  • test2.jpg
    test2.jpg
    608.1 KB · Views: 29
I did find something interesting. I found a way to give ownership of tiles to any player at any location. It should function as normal, it gives vision (probably prevents settlement / building of other players) however they are not expandable plots. So they don't towards city growth or districts / improvements. They also don't count towards yields. So it's slightly more than cosmetic but only slightly. The view is a bit buggy when you have the expand plot lens open. It shows you own the tiles but they are disconnected from your border. I might know how to fix that.

Probably that's how the american unique unit works.
 
Probably that's how the american unique unit works.
No I placed those manually by clicking. If you look in the second image I accidentally clicked on the water while still in expandplot lens so I own a random plot in the water. I have however found a function to 'purchase' tiles from the debug code. They show up as owned tiles in all lenses but they aren't working with improvements....yet. But good to know about that unique, I'll look for it in the code maybe it has something interesting in it, thanks!
 
Last edited:
It's almost working. The problem is making the plots have improvements, use resources and allow districts. Which might be the problem. I didn't find anything that would allow that in the debug code sadly. I did find a way to place improvements and I can do it on any base tile but when I try and place it on my expanded plots it just...doesn't. Weird part is in the code it doesn't error. Everything still works I can still play it just won't place it....Idk? I'll keep thinking on it.
test.png
test2.png
test.png
test2.png
 
Last edited:
So here is where I got to. I can get up to the point of this function: Game.CityCommands.sendRequest(context.CityID, CityCommandTypes.EXPAND, args), If I change the args to include a plot outside of the default range I get undefined as a result instead of an array of plots. And thus the plot is accepted but nothing happens. When I click the tile again I get the error, Plot already being proposed, because it IS being proposed, it's just the private function is erroring out and not completing. I should be able to handle this error with appropriate logic, the problem becomes actually working on the plot. Placing improvements, getting yields, placing districts / buildings. However not all hope is lost, I think what is happening is CityCommands is simply initializing all the proper elements / adding them to the city. (All hope does seem to be lost for improvements, unless I'm misunderstanding something.)

CityCommandTypes =
{"INVALID":0,"DESTROY":2071124458,"EXPAND":-383545691,"EXPORT_POPULATION":19002474,"CHANGE_GROWTH_MODE":-580125556,"MANAGE":729125427,"NAME_CITY":-678358572,"PURCHASE"
:1490793096,"RANGE_ATTACK":-1967338303,"SET_FOCUS":-1927260886,"SWAP_TILE_OWNER":-1198206025,"TYPE":445758064,"WMD_STRIKE":681683161}

Game.CityCommands.sendRequest(context.CityID, CityCommandTypes.EXPAND, args) =
[{"plotIndex":2119,"constructibleType":5},{"plotIndex":1919,"constructibleType":7},{"plotIndex":1918,"constructibleType":4},{"plotIndex":2018,"constructibleType":8},{"
plotIndex":2118,"constructibleType":8},{"plotIndex":2120,"constructibleType":10},{"plotIndex":2121,"constructibleType":12},{"plotIndex":2022,"constructibleType":6},{"plotIndex":1921,"construc
tibleType":13}]

CapitalCityObject =
{"Workers":{},"Yields":{},"TurnManager":{},"Trade":{"routes":[],"waterTradeRouteRange":{"value":45,"type":1,"id":186225367,"description":"LOC_ATTR_WATER_TRADE_ROUTE_RA
NGE","steps":[{"value":45,"type":0,"id":-665120508,"description":"Base Value"}]},"landTradeRouteRange":{"value":15,"type":1,"id":1976422831,"description":"LOC_ATTR_LAND_TRADE_ROUTE_RANGE","st
eps":[{"value":15,"type":0,"id":-665120508,"description":"Base Value"}]},"numRoutes":{"value":0,"type":1,"id":-2028113185,"description":"LOC_ATTR_TOTAL_TRADE_ROUTES"}},"Resources":{},"Religio
n":{"isHolyCity":false,"numExtraPromotions":0,"ruralReligion":-1,"urbanReligion":-1,"majorityReligion":-1},"Production":{"productionYieldSource":-1155936758},"Happiness":{"turnsOfUnrest":-1,"
hasWarWeariness":false,"hasUnrest":false,"netHappinessPerTurn":6},"Growth":{"isReadyToPlacePopulation":true,"isReadyToExpand":true,"turnsUntilStarvation":-1,"turnsUntilGrowth":807,"projectTyp
e":-1,"migrationCityID":{"owner":0,"id":65536,"type":1},"growthType":


Game.PlayerOperations.sendRequest(0, 'CREATE_ELEMENT', args) =
{"$index":8,"ConstructibleType":"IMPROVEMENT_WOODCUTTER","AdjacentDistrict":null,"AdjacentLake":false,"AdjacentRiver":false,"AdjacentTerrain":null,"Age":null,"Archaeol
ogy":false,"CanBeHidden":false,"ConstructibleClass":"IMPROVEMENT","Cost":25,"CostProgressionModel":"NO_COST_PROGRESSION","CostProgressionParam1":0,"Defense":0,"Description":"LOC_IMPROVEMENT_W
OODCUTTER_DESCRIPTION","Discovery":false,"DistrictDefense":false,"ExistingDistrictOnly":false,"ImmuneDamage":false,"InRailNetwork":false,"MilitaryDomain":"NO_DOMAIN","Name":"LOC_IMPROVEMENT_W
OODCUTTER_NAME","NoFeature":false,"NoRiver":false,"Population":1,"ProductionBoostOverRoute":0,"Repairable":true,"RequiresAppealPlacement":false,"RequiresDistantLands":false,"RequiresHomeland"
:false,"RequiresUnlock":false,"RiverPlacement":null,"Tooltip":null,"VictoryItem":false,"$hash":1105115884}
While executing JS :15:23
[2025-02-22 05:08:49] true (Even though it isn't when I place it outside of the 3 plot range. Still reports as true. It is a debug command so....?)

I don't see a way to add improvements to cities since it seems to be done in a private function.

I think I'll just skip over improvements and try to get functioning districts / buildings (with proper yields). Thankfully that seems to all be local objects (since they are selectable and have to be constructed / placed in real time.)
 
Last edited:
Back
Top Bottom