Both sides previous revisionPrevious revisionNext revision | Previous revision |
cw4:4rpl_tools [2024/09/10 21:41] – Updated manipulateUnitPosition and added SetTerrainHigherThan20 Vertu | cw4:4rpl_tools [2025/06/30 20:12] (current) – [Pseudo Random Number Generator, based on sinus] kalli |
---|
endonce | endonce |
endif | endif |
| </code> |
| |
| </hidden> |
| |
| ---- |
| |
| |
| ===== Request Viable Build- or Move-Cell ===== |
| |
| <hidden click here for source code> |
| |
| This script allows for other scripts to SEND A MESSAGE TO REQUEST A VIABLE BUILD- OR MOVE-CELL. |
| |
| The script is a compromise between high performance and high fidelity. The reason that it's setup to run as a separate script and not as a function, is so that it can take move/build requests from multiple sources into account: by default it won't recheck cells that were recently rejected + new searches will continue where recent previous queries stopped after finding a solution. A very high performance version exists for 3x3 units in my SQUADS map. |
| |
| |
| How to use in a cpack: |
| Run the script as a global script in Pre. Do not run while paused. |
| |
| |
| Use example for building miners: |
| |
| |
| <code 4rpl example.4rpl>500 0 do |
| "requestWxWviableCell" 0 5 3 128 128 v2 256 0 0 0 0 1 0 1 1 0 14 listN sendmsg # Length > width = x direction. |
| <-*WxWviableCellFound if |
| "miner" <-*WxWviableCell ev2 0 createunitonterrain dup 1 SetUnitOccupiesLand 999 constructunit |
| # Unlike existing units that move, newly build units do not instantly occupy the cells that they were created on, so we force a refresh of that with SetUnitOccupiesLand. |
| endif |
| loop |
| |
| 500 0 do |
| "requestWxWviableCell" 0 3 5 128 128 v2 256 0 0 0 0 1 0 1 1 0 14 listN sendmsg # Length > width = z direction. With Z-direction we also need to change the orientation below. |
| <-*WxWviableCellFound if |
| "miner" <-*WxWviableCell ev2 0 createunitonterrain dup dup 1 setunitorientation 1 SetUnitOccupiesLand 999 constructunit |
| endif |
| loop</code> |
| |
| INPUT variables explained: |
| |
| <-moveUID: can be left 0, but it's needed if you want to allow a unit to check if it's current location is viable. |
| |
| <-unitLength: footprint length in X direction. |
| |
| <-unitWidth: footprint length in Z direction. |
| |
| <-cellV2: V2 vector. The cell where your requesting script wants to build/move the unit. |
| |
| <-searchRange: the cell distance around the requested cell location, where the function will look for a viable build/move cell. |
| |
| <-maxCreeperDepth: this will check the average creeper depth in a circle with range 4. |
| |
| <-noMesh: set to 1 to not allow solutions with ACTIVE mesh. |
| |
| <-likePlatform: the unit can be placed in the void. |
| |
| <-likeBeacon: the unit can be placed in the void and on uneven terrain. |
| |
| <-likeMiner: the unit can only be placed on resource special terrain that covers atleast 75% of the unit footprint. |
| |
| <-likeNullifier: the unit has to be placed within nullification range of an enemy creeper. The creeper enemey has to be known to a list inside the function script. |
| |
| <-ignoreCooldown: set to 1 to ignore time based computation optimizations. Best left at 0 when the function is called by an AI script (more optimized) and set to 1 when it's initiated by the player (feels more responsive). |
| |
| <-noSkip: set to 1 for max placement coverage, but slower computations. If left at 0, then the script will not check cells that are likely to not have a proper solution, but it will also mistakenly not check some cells that do have a proper solution. For spamming 3x3 units, "1" tends to be 40% slower, while only placing about 10% more units. |
| |
| <-resetSearch: if set to 1, then all previous search history will be forgotten. |
| |
| |
| |
| <code 4rpl LxW-FindViableCell-NRP-Pre.4rpl> |
| # --LxW-FindViableCell-NRP-Pre-- |
| |
| # INPUTS: "requestWxWviableCell" <-moveUID <-unitLength <-unitWidth <-cellV2 <-searchRange <-maxCreeperDepth <-noMesh <-likePlatform <-likeBeacon <-likeMiner <-likeNullifier <-ignoreCooldown <-noSkip <-resetSearch 14 listN |
| # OUTPUT: global varialbles named <-*WxWviableCellFound <-*WxWviableCell |
| |
| # Contains snapping tool movecell requests, so that multiple units can request a viable cell from a single source, without double checking cells. |
| # BASED ON SNAPPING TOOL CODE, but without the Indicator unit or the Mouse commands, and only for SQUARE units. |
| |
| $radiusLandingSpot:4 # INTEGER. The creeper threshhold will be checked as an average over more than the unit landing location. |
| $cellsWithinLandingRange:69 # INTEGER, see previous radius. The creeper threshhold will be checked as an average over more than the unit landing location. |
| |
| $$defaultSearchRange:20 # INTEGER. The floodfill distance. If no searchRange is send in the message, this is the range that will be used. |
| |
| $groupDist:7 # Integer or float distance. Requests that are closer together than this distance, will be grouped together, so that later searches can continue on from previous searches. |
| $blockCellTimer:90 # INTEGER. Amount of frames under which queries will be grouped together & searches around cells that were found to be blocked, will not be done again. |
| $clearCellTimer:270 # INTEGER. Amount of frames after which the blocked cells will be forgotten. In between these 2 values, searches will only be done again if there are less units around than when the cell was last found to be blocked. |
| |
| $$contSlot:8 # VALUE EDITOR SETTING. Units cannot be placed on this terrain slot. 8 is the default contaminant slot. |
| $$resoSlot:1 # VALUE EDITOR SETTING. Miners can only be placed on resource terrain. 1 is the default resource slot. |
| $$meshSlot:6 # VALUE EDITOR SETTING. Units that can be placed in the void, cannot be placed on mesh inside the void. 6 is the default mesh slot. |
| |
| $$nullifyUnits:" emitter; airsaccauldron; blobnest; darktower; skimmerfactory; sporelauncher" # Enemy units that would be suppressed by building a nullifier near them. |
| |
| :requestWxWviableCellFunction |
| <-_DATA[0] 0 setunitoccupiesland # With this line, an existing unit's current cell will be a possible result from the search for a viable cell. |
| <-_DATA ListToStack @searchWxWviableCell ->*WxWviableCellFound ->*WxWviableCell |
| <-_DATA[0] 1 setunitoccupiesland |
| |
| :once |
| # listening channels: |
| "requestWxWviableCell" "requestWxWviableCellFunction" registerformsg |
| 1 ->resetSearch |
| 1 ->ignoreCooldown |
| |
| # When the loop was ran through fully without finding a resolution, remember that cell for 90 frames before trying the same cell again. |
| list ->blockedCellV2List |
| list ->blockedCellTimeList |
| list ->nearbyUnitsCountList |
| |
| <-nullifyUnits removewhitespace ";" split ->suppressList |
| |
| :searchWxWviableCell # Output: viableCell as v2 + true/false |
| # See vanilla snapping tool cpack for full commments. |
| ->resetSearch |
| ->noSkip |
| ->ignoreCooldown |
| ->checkN |
| ->checkR |
| ->placeA |
| ->placeV |
| ->noMesh |
| ->creDepth |
| ->searchRange |
| ->cell |
| ->width |
| ->length |
| pop # The UID has no further use. |
| |
| # The following switch checks if the search should start from scratch (=1), or if it should continue (=0) on from a previous search. |
| switch |
| <-resetSearch case 1 endcase |
| <-length <-prevLength neq case 1 endcase |
| <-width <-prevWidth neq case 1 endcase |
| <-checkN <-prevCheckN neq case 1 endcase |
| <-checkR <-prevCheckR neq case 1 endcase |
| <-placeA <-prevPlaceA neq case 1 endcase |
| <-placeV <-prevPlaceV neq case 1 endcase |
| <-cell ev2 <-prevSearchCell ev2 distancecell <-groupDist gt if 1 else |
| <-prevSearchCell ->cell # Group the search query together with previous closeby queries. |
| <-ignoreCooldown if getgametickcount else getgameupdatecount endif dup <-prevSearchUpdateCount gt if |
| ->prevSearchUpdateCount |
| 1 |
| else |
| pop |
| 0 |
| endif |
| endif |
| endswitch if # If the search starts from scratch, then the new settings have to be remembered for the next function call. |
| # Adjust the footprint array depending on the requested width in the function call. |
| <-width dup <-prevWidth neq if |
| dup ->prevWidth |
| 1 add 2.0 div asint dup ->rHz 1 sub ->rLz # For the footprint loop. |
| else pop endif |
| <-length dup <-prevLength neq if |
| dup ->prevLength |
| 1 add 2.0 div asint dup ->rHx 1 sub ->rLx # For the footprint loop. |
| else pop endif |
| |
| <-checkN ->prevCheckN |
| <-checkR ->prevCheckR |
| <-placeA ->prevPlaceA |
| <-placeV ->prevPlaceV |
| <-cell ->prevSearchCell |
| |
| # Recreate the area to search: |
| <-searchRange 1 sub dup dup mul swap add 2 mul 1 add ->floodRange |
| <-cell ev2 0 21 <-floodRange floodfillterrain dup getlistcount ->potentialCellCount ->potentialCells |
| 0 ->startLoop |
| 0 ->skip |
| |
| # If the search is being reset, then clear the blocked lists. |
| <-resetSearch If |
| 1 ->ignoreCooldown |
| <-blockedCellV2List clearlist |
| <-blockedCellTimeList clearlist |
| <-nearbyUnitsCountList clearlist |
| endif |
| else |
| # If the search continues on from before, but the search range was changed, then the potential cells will need to be adjusted, without adjusting the start of the loop. |
| <-searchRange dup <-prevSearchRange neq if |
| dup ->prevSearchRange 1 sub dup dup mul swap add 2 mul 1 add ->floodRange |
| <-cell ev2 0 21 <-floodRange floodfillterrain dup getlistcount ->potentialCellCount ->potentialCells |
| else pop endif |
| endif |
| |
| # If the new searchRange is larger than the old one, then do not check if the cell was recently found to be blocked. |
| <-searchRange <-prevSearchRange gt <-ignoreCooldown OR not if |
| # Check if the cell is not on the list of blocked cells. |
| <-blockedCellV2List <-cell getlistindex dup -1 eq if pop else |
| ->index |
| |
| getgameupdatecount ->updateCount |
| # Try to purge a few old blocked cells from the lists, otherwise the list bloats. |
| -1 <-index 1 sub do |
| <-blockedCellTimeList[i] <-clearCellTimer add <-updateCount lt if |
| <-blockedCellV2List i removelistelement |
| <-blockedCellTimeList i removelistelement |
| <-nearbyUnitsCountList <-index removelistelement |
| <-index 1 sub ->index |
| endif |
| loop |
| |
| switch |
| # The last check on this spot was less than 3 seconds ago, so we're not checking it again now. |
| <-blockedCellTimeList[<-index] <-blockCellTimer add <-updateCount gt case |
| -1 -1 v2 false return |
| endcase |
| # The last check on this spot was longer than 9 seconds ago, so we can check it again now. |
| <-blockedCellTimeList[<-index] <-clearCellTimer add <-updateCount lt case |
| <-blockedCellV2List <-index removelistelement |
| <-blockedCellTimeList <-index removelistelement |
| <-nearbyUnitsCountList <-index removelistelement |
| endcase |
| # Check the cell again if there are now less units nearby than when it was blocked. |
| <-cell ev2 fromcell 13 0 0 0 0 0 0 getunitsinrange getlistcount <-nearbyUnitsCountList[<-index] lt case |
| <-blockedCellV2List <-index removelistelement |
| <-blockedCellTimeList <-index removelistelement |
| <-nearbyUnitsCountList <-index removelistelement |
| endcase |
| # Too recent and still the same amount of units nearby, so don't check again: |
| -1 -1 v2 false return |
| endswitch |
| endif |
| endif |
| |
| <-potentialCellCount <-startLoop do |
| <-skip if |
| <-skip 1 sub ->skip |
| else |
| <-potentialCells[i] ev2 @checkCenterCell if |
| @checkFootPrint if |
| i <-noSkip if 1 else <-width endif add ->startLoop # Optimization for mass placement. When placing the next unit, skip the next few potential cells. |
| <-potentialCells[i] true return # LEFT ON STACK. |
| endif |
| else |
| <-noSkip if 0 else <-width 3 div asint 1 add endif ->skip # If the center cell was blocked, then also skip the next X potential cells. |
| endif |
| endif |
| loop |
| <-potentialCellCount ->startLoop |
| # If the loop was not interrupted by return, then all cells were blocked in some way. |
| <-blockedCellV2List <-cell appendtolist |
| <-blockedCellTimeList getgameupdatecount appendtolist |
| <-nearbyUnitsCountList <-cell ev2 fromcell 13 0 0 0 0 0 0 getunitsinrange getlistcount appendtolist |
| -1 -1 v2 false # LEFT ON STACK. |
| |
| :checkCenterCell # Inputs: x and z of cell. |
| dup2 <-radiusLandingSpot 0 1 0 getcreeperinrange <-cellsWithinLandingRange div <-creDepth gt if pop pop false return endif |
| dup2 getterrain dup ->height if |
| <-placeV if pop pop false return endif |
| 0 ->checkO |
| else |
| <-placeV <-placeA OR if 0 else 1 endif ->checkO |
| endif |
| <-checkR if 0 ->minePot endif |
| # dup2 is still on the stack. |
| switch |
| dup2 getcelloccupiedcount <-checkO neq case pop pop false return endcase |
| # dup2 getterrain <-height neq case pop pop false return endcase # Useless on center cell. |
| # dup2 getcreeper <-creDepth gt case pop pop false return endcase |
| dup2 getmeshhealth gt0 <-noMesh AND case pop pop false return endcase |
| dup2 getterraformmarker gt0 case pop pop false return endcase |
| dup2 getterrainspecial dup ->special <-contSlot eq case pop pop false return endcase |
| <-special <-meshSlot eq <-height 0 eq AND case pop pop false return endcase # Units cannot be placed on mesh in the void. |
| ->cZ ->cX # Used in checkFootPrint. |
| <-checkR if <-special <-resoSlot eq if |
| <-minePot 1 add ->minePot |
| else |
| pop pop false return |
| endif endif |
| <-checkO if |
| "platform" <-cX 0 <-cZ v3 2.9 0 0 0 2 1 0 getunits 0 getlistelement |
| getunitcell <-cX <-cZ distancecell 2.828427 approximately not if pop pop false return endif |
| endif |
| <-checkN if |
| <-suppressList <-cX 0 <-cZ v3 9 0 0 0 1 0 1 getunitsinrange 0 getlistelement getunittype listcontains not if pop pop false return endif |
| endif |
| endswitch |
| true |
| |
| :checkFootPrint |
| # See vanilla snapping tool cpack for full commments. |
| |
| <-cZ <-rHz add <-cZ <-rLz sub do |
| <-cX <-rHx add <-cX <-rLx sub do |
| switch |
| i j |
| # dup2 <-cZ eq swap <-cX eq AND case pop pop endcase # The center cell was already checked and OK. |
| dup2 getcelloccupiedcount <-checkO neq case pop pop false return endcase |
| dup2 getterrain <-height neq case pop pop false return endcase |
| # dup2 getcreeper <-creDepth gt case pop pop false return endcase |
| dup2 getmeshhealth gt0 <-noMesh AND case pop pop false return endcase |
| dup2 getterraformmarker gt0 case pop pop false return endcase |
| dup2 getterrainspecial dup ->special <-contSlot eq case pop pop false return endcase |
| # dup2 getterrainspecial <-meshSlot eq <-height 0 eq AND case pop pop false return endcase # Prevents platforms from being placed on mesh in the void. Not relevant for this script. |
| <-special <-meshSlot eq <-height 0 eq AND case pop pop false return endcase # Units cannot be placed on mesh in the void. |
| <-placeA case pop pop endcase |
| pop pop |
| <-checkR if <-special <-resoSlot eq if <-minePot 1 add ->minePot endif endif |
| endswitch |
| loop |
| loop |
| <-checkR if <-minePot <-length <-width mul 0.75 mul lt if false return endif endif |
| true |
| |
| </code> |
| |
| </hidden> |
| |
| ---- |
| |
| |
| ===== Pseudo Random Number Generator, based on sinus ===== |
| |
| <hidden click here for source code> |
| |
| Several basic functions based on Grabz' rng lite function of "<-seed sinus 10000 mul dup floor sub". |
| |
| |
| Copy the functions directly into your script or run the below script in your cpack and have other scripts send messages to request a randfloat or randint. |
| |
| |
| Difference between the functions: |
| * "seeded": provide a seed to the generator. The generated number will always be the same for the same seed. |
| * "index": these seeds are sequenced. In the same map for the same index, the generated number 1,2,3,... will be the same after map restart. |
| * "spiked": the generator is seeded with the elapsedtime of the gaming session, making the outcome uncontrollable. |
| * No prefix: these are sequenced seeds and they use index 0. |
| |
| Example code with messages: |
| <code example.4rpl> |
| 12345 ->int |
| 1 ->index |
| 0 ->first |
| 100 ->last # The last integer itself will be excluded. |
| "sinRandInt" <-first <-last 2 listN sendmsg |
| <-*sinRandInt trace |
| "sinRand01" 0 sendmsg |
| <-*sinRand01 trace |
| "indexSinRandInt" <-index <-first <-last 3 listN sendmsg |
| <-*sinRandInt trace |
| "indexSinRand01" <-index sendmsg |
| <-*sinRand01 trace |
| "spikedSinRandInt" <-first <-last 2 listN sendmsg |
| <-*sinRandInt trace |
| "spikedSinRand01" 0 sendmsg |
| <-*sinRand01 trace |
| "seededSinRandInt" <-seed <-first <-last 3 listN sendmsg |
| <-*sinRandInt trace |
| "seededSinRand01" <-seed sendmsg |
| <-*sinRand01 trace |
| </code> |
| |
| ---- |
| |
| <code SinusRNG.4rpl> |
| :once |
| # Creating lists and a starting mapconstant for the seed sequences. |
| createlist ->prevSeedList |
| createlist ->seedCountList |
| getmapsize 2 div swap 2 div swap dup2 getterrain 1 add dup 99999 floodfillterrain getlistcount ->mapConstant |
| |
| # Setting up the messages so that other scripts can request a random seed. |
| "sinRandInt" "sinRandInt_CALL" registerformsg |
| "sinRand01" "sinRand01_CALL" registerformsg |
| "indexSinRandInt" "indexSinRandInt_CALL" registerformsg |
| "indexSinRand01" "indexSinRand01_CALL" registerformsg |
| "spikedSinRandInt" "spikedSinRandInt_CALL" registerformsg |
| "spikedSinRand01" "spikedSinRand01_CALL" registerformsg |
| "seededSinRandInt" "seededSinRandInt_CALL" registerformsg |
| "seededSinRand01" "seededSinRand01_CALL" registerformsg |
| |
| :sinRandInt_CALL |
| <-_DATA listtostack @sinRandInt ->*sinRandInt |
| :sinRand01_CALL |
| @sinRand01 ->*sinRand01 |
| :indexSinRandInt_CALL |
| <-_DATA listtostack @indexSinRandInt ->*sinRandInt |
| :indexSinRand01_CALL |
| <-_DATA @indexSinRand01 ->*sinRand01 |
| :spikedSinRandInt_CALL |
| <-_DATA listtostack @spikedSinRandInt ->*sinRandInt |
| :spikedSinRand01_CALL |
| @spikedSinRand01 ->*sinRand01 |
| :seededSinRandInt_CALL |
| <-_DATA listtostack @seededSinRandInt ->*sinRandInt |
| :seededSinRand01_CALL |
| <-_DATA @seededSinRand01 ->*sinRand01 |
| |
| :sinRandInt # INPUT: integer first randInt + integer last randInt. OUTPUT: an integer in between the first and last randInt, excluding the last randInt. |
| ->last |
| ->first |
| 0 @indexSinRand01 <-last <-first sub mul <-first add asint |
| |
| :sinRand01 # INPUT: none. |
| 0 @indexSinRand01 |
| |
| :indexSinRandInt # INPUT: the index of the seed sequence + integer first randInt + integer last randInt. OUTPUT: an integer in between the first and last randInt, excluding the last randInt. |
| ->last |
| ->first |
| @indexSinRand01 <-last <-first sub mul <-first add asint |
| |
| :indexSinRand01 # INPUT: the "index" of the seed sequence. OUTPUT: the next seed from that sequence. |
| # The random numbers will be generated with the previous seed that is stored under that index. |
| ->i |
| <-prevSeedList[<-i] eq0 if |
| <-i <-mapConstant add 2357 mul @seededSinRand01 dup 10000000 mul 1 add asint ->prevSeedList[<-i] |
| 1 ->seedCountList[<-i] |
| else |
| <-prevSeedList[<-i] @seededSinRand01 dup 10000000 mul <-seedCountList[<-i] 1 add dup ->seedCountList[<-i] add asint ->prevSeedList[<-i] |
| endif |
| |
| :spikedSinRandInt # INPUT: integer first randInt + integer last randInt. OUTPUT: an integer in between the first and last randInt, excluding the last randInt. |
| ->last |
| ->first |
| elapsedtime asint @seededSinRand01 <-last <-first sub mul <-first add asint |
| |
| :spikedSinRand01 |
| elapsedtime asint @seededSinRand01 |
| |
| :seededSinRandInt # INPUT: integer seed + integer first randInt + integer last randInt. OUTPUT: an integer in between the first and last randInt, excluding the last randInt. |
| ->last |
| ->first |
| @seededSinRand01 <-last <-first sub mul <-first add asint |
| |
| :seededSinRand01 |
| # abs <-power pow <-add add sin <-sinMul mul dup floor sub |
| 1.05 pow 99419 sub sin 619 mul dup floor sub |
</code> | </code> |
| |