diff --git a/.github/deploy/rtx/dev.json b/.github/deploy/rtx/dev.json new file mode 100644 index 000000000..fd9ce6ef0 --- /dev/null +++ b/.github/deploy/rtx/dev.json @@ -0,0 +1,12 @@ +{ + "args": [ + "--dev", + "--map-server", + "map-gen-2:5000", + "--seed-repository-url", + "s3:map-rando-seeds", + "--video-storage-url", + "https://map-rando-videos.b-cdn.net", + "--test-map-server" + ] +} diff --git a/.github/scripts/create_image_metadata.py b/.github/scripts/create_image_metadata.py new file mode 100644 index 000000000..a07e9d5fe --- /dev/null +++ b/.github/scripts/create_image_metadata.py @@ -0,0 +1,35 @@ +import json +import os +from pathlib import Path + + +def main(): + config_root = Path(".github/deploy") + deploy_configs = { + path.relative_to(config_root).with_suffix("").as_posix(): json.loads(path.read_text()) + for path in sorted(config_root.glob("*/*.json")) + } + if not deploy_configs: + raise ValueError("No deployment configurations found in .github/deploy/*/*.json") + metadata = { + "digest": os.environ["IMAGE_DIGEST"], + "commit": os.environ["GITHUB_SHA"], + "run_id": int(os.environ["GITHUB_RUN_ID"]), + "run_attempt": int(os.environ["GITHUB_RUN_ATTEMPT"]), + "deploy_configs": deploy_configs, + } + Path("image-metadata.json").write_text(json.dumps(metadata, indent=2) + "\n") + repository_url = f"{os.environ['GITHUB_SERVER_URL']}/{os.environ['GITHUB_REPOSITORY']}" + summary = ( + "### Published image\n\n" + f"- Image: `docker.io/bkerby/map-rando@{metadata['digest']}`\n" + f"- Commit: [{metadata['commit']}]({repository_url}/commit/{metadata['commit']})\n" + f"- Build: [run {metadata['run_id']}, attempt {metadata['run_attempt']}]" + f"({repository_url}/actions/runs/{metadata['run_id']}/attempts/{metadata['run_attempt']})\n" + ) + with Path(os.environ["GITHUB_STEP_SUMMARY"]).open("a") as summary_file: + summary_file.write(summary) + + +if __name__ == "__main__": + main() diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2a5746378..a306c068b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -65,6 +65,9 @@ jobs: env: DOCKER_TAG: ${{ github.ref == 'refs/heads/main' && 'latest' || github.ref_name }} steps: + - + name: Check out deployment configurations + uses: actions/checkout@v4 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v2 @@ -91,21 +94,7 @@ jobs: name: Write image metadata env: IMAGE_DIGEST: ${{ steps.build.outputs.digest }} - run: | - jq -n \ - --arg digest "$IMAGE_DIGEST" \ - --arg commit "$GITHUB_SHA" \ - --argjson run_id "$GITHUB_RUN_ID" \ - --argjson run_attempt "$GITHUB_RUN_ATTEMPT" \ - '{digest: $digest, commit: $commit, run_id: $run_id, run_attempt: $run_attempt}' \ - > image-metadata.json - - printf '### Published image\n\n- Image: `docker.io/bkerby/map-rando@%s`\n- Commit: [%s](%s/%s/commit/%s)\n- Build: [run %s, attempt %s](%s/%s/actions/runs/%s/attempts/%s)\n' \ - "$IMAGE_DIGEST" \ - "$GITHUB_SHA" "$GITHUB_SERVER_URL" "$GITHUB_REPOSITORY" "$GITHUB_SHA" \ - "$GITHUB_RUN_ID" "$GITHUB_RUN_ATTEMPT" \ - "$GITHUB_SERVER_URL" "$GITHUB_REPOSITORY" "$GITHUB_RUN_ID" "$GITHUB_RUN_ATTEMPT" \ - >> "$GITHUB_STEP_SUMMARY" + run: python3 .github/scripts/create_image_metadata.py - name: Upload image metadata uses: actions/upload-artifact@v7 diff --git a/.github/workflows/deploy-dev.yml b/.github/workflows/deploy-dev.yml index 7eb63ce51..f75f33d3a 100644 --- a/.github/workflows/deploy-dev.yml +++ b/.github/workflows/deploy-dev.yml @@ -35,6 +35,10 @@ jobs: const fs = require('node:fs/promises'); const run = context.payload.workflow_run; const metadata = JSON.parse(await fs.readFile('image-metadata/image-metadata.json', 'utf8')); + const deployConfig = metadata.deploy_configs['rtx/dev']; + if (deployConfig === undefined) { + throw new Error('Build metadata is missing .github/deploy/rtx/dev.json'); + } // Rerunning this publisher creates a new request, including to retry a failed rollout. const response = await github.rest.repos.createDeployment({ ...context.repo, @@ -48,6 +52,7 @@ jobs: description: `CI run ${run.id}, attempt ${run.run_attempt}`, payload: { digest: metadata.digest, + deploy_config: deployConfig, build_run_id: run.id, build_run_attempt: run.run_attempt } diff --git a/.github/workflows/deploy-experimental.yml b/.github/workflows/deploy-experimental.yml index 4f75c0781..474075397 100644 --- a/.github/workflows/deploy-experimental.yml +++ b/.github/workflows/deploy-experimental.yml @@ -35,6 +35,10 @@ jobs: const fs = require('node:fs/promises'); const run = context.payload.workflow_run; const metadata = JSON.parse(await fs.readFile('image-metadata/image-metadata.json', 'utf8')); + const deployConfig = metadata.deploy_configs['rtx/experimental']; + if (deployConfig === undefined) { + throw new Error('Build metadata is missing .github/deploy/rtx/experimental.json'); + } // Rerunning this publisher creates a new request, including to retry a failed rollout. const response = await github.rest.repos.createDeployment({ ...context.repo, @@ -48,6 +52,7 @@ jobs: description: `Experimental CI run ${run.id}, attempt ${run.run_attempt}`, payload: { digest: metadata.digest, + deploy_config: deployConfig, build_run_id: run.id, build_run_attempt: run.run_attempt } diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 6d6c75e89..d70853586 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -1787,6 +1787,7 @@ dependencies = [ "parse_int", "pathfinding", "rand 0.8.5", + "reqwest", "serde", "serde-xml-rs", "serde_derive", @@ -2502,6 +2503,7 @@ checksum = "43e734407157c3c2034e0258f5e4473ddb361b1e85f95a66690d67264d7cd1da" dependencies = [ "base64", "bytes", + "futures-channel", "futures-core", "futures-util", "h2 0.4.10", diff --git a/rust/data/presets/full-settings/Community Race Season 5.json b/rust/data/presets/full-settings/Community Race Season 5.json index c17023f44..28d19a6ba 100644 --- a/rust/data/presets/full-settings/Community Race Season 5.json +++ b/rust/data/presets/full-settings/Community Race Season 5.json @@ -4904,7 +4904,7 @@ 0.21052631578947367 ] }, - "reward_missing_connect_utility": 1.0, + "reward_missing_connect_utility": 2.0, "reward_area_x": 10.0, "reward_area_y": 10.0, "target_area_rooms": [ diff --git a/rust/data/presets/full-settings/Default.json b/rust/data/presets/full-settings/Default.json index 8e9ac0d22..991854e2e 100644 --- a/rust/data/presets/full-settings/Default.json +++ b/rust/data/presets/full-settings/Default.json @@ -4820,7 +4820,8 @@ 0.7 ] }, - "reward_missing_connect_utility": 1.0, + "reward_missing_connect_utility": 2.0, + "max_return_distance": 13.0, "reward_area_x": 10.0, "reward_area_y": 10.0, "target_area_rooms": [ diff --git a/rust/data/presets/full-settings/Mentor Tournament.json b/rust/data/presets/full-settings/Mentor Tournament.json index 28c7d4ab1..1615e97b8 100644 --- a/rust/data/presets/full-settings/Mentor Tournament.json +++ b/rust/data/presets/full-settings/Mentor Tournament.json @@ -4904,7 +4904,7 @@ 0.21052631578947367 ] }, - "reward_missing_connect_utility": 1.0, + "reward_missing_connect_utility": 2.0, "reward_area_x": 10.0, "reward_area_y": 10.0, "target_area_rooms": [ diff --git a/rust/data/presets/full-settings/Summer Series Expert Challenge.json b/rust/data/presets/full-settings/Summer Series Expert Challenge.json index e97f98fdb..9d5c5a312 100644 --- a/rust/data/presets/full-settings/Summer Series Expert Challenge.json +++ b/rust/data/presets/full-settings/Summer Series Expert Challenge.json @@ -4816,7 +4816,7 @@ 0.21052631578947367 ] }, - "reward_missing_connect_utility": 1.0, + "reward_missing_connect_utility": 2.0, "reward_area_x": 10.0, "reward_area_y": 10.0, "target_area_rooms": [ diff --git a/rust/maprando-game/src/lib.rs b/rust/maprando-game/src/lib.rs index ae5bb301b..118b9c2d0 100644 --- a/rust/maprando-game/src/lib.rs +++ b/rust/maprando-game/src/lib.rs @@ -3107,7 +3107,7 @@ impl GameData { .notable_map .unwrap() .get(¬able_name) - .context(format!("Undefined notable: {}", ¬able_name))?; + .context(format!("Undefined notable: {}", notable_name))?; return Ok(Requirement::Notable(notable_idx)); } else if key == "heatFramesWithEnergyDrops" { let frames = self.parse_numeric(&value["frames"]).unwrap_or_else(|e| { diff --git a/rust/maprando-web/src/main.rs b/rust/maprando-web/src/main.rs index 3aae6a80c..c42427e21 100644 --- a/rust/maprando-web/src/main.rs +++ b/rust/maprando-web/src/main.rs @@ -18,7 +18,7 @@ use actix_web::{ use askama::Template; use clap::Parser; use hashbrown::HashMap; -use log::{error, info}; +use log::{error, info, warn}; use rand::{RngCore, SeedableRng}; use serde_derive::{Deserialize, Serialize}; use serde_variant::to_variant_name; @@ -71,7 +71,7 @@ struct Args { port: u16, #[arg(long)] map_server: Option, - /// Send a test generation request during startup. + /// Test each full-settings preset's map source and request fresh-map refills during startup. #[arg(long, requires = "map_server")] test_map_server: bool, } @@ -149,12 +149,10 @@ fn build_app_data() -> AppData { let vanilla_map_repository = OfflineMapRepository::new(vec![("Vanilla", vanilla_map_path)]).unwrap(); + let mut vanilla_settings = preset_data.default_preset.clone(); + vanilla_settings.map_settings = MapSettings::from_preset(MapPreset::Vanilla, false); let vanilla_map = vanilla_map_repository - .get_map_batch( - 0, - &MapSettings::from_preset(MapPreset::Vanilla, false), - &game_data, - ) + .get_map_batch(0, &vanilla_settings, &game_data) .unwrap()[0] .clone(); let map_repository: Box = Box::new(SelectableMapRepository { @@ -165,27 +163,32 @@ fn build_app_data() -> AppData { ("Wild", wild_maps_path), ]) .unwrap(), - fresh: args.map_server.as_ref().map(|server| { - let repository = HttpMapRepository::new(server).unwrap(); - if args.test_map_server { - info!("Sending test generation request to map server {server}"); - let start = Instant::now(); - let maps = repository - .get_map_batch( - 0, - &MapSettings::from_preset(MapPreset::Standard, true), - &game_data, - ) - .expect("Map server test request failed"); - info!( - "Map server test request succeeded: {} maps in {:.3}s", - maps.len(), - start.elapsed().as_secs_f32() - ); - } - repository - }), + fresh: args + .map_server + .as_ref() + .map(|server| HttpMapRepository::new(server, &preset_data.skill_presets).unwrap()), }); + if args.test_map_server { + for settings in &preset_data.full_presets { + let name = settings.name.as_deref().unwrap(); + info!( + "Testing map source {:?} for {name}", + settings.map_settings.source + ); + let start = Instant::now(); + let maps = map_repository + .get_map_batch(0, settings, &game_data) + .unwrap_or_else(|e| panic!("Map source test failed for {name}: {e:#}")); + map_repository + .request_refill(settings) + .unwrap_or_else(|e| panic!("Map server refill failed for {name}: {e:#}")); + info!( + "Map source test succeeded for {name}: {} maps in {:.3}s", + maps.len(), + start.elapsed().as_secs_f32() + ); + } + } let logic_data = LogicData::new( &game_data, @@ -547,7 +550,6 @@ struct AttemptOutput { #[derive(Debug)] enum AttemptError { - TooManyAttempts, TimedOut, MapGeneration(anyhow::Error), } @@ -589,31 +591,33 @@ fn handle_randomize_request( app_data.game_data.vertex_isv.keys.len(), 0, ); - let max_attempts = 2000; - let attempts_timeout = Duration::from_secs(25); + let attempts_timeout = Duration::from_secs(60); let max_attempts_per_map = if settings.start_location_settings.mode == StartLocationMode::Random { - 10 + 100 } else { - 1 + 10 }; - let max_map_attempts = max_attempts / max_attempts_per_map; info!( - "Random seed={random_seed}, max_attempts_per_map={max_attempts_per_map}, max_map_attempts={max_map_attempts}, difficulty={:?}", + "Random seed={random_seed}, max_attempts_per_map={max_attempts_per_map}, difficulty={:?}", difficulty_tiers[0] ); let time_start_attempts = Instant::now(); let mut attempt_num = 0; + let mut map_batch_attempt = 0; let mut map_batch: Vec = vec![]; - for _ in 0..max_map_attempts { + loop { let map_seed = (rng.next_u64() & 0xFFFFFFFF) as usize; - let door_randomization_seed = (rng.next_u64() & 0xFFFFFFFF) as usize; if map_batch.is_empty() { + if time_start_attempts.elapsed() > attempts_timeout { + return Err(AttemptError::TimedOut); + } + map_batch_attempt += 1; map_batch = app_data .map_repository - .get_map_batch(map_seed, &settings.map_settings, &app_data.game_data) + .get_map_batch(map_seed, &settings, &app_data.game_data) .map_err(AttemptError::MapGeneration)?; } @@ -623,44 +627,44 @@ fn handle_randomize_request( continue; } let objectives = get_objectives(&settings, Some(&map), &app_data.game_data, &mut rng); - let locked_door_data = randomize_doors( - &app_data.game_data, - &map, - &settings, - &objectives, - door_randomization_seed, - ); - let randomizer = Randomizer::new( - &map, - &locked_door_data, - objectives.clone(), - &settings, - &difficulty_tiers, - &app_data.game_data, - &filtered_base_links_data, - &mut rng, - ); for _ in 0..max_attempts_per_map { + let door_randomization_seed = (rng.next_u64() & 0xFFFFFFFF) as usize; + let locked_door_data = randomize_doors( + &app_data.game_data, + &map, + &settings, + &objectives, + door_randomization_seed, + ); + let randomizer = Randomizer::new( + &map, + &locked_door_data, + objectives.clone(), + &settings, + &difficulty_tiers, + &app_data.game_data, + &filtered_base_links_data, + &mut rng, + ); let item_placement_seed = (rng.next_u64() & 0xFFFFFFFF) as usize; attempt_num += 1; info!( - "Attempt {attempt_num}/{max_attempts}: Map seed={map_seed}, door randomization seed={door_randomization_seed}, item placement seed={item_placement_seed}" + "Attempt {attempt_num}, map batch {map_batch_attempt}: Map seed={map_seed}, door randomization seed={door_randomization_seed}, item placement seed={item_placement_seed}" ); let randomization_result = randomizer.randomize(attempt_num, item_placement_seed, display_seed, true); let (randomization, spoiler_log) = match randomization_result { Ok(x) => x, Err(e) => { - info!("Attempt {attempt_num}/{max_attempts}: Randomization failed: {e}"); - if time_start_attempts.elapsed() > attempts_timeout { - return Err(AttemptError::TimedOut); - } + info!( + "Attempt {attempt_num}, map batch {map_batch_attempt}: Randomization failed: {e}" + ); continue; } }; info!( - "Successful attempt {attempt_num}/{attempt_num}/{max_attempts}: display_seed={}, random_seed={random_seed}, map_seed={map_seed}, door_randomization_seed={door_randomization_seed}, item_placement_seed={item_placement_seed}", + "Successful attempt {attempt_num}, map batch {map_batch_attempt}: display_seed={}, random_seed={random_seed}, map_seed={map_seed}, door_randomization_seed={door_randomization_seed}, item_placement_seed={item_placement_seed}", randomization.display_seed, ); @@ -680,7 +684,6 @@ fn handle_randomize_request( return output_result; } } - Err(AttemptError::TooManyAttempts) } #[post("/randomize")] @@ -750,10 +753,6 @@ async fn randomize( return HttpResponse::InternalServerError() .body("Failed too many randomization attempts (timeout reached)"); } - Err(AttemptError::TooManyAttempts) => { - return HttpResponse::InternalServerError() - .body("Failed too many randomization attempts (maximum attempt count reached)"); - } }; let timestamp = match SystemTime::now().duration_since(SystemTime::UNIX_EPOCH) { @@ -860,6 +859,18 @@ async fn randomize( .await .unwrap(); + let refill_app = app_data.clone(); + let refill_settings = settings.clone(); + match actix_web::rt::task::spawn_blocking(move || { + refill_app.map_repository.request_refill(&refill_settings) + }) + .await + { + Ok(Ok(())) => {} + Ok(Err(e)) => warn!("Map server refill notification failed: {e:#}"), + Err(e) => warn!("Map server refill notification task failed: {e}"), + } + HttpResponse::Ok().json(RandomizeResponse { seed_url: format!("/seed/{seed_name}/"), seed_hash: get_seed_hash(output.randomization.display_seed), @@ -967,7 +978,11 @@ async fn main() { env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")) .format_timestamp_millis() .init(); - let app_data = actix_web::web::Data::new(build_app_data()); + let app_data = actix_web::web::Data::new( + actix_web::rt::task::spawn_blocking(build_app_data) + .await + .unwrap(), + ); let port = app_data.port; diff --git a/rust/maprando-web/templates/generate/map_settings.html b/rust/maprando-web/templates/generate/map_settings.html index 96edcaac1..5a6ed5f88 100644 --- a/rust/maprando-web/templates/generate/map_settings.html +++ b/rust/maprando-web/templates/generate/map_settings.html @@ -499,6 +499,14 @@

Return paths