Map generation

DeathDawn uses a random map generator for two simple reasons: It would take me ages to make all the maps myself (unless I write a funky level editor), and they will be shit (unless I write a really funky level generator). A random map generator should be able to reduce the development time of the game significantly.

The code

The core low-level map manipulation functions are map_getblock() and map_getstack(). These accept the building blocks of blocks and stacks as their input, and return a pointer to a block or stack instance as output, either by fetching an extant instance from the block and stack definition lists, or by inserting a new one. Thus, there is one cardinal rule of map editing:

When editing a block you must request a new block ptr using map_getblock(). To insert the block into the map, you must then copy the stack for the location you want the block inserting, insert the entry, then request a new stack using map_getstack(). Then, you can simply replace the stack pointer in the column structure returned by map_column().

If you modify an extant block or stack definition, all stacks which use that block, or all columns which use that stack, will be affected by your change, and you may break the hash map structure that is used to contain the blocks and stacks (Although this will merely mean that calls to map_getblock() or map_getstack() will return a new definition instead of the extant one). There are, of course, some instances where global modification of blocks and stacks can be useful, but until I find an instance where I need to perform a global modification there is unlikely to be any official support for it in the code (i.e. by rearranging the hash maps so that the blocks and stacks are stored properly).

Mission scripts

Mission scripts are able to perform low-level map manipulation, via the GETBLOCK, SETBLOCK, GETSTACK and SETSTACK commands. They are also able to kill the map and create new, blank maps, using KILLMAP and NEWMAP. They can also trigger the main map generator algorithm, using MAPGEN.

The algorithm

This is the biggy. How do you convert 1 square kilometer of space into a living, breathing city in a matter of seconds?

Map generation in Death Dawn follows the following basic outline:

  1. Add geographic elements to the map (rivers, cliffs, etc.)
  2. Split the map into a semi-regular grid of rectangular city blocks, in traditional American fashion
  3. Place roads along the edges between the blocks
  4. Weight the roads by how often they are used; this gives them their width
  5. Zone the map into ganglands, commercial, industrial, residential, etc. areas
  6. Place buildings into each city block, using styles appropriate to the zone
  7. Painting the roads and buildings onto the real map structure

Each element of the list above will now be discussed in detail (whether currently implemented or not)

1. Geography

I currently have no idea how this will fit into the current design. Presumably, just doodle a few rivers into the map, and tweak the stack base heights for cliffs and hills.

2. City block generation

This is performed by the magic_road_algorithm(). The basic operation is to start with one city block (cblock) of a random size in the top-left of the map, then repeatedly add more cblocks of random sizes to the edges, until the entire map is filled. A uniformity parameter is used as part of the algorithm to control how similar adjacent blocks are to each other, and thus how grid-like the result is (although there is a limit to how grid-like it can become due to the way the algorithm is implemented).

Data structures

The city block generation stage is the first time the cblock structure is introduced. This contains a list of rectangles, giving the shape of the city block (although the current code assumes there is one rectangle per block), and a list of edges that join this block with its neighbours. Each edge has (via gdll list item pointers) pointers to the two cblocks that it runs between. It also has direct pointers to the two corners at the ends of the edges, along with edge type information (road, pavement, cliff, etc.) and the weight of the road. Each corner has x and y coordinates, an array of 4 edge pointers (since there can be no more than 4 roads per corner), and a visited flag (which is used by the A* algorithm later on).

To keep track of all this data, there is one global linked list of cblocks, and one list of corners. No list of edges is necessary; all edges can be iterated through by processing half of the edges of each cblock (where 'half' is some determinstic measure, such as only the edges where the first cblock pointer points to the cblock we're accessing the edge from). To allow for speedy lookup of which cblock is at which grid square, a linear array is used to map each grid sqaure to a cblock pointer.

Algorithm implementation

Starting with an empty cblock (and thus corner list):

  1. Scan the map from top to bottom, left to right, until an unfilled block is found
  2. From that location, scan across to the left and down, thus finding the maximum size of cblock that can be inserted. Due to the featureless state of the current starting map, not every point inside the potential rectangle needs to be checked, only the top and left edges.
  3. If the width is less than the minimum block size, then, if possible, the height will be recalculated so that the cblock to be placed below this one will be wider (or equal to) the minimum size. This helps avoid situations where lots of small blocks clump together.
  4. Similarly, if the height is less than the minimum block size, then the width is recalculated so that (if possible) the next block placed horizontally will be taller (or equal to) the minimum size.
  5. If, however, the width and height is above (or equal to) the minimum size, then the uniformity value is checked against a random number from 0 to 100. If the random number is greater than the uniformity (making uniformity somewhat of a misnomoner), then the height of the block to be inserted will be recalculated to be the same height as the block to the left (or rather, so that it stops at the same point the cblock to the left stops).
  6. Otherwise, a random block height is chosen, between the minimum size and the maximum measured size.
  7. Code also protects against making the next block too small; i.e. if the measured maximum size is less than twice the minimum size, then the measured maximum will be used. This check for the size of the next block is performed on both the width and height (unless other rules override it, for example the uniformity rule, or the we're-stuck-in-a-small-space rules).
  8. Finally, the block of the chosen size is inserted into the cblock list, and the map squares are flagged as belonging to that cblock.

3. Placing roads

This is implemented in the build_edges_and_nodes() function. This function is split into two parts; the first iterates through the cblock list, and scans each neighbouring map block to see what cblocks it touches, and creating edges for each one as appropriate.

The second part of the function adds corners to the edges. It operates by iterating through the cblock list again, and for each corner of the block, making a list of the adjacent cblocks (by examining the map for the adjoining squares). Some code makes sure all the same cblock isn't listed twice, and then the list is turned into a corner definition using the add_corners() function (Which will merely do nothing if it detects an existing corner at that location).

4. Weighting roads

This section of the algorithm is implemented in the aptly-named weight_roads_algorithm() function. Weighting is performed using an 'ants' algorithm; i.e. simulating a number of journeys through the city between random locations, as seen in Johnathan Teutenberg's Robocup Rescue city generator. As input the function takes an interation count, indicating how many journeys to simulate.

Journey simulation involves picking two random corners from the global corner list, and finding the shortest distance between them using the A* algorithm. Each stage of each journey is represented by a road_path structure, containing the a pointer to the corner to visit, a pointer to the next road_path in the list, the length of the journey so far, and a pointer to the next road_path in the global list that's used to keep track of all road_paths for memory management purposes.

To try and promote the development of highways, the straightness of a journey will have an effect on how long it takes. I.e. if the next road segment we're to drive down involves us turning a corner, add 1 to the length of the journey. Although this modification worked and was found to produce long, straight highways (as opposed to all roads being used nearly equally), it resulted in a clustering of highways around the center of the map. This is somewhat unrealistic as most city centers are too crowded to support highways; so a further tweak was applied, to increase the journey length for each section which involves travelling through the city center. This resulted in the desired effect, of forcing highways to be developed at the edge, in a ringroad style.

Once the use count for each road has been calculated, this count is converted into a road width value, using a to-be-decided algorithm.

Further areas to improve

The current algorithm has two main problems:

  1. Not all highways are connected
  2. Some highways are unrealisticly close together

Problem 1 may be fixed by performing the algorithm several times, using the results of the previous iterations to guide road development; i.e. a road which is already designated as a highway will have an increased chance of being used in the future, thus in the process helping the links between highway segments to develop. However, when converting weights to widths, care may be required to ensure that the existing highways don't throw the algorithm off and cause very little highway expansion to occur (i.e., ideally it should ignore all existing highways when deciding on the weight cutoff points). Assuming, of course, that a weight cutoff algorithm is used when deciding on the widths

Another solution would be to identify each seperate highway section, and the dead-ends of those sections, and then join the closest ends of each section together, favouring the most-used roads that run inbetween. Another alternative would be to just pick the closest points on each highway rather than the dead-ends.

Problem 2 may be solved by some magical un-weighting algorith, or perhaps by using the generation approach as proposed in the solution for problem 1.

5. Zoning the map

Proposal:

The call to the MAPGEN function will take as a parameter a list of factions which will inhabit the map. This list will be used to search the zonedef list, and find all zonedefs which spawn peds and cars belonging to (and only to?) the factions listed. This zone list will then be processed, to produce the center points of each zone. Each zone will then be grown from that center point until the whole map is zoned.

There are several possibilities for the zone centering algorithm:

  1. The simplest is to just pick random map coordinates for each zone
  2. The next level of complexity is to use some spatial data structure to plan the zones; e.g. a spiral list, which spreads the zones out from the city center in a spiral pattern. Depending on the size of each segment in the list, each zone will end up covering a different proportion of the map area.
  3. The next level of complexity would be to add locality information - so zones belonging to the same faction will be more likely to be near to each other. This could be implemented by sorting the spiral list by faction, for example (although it may result in concentric rings of factional control)
  4. An alternative is to sort the zones by distance from city center. E.g. business zones would be in the center, and industrial at the edge, with residential inbetween. Zones could even be flagged as being outside the city, for the groups of tribes/insurgent corps who guard secret entrances.
    If the list is sorted by distance from center, it may be possible to reorder each 'ring' of the list so as to increase the locality of similar factions. One method could be to simply perform random list operations, measuring the changes it makes to the overall locality of the full list, and selecting the operations that increase the locality the most. Only a few hundred thousand operations may be required to produce a good layout, something that can easily be done without adding any real time to the map generation process.

Regardless of the zone centering algorithm that is used, it is merely a small component of the map generator that can easily be tweaked and improved during development - so as a first step only the simplest implementation (#1) will be used.

The current zone growth implementation is to calculate the manchester distance of each cblock from the center of each zone, and assign the cblock to the zone that is closest. Alternative algorithms include assigning based around the distance as-the-crow-flies, or assigning based around road distance, or using a random growth system (where an unclaimed cblock next to a claimed cblock is picked at random and made to the same zone type as its neighbour), or shell-based growth (where all unclaimed cblocks surrounding a zone are made the property of that zone, for each zone in turn). It is presently unknown which algorithm is the best.

Generation of zone structs

Once each cblock has been assigned a zone, this cblock list needs to be translated to a set of zone structs, suitable for use during gameplay. The current implementation is as follows:

  1. For each zonedef, the maximum bounds of its territory is calculated
  2. The rectangle that represents those bounds is then passed to the insert_rect() function, which will attempt to insert it into the zone struct list
  3. insert_rect() is a recursive function that identifies any overlap areas between the new rectangle and any existing zones, splitting the rectangles into non-overlapping areas
  4. Once all zones have been processed by the master loop, the rectangle list is optimised, if possible (Note: Not currently performed)
  5. Finally, each zone in the zone struct list has its territory checked, and the zonedef which has the most territory (map tiles) within the bounds of that zone struct is assigned as the zonedef for that zone struct.

This algorithm should work well if there are a good number of zonedefs in use (i.e. 4+), but will work poorly if there are few (i.e. the overlap areas will be very large, resulting in large areas of territory appearing under the control of rival factions)

6. Placing buildings

Building placement is currently performed during the painting stage. The rules to govern which buildings are painted, and where, are based around the list of buildings that are spawnable in the current zone. If the building has a spawn chance, and its min max bounds fit inside the cblock, then it will be considered for placement within that block.

7. Painting onto the map

This involves taking all the data structures describing the map and painting each element into the map proper. Other structures used by the game such as traffic lights are also prepared at this time.

For details about this process, see the map painting doc.

Map generator mission script support

Mission script support in the random map generator is vital to the correct operation of missions; without this support, missions would have to resort to manual examination of the map to get the coordinates needed for mission waypoints.

The current proposal for mission script support is fairly simple:

As a parameter to the MAPGEN script command, a list of buildings will be provided. At the start of the building placement stage, this list will be examined, and all the buildings listed will be spawned in the map an appropriate number of times (i.e. if the same building is listed twice, it will be spawned two times). This building list does not affect the chance of the building spawning randomly on its own. In addition to the building list, a zone list will also be given, which will indicate which zone each building should spawn in. To simplify programming, it is expected that you are only allowed to spawn buildings which are listed in that zone's zonedef file. So if you want certain buildings to only spawn at the request of mission scripts, you must mark that building as having a 0 spawn chance.

As each building is placed, its spawn parameters will be rememberd, and passed back to the game through some structure that is yet to be designed. It is possible that this information will be made available only through script commands, e.g. GETBUILDING <n> to get the information about the Nth building that was in the MAPGEN input array. Each building is likely to have a series of properties associated with it - minimum and maximum bounds, orientation, and the locations of any special markers in the building definition file (i.e. spawn points for map objects, garage doors, stairways, etc.)

It is possible that in some situations the generator will be unable to immediately build a map which allows for all the requested buildings to be spawned. A contingency plan for this eventuality has yet to be decided; it may simply be the case that the generator will report to the mission script that certain buildings were unable to be spawned, or it may resort to a fail-safe mode where it will replan the city layout based around the given building list, or it may retry the zoning algorithm several times until success or failure is reached.

Optional buildings

It's possible that the script may want some buildings to be spawned optionally (e.g. for rare, hidden missions). In this case, to keep the map generator as simple as possible, it would make sense for the mission script to decide beforehand whether those optional buildings are to be required - and if so, add them to the list given to MAPGEN.

Generator MK 2

In recognition of a number of deficiencies in the current generator, work has begun on designing a MK 2 map generator.

Generator MK 1 problems

  1. The map is flat and featureless
  2. The generator only has scope for adding roads - no train tracks
  3. Mission script support is as-yet unimplemented (but can be added easily to current code)
  4. The road weighting algorithm sometimes generates parallel roads that overlap
  5. There is no support for terraces of buildings (?)
  6. Each cblock has the capacity to store multiple rects, but currently only one is used and/or supported
  7. Road tiles are not painted correctly
  8. No support for traffic lights
  9. Still no design for what terrain features to put in the map
  10. Few tuning variables that can be specified by the script
  11. Roads are sometimes painted beyond their bounds
  12. Intersections sometimes have holes in them

Potential solutions for implementation in generator MK 2

  1. A layering system would seem to be most appropriate. Each layer is a essentially a subsection of the main map; it has its own per-block cblock lookup array, a surface type array (to be used by some of the other planned improvements), x/y/z offset into main map, and x/y/z size.

    The map would be generated in a bottom-up fashion; once the buildings for one layer have been decided upon, the next layer above is constructed. Any blocks which intersect the buildings below will be marked as unbuildable. The road/railway/pavement planning algorithm will then know where it can and cannot place paths.

    Support pillars for raised sections will need to be placed using an intelligent algorithm - perhaps have two or three different 'air' surface types - air where a pillar doesn't need to be placed (because it's hanging onto a building), air where a pillar can be placed (because it's above an open area like grass or pavement), and air where a pillar cannot be placed (because it's above road).

    For simplicity, each layer will stay within the Z bounds of one cblock. This means that existing canvas painting code can be used, with a few simple amendments to decide how buildings should lie on slopes (shift up, shift down, or slope).

    The only open question is how to represent joins between layers - presumably this can be done directly via edges connecting corners that lie on different layers. There is also the question of deciding where to place certain features that require the use of layers, such as bridges. One approach would be to do it based solely upon the slope of the ground (i.e. place bridges in situations where sloping roads would be too steep), another would be to do it based around the weights of roads (e.g. if a heavy-use road crosses a light-use road, turn one or the other into a bridge).

  2. Train tracks will require special planning algorithms. Assuming that the majority of trains are inner-city, only a single ring of track will be required, passing through each station locations (the stations being specially flagged buildings placed in the map). The train track itself will be elevated on a seperate layer, to provide greater freedom in where it can be placed with respect to crossing road and pavement. Trains that lead between levels will be placed also; some of the stations will be flagged as being inter-city and will therefore have track generated that runs to one of the map edges. Inter-city roads are likely to be generated the same way; however it's true that the road painting algorithm will need amendments to make sure that roads and tracks are painted right up to the edge of the map.

    With regards to planning of the tracks, it's likely that an A* algorithm can be used, which looks for the quickest route to the destination while obeying rules for tightness of bends, spacing of supports, and (artificial) travel-time affects introduced by bends and slopes in the track.

  3. As stated above, there is no great technical problem preventing mission script support from being implemented. It merely needs some focus to finish off the design and get implemented.
  4. Overlapping parallel roads can be prevented by shrinking the neighbouring cblocks as the roads are increased in width, ensuring that no overlap occurs. In situations where two roads do run side by side, code could be introduced to add crash barriers between them, to at least make it look like it was intentional. Alternative solutions once intersection has been detected would be to remove one road, shrink both until a gap appears, or to merge them both into one road (tricky)
  5. This could be solved by amending the building definition system so that the neighbour surface type for each edge of the building can be specified (e.g. nothing, pavement, road, anything).
  6. Adding support for multiple rects would introduce major complexity into the code, so in the interest of simplicity this feature will be removed.
  7. This is where the above-mentioned surface type array will come in. A pre-painting phase will fill the surface type array with the surface type of each block; this will then allow the road painting algorithm to accurately do one-time detection of surrounding roads. Extra rules can also be used for painting of crossings; i.e. don't draw white lines if within range of a corner that has a 4-lane road attached.
  8. Traffic lights can be easily added to the current road planning/painting system.
  9. Although there is no design for this, the introduction of the layering system should at least allow terrain features to be added without breaking the generator.
  10. Potential tuning values so far are the min/max cblock sizes, cblock uniformity, number of ants, and max road width.
  11. This can also be fixed by use of the surface type array, to double-check that a block should be road before it is painted.
  12. This can also be fixed by use of the surface type array?

Citydef files

Due to the number of variables present in the map generator, it might make sense if a citydef file system was used. Each citydef file would list:

TODO:
CODE: MED: Write funcs to unmap and reinsert the given block/stack definitions, to allow for global block/stack changes without breaking the hash map.
CODE: QUICK: Mission script funcs to expand/implode block flags
MUST-DOCS: UNKNOWN: Discussion about seeding. Different maps each time the user plays, or mostly the same to allow them to learn the layouts?
MUST-DOCS: MED: Work out how zone structs are given names. Does each zonedef have a pool of names to select from? Or is it per map, with no real bias to the type of zone? (probably bad idea, since industrial names in residential areas wouldn't make sense) What about making attempts to only use each name once?
MUST-CODE: MED: Examine map generator, and try to make it so that it always generates the same map when given the same input seed and resources. Need to be careful about arbitration by comparing memory pointers, the order of results from calls to hmap_values(), etc.
MUST-CODE: MED: Implement mission script support, as outlined above. Also need to decide on the information passed back to the game - retaining an array of verticies for each specified building would seem like an obvious choice, along with any extra flags (orientation, whether NSEW sides have roads or paths, or whether they're blocked by a terrace of buildings, etc.)
CODE: QUICK: More building size constraints - e.g. min/max ratios of lengths of sides
MUST-CODE: UNKNOWN: Rivers - how? If river is doodled into surfmap before magic_road_algorithm(), then magic_road_algorithm() would need modifying to allow larger than usual cblocks to be constructed in order to form bridges over the river. (These may actually be multiple cblocks, in order to allow buildings to be placed on both sides of the river). The road weight-to-width algorithm may also have to be updated to convert some low-use roads into 'air' in recognition of the fact that people can't be bothered building lots of bridges.
MUST-CODE: MED: Train track support, as designed above.
MUST-CODE: MED: Terraced building support
MUST-CODE: MED: Expose tuning variables to mission script
MUST-CODE: LONG: Finish integrating layering system into map generator - handy funcs for generating layers, map painting changes to apply slopes to surfaces, ways of checking for whether blocks should have wall textures to prevent holes in the map, etc.
MUST-CODE: MED: Traffic light support; just set up around corners with 3+ roads attached. Would need to detect overlapping corners and expand the traffic lights to cover multiple corners. May be some difficulty if a 3-road corner touches a 2-road corner?
MUST-DOCS: LONG: Update this doc with details of the implemented MK 2 generator - surfmap usage, hill algorithm, new corner/edge size properties and effect on road painting, etc.
MUST-CODE: MED: More layering stuff - upper layer should examine surfmap & heightmap of lower layer, flagging each block as buildable/nonbuildable etc. Required for trains!
MUST-CODE: MED: More MK 2 tweaks - corners need surftypes
MUST-CODE: MED: Better sloping paths - need 1 block at start/finish that doesn't slope, so it can connect with other paths properly
DOCS: LONG: Generator MK 3 plans?
MUST-CODE: MED: Add surfdef to cblock, edge, corner so that hill sides, intersections, etc. can be painted more accurately. Would this allow us to get rid of cbmap inheritance? Do we need to get rid of cbmap inheritance?
MUST-CODE: MED: Examine current generator for overdraw. Design new that eliminates it?
MUST-CODE: MED: Fix buildings-over-intersections fix!
MUST-DOCS: LONG: Ideas for how to restructure map generator to provide guaranteed mission building placement. cblocks -> zoning -> mission buildings? Or zoning -> cblocks+mission buildings? Would also need to decide how to deal with road widths, since if selecting mission building locations before weighting roads, the road weighting will result in altered cblock sizes. Maybe start with minimum-size cblocks for selection, and then upgrade to larger (better-fit) ones where possible prior to painting?
Best course of action would probably be to rank the buildings by how many potential spawn locations they have, so that the ones with the least possibilities can be spawned first. An algorithm with backtracking could also be a possibility, so that it can try all combinations until it succeeds or fails; however with large numbers of buildings/spawn locations this could take far too long to run.
zoning->cblocks+mission buildings approach: Could deliberately insert cblocks into the map that are the correct size for the mission buildings. However this may result in mission buildings clustering together at the start of each zone, as it would be almost impossible to get them to spawn in truly random locations. A variant on this idea would be to set up the cblocks and then resize some of them until the buildings fit; but this may give an artificial appearance to the map, where it's clear that the flow of buildings has been modified just to fit certain ones in. Maybe a hybrid approach would be best - use a probability function that gives right-size cblocks a good chance of spawning (at the usual locations where cblock sizes change), followed by selecting similar-size cblocks if possible (i.e. putting borders at the edge), followed by size manipulation of remaining cblocks to fit the last few buildings. Zone manipulation could also be relied upon if needed, if a zone runs out of space.
Flocking approach(?): Spawn all the buildings as a series of points; then, apply migration logic to the points so that they organise themselves by zone, all the while trying to keep equal distances from each other. Then somehow build those locations into the cblock map? Could use different cblock planning algorithm that grows out from the mission buildings? Would that look too artificial? Main aim of the algorithm would be to ensure that zones are actually large enough to contain all the buildings they need to.
Guaranteed zoning ideas:

  1. Calculate (min? max? average?) area each zone needs to be in order to spawn the mission buildings
  2. For non-mission zones, apply the area of the smallest mission zone? Or differing areas depending on the spawn chance of each zone? (If spawn chances of zones are implemented)
  3. Lay out initial zone locations as non-overlapping rectangles/spheres
  4. Space the zones out a bit so the resultant sizes are more random

Variations/notes:

DOCS: LONG: Find way of describing custom buildings that can come in a variety of sizes - just a case of marking certain blocks as being able to repeat in X/Y/Z direction?
MUST-CODE: MED: Fix buildings being selected and then not spawning - need the building_fits() function updating to take into account pavement size (or building painting code to not subtract pavement size)
MUST-CODE: QUICK: Tweak track planner to discourage placement above roads - just increase path weight for each road block it's ontop of? Or disallow placement if previous N blocks have been above road?
MUST-CODE: MED: Shrink track_path struct? Use cbma for allocations, reduce vecints to chars, etc.
MUST-CODE: MED: Tweak corner placement - can possibly place 4-size and then upgrade to 5-size where possible just before painting?
MUST-CODE: LONG: Fix crashing when doing some paths
MUST-CODE: MED: Code for intercity paths - just need a weighting func that measures how far it is to the closest map edge?
MUST-CODE: MED: Better corner painting decisions - examine the pattern to see which tiles are used
MUST-CODE: MED: Fix straight section block rot flags
MUST-CODE: MED: All other map painting funcs need to take into account patloc rot as well as tilepat rot?
MUST-CODE: MED: Can remove the mandatory +1-per-section weight?
MUST-CODE: MED: Make it so that minimum corner width comes from a definition file somewhere (map generator parameters?)
MUST-CODE: MED: Zone specification in mission buildings: MK2.5-ish generator will need to decide quite early on which zone a building is going in, and then remember that zone? So maybe if the user passes a null zone name, we should feed him back the name of the actual zone it spawned in? It's probably going to be a useful piece of information to have.
MUST-CODE: MED: Fix train track planner to not try to connect paths that belong to the same station (i.e. if a building definition contains multiple platforms)
MUST-CODE: MED: Track planner sometimes places lots of corners one after another, causing snaking track; need to discourage this (e.g. weight to add when placing a corner is affected by whether the previous piece was also a corner, or if it was a straight, how long it was?)
MUST-CODE: MED: Since corners have differing weights to straights, is it possible that a straight that is far away could get ignored in place of a corner that is closer, resulting in more corners than needed? Corner weight tweaks should hopefully make this impossible.
???
Profit!