├── .poggit.yml ├── plugin.yml └── src └── muqsit └── chunkgenerator └── Loader.php /.poggit.yml: -------------------------------------------------------------------------------- 1 | --- # Poggit-CI Manifest. Open the CI at https://poggit.pmmp.io/ci/Muqsit/ChunkGenerator 2 | build-by-default: true 3 | branches: 4 | - master 5 | projects: 6 | ChunkGenerator: 7 | path: "" 8 | libs: 9 | - src: SOF3/await-generator/await-generator 10 | version: ^3.6.1 11 | ... 12 | -------------------------------------------------------------------------------- /plugin.yml: -------------------------------------------------------------------------------- 1 | name: ChunkGenerator 2 | main: muqsit\chunkgenerator\Loader 3 | api: 5.0.0 4 | version: 0.0.4 5 | commands: 6 | cgen: 7 | usage: /cgen [n_batch] 8 | permission: chunkgenerator.command 9 | permissions: 10 | chunkgenerator.command: 11 | default: op -------------------------------------------------------------------------------- /src/muqsit/chunkgenerator/Loader.php: -------------------------------------------------------------------------------- 1 | 35 | */ 36 | private function betweenPoints(int $x1, int $x2, int $z1, int $z2) : Generator{ 37 | $x2 >= $x1 || throw new InvalidArgumentException("x2 ({$x2}) must be >= x1 ({$x1})"); 38 | $z2 >= $z1 || throw new InvalidArgumentException("z2 ({$z2}) must be >= z1 ({$z1})"); 39 | 40 | $k = 3; 41 | $min_x_mod = ($x1 % $k + $k) % $k; 42 | $min_z_mod = ($z1 % $k + $k) % $k; 43 | for($offset_x = 0; $offset_x < $k; $offset_x++){ 44 | for($offset_z = 0; $offset_z < $k; $offset_z++){ 45 | $x_start = $x1 + (($offset_x - $min_x_mod + $k) % $k); 46 | $z_start = $z1 + (($offset_z - $min_z_mod + $k) % $k); 47 | for($z = $z_start; $z <= $z2; $z += $k){ 48 | for($x = $x_start; $x <= $x2; $x += $k){ 49 | yield [$x, $z]; 50 | } 51 | } 52 | } 53 | } 54 | } 55 | 56 | /** 57 | * @param World $world 58 | * @param int $x1 59 | * @param int $x2 60 | * @param int $z1 61 | * @param int $z2 62 | * @param int|null $n_batch 63 | * @return Generator 64 | */ 65 | public function generateChunksBetween(World $world, int $x1, int $x2, int $z1, int $z2, ?int $n_batch = null) : Generator{ 66 | $points = $this->betweenPoints($x1, $x2, $z1, $z2); 67 | $count = (1 + ($x2 - $x1)) * (1 + ($z2 - $z1)); 68 | return $this->generateChunks($world, $points, $count, $n_batch); 69 | } 70 | 71 | /** 72 | * @param World $world 73 | * @param Iterator $points 74 | * @param int $count 75 | * @param int|null $n_batch 76 | * @return Generator 77 | */ 78 | public function generateChunks(World $world, Iterator $points, int $count, ?int $n_batch = null) : Generator{ 79 | $n_batch ??= $this->getServer()->getConfigGroup()->getPropertyInt(YmlServerProperties::CHUNK_GENERATION_POPULATION_QUEUE_SIZE, 2); 80 | $n_batch > 0 || throw new InvalidArgumentException("n_batch ({$n_batch}) must be > 0"); 81 | $loader = new class implements ChunkLoader{}; 82 | 83 | $channel = new Channel(); 84 | $schedule = static fn(int $x, int $z) => $world->orderChunkPopulation($x, $z, $loader)->onCompletion( 85 | fn($c) => $channel->sendWithoutWait([$x, $z, true]), 86 | fn() => $channel->sendWithoutWait([$x, $z, false]), 87 | ); 88 | 89 | $i = 0; 90 | while($i < $n_batch && $points->valid()){ 91 | [$x, $z] = $points->current(); 92 | $schedule($x, $z); 93 | $points->next(); 94 | $i++; 95 | } 96 | 97 | $manager = $this->getServer()->getWorldManager(); 98 | $id = $world->getId(); 99 | $completed = 0; 100 | $failed = []; 101 | $retry = true; 102 | while($i > 0){ 103 | [$x, $z, $success] = yield from $channel->receive(); 104 | $i--; 105 | 106 | $world->unregisterChunkLoader($loader, $x, $z); 107 | if($success){ 108 | $completed++; 109 | }else{ 110 | $failed[] = [$x, $z]; 111 | } 112 | 113 | if($points->valid() && $manager->getWorld($id) !== null /* equivalent of $world->isLoaded() but we cant rely on that method here */){ 114 | [$x, $z] = $points->current(); 115 | $schedule($x, $z); 116 | $points->next(); 117 | $i++; 118 | } 119 | 120 | yield [$completed, count($failed), $count] => Traverser::VALUE; 121 | if($i === 0 && $retry && count($failed) > 0){ // retry failed requests once before we quit 122 | $retry = false; 123 | $points = new ArrayIterator($failed); 124 | $failed = []; 125 | } 126 | } 127 | } 128 | 129 | /** 130 | * @param World $world 131 | * @param Generator $task 132 | * @param CommandSender $sender 133 | * @return Generator 134 | */ 135 | public function generateAndReport(World $world, Generator $task, CommandSender $sender) : Generator{ 136 | $world_name = $world->getFolderName(); 137 | $operation = new Traverser($task); 138 | $message = null; 139 | $progress = null; 140 | $states = [0]; 141 | $last_pct = -0.01; 142 | while(($state = array_shift($states)) !== null){ 143 | if($state === 0){ // state=initialization - check for errors 144 | try{ 145 | yield from $operation->next($progress); 146 | array_push($states, 3, 1, 2); 147 | }catch(InvalidArgumentException $e){ 148 | $message = $e->getMessage(); 149 | $states[] = 1; 150 | } 151 | }elseif($state === 1){ // state=send message 152 | $message ?? throw new RuntimeException("Requested state without setting a message"); 153 | if($message === ""){ 154 | $message = null; 155 | continue; 156 | } 157 | $sender = $sender instanceof Player && $sender->isConnected() ? $sender : null; 158 | if($sender !== null){ 159 | $sender->sendTip($message); 160 | }else{ 161 | $this->getLogger()->info($message); 162 | } 163 | $message = null; 164 | }elseif($state === 2){ // state=perform chunk generation 165 | if(yield from $operation->next($progress)){ 166 | array_push($states, 3, 1, 2); 167 | }else{ 168 | $message = "Generation completed."; 169 | $states[] = 1; 170 | } 171 | }elseif($state === 3){ // state=create progress message 172 | [$completed, $failed, $count] = $progress; 173 | $pct = ($completed / $count) * 100; 174 | if($pct - $last_pct >= 0.01){ 175 | $last_pct = $pct; 176 | $message = "{$world_name}: {$completed} / {$count} succeeded [" . sprintf("%.2f", ($completed / $count) * 100) . "%], {$failed} failed"; 177 | }else{ 178 | $message = ""; 179 | } 180 | }else{ 181 | throw new RuntimeException("Unexpected state ({$state}) encountered"); 182 | } 183 | } 184 | } 185 | 186 | public function onCommand(CommandSender $sender, Command $command, string $label, array $args) : bool{ 187 | if(!isset($args[0])){ 188 | return false; 189 | } 190 | 191 | $world_name = array_shift($args); 192 | $world = $this->getServer()->getWorldManager()->getWorldByName($world_name); 193 | if($world === null){ 194 | $sender->sendMessage("No world with the folder name \"{$world_name}\" found."); 195 | return false; 196 | } 197 | 198 | $int_args = []; 199 | foreach($args as $arg){ 200 | if(is_numeric($arg)){ 201 | $int_args[] = (int) $arg; 202 | }else{ 203 | return false; 204 | } 205 | } 206 | 207 | if(count($int_args) < 4){ 208 | return false; 209 | } 210 | 211 | [$x1, $z1, $x2, $z2] = $int_args; 212 | $n_batch = $int_args[4] ?? null; 213 | $task = $this->generateChunksBetween($world, $x1, $x2, $z1, $z2, $n_batch); 214 | $sender->sendMessage("Generating..."); 215 | Await::g2c($this->generateAndReport($world, $task, $sender)); 216 | return true; 217 | } 218 | } --------------------------------------------------------------------------------