From cebf3f5d63f94b4409afc35a81278e1b11be474d Mon Sep 17 00:00:00 2001 From: Kevin Turner <83819+keturn@users.noreply.github.com> Date: Thu, 7 Oct 2021 14:32:03 -0700 Subject: [PATCH] test: add SettlementEntityManagerTest for placeParcel (#107) --- module.txt | 2 +- .../buildings/BuildingManager.java | 2 +- .../construction/Construction.java | 6 +- .../districts/DistrictManager.java | 35 ++--- .../dynamicCities/districts/DistrictType.java | 8 +- .../population/CultureManager.java | 67 ++++----- .../region/RegionEntityManager.java | 8 +- .../region/RegionEntityProvider.java | 31 ++--- .../settlements/SettlementEntityManager.java | 93 ++++++++----- .../components/DistrictFacetComponent.java | 30 ++-- .../dynamicCities/sites/SiteComponent.java | 3 +- .../utilities/ProbabilityDistribution.java | 76 ----------- .../dynamicCities/world/SolidRasterizer.java | 41 +++--- .../world/testbench/ConstantBiomeFacet.java | 29 ++++ .../testbench/ConstantBiomeProvider.java | 26 ++++ .../world/testbench/FlatFacetedWorld.java | 49 +++++++ .../SettlementEntityManagerTest.java | 128 ++++++++++++++++++ 17 files changed, 399 insertions(+), 235 deletions(-) delete mode 100644 src/main/java/org/terasology/dynamicCities/utilities/ProbabilityDistribution.java create mode 100644 src/main/java/org/terasology/dynamicCities/world/testbench/ConstantBiomeFacet.java create mode 100644 src/main/java/org/terasology/dynamicCities/world/testbench/ConstantBiomeProvider.java create mode 100644 src/main/java/org/terasology/dynamicCities/world/testbench/FlatFacetedWorld.java create mode 100644 src/test/java/org/terasology/dynamicCities/settlements/SettlementEntityManagerTest.java diff --git a/module.txt b/module.txt index 10937f8..dff2687 100644 --- a/module.txt +++ b/module.txt @@ -43,7 +43,7 @@ }, { "id": "ModuleTestingEnvironment", - "minVersion": "0.2.0", + "minVersion": "0.3.2", "optional": true }, { diff --git a/src/main/java/org/terasology/dynamicCities/buildings/BuildingManager.java b/src/main/java/org/terasology/dynamicCities/buildings/BuildingManager.java index 09f78c3..6517939 100644 --- a/src/main/java/org/terasology/dynamicCities/buildings/BuildingManager.java +++ b/src/main/java/org/terasology/dynamicCities/buildings/BuildingManager.java @@ -209,7 +209,7 @@ public Optional getRandomBuildingOfZoneForCulture(Stri private Optional getGenerator(String generatorName) { - Class generatorClass = GeneratorRegistry.GENERATORS.get(generatorName); + Class generatorClass = GeneratorRegistry.GENERATORS.get(generatorName); for (BuildingGenerator generator : generators) { if (generator.getClass() == generatorClass) { return Optional.of(generator); diff --git a/src/main/java/org/terasology/dynamicCities/construction/Construction.java b/src/main/java/org/terasology/dynamicCities/construction/Construction.java index dcfb317..fd46c80 100644 --- a/src/main/java/org/terasology/dynamicCities/construction/Construction.java +++ b/src/main/java/org/terasology/dynamicCities/construction/Construction.java @@ -8,7 +8,6 @@ import org.joml.Vector3ic; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.terasology.gestalt.assets.management.AssetManager; import org.terasology.cities.BlockTheme; import org.terasology.cities.DefaultBlockType; import org.terasology.cities.bldg.Building; @@ -72,7 +71,6 @@ import org.terasology.engine.entitySystem.systems.BaseComponentSystem; import org.terasology.engine.entitySystem.systems.RegisterMode; import org.terasology.engine.entitySystem.systems.RegisterSystem; -import org.terasology.module.inventory.systems.InventoryManager; import org.terasology.engine.logic.location.LocationComponent; import org.terasology.engine.math.Side; import org.terasology.engine.network.NetworkSystem; @@ -89,6 +87,8 @@ import org.terasology.engine.world.block.entity.placement.PlaceBlocks; import org.terasology.engine.world.generation.Border3D; import org.terasology.engine.world.generation.facets.ElevationFacet; +import org.terasology.gestalt.assets.management.AssetManager; +import org.terasology.module.inventory.systems.InventoryManager; import org.terasology.structureTemplates.components.SpawnBlockRegionsComponent; import org.terasology.structureTemplates.interfaces.StructureTemplateProvider; import org.terasology.structureTemplates.util.BlockRegionTransform; @@ -554,7 +554,7 @@ public RoadStatus buildRoadParcel(RoadParcel parcel, EntityRef settlement) { // Flatten the rect // TODO: Find a way to store the surface height at that point to the segment here. - segment.height = flatten(segment.getRect().expand(rectExpansionFactor,new BlockArea(BlockArea.INVALID)), segmentHeight); + segment.height = flatten(segment.getRect().expand(rectExpansionFactor, new BlockArea(BlockArea.INVALID)), segmentHeight); // Create raster targets RasterTarget rasterTarget = new BufferRasterTarget(blockBufferSystem, roadTheme, segment.rect); diff --git a/src/main/java/org/terasology/dynamicCities/districts/DistrictManager.java b/src/main/java/org/terasology/dynamicCities/districts/DistrictManager.java index 2803e76..74a8326 100644 --- a/src/main/java/org/terasology/dynamicCities/districts/DistrictManager.java +++ b/src/main/java/org/terasology/dynamicCities/districts/DistrictManager.java @@ -1,24 +1,10 @@ -/* - * Copyright 2016 MovingBlocks - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ +// Copyright 2021 The Terasology Foundation +// SPDX-License-Identifier: Apache-2.0 package org.terasology.dynamicCities.districts; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.terasology.gestalt.assets.management.AssetManager; import org.terasology.dynamicCities.utilities.Toolbox; import org.terasology.engine.entitySystem.prefab.Prefab; import org.terasology.engine.entitySystem.systems.BaseComponentSystem; @@ -26,6 +12,7 @@ import org.terasology.engine.entitySystem.systems.RegisterSystem; import org.terasology.engine.registry.In; import org.terasology.engine.registry.Share; +import org.terasology.gestalt.assets.management.AssetManager; import java.util.ArrayList; import java.util.HashSet; @@ -52,12 +39,7 @@ public void postBegin() { //Get building data if (prefab.hasComponent(DistrictType.class)) { DistrictType districtType = prefab.getComponent(DistrictType.class); - if (!districtType.zones.isEmpty()) { - Toolbox.stringsToLowerCase(districtType.zones); - districts.add(districtType); - } else { - logger.warn("Found district prefab with empty zone list"); - } + addDistrict(districtType); } } @@ -73,6 +55,15 @@ public void postBegin() { logger.info("Finished loading districts: " + districts.size() + " district types found: " + districtNames); } + public void addDistrict(DistrictType districtType) { + if (!districtType.zones.isEmpty()) { + Toolbox.stringsToLowerCase(districtType.zones); + districts.add(districtType); + } else { + logger.warn("Found district prefab with empty zone list"); + } + } + public Optional getDistrictFromName(String name) { for (DistrictType districtType : districts) { if (districtType.name.equals(name)) { diff --git a/src/main/java/org/terasology/dynamicCities/districts/DistrictType.java b/src/main/java/org/terasology/dynamicCities/districts/DistrictType.java index eee34a1..4460135 100644 --- a/src/main/java/org/terasology/dynamicCities/districts/DistrictType.java +++ b/src/main/java/org/terasology/dynamicCities/districts/DistrictType.java @@ -9,6 +9,7 @@ import org.terasology.nui.Color; import org.terasology.reflection.MappedContainer; +import java.util.Arrays; import java.util.List; //TODO: give mixing factors for zones @@ -20,7 +21,12 @@ public class DistrictType implements Component { public int color; public List zones = Lists.newArrayList(); - public DistrictType ( ) { } + public DistrictType() { } + + public DistrictType(String name, String... zones) { + this.name = name; + this.zones.addAll(Arrays.asList(zones)); + }; public boolean isValidType(DynParcel parcel) { String zone = parcel.getZone(); diff --git a/src/main/java/org/terasology/dynamicCities/population/CultureManager.java b/src/main/java/org/terasology/dynamicCities/population/CultureManager.java index 9713c50..5c14c51 100644 --- a/src/main/java/org/terasology/dynamicCities/population/CultureManager.java +++ b/src/main/java/org/terasology/dynamicCities/population/CultureManager.java @@ -1,24 +1,10 @@ -/* - * Copyright 2016 MovingBlocks - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ +// Copyright 2021 The Terasology Foundation +// SPDX-License-Identifier: Apache-2.0 package org.terasology.dynamicCities.population; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.terasology.gestalt.assets.management.AssetManager; import org.terasology.dynamicCities.utilities.Toolbox; import org.terasology.engine.entitySystem.prefab.Prefab; import org.terasology.engine.entitySystem.systems.BaseComponentSystem; @@ -27,6 +13,7 @@ import org.terasology.engine.registry.In; import org.terasology.engine.registry.Share; import org.terasology.engine.utilities.random.MersenneRandom; +import org.terasology.gestalt.assets.management.AssetManager; import java.util.HashSet; import java.util.Set; @@ -49,27 +36,7 @@ public void postBegin() { //Get building data if (prefab.hasComponent(CultureComponent.class)) { CultureComponent cultureComponent = prefab.getComponent(CultureComponent.class); - if (cultureComponent.theme != null) { - cultureComponent.theme = cultureComponent.theme.toLowerCase(); - } else { - logger.warn("No theme defined for culture " + cultureComponent.name); - } - if (!cultureComponent.buildingNeedPerZone.isEmpty()) { - cultureComponents.add(cultureComponent); - cultureComponent.buildingNeedPerZone = Toolbox.stringsToLowerCase(cultureComponent.buildingNeedPerZone); - } else { - logger.warn("Found culture prefab with empty buildingNeedPerZone list"); - } - if (cultureComponent.availableBuildings != null) { - Toolbox.stringsToLowerCase(cultureComponent.availableBuildings); - } else { - logger.warn("No available Buildings defined for culture " + cultureComponent.name); - } - if (cultureComponent.residentialZones != null) { - Toolbox.stringsToLowerCase(cultureComponent.residentialZones); - } else { - logger.warn("No residential zones defined for culture " + cultureComponent.name); - } + addCulture(cultureComponent); } } @@ -83,13 +50,37 @@ public void postBegin() { rng = new MersenneRandom(assetManager.hashCode() * 5 + this.hashCode()); } + public void addCulture(CultureComponent cultureComponent) { + if (cultureComponent.theme != null) { + cultureComponent.theme = cultureComponent.theme.toLowerCase(); + } else { + logger.warn("No theme defined for culture " + cultureComponent.name); + } + if (!cultureComponent.buildingNeedPerZone.isEmpty()) { + cultureComponents.add(cultureComponent); + cultureComponent.buildingNeedPerZone = Toolbox.stringsToLowerCase(cultureComponent.buildingNeedPerZone); + } else { + logger.warn("Found culture prefab with empty buildingNeedPerZone list"); + } + if (cultureComponent.availableBuildings != null) { + Toolbox.stringsToLowerCase(cultureComponent.availableBuildings); + } else { + logger.warn("No available Buildings defined for culture " + cultureComponent.name); + } + if (cultureComponent.residentialZones != null) { + Toolbox.stringsToLowerCase(cultureComponent.residentialZones); + } else { + logger.warn("No residential zones defined for culture " + cultureComponent.name); + } + } + public CultureComponent getRandomCulture() { if (!cultureComponents.isEmpty()) { int max = cultureComponents.size(); int index = rng.nextInt(max); return (CultureComponent) cultureComponents.toArray()[index]; } - logger.error("No culture found...barbarians..." ); + logger.error("No culture found...barbarians..."); return null; } diff --git a/src/main/java/org/terasology/dynamicCities/region/RegionEntityManager.java b/src/main/java/org/terasology/dynamicCities/region/RegionEntityManager.java index a7e406b..5ad1c8d 100644 --- a/src/main/java/org/terasology/dynamicCities/region/RegionEntityManager.java +++ b/src/main/java/org/terasology/dynamicCities/region/RegionEntityManager.java @@ -295,10 +295,14 @@ public String toggleRegionTags( public List getRegionsInArea(BlockAreac area) { List result = new ArrayList<>(); for (Vector2ic pos : area) { - EntityRef region = getNearest(new Vector2i(pos.x(), pos.y())); + EntityRef region = getNearest(pos); if (region == null || !region.isActive() || !region.exists()) { - logger.debug("Failed to get nearest region for " + pos.toString()); + logger.debug("Failed to get nearest region for {}: {}, {}, {}", + pos, region, + region != null && region.isActive(), + region != null && region.exists() + ); } else if (!result.contains(region)) { result.add(region); } diff --git a/src/main/java/org/terasology/dynamicCities/region/RegionEntityProvider.java b/src/main/java/org/terasology/dynamicCities/region/RegionEntityProvider.java index dd06069..78558b2 100644 --- a/src/main/java/org/terasology/dynamicCities/region/RegionEntityProvider.java +++ b/src/main/java/org/terasology/dynamicCities/region/RegionEntityProvider.java @@ -1,18 +1,5 @@ -/* - * Copyright 2016 MovingBlocks - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ +// Copyright 2021 The Terasology Foundation +// SPDX-License-Identifier: Apache-2.0 package org.terasology.dynamicCities.region; @@ -57,7 +44,6 @@ public void process(Region region, EntityBuffer buffer) { if (checkCorners(worldRegion, elevationFacet)) { RoughnessFacet roughnessFacet = region.getFacet(RoughnessFacet.class); ResourceFacet resourceFacet = region.getFacet(ResourceFacet.class); - TreeFacet treeFacet = region.getFacet(TreeFacet.class); SiteFacet siteFacet = region.getFacet(SiteFacet.class); SettlementFacet settlementFacet = region.getFacet(SettlementFacet.class); @@ -65,10 +51,15 @@ public void process(Region region, EntityBuffer buffer) { RoughnessFacetComponent roughnessFacetComponent = new RoughnessFacetComponent(roughnessFacet); ResourceFacetComponent resourceFacetComponent = new ResourceFacetComponent(resourceFacet); - TreeFacetComponent treeFacetComponent = new TreeFacetComponent(treeFacet); entityStore.addComponent(roughnessFacetComponent); entityStore.addComponent(resourceFacetComponent); - entityStore.addComponent(treeFacetComponent); + + // FIXME: is TreeFacet optional? + TreeFacet treeFacet = region.getFacet(TreeFacet.class); + if (treeFacet != null) { + TreeFacetComponent treeFacetComponent = new TreeFacetComponent(treeFacet); + entityStore.addComponent(treeFacetComponent); + } LocationComponent locationComponent = new LocationComponent(worldRegion.center(new Vector3f())); entityStore.addComponent(locationComponent); @@ -99,8 +90,8 @@ protected boolean checkCorners(BlockRegion worldRegion, BaseFieldFacet2D facet) positions[0] = new Vector2i(max.x(), max.z()); positions[1] = new Vector2i(min.x(), min.z()); - positions[2] = new Vector2i(min.x() + worldRegion.getSizeX(), min.z()); - positions[3] = new Vector2i(min.x(), min.z() + worldRegion.getSizeZ()); + positions[2] = new Vector2i(max.x(), min.z()); + positions[3] = new Vector2i(min.x(), max.z()); positions[4] = new Vector2i(worldRegion.center(new Vector3f()).x, worldRegion.center(new Vector3f()).z, RoundingMode.FLOOR); diff --git a/src/main/java/org/terasology/dynamicCities/settlements/SettlementEntityManager.java b/src/main/java/org/terasology/dynamicCities/settlements/SettlementEntityManager.java index 246e9e0..cc6f415 100644 --- a/src/main/java/org/terasology/dynamicCities/settlements/SettlementEntityManager.java +++ b/src/main/java/org/terasology/dynamicCities/settlements/SettlementEntityManager.java @@ -1,4 +1,4 @@ -// Copyright 2020 The Terasology Foundation +// Copyright 2021 The Terasology Foundation // SPDX-License-Identifier: Apache-2.0 package org.terasology.dynamicCities.settlements; @@ -58,7 +58,6 @@ import org.terasology.engine.entitySystem.systems.RegisterSystem; import org.terasology.engine.logic.location.LocationComponent; import org.terasology.engine.logic.nameTags.NameTagComponent; -import org.terasology.logic.players.MinimapSystem; import org.terasology.engine.network.NetworkComponent; import org.terasology.engine.registry.In; import org.terasology.engine.registry.Share; @@ -70,6 +69,7 @@ import org.terasology.engine.world.generation.Border3D; import org.terasology.engine.world.time.WorldTimeEvent; import org.terasology.joml.geom.Circlef; +import org.terasology.logic.players.MinimapSystem; import org.terasology.namegenerator.town.TownNameProvider; import org.terasology.nui.Color; @@ -81,9 +81,10 @@ import java.util.Set; import java.util.Vector; -@Share(value = SettlementEntityManager.class) +@Share(SettlementEntityManager.class) @RegisterSystem(RegisterMode.AUTHORITY) public class SettlementEntityManager extends BaseComponentSystem { + private static final Logger logger = LoggerFactory.getLogger(SettlementEntityManager.class); @In private EntityManager entityManager; @@ -124,8 +125,6 @@ public class SettlementEntityManager extends BaseComponentSystem { private Random rng; private Multimap roadCache = MultimapBuilder.hashKeys().hashSetValues().build(); - private Logger logger = LoggerFactory.getLogger(SettlementEntityManager.class); - @Override public void postBegin() { long seed = regionEntityManager.hashCode() & 0x921233; @@ -183,7 +182,7 @@ public void onWorldTimeEvent(WorldTimeEvent worldTimeEvent, EntityRef entityRef) * @param event * @param siteRegion */ - @ReceiveEvent(components = {SiteComponent.class}) + @ReceiveEvent(components = SiteComponent.class) public void filterSettlement(CheckSiteSuitabilityEvent event, EntityRef siteRegion) { boolean checkDistance = checkMinDistance(siteRegion); boolean checkBuildArea = checkBuildArea(siteRegion); @@ -267,7 +266,7 @@ public boolean checkOutsideAllSettlements(Vector2ic pos) { return true; } - private EntityRef createSettlement(EntityRef siteRegion) { + EntityRef createSettlement(EntityRef siteRegion) { EntityRef settlementEntity = entityManager.create(); SiteComponent siteComponent = siteRegion.getComponent(SiteComponent.class); @@ -352,7 +351,7 @@ private void getSurroundingRegions(EntityRef settlement) { float radius = parcelList.cityRadius; int size = Math.max(Math.round(radius / 32), 1); BlockArea settlementRectArea = new BlockArea(-size, -size, size, size); - Circlef settlementCircle = new Circlef(pos.x,pos.y, radius); + Circlef settlementCircle = new Circlef(pos.x, pos.y, radius); Vector2i regionWorldPos = new Vector2i(); for (Vector2ic regionPos : settlementRectArea) { @@ -628,8 +627,8 @@ public void buildRoads(EntityRef sourceSettlement) { sourceSettlement.saveComponent(parcelList); } - private Optional placeParcel(Vector3ic center, String zone, ParcelList parcels, - BuildingQueue buildingQueue, DistrictFacetComponent districtFacetComponent, int maxIterations) { + Optional placeParcel(Vector3ic center, String zone, ParcelList parcels, + BuildingQueue buildingQueue, DistrictFacetComponent districtFacetComponent, int maxIterations) { int iter = 0; Map> minMaxSizes = buildingManager.getMinMaxSizePerZone(); int minSize = (minMaxSizes.get(zone).get(0).x() < minMaxSizes.get(zone).get(0).y()) @@ -640,19 +639,19 @@ private Optional placeParcel(Vector3ic center, String zone, ParcelLis int sizeY = rng.nextInt(minSize, maxSize); BlockArea shape; Orientation orientation = Orientation.NORTH.getRotated(90 * rng.nextInt(5)); - Vector2i rectPosition = new Vector2i(); float radius; + ParcelLocationValidator parcelLocationValidator = new ParcelLocationValidator(parcels, buildingQueue, districtFacetComponent, + zone, regionEntityManager); do { iter++; float angle = rng.nextFloat(0, 360); //Subtract the maximum tree radius (13) from the parcel radius -> Some bigger buildings still could cause issues radius = rng.nextFloat(0, parcels.cityRadius - 32); - rectPosition.set((int) Math.round(radius * Math.sin((double) angle) + center.x()), - (int) Math.round(radius * Math.cos((double) angle)) + center.z()); - shape = new BlockArea(rectPosition.x(), rectPosition.y()).setSize(sizeX, sizeY); - } while ((!parcels.isNotIntersecting(shape) || !buildingQueue.isNotIntersecting(shape) - || !(districtFacetComponent.getDistrict(rectPosition.x(), rectPosition.y()).isValidType(zone)) || !checkIfTerrainIsBuildable(shape)) - && iter != maxIterations); + int x = (int) Math.round(radius * Math.sin(angle)) + center.x(); + int z = (int) Math.round(radius * Math.cos(angle)) + center.z(); + shape = new BlockArea(x, z).setSize(sizeX, sizeY); + } while (!parcelLocationValidator.isValid(shape) && iter < maxIterations); + //Keep track of the most distant building to the center if (radius > parcels.builtUpRadius) { parcels.builtUpRadius = radius; @@ -664,26 +663,52 @@ private Optional placeParcel(Vector3ic center, String zone, ParcelLis } } - private boolean checkIfTerrainIsBuildable(BlockAreac area) { - List regions = regionEntityManager.getRegionsInArea(area); - if (regions.isEmpty()) { - //logger.debug("No regions found in area " + area.toString()); - return false; - } - for (EntityRef region : regions) { - RoughnessFacetComponent roughnessFacetComponent = region.getComponent(RoughnessFacetComponent.class); - ResourceFacetComponent resourceFacetComponent = region.getComponent(ResourceFacetComponent.class); - if (roughnessFacetComponent == null) { - logger.error("No RoughnessFacetComponent found for region"); + static class ParcelLocationValidator { + private final ParcelList parcels; + private final BuildingQueue buildingQueue; + private final DistrictFacetComponent districtFacetComponent; + private final String zone; + private final RegionEntityManager regionEntityManager; + + ParcelLocationValidator(ParcelList parcels, BuildingQueue buildingQueue, DistrictFacetComponent districtFacetComponent, String zone, RegionEntityManager regionEntityManager) { + this.parcels = parcels; + this.buildingQueue = buildingQueue; + this.districtFacetComponent = districtFacetComponent; + this.zone = zone; + this.regionEntityManager = regionEntityManager; + } + + private boolean checkIfTerrainIsBuildable(BlockAreac area) { + List regions = regionEntityManager.getRegionsInArea(area); + if (regions.isEmpty()) { + logger.debug("No regions found in area {}", area); return false; } - if (roughnessFacetComponent.meanDeviation > SettlementConstants.MAX_BUILDABLE_ROUGHNESS) { - return false; - } - if (resourceFacetComponent.getResourceSum(ResourceType.WATER.toString()) != 0) { - return false; + for (EntityRef region : regions) { + RoughnessFacetComponent roughnessFacetComponent = region.getComponent(RoughnessFacetComponent.class); + ResourceFacetComponent resourceFacetComponent = region.getComponent(ResourceFacetComponent.class); + if (roughnessFacetComponent == null) { + logger.error("No RoughnessFacetComponent found for region"); + return false; + } + if (roughnessFacetComponent.meanDeviation > SettlementConstants.MAX_BUILDABLE_ROUGHNESS) { + return false; + } + if (resourceFacetComponent.getResourceSum(ResourceType.WATER.toString()) != 0) { + return false; + } } + return true; + } + + boolean isValid(BlockAreac shape) { + // TODO: This no longer short-circuits on the first false, is that a performance problem? + boolean freeOfOtherParcels = parcels.isNotIntersecting(shape); + boolean freeOfOtherBuildings = buildingQueue.isNotIntersecting(shape); + boolean districtAllowsZone = districtFacetComponent.getDistrict(shape.minX(), shape.minY()).isValidType(zone); + boolean terrainBuildable = checkIfTerrainIsBuildable(shape); + logger.debug("P:{} B:{} Z:{} T:{}", freeOfOtherBuildings, freeOfOtherBuildings, districtAllowsZone, terrainBuildable); + return freeOfOtherParcels && freeOfOtherBuildings && districtAllowsZone && terrainBuildable; } - return true; } } diff --git a/src/main/java/org/terasology/dynamicCities/settlements/components/DistrictFacetComponent.java b/src/main/java/org/terasology/dynamicCities/settlements/components/DistrictFacetComponent.java index 57cc947..1d4e888 100644 --- a/src/main/java/org/terasology/dynamicCities/settlements/components/DistrictFacetComponent.java +++ b/src/main/java/org/terasology/dynamicCities/settlements/components/DistrictFacetComponent.java @@ -14,9 +14,10 @@ import org.terasology.dynamicCities.districts.Kmeans; import org.terasology.dynamicCities.population.CultureComponent; import org.terasology.dynamicCities.settlements.SettlementConstants; -import org.terasology.dynamicCities.utilities.ProbabilityDistribution; import org.terasology.engine.network.Replicate; import org.terasology.engine.utilities.procedural.WhiteNoise; +import org.terasology.engine.utilities.random.DiscreteDistribution; +import org.terasology.engine.utilities.random.MersenneRandom; import org.terasology.engine.world.block.BlockArea; import org.terasology.engine.world.block.BlockAreac; import org.terasology.engine.world.block.BlockRegion; @@ -32,6 +33,8 @@ import java.util.stream.Collectors; import java.util.stream.IntStream; +import static com.google.common.base.Preconditions.checkArgument; + public class DistrictFacetComponent implements Component { /** @@ -39,7 +42,7 @@ public class DistrictFacetComponent implements Component * TODO: Assign districts similar to parcels (look up if needs are already fulfilled before placement) */ @Replicate - public BlockAreac relativeRegion =new BlockArea(BlockArea.INVALID); + public BlockAreac relativeRegion = new BlockArea(BlockArea.INVALID); @Replicate public BlockAreac worldRegion = new BlockArea(BlockArea.INVALID); @Replicate @@ -63,7 +66,8 @@ public class DistrictFacetComponent implements Component public DistrictFacetComponent() { } - public DistrictFacetComponent(BlockRegion targetRegion, Border3D border, int gridSize, long seed, DistrictManager districtManager, CultureComponent cultureComponent) { + public DistrictFacetComponent(BlockRegion targetRegion, Border3D border, int gridSize, long seed, DistrictManager districtManager, + CultureComponent cultureComponent) { worldRegion = border.expandTo2D(targetRegion); relativeRegion = border.expandTo2D(targetRegion.getSize(new Vector3i())); this.gridSize = gridSize; @@ -110,8 +114,10 @@ public DistrictFacetComponent(BlockRegion targetRegion, Border3D border, int gri private void mapDistrictTypes(DistrictManager districtManager, CultureComponent cultureComponent) { + checkArgument(!districtManager.getDistrictTypes().isEmpty(), "There are no district types!"); Map zoneArea = new HashMap<>(); - ProbabilityDistribution probabilityDistribution = new ProbabilityDistribution<>(districtManager.hashCode() | 413357); + DiscreteDistribution probabilityDistribution = new DiscreteDistribution<>(); + MersenneRandom rng = new MersenneRandom(districtManager.hashCode() | 413357); Map culturalNeedsPercentage = cultureComponent.getProcentualsForZone(); int totalAssignedArea = 0; @@ -133,14 +139,11 @@ private void mapDistrictTypes(DistrictManager districtManager, CultureComponent totalAssignedArea += districtSize.get(i); //Calculate probabilities - Map probabilites = new HashMap<>(districtManager.getDistrictTypes().size()); - float totalDiff = 0; - for (DistrictType districtType : districtManager.getDistrictTypes()) { float diff = 0; Map tempZoneArea = new HashMap<>(zoneArea); for (String zone : districtType.zones) { - float area = districtSize.get(i) / districtType.zones.size(); + float area = (float) districtSize.get(i) / districtType.zones.size(); tempZoneArea.put(zone, tempZoneArea.getOrDefault(zone, 0f) + area); if (!culturalNeedsPercentage.containsKey(zone)) { diff = Float.MAX_VALUE; @@ -152,18 +155,13 @@ private void mapDistrictTypes(DistrictManager districtManager, CultureComponent } diff = (diff == 0) ? 0 : 1 / diff; - probabilites.put(districtType, diff); - totalDiff += diff; - } - for (DistrictType districtType : districtManager.getDistrictTypes()) { - probabilites.put(districtType, probabilites.getOrDefault(districtType, 0f) / totalDiff); + probabilityDistribution.add(districtType, diff); } //Assign District - probabilityDistribution.initialise(probabilites); - DistrictType nextDistrict = probabilityDistribution.get(); + DistrictType nextDistrict = probabilityDistribution.sample(rng); for (String zone : nextDistrict.zones) { - float area = districtSize.get(i) / nextDistrict.zones.size(); + float area = (float) districtSize.get(i) / nextDistrict.zones.size(); zoneArea.put(zone, zoneArea.getOrDefault(zone, 0f) + area); } districtTypeMap.put(Integer.toString(districtCenters.indexOf(minCenter)), nextDistrict); diff --git a/src/main/java/org/terasology/dynamicCities/sites/SiteComponent.java b/src/main/java/org/terasology/dynamicCities/sites/SiteComponent.java index babfae3..efb6505 100644 --- a/src/main/java/org/terasology/dynamicCities/sites/SiteComponent.java +++ b/src/main/java/org/terasology/dynamicCities/sites/SiteComponent.java @@ -66,6 +66,7 @@ public String toString() { @Override public void copyFrom(SiteComponent other) { - + this.coords.set(other.coords); + this.radius = other.radius; } } diff --git a/src/main/java/org/terasology/dynamicCities/utilities/ProbabilityDistribution.java b/src/main/java/org/terasology/dynamicCities/utilities/ProbabilityDistribution.java deleted file mode 100644 index 0c84055..0000000 --- a/src/main/java/org/terasology/dynamicCities/utilities/ProbabilityDistribution.java +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright 2016 MovingBlocks - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.terasology.dynamicCities.utilities; - -import com.google.common.collect.Range; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.terasology.engine.utilities.random.MersenneRandom; -import org.terasology.math.TeraMath; - -import java.util.HashMap; -import java.util.Map; - -/** - * Used to define a probability distribution for values of type T. - * They are retrieved with a uniform random number generation. - * @param - */ -public class ProbabilityDistribution { - - private Logger logger = LoggerFactory.getLogger(ProbabilityDistribution.class); - private Map ranges; - private MersenneRandom rng; - - - public ProbabilityDistribution (long seed) { - rng = new MersenneRandom(seed); - } - - public void initialise (Map probabilites) { - ranges = new HashMap<>(); - //check if sum of probabilites is 1 - Float sum = 0f; - for (Float probability : probabilites.values()) { - sum += probability; - } - if (!(TeraMath.fastAbs(sum - 1) < 0.01f)) { - logger.error("Error initialising ProbabilityDistribution! Sum of probabilites was not 1!"); - return; - } - float lastIndex = 0; - for(Map.Entry entry : probabilites.entrySet()) { - Range range = Range.closedOpen(lastIndex, lastIndex + entry.getValue()); - lastIndex += entry.getValue(); - ranges.put(range, entry.getKey()); - } - - - - } - - public T get() { - float random = rng.nextFloat(0, 1); - for (Range range : ranges.keySet()) { - if (range.contains(random)) { - return ranges.get(range); - } - } - logger.error("Could not retrieve a valid value with a random number value of " + random); - return null; - } - -} diff --git a/src/main/java/org/terasology/dynamicCities/world/SolidRasterizer.java b/src/main/java/org/terasology/dynamicCities/world/SolidRasterizer.java index 7561a62..bde7e9a 100644 --- a/src/main/java/org/terasology/dynamicCities/world/SolidRasterizer.java +++ b/src/main/java/org/terasology/dynamicCities/world/SolidRasterizer.java @@ -1,18 +1,5 @@ -/* - * Copyright 2016 MovingBlocks - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ +// Copyright 2021 The Terasology Foundation +// SPDX-License-Identifier: Apache-2.0 package org.terasology.dynamicCities.world; import org.joml.Vector2i; @@ -31,8 +18,8 @@ import org.terasology.engine.world.generation.facets.SeaLevelFacet; import org.terasology.engine.world.generation.facets.SurfacesFacet; -/** - */ +import static com.google.common.base.Preconditions.checkNotNull; + public class SolidRasterizer extends CompatibleRasterizer implements ScalableWorldRasterizer { /** @@ -47,10 +34,20 @@ public void generateChunk(Chunk chunk, Region chunkRegion) { public void generateChunk(Chunk chunk, Region chunkRegion, float scale) { DensityFacet solidityFacet = chunkRegion.getFacet(DensityFacet.class); SurfacesFacet surfaceFacet = chunkRegion.getFacet(SurfacesFacet.class); - BiomeFacet biomeFacet = chunkRegion.getFacet(BiomeFacet.class); ResourceFacet resourceFacet = chunkRegion.getFacet(ResourceFacet.class); SeaLevelFacet seaLevelFacet = chunkRegion.getFacet(SeaLevelFacet.class); - int seaLevel = seaLevelFacet.getSeaLevel(); + + // FIXME: How should this handle a lack of sea level? + // Or should there be some facet dependency declared to make sure + // there always is a sea level facet? + int seaLevel = (seaLevelFacet != null) + ? seaLevelFacet.getSeaLevel() + : Integer.MIN_VALUE; + + // FIXME: BiomeFacet does depend on SeaLevelFacet, but that doesn't help if + // there is no BiomeFacet! + BiomeFacet biomeFacet = checkNotNull(chunkRegion.getFacet(BiomeFacet.class), + "world must have a biome facet"); Vector2i pos2d = new Vector2i(); Vector3i worldPos = new Vector3i(); @@ -60,7 +57,11 @@ public void generateChunk(Chunk chunk, Region chunkRegion, float scale) { biomeRegistry.setBiome(biome, chunk, pos.x(), pos.y(), pos.z()); float posY = (pos.y() + chunk.getChunkWorldOffsetY()) * scale; - float density = solidityFacet.get(pos); + + // FIXME: require DensityFacet + float density = (solidityFacet != null) + ? solidityFacet.get(pos) + : 1f; chunk.chunkToWorldPosition(pos, worldPos); if (surfaceFacet.get(pos)) { diff --git a/src/main/java/org/terasology/dynamicCities/world/testbench/ConstantBiomeFacet.java b/src/main/java/org/terasology/dynamicCities/world/testbench/ConstantBiomeFacet.java new file mode 100644 index 0000000..2535a52 --- /dev/null +++ b/src/main/java/org/terasology/dynamicCities/world/testbench/ConstantBiomeFacet.java @@ -0,0 +1,29 @@ +// Copyright 2021 The Terasology Foundation +// SPDX-License-Identifier: Apache-2.0 + +package org.terasology.dynamicCities.world.testbench; + +import org.terasology.biomesAPI.Biome; +import org.terasology.core.world.CoreBiome; +import org.terasology.core.world.generator.facets.BiomeFacet; +import org.terasology.engine.world.block.BlockRegion; +import org.terasology.engine.world.generation.Border3D; + +public class ConstantBiomeFacet extends BiomeFacet { + private final CoreBiome biome; + + public ConstantBiomeFacet(BlockRegion region, Border3D border, CoreBiome biome) { + super(region, border); + this.biome = biome; + } + + @Override + public Biome get(int x, int y) { + return biome; + } + + @Override + public Biome getWorld(int x, int y) { + return biome; + } +} diff --git a/src/main/java/org/terasology/dynamicCities/world/testbench/ConstantBiomeProvider.java b/src/main/java/org/terasology/dynamicCities/world/testbench/ConstantBiomeProvider.java new file mode 100644 index 0000000..9e637b1 --- /dev/null +++ b/src/main/java/org/terasology/dynamicCities/world/testbench/ConstantBiomeProvider.java @@ -0,0 +1,26 @@ +// Copyright 2021 The Terasology Foundation +// SPDX-License-Identifier: Apache-2.0 + +package org.terasology.dynamicCities.world.testbench; + +import org.terasology.core.world.CoreBiome; +import org.terasology.core.world.generator.facetProviders.BiomeProvider; +import org.terasology.core.world.generator.facets.BiomeFacet; +import org.terasology.engine.world.generation.GeneratingRegion; +import org.terasology.engine.world.generation.Produces; + +// TODO: Make BiomeProvider an interface so we don't have to extend the array-backed implementation +@Produces(BiomeFacet.class) +public class ConstantBiomeProvider extends BiomeProvider { + private final CoreBiome biome; + + public ConstantBiomeProvider(CoreBiome biome) { + this.biome = biome; + } + + @Override + public void process(GeneratingRegion region, float scale) { + BiomeFacet biomeFacet = new ConstantBiomeFacet(region.getRegion(), region.getBorderForFacet(BiomeFacet.class), biome); + region.setRegionFacet(BiomeFacet.class, biomeFacet); + } +} diff --git a/src/main/java/org/terasology/dynamicCities/world/testbench/FlatFacetedWorld.java b/src/main/java/org/terasology/dynamicCities/world/testbench/FlatFacetedWorld.java new file mode 100644 index 0000000..5c7e0ec --- /dev/null +++ b/src/main/java/org/terasology/dynamicCities/world/testbench/FlatFacetedWorld.java @@ -0,0 +1,49 @@ +// Copyright 2021 The Terasology Foundation +// SPDX-License-Identifier: Apache-2.0 + +package org.terasology.dynamicCities.world.testbench; + +import org.terasology.core.world.CoreBiome; +import org.terasology.core.world.generator.facetProviders.SeaLevelProvider; +import org.terasology.dynamicCities.region.RegionEntityProvider; +import org.terasology.dynamicCities.region.ResourceProvider; +import org.terasology.dynamicCities.region.RoughnessProvider; +import org.terasology.dynamicCities.settlements.SettlementFacetProvider; +import org.terasology.dynamicCities.sites.SiteFacetProvider; +import org.terasology.dynamicCities.world.SolidRasterizer; +import org.terasology.engine.core.SimpleUri; +import org.terasology.engine.registry.In; +import org.terasology.engine.world.generation.BaseFacetedWorldGenerator; +import org.terasology.engine.world.generation.WorldBuilder; +import org.terasology.engine.world.generator.RegisterWorldGenerator; +import org.terasology.engine.world.generator.plugin.WorldGeneratorPluginLibrary; +import org.terasology.moduletestingenvironment.fixtures.FlatSurfaceHeightProvider; + +@RegisterWorldGenerator(id = "FlatFaceted", displayName = "Faceted world with one uniform flat surface.") +public class FlatFacetedWorld extends BaseFacetedWorldGenerator { + public static final int SURFACE_HEIGHT = 40; + public static final int SEA_LEVEL = 15; + public static final CoreBiome BIOME = CoreBiome.PLAINS; + + @In + private WorldGeneratorPluginLibrary worldGeneratorPluginLibrary; + + public FlatFacetedWorld(SimpleUri uri) { + super(uri); + } + + @Override + protected WorldBuilder createWorld() { + return new WorldBuilder(worldGeneratorPluginLibrary) + .addProvider(new FlatSurfaceHeightProvider(SURFACE_HEIGHT)) + .addProvider(new SeaLevelProvider(SEA_LEVEL)) + .addProvider(new ConstantBiomeProvider(BIOME)) + .addProvider(new RoughnessProvider()) + .addProvider(new SiteFacetProvider()) + .addProvider(new SettlementFacetProvider()) + .addProvider(new ResourceProvider()) + .addEntities(new RegionEntityProvider()) + .addRasterizer(new SolidRasterizer()) + .addPlugins(); + } +} diff --git a/src/test/java/org/terasology/dynamicCities/settlements/SettlementEntityManagerTest.java b/src/test/java/org/terasology/dynamicCities/settlements/SettlementEntityManagerTest.java new file mode 100644 index 0000000..ef96d89 --- /dev/null +++ b/src/test/java/org/terasology/dynamicCities/settlements/SettlementEntityManagerTest.java @@ -0,0 +1,128 @@ +// Copyright 2021 The Terasology Foundation +// SPDX-License-Identifier: Apache-2.0 + +package org.terasology.dynamicCities.settlements; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import org.joml.RoundingMode; +import org.joml.Vector2i; +import org.joml.Vector3fc; +import org.joml.Vector3i; +import org.joml.Vector3ic; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; +import org.terasology.dynamicCities.buildings.BuildingManager; +import org.terasology.dynamicCities.buildings.BuildingQueue; +import org.terasology.dynamicCities.districts.DistrictManager; +import org.terasology.dynamicCities.districts.DistrictType; +import org.terasology.dynamicCities.parcels.DynParcel; +import org.terasology.dynamicCities.parcels.ParcelList; +import org.terasology.dynamicCities.population.CultureComponent; +import org.terasology.dynamicCities.population.CultureManager; +import org.terasology.dynamicCities.settlements.components.DistrictFacetComponent; +import org.terasology.dynamicCities.sites.SiteComponent; +import org.terasology.dynamicCities.world.testbench.FlatFacetedWorld; +import org.terasology.engine.context.Context; +import org.terasology.engine.entitySystem.entity.EntityManager; +import org.terasology.engine.entitySystem.entity.EntityRef; +import org.terasology.engine.logic.location.LocationComponent; +import org.terasology.engine.registry.In; +import org.terasology.engine.registry.InjectionHelper; +import org.terasology.moduletestingenvironment.MTEExtension; +import org.terasology.moduletestingenvironment.ModuleTestingHelper; +import org.terasology.moduletestingenvironment.extension.Dependencies; +import org.terasology.moduletestingenvironment.extension.UseWorldGenerator; +import org.terasology.namegenerator.town.TownAssetTheme; + +import java.util.Optional; + +import static com.google.common.truth.Truth8.assertThat; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; + +@Tag("MteTest") +@ExtendWith({MTEExtension.class, MockitoExtension.class}) +@Dependencies("DynamicCities") +@UseWorldGenerator("DynamicCities:FlatFaceted") +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class SettlementEntityManagerTest { + + static final int MAX_PLACEMENT_ATTEMPTS = 20; + static final String zone = "testzone"; // lowercase + static final Vector2i buildingSize = new Vector2i(9, 9); + static final Vector3ic siteLocation = new Vector3i(1234, FlatFacetedWorld.SURFACE_HEIGHT, -5678); + static final int population = 3; + + @In + Context context; + + @BeforeAll + void initialiseZones() { + // FIXME: mockito kludge to give us a way to define buildings. + // Should have a way for this test to add a building prefab, + // or a way to add to BuildingManager without prefabs. + BuildingManager buildingManager = spy(context.get(BuildingManager.class)); + when(buildingManager.getMinMaxSizePerZone()).thenReturn(ImmutableMap.of(zone, ImmutableList.of(buildingSize, buildingSize))); + context.put(BuildingManager.class, buildingManager); + + // re-inject this so it has the mock version. + InjectionHelper.inject(context.get(SettlementEntityManager.class), context); + } + + @BeforeAll + void initialiseCulture(CultureManager cultures) { + CultureComponent culture = new CultureComponent(); + culture.name = "Testers"; + culture.theme = TownAssetTheme.FANTASY.name(); + culture.availableBuildings.add("Test Building"); + culture.buildingNeedPerZone.put(zone, 1f); + culture.residentialZones.add(zone); + + cultures.addCulture(culture); + } + + @BeforeAll + void initialiseDistricts(DistrictManager districts) { + districts.addDistrict(new DistrictType("The District", zone)); + } + + EntityRef newSite() { + SiteComponent site = new SiteComponent(siteLocation.x(), siteLocation.z()); + return context.get(EntityManager.class).create( + site, + new SettlementComponent(site, population), + new LocationComponent(siteLocation) + ); + } + + @Test + void placeParcel(SettlementEntityManager settlements, ModuleTestingHelper mte) { + ParcelList parcels = new ParcelList(); + BuildingQueue buildingQueue = new BuildingQueue(); + + // Regions are initialized during world generation. + mte.forceAndWaitForGeneration(siteLocation); + // Loading a wider area requires https://github.com/Terasology/ModuleTestingEnvironment/pull/66 +// mte.runUntil(mte.makeBlocksRelevant( +// new BlockRegion(siteLocation) +// .expand(Chunks.SIZE_X * 4, Chunks.SIZE_Y, Chunks.SIZE_Z * 4) +// )); + + EntityRef site = newSite(); + + EntityRef settlement = settlements.createSettlement(site); + + Vector3fc center = settlement.getComponent(LocationComponent.class).getLocalPosition(); + + Optional parcel = settlements.placeParcel( + new Vector3i(center, RoundingMode.FLOOR), zone, parcels, buildingQueue, + settlement.getComponent(DistrictFacetComponent.class), MAX_PLACEMENT_ATTEMPTS + ); + assertThat(parcel).isPresent(); + } +}