Files
bettergi-scripts-list/repo/js/AbundantOre/main.js
2025-09-01 14:34:01 +08:00

525 lines
20 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
function forge_pathing_start_log(name) {
const t = new Date();
const timestamp = t.toTimeString().slice(0, 8) + "." + String(t.getMilliseconds()).padStart(3, "0");
var c = "Forging start log\n\n";
c += `[${timestamp}] [INF] BetterGenshinImpact.Service.ScriptService\n------------------------------\n\n`;
c += `[${timestamp}] [INF] BetterGenshinImpact.Service.ScriptService\n→ 开始执行地图追踪任务: "${name}"`;
log.debug(c);
}
function forge_pathing_end_log(name, elapsed_time) {
const elapsed_min = Math.floor(elapsed_time / 1000 / 60);
const elapsed_sec = (elapsed_time / 1000 % 60).toFixed(3);
const t = new Date();
const timestamp = t.toTimeString().slice(0, 8) + "." + String(t.getMilliseconds()).padStart(3, "0");
var c = "Forging end log\n\n";
c += `[${timestamp}] [INF] BetterGenshinImpact.Service.ScriptService\n→ 脚本执行结束: "${name}", 耗时: ${elapsed_min}${elapsed_sec}\n\n`;
c += `[${timestamp}] [INF] BetterGenshinImpact.Service.ScriptService\n------------------------------`;
log.debug(c);
}
function get_exclude_tags() {
var tags = [];
if (settings.exclude_fights) {
tags.push("fight");
}
if (settings.exclude_natlan) {
tags.push("natlan");
}
if (settings.exclude_fontaine_underwater) {
tags.push("fontaine underwater");
}
if (settings.exclude_fontaine_terrestrial) {
tags.push("fontaine terrestrial");
}
if (settings.exclude_sumeru) {
tags.push("sumeru");
}
if (settings.exclude_inazuma) {
tags.push("inazuma");
}
if (settings.exclude_liyue) {
tags.push("liyue");
}
if (settings.exclude_chasm_underground) {
tags.push("chasm underground");
}
if (settings.exclude_mondstadt) {
tags.push("mondstadt");
}
if (settings.exclude_crystal_chunks) {
tags.push("crystal chunk");
}
if (settings.exclude_condessence_crystals) {
tags.push("condessence crystal");
}
if (settings.exclude_amethyst_lumps) {
tags.push("amethyst lump");
}
return tags;
}
function get_profile_name() {
if (!settings.profile_id) {
return null;
}
return settings.profile_id;
}
const filename_to_path_map = {};
function load_filename_to_path_map() {
var all_paths = [];
const read_dir = (path) => {
for (const i of file.readPathSync(path)) {
if (file.isFolder(i)) {
read_dir(i);
} else {
all_paths.push(i);
}
}
};
read_dir("assets/矿物");
for (const i of all_paths) {
const filename = i.replace(/^.*[\\/]/, "");
filename_to_path_map[filename] = i;
}
}
var persistent_data = {};
const in_memory_skip_tasks = new Set();
function load_persistent_data() {
var file_content = "";
try {
file_content = file.readTextSync("local/persistent_data.json");
} catch (error) {}
if (file_content.length !== 0) {
persistent_data = JSON.parse(file_content);
}
}
const disabled_paths = new Set();
function load_disabled_paths() {
for (const path of ["assets/disabled_paths.conf", "local/disabled_paths.txt"]) {
var file_content = "";
try {
file_content = file.readTextSync(path);
} catch (error) {}
for (var l of file_content.split("\n")) {
l = l.trim();
if (l.length === 0) {
continue;
}
if (l.startsWith("//") || l.startsWith("#")) {
continue;
}
disabled_paths.add(l);
}
}
}
var statistics = {};
function load_statistics_data() {
statistics = JSON.parse(file.readTextSync("assets/statistics.json")).data;
}
async function flush_persistent_data() {
await file.writeText("local/persistent_data.json", JSON.stringify(persistent_data, null, " "));
}
async function mark_task_finished(task_name) {
const profile_name = get_profile_name();
const profile_key = !profile_name ? "default-profile" : ("profile-" + profile_name);
if (!persistent_data.hasOwnProperty(profile_key)) {
persistent_data[profile_key] = {};
}
persistent_data[profile_key][task_name] = {
"last_run_time": Date.now(),
};
await flush_persistent_data();
}
function get_task_last_run_time(task_name) {
const profile_name = get_profile_name();
const profile_key = !profile_name ? "default-profile" : ("profile-" + profile_name);
return persistent_data[profile_key]?.[task_name]?.last_run_time || 0;
}
function is_ore_respawned(t) {
t /= 1000;
var t0 = Math.floor(t / 86400) * 86400 + 57600;
if (t0 > t) {
t0 -= 86400;
}
const respawn_time = t0 + 86400 * 3;
return respawn_time < Date.now() / 1000;
}
function get_some_tasks(hints) {
hints.target_running_seconds = Math.min(7200, hints.target_running_seconds || Number.MAX_VALUE);
if (hints.target_yield) {
log.debug("Schedule with target yield {a}", hints.target_yield);
}
if (hints.target_running_seconds) {
log.debug("Schedule with target runnning seconds {a}", hints.target_running_seconds);
}
const exclude_tags = new Set(get_exclude_tags());
var filtered_statistics = [];
for (const [key, value] of Object.entries(statistics)) {
if (in_memory_skip_tasks.has(key)) {
continue;
}
if (disabled_paths.has(key)) {
continue;
}
if (value.tags.some(i => exclude_tags.has(i))) {
continue;
}
if (value.statistics.avg_num_defeats > 0) {
continue;
}
if (value.statistics.avg_abnormal_exits > 0) {
continue;
}
if (!filename_to_path_map.hasOwnProperty(key)) {
continue;
}
if (!is_ore_respawned(get_task_last_run_time(key))) {
log.debug("{name} not respawned, skip", key);
continue;
}
value.statistics.avg_yield_per_min = value.statistics.avg_yield / value.statistics.avg_time_consumed * 60;
filtered_statistics.push([key, value]);
}
filtered_statistics.sort((a, b) =>
b[1].statistics.avg_yield_per_min - a[1].statistics.avg_yield_per_min
);
var candidates = [];
var sum_yield = 0;
var sum_running_seconds = 0;
for (const [key, value] of filtered_statistics) {
candidates.push([key, value]);
sum_yield += value.statistics.avg_yield;
sum_running_seconds += value.statistics.avg_time_consumed;
if (hints.target_yield !== null && sum_yield >= hints.target_yield) {
break;
}
if (hints.target_running_seconds !== null && sum_running_seconds >= hints.target_running_seconds) {
break;
}
}
const candidate_groups = {};
for (const [key, value] of candidates) {
const group_name = value.group;
if (!candidate_groups.hasOwnProperty(group_name)) {
candidate_groups[group_name] = {
sum_yield: 0,
sum_running_seconds: 0,
tasks: [],
};
}
candidate_groups[group_name].tasks.push(key);
candidate_groups[group_name].sum_yield += value.statistics.avg_yield;
candidate_groups[group_name].sum_running_seconds += value.statistics.avg_time_consumed;
}
for (const i of Object.values(candidate_groups)) {
i.avg_yield_per_min = sum_yield / sum_running_seconds * 60;
i.tasks.sort();
}
const tasks = Array.from(Object.values(candidate_groups)).sort((a, b) => b.avg_yield_per_min - a.avg_yield_per_min).map(i => i.tasks).flat();
var log_content = "";
sum_yield = 0;
sum_running_seconds = 0;
for (const i of tasks) {
const s = statistics[i]
log_content += ` ${s.statistics.avg_yield_per_min.toFixed(2)} ${i}\n`;
sum_yield += s.statistics.avg_yield;
sum_running_seconds += s.statistics.avg_time_consumed;
}
log.debug(log_content);
log.debug("Expected yield {a}, time {b} min", sum_yield, sum_running_seconds / 60);
return tasks.map(i => [i, statistics[i]]);
}
async function get_inventory() {
const ore_image_map = {
amethyst_lumps: "assets/images/amethyst_lump.png",
crystal_chunks: "assets/images/crystal_chunk.png",
condessence_crystals: "assets/images/condessence_crystal.png",
};
await genshin.returnMainUi();
keyPress("b")
await sleep(1000);
click(964, 53);
await sleep(500);
const game_region = captureGameRegion();
const inventory_result = {
crystal_chunks: 0,
condessence_crystals: 0,
amethyst_lumps: 0
};
for (const [name, path] of Object.entries(ore_image_map)) {
let match_obj = RecognitionObject.TemplateMatch(file.ReadImageMatSync(path));
match_obj.threshold = 0.85;
match_obj.Use3Channels = true;
const match_res = game_region.Find(match_obj);
if (match_res.isExist()) {
log.debug(`Found ${name} image at (${match_res.x}, ${match_res.y})`);
const text_x = match_res.x - 0;
const text_y = match_res.y + 120;
const text_w = 120;
const text_h = 40;
const ocr_res = game_region.find(RecognitionObject.ocr(text_x, text_y, text_w, text_h));
if (ocr_res) {
inventory_result[name] = Number(ocr_res.text);
}
}
}
await genshin.returnMainUi();
return inventory_result;
}
async function run_pathing_script(name, path_state_change, current_states) {
path_state_change ||= {};
path_state_change.require ||= [];
path_state_change.add ||= [];
path_state_change.sustain ||= [];
for (const s of path_state_change.require) {
if (!current_states.has(s)) {
log.debug("Trying to get {s}", s);
for (const [name, data] of Object.entries(statistics)) {
const add_states = data.state_change?.add || [];
if (add_states.includes(s)) {
await run_pathing_script(name, data.state_change, current_states);
break;
}
}
}
}
log.info("运行 {name}", name);
var json_content = await file.readText(filename_to_path_map[name]);
{
// set Noelle mining action
const json_obj = JSON.parse(json_content);
var modified = false;
for (const i of json_obj.positions) {
if (i.action === "mining" && !i.action_params) {
i.action = "combat_script";
i.action_params = "诺艾尔 attack(2.0)";
modified = true;
}
}
// scale underwater mining actions
if (settings.enable_dpi_scaling && statistics[name].tags.includes("fontaine underwater") && genshin.screenDpiScale !== 1.0) {
for (const i of json_obj.positions) {
if (i.action_params) {
const new_actions = [];
for (const a of i.action_params.split(";")) {
if (a.startsWith("moveby(")) {
const [x, y] = a.slice(7, -1).split(",");
const new_val = "moveby(" + String(Math.round(x * genshin.screenDpiScale)) + "," + String(Math.round(y * genshin.screenDpiScale)) + ")";
new_actions.push(new_val);
} else {
new_actions.push(a);
}
}
const new_action_params = new_actions.join(";");
if (new_action_params !== i.action_params) {
i.action_params = new_action_params;
modified = true;
}
}
}
}
if (modified) {
log.debug("Patched mining action");
json_content = JSON.stringify(json_obj);
}
}
const cancellation_token = dispatcher.getLinkedCancellationToken();
const t0 = Date.now();
forge_pathing_start_log(name);
await pathingScript.run(json_content);
const elapsed_time = Date.now() - t0;
forge_pathing_end_log(name, elapsed_time);
if (!cancellation_token.isCancellationRequested) {
if (elapsed_time > 5000) {
await mark_task_finished(name);
} else {
in_memory_skip_tasks.add(name);
log.warn("脚本运行时间小于5秒可能发生了错误不写记录");
}
} else {
throw new Error("Cancelled");
}
const new_states = current_states.intersection(new Set(path_state_change.sustain)).union(new Set(path_state_change.add));
current_states.clear();
for (const s of new_states) {
current_states.add(s);
}
}
async function main() {
await genshin.returnMainUi();
load_filename_to_path_map();
load_persistent_data();
load_disabled_paths();
load_statistics_data();
dispatcher.addTimer(new RealtimeTimer("AutoPick"));
// Run an empty pathing script to give BGI a chance to switch team if the user specifies one.
await pathingScript.runFile("assets/empty_pathing.json");
if (["natlan", "fontaine terrestrial", "sumeru", "inazuma", "liyue", "chasm underground", "mondstadt"].filter(i => !get_exclude_tags().includes(i)).length > 0) {
if (!Array.from(getAvatars()).includes("诺艾尔")) {
log.error("地面挖矿必须带诺艾尔");
return;
}
}
const get_current_cst_hour = () => (Date.now() / 1000 + 8 * 3600) % 86400 / 3600;
var run_until_unix_time = settings.target_running_minutes ? (Date.now() + Number(settings.target_running_minutes) * 60 * 1000) : null;
if (settings.time_range) {
const time_range = settings.time_range.replace("", "~").replace("", ":");
if (time_range.includes("~")) {
const [
[start_h, start_m],
[end_h, end_m]
] = time_range.split("~").map(i => i.split(":").map(Number));
const start_time = start_h + start_m / 60;
const end_time = end_h + end_m / 60;
const current_time = get_current_cst_hour();
if (start_time < end_time && !(current_time >= start_time && current_time < end_time)) {
// format like 01:30~03:50
log.info("不在允许运行的时间内,退出");
return;
}
if (start_time > end_time && current_time < start_time && current_time > end_time) {
// format like 23:30~4:00
log.info("不在允许运行的时间内,退出");
return;
}
const run_until_unix_time2 = ((end_time - current_time + 24) % 24) * 3600 * 1000 + Date.now();
run_until_unix_time = Math.min(run_until_unix_time2, run_until_unix_time || Number.MAX_VALUE);
} else {
// format like 03:50
const [end_h, end_m] = time_range.split(":").map(Number);
const end_time = end_h + end_m / 60;
const run_until_unix_time2 = (end_time - get_current_cst_hour() + 24) % 24 * 3600 * 1000 + Date.now();
run_until_unix_time = Math.min(run_until_unix_time2, run_until_unix_time || Number.MAX_VALUE);
}
}
const original_inventory = await get_inventory();
log.info("已有水晶块{a}个,紫晶块{b}个,萃凝晶{c}个", original_inventory.crystal_chunks, original_inventory.amethyst_lumps, original_inventory.condessence_crystals);
const target_yield = settings.target_amount ? Math.ceil(Number(settings.target_amount)) : null;
if (target_yield && !run_until_unix_time) {
log.info("将挖矿{a}个", target_yield);
} else if (!target_yield && run_until_unix_time) {
const running_minutes = Math.round((run_until_unix_time - Date.now()) / 60 / 1000);
log.info("将挖矿{a}分钟", running_minutes);
} else if (target_yield && run_until_unix_time) {
const running_minutes = Math.round((run_until_unix_time - Date.now()) / 60 / 1000);
log.info("将挖矿{a}个或{b}分钟,任何一个先发生", target_yield, running_minutes);
} else {
log.info("将持续挖矿");
}
const start_time = Date.now();
var accurate_yield = 0;
var estimated_yield = 0;
var cached_inventory_data = original_inventory;
var finished = false;
const current_states = new Set();
while (!finished) {
const hints = {
target_yield: target_yield ? (target_yield - estimated_yield + 5) : null,
target_running_seconds: run_until_unix_time ? (run_until_unix_time - Date.now()) / 1000 : null,
};
{
const now = Math.floor(Date.now() / 1000);
var next_refresh_unix_time = Math.floor(now / 86400) * 86400 + 16 * 3600;
next_refresh_unix_time += 180; // to avoid the effect of clock skew
if (next_refresh_unix_time < now) {
next_refresh_unix_time += 86400;
}
if (!(hints.target_yield || hints.target_running_seconds)) {
hints.target_yield = 500;
}
const target_running_seconds2 = next_refresh_unix_time - now;
if (!hints.target_running_seconds || target_running_seconds2 < hints.target_running_seconds) {
hints.target_running_seconds = target_running_seconds2;
}
}
const tasks = get_some_tasks(hints);
if (tasks.length === 0) {
log.info("没有更多任务可运行,退出");
finished = true;
} else {
log.debug("Running {num} tasks as a group", tasks.length);
}
for (const [name, data] of tasks) {
cached_inventory_data = null;
try {
await run_pathing_script(name, data.state_change, current_states);
} catch (e) {
finished = true;
break;
}
estimated_yield += data.statistics.avg_yield;
if (target_yield !== null && estimated_yield >= target_yield + 5) {
const current_inventory = await get_inventory();
cached_inventory_data = current_inventory;
accurate_yield += current_inventory.crystal_chunks - original_inventory.crystal_chunks;
accurate_yield += current_inventory.condessence_crystals - original_inventory.condessence_crystals;
accurate_yield += current_inventory.amethyst_lumps - original_inventory.amethyst_lumps;
estimated_yield = accurate_yield;
}
if (target_yield !== null && accurate_yield >= target_yield) {
finished = true;
break;
}
if (run_until_unix_time !== null && Date.now() >= run_until_unix_time) {
finished = true;
break;
}
}
}
const end_time = Date.now();
const running_minutes = (end_time - start_time) / 1000 / 60;
var total_yield_str = [];
const latest_inventory = cached_inventory_data ? cached_inventory_data : (await get_inventory());
if (latest_inventory.crystal_chunks - original_inventory.crystal_chunks) {
total_yield_str.push(`${latest_inventory.crystal_chunks - original_inventory.crystal_chunks}水晶块`);
}
if (latest_inventory.condessence_crystals - original_inventory.condessence_crystals) {
total_yield_str.push(`${latest_inventory.condessence_crystals - original_inventory.condessence_crystals}萃凝晶`);
}
if (latest_inventory.amethyst_lumps - original_inventory.amethyst_lumps) {
total_yield_str.push(`${latest_inventory.amethyst_lumps - original_inventory.amethyst_lumps}紫晶块`);
}
if (total_yield_str.length > 0) {
total_yield_str = "收获" + total_yield_str.join("");
} else {
total_yield_str = "无收获";
}
log.info("现有水晶块{a}个,紫晶块{b}个,萃凝晶{c}个", latest_inventory.crystal_chunks, latest_inventory.amethyst_lumps, latest_inventory.condessence_crystals);
log.info("运行{m}分钟,{y}", running_minutes.toFixed(2), total_yield_str);
}
(async function() {
await main();
})();