[Mod Request] Allow designation of walls on natural rock

Started by lost_RD, May 11, 2016, 12:54:47 AM

Previous topic - Next topic

lost_RD

I'd like to be able to build my slate walls on tiles that contain slate rock. Having to mine away the rock first makes no sense when all I want my colonists to do is mine away the face of the wall and replace it with a beautiful brick face, not remove the whole ugly rock wall and then have to rejig my prison rooms and whatnot.

This mod would not add any new tools, it would just change the list of tiles that walls are allowed to be built on.

I've suggested it and a good few people were interested.

Edit 8: I posted in the help forum and got some great advice. Check out this topic to see the progress I've made.

Everything below here is somewhat redundant now but I'll leave it.




Edit: I'll try to make it myself but I've never used C# and am not an experienced modder.

NB: Everything past this point is stream-of-consciousness and you probably shouldn't read all of it but feel free skip to the bottom of the post and tell me if my proposed solution is terrible or not

So I'm peeking around the source code and I've found some relevant code

public static bool CanBuildOnTerrain(BuildableDef entDef, IntVec3 c, Rot4 rot)
{
TerrainDef terrainDef = entDef as TerrainDef;
if (terrainDef != null && !c.GetTerrain().changeable)
{
return false;
}
if (entDef.terrainAffordanceNeeded == TerrainAffordance.Any)
{
return true;
}
CellRect cellRect = GenAdj.OccupiedRect(c, rot, entDef.Size);
cellRect.ClipInsideMap();
CellRect.CellRectIterator iterator = cellRect.GetIterator();
while (!iterator.Done())
{
TerrainDef terrainDef2 = Find.TerrainGrid.TerrainAt(iterator.Current);
if (!terrainDef2.affordances.Contains(entDef.terrainAffordanceNeeded))
{
return false;
}
iterator.MoveNext();
}
return true;
}


Specifically:

if (!terrainDef2.affordances.Contains(entDef.terrainAffordanceNeeded))
{
return false;
}


So I'll need to modify the affordance that walls need.

Edit 2: Nope, affordances are a floor thing, not a walls thing. Back to the drawing board.

Looking at Various_Stone.xml, UglyRockBase is the ParentName of the stone that I want to be able to designate walls onto.

From Buildings_Structure.xml:

  <ThingDef ParentName="BuildingBase" Name="Wall">
    <defName>Wall</defName>
    <label>wall</label>
    <thingClass>Building</thingClass>
    <category>Building</category>
    <description>An impassable wall. Capable of holding up a roof.</description>


Next objective is to find where Building things are defined.

Edit 3: Verse.ThingCategory contains Building. Cool.

There isn't a Building_Wall in the RimWorld namespace which makes sense because walls don't do anything, they just exist and can be destroyed. Instead I'll look at BuildingProperties.  Within it I see public bool isNaturalRock;. I found nothing useful when analysing that bool.

Edit 4: My next idea is to use the canPlaceOverWall tag and re-purpose it so that walls can be placed over other walls, then take it further and somehow convince the game engine that rough stone is a wall so that anything that canBePlacedOverWall [sic] can be placed on it.

The relevant code is in Verse.GenSpawn:

// Verse.GenSpawn
public static bool CanPlaceBlueprintOver(BuildableDef newDef, ThingDef oldDef)
{
if (oldDef.EverHaulable)
{
return true;
}
if (oldDef == ThingDefOf.SteamGeyser)
{
return newDef == ThingDefOf.GeothermalGenerator;
}
ThingDef thingDef = newDef as ThingDef;
BuildableDef buildableDef = GenSpawn.BuiltDefOf(oldDef);
ThingDef thingDef2 = buildableDef as ThingDef;
if (oldDef.category == ThingCategory.Plant && oldDef.passability == Traversability.Impassable && thingDef != null && thingDef.category == ThingCategory.Building && !thingDef.building.canPlaceOverImpassablePlant)
{
return false;
}
if (oldDef.category == ThingCategory.Building || oldDef.IsBlueprint || oldDef.IsFrame)
{
if (thingDef != null)
{
if (!thingDef.IsEdifice())
{
return (oldDef.building == null || oldDef.building.canBuildNonEdificesUnder) && (!thingDef.EverTransmitsPower || !oldDef.EverTransmitsPower);
}
if (thingDef.IsEdifice() && oldDef != null && oldDef.category == ThingCategory.Building && !oldDef.IsEdifice())
{
return thingDef.building == null || thingDef.building.canBuildNonEdificesUnder;
}
if (thingDef2 != null && thingDef2 == ThingDefOf.Wall && thingDef.building != null && thingDef.building.canPlaceOverWall)
{
return true;
}
if (newDef != ThingDefOf.PowerConduit && buildableDef == ThingDefOf.PowerConduit)
{
return true;
}
}
return (newDef is TerrainDef && buildableDef is ThingDef && ((ThingDef)buildableDef).CoexistsWithFloors) || (buildableDef is TerrainDef && !(newDef is TerrainDef));
}
return true;
}


In that block is the code that allows geothermals to be placed on steam geysers. Also, if you're trying to build on something that is haulable, congratulations, that's a valid place to build. Also, if the new thing is not a power conduit and the old thing is, you're golden. Now we're getting somewhere. I want to put some code in there that says:

if (oldDef == ThingDefOf.Stone && newDef == ThingDefOf.Wall || oldDef == ThingDefOf.Wall && newDef == ThingDefOf.Door)
{
return true;
}


I'm not sure it's even possible to solve the problem that way though because SteamGeyser is defined using a defName tag and stone is not. The problem I'm having is that I'm not sure how stone is defined and how I should select or interact with it. Door, Autodoor and Wall are all defName compatible.

I might be able to use the isNaturalRock tag. I'll have to study Verse.GenSpawn some more.

Edit 5: Alright, I deciphered GenSpawn:

// Verse.GenSpawn
public static bool CanPlaceBlueprintOver(BuildableDef newDef, ThingDef oldDef)
{
// true if the old building is haulable
if (oldDef.EverHaulable)
{
return true;
}
// return geothermal if the old building is a geyser
if (oldDef == ThingDefOf.SteamGeyser)
{
return newDef == ThingDefOf.GeothermalGenerator;
}
ThingDef thingDef = newDef as ThingDef;
BuildableDef buildableDef = GenSpawn.BuiltDefOf(oldDef);
ThingDef thingDef2 = buildableDef as ThingDef;
// false if old building is an impassable plant and the new building can't be placed on impassable plants
if (oldDef.category == ThingCategory.Plant && oldDef.passability == Traversability.Impassable && thingDef != null && thingDef.category == ThingCategory.Building && !thingDef.building.canPlaceOverImpassablePlant)
{
return false;
}
// if old building is a building or a blueprint or an "unspecified building frame"
if (oldDef.category == ThingCategory.Building || oldDef.IsBlueprint || oldDef.IsFrame)
{
// if the new building is not null (very sensible start)
if (thingDef != null)
{
// if new building is not an edifice - i.e. is a power conduit
// return true if no old building or can build conduit under AND either the old or new building does not transmit power
// this is the place conduits under things case
if (!thingDef.IsEdifice())
{
return (oldDef.building == null || oldDef.building.canBuildNonEdificesUnder) && (!thingDef.EverTransmitsPower || !oldDef.EverTransmitsPower);
}
// if new building is not a conduit and old building is a conduit
// return true if conduits can exist under new building
// this is the place things on conduits case
if (thingDef.IsEdifice() && oldDef != null && oldDef.category == ThingCategory.Building && !oldDef.IsEdifice())
{
return thingDef.building == null || thingDef.building.canBuildNonEdificesUnder;
}
// return true if old building exists and is a wall and new building exists and can be placed over a wall
// this is the door case
if (thingDef2 != null && thingDef2 == ThingDefOf.Wall && thingDef.building != null && thingDef.building.canPlaceOverWall)
{
return true;
}
// return true if new building is not a conduit and old building is a conduit
// this is another place things on conduits case
if (newDef != ThingDefOf.PowerConduit && buildableDef == ThingDefOf.PowerConduit)
{
return true;
}
}
// return true if placing a floor and existing structure coexists with floors or is already a floor
return (newDef is TerrainDef && buildableDef is ThingDef && ((ThingDef)buildableDef).CoexistsWithFloors) || (buildableDef is TerrainDef && !(newDef is TerrainDef));
}
// return true if all the other shit is not relevant
return true;
}


Line 22 says "// if old building is a building or a blueprint or an "unspecified building frame" " and I'm willing to bet that placing a wall onto stone does not satisfy the conditions. A quick and dirty way to make this work might be to do an else and check whether the new building is a wall and return true if so. Kinda like this:

// Verse.GenSpawn
public static bool CanPlaceBlueprintOver(BuildableDef newDef, ThingDef oldDef)
{

...

// if old building is a building or a blueprint or an "unspecified building frame"
if (oldDef.category == ThingCategory.Building || oldDef.IsBlueprint || oldDef.IsFrame)
{

...

}
else
{
if (newDef == ThingDefOf.Wall)
{
return true;
}
}
// return true if all the other shit is not relevant
return true;
}


Would the above code cause any serious issues?

Edit 6: That was dumb. Whether I put in my else or not, there's a return true regardless. If building a wall is going to fail because it's on stone, it's going to fail before it reaches this method.

Verse.GenSpawn.CanPlaceBlueprintOver is used by RimWorld.GenConstruct.CanPlaceBlueprintAt. The code block is as follows:


if (!godMode)
{
foreach (IntVec3 current4 in GenAdj.CellsOccupiedBy(center, rot, entDef.Size))
{
foreach (Thing current5 in Find.ThingGrid.ThingsAt(current4))
{
if (!GenSpawn.CanPlaceBlueprintOver(entDef, current5.def))
{
AcceptanceReport result2 = new AcceptanceReport("SpaceAlreadyOccupied".Translate());
return result2;
}
}
}
}


If you're playing normally (not in god/dev mode), for each cell that the building would occupy, for each thing in each cell, check whether the thing already there is compatible with the thing you're trying to build. If it can't be built, generate a report that says the space is occupied. If all is well, generate a report that says the blueprint can be placed. If it's going to fail, it's going to fail before this.

I'm going to check what the game says when I try to build a wall on stone in vanilla.

Edit 7: I'm glad I checked because it's definitely the SpaceAlreadyOccupied report. I'm really not sure where false is being returned. I'm going to try compiling a version of the method that simply returns true.