diff --git a/Cargo.toml b/Cargo.toml index 70b19cde..356beb93 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -52,3 +52,11 @@ winres = "0.1" lto = true panic = "abort" strip = true + +[[bin]] +name = "yas_scanner" +path = "src/main.rs" + +[[bin]] +name = "yas_scanner_starrail" +path = "src/main_starrail.rs" \ No newline at end of file diff --git a/models/index_2_word_starrail.json b/models/index_2_word_starrail.json new file mode 100644 index 00000000..4ba5d4b8 --- /dev/null +++ b/models/index_2_word_starrail.json @@ -0,0 +1,371 @@ +{ + "0": "-", + "1": " ", + "2": "%", + "3": "'", + "4": "+", + "5": ",", + "6": ".", + "7": "/", + "8": "0", + "9": "1", + "10": "2", + "11": "3", + "12": "4", + "13": "5", + "14": "6", + "15": "7", + "16": "8", + "17": "9", + "18": "「", + "19": "」", + "20": "七", + "21": "三", + "22": "丝", + "23": "中", + "24": "丹", + "25": "义", + "26": "之", + "27": "乐", + "28": "云", + "29": "亚", + "30": "人", + "31": "仙", + "32": "伊", + "33": "伤", + "34": "伯", + "35": "佩", + "36": "使", + "37": "信", + "38": "修", + "39": "值", + "40": "假", + "41": "偏", + "42": "停", + "43": "儿", + "44": "元", + "45": "光", + "46": "克", + "47": "兜", + "48": "全", + "49": "公", + "50": "兰", + "51": "军", + "52": "冠", + "53": "冥", + "54": "冰", + "55": "击", + "56": "刃", + "57": "利", + "58": "制", + "59": "刹", + "60": "刺", + "61": "力", + "62": "加", + "63": "动", + "64": "包", + "65": "匠", + "66": "千", + "67": "博", + "68": "卡", + "69": "卢", + "70": "卫", + "71": "卿", + "72": "历", + "73": "变", + "74": "司", + "75": "合", + "76": "吸", + "77": "呼", + "78": "命", + "79": "喙", + "80": "器", + "81": "围", + "82": "图", + "83": "土", + "84": "圣", + "85": "场", + "86": "坼", + "87": "垒", + "88": "城", + "89": "域", + "90": "堡", + "91": "塔", + "92": "墨", + "93": "士", + "94": "壳", + "95": "备", + "96": "复", + "97": "外", + "98": "天", + "99": "头", + "100": "套", + "101": "妮", + "102": "妲", + "103": "姬", + "104": "娅", + "105": "娜", + "106": "子", + "107": "孔", + "108": "存", + "109": "客", + "110": "害", + "111": "宽", + "112": "密", + "113": "射", + "114": "小", + "115": "尔", + "116": "属", + "117": "履", + "118": "岛", + "119": "岸", + "120": "巡", + "121": "巧", + "122": "巨", + "123": "已", + "124": "市", + "125": "布", + "126": "希", + "127": "帕", + "128": "带", + "129": "帽", + "130": "序", + "131": "废", + "132": "度", + "133": "建", + "134": "引", + "135": "弧", + "136": "彦", + "137": "御", + "138": "德", + "139": "快", + "140": "性", + "141": "怪", + "142": "总", + "143": "恒", + "144": "恕", + "145": "恢", + "146": "息", + "147": "感", + "148": "成", + "149": "戒", + "150": "战", + "151": "手", + "152": "才", + "153": "承", + "154": "抗", + "155": "护", + "156": "披", + "157": "抵", + "158": "拉", + "159": "拳", + "160": "指", + "161": "挎", + "162": "捕", + "163": "提", + "164": "攻", + "165": "效", + "166": "数", + "167": "旧", + "168": "明", + "169": "昏", + "170": "易", + "171": "星", + "172": "春", + "173": "晨", + "174": "景", + "175": "暴", + "176": "月", + "177": "服", + "178": "木", + "179": "机", + "180": "束", + "181": "杰", + "182": "板", + "183": "构", + "184": "果", + "185": "枝", + "186": "枪", + "187": "格", + "188": "桑", + "189": "械", + "190": "楼", + "191": "步", + "192": "残", + "193": "毡", + "194": "沉", + "195": "治", + "196": "泰", + "197": "洛", + "198": "洲", + "199": "流", + "200": "浮", + "201": "海", + "202": "深", + "203": "游", + "204": "演", + "205": "漠", + "206": "漫", + "207": "潜", + "208": "火", + "209": "炮", + "210": "点", + "211": "烈", + "212": "燃", + "213": "爪", + "214": "物", + "215": "特", + "216": "狼", + "217": "猎", + "218": "率", + "219": "王", + "220": "环", + "221": "球", + "222": "理", + "223": "瓦", + "224": "生", + "225": "甲", + "226": "电", + "227": "界", + "228": "疗", + "229": "白", + "230": "百", + "231": "的", + "232": "皮", + "233": "盔", + "234": "盗", + "235": "目", + "236": "眼", + "237": "短", + "238": "破", + "239": "磨", + "240": "神", + "241": "科", + "242": "秩", + "243": "移", + "244": "穆", + "245": "穗", + "246": "空", + "247": "站", + "248": "端", + "249": "簪", + "250": "粗", + "251": "素", + "252": "索", + "253": "纤", + "254": "纹", + "255": "线", + "256": "终", + "257": "绑", + "258": "绒", + "259": "绘", + "260": "绣", + "261": "绳", + "262": "缆", + "263": "罗", + "264": "罩", + "265": "羽", + "266": "翁", + "267": "翔", + "268": "翼", + "269": "耀", + "270": "者", + "271": "肃", + "272": "肢", + "273": "肩", + "274": "胫", + "275": "胸", + "276": "能", + "277": "腿", + "278": "臂", + "279": "舟", + "280": "航", + "281": "船", + "282": "艾", + "283": "芙", + "284": "荒", + "285": "莎", + "286": "莳", + "287": "萨", + "288": "落", + "289": "蔓", + "290": "虎", + "291": "虚", + "292": "蜥", + "293": "螺", + "294": "衣", + "295": "袍", + "296": "裂", + "297": "装", + "298": "裙", + "299": "裳", + "300": "裸", + "301": "誓", + "302": "诞", + "303": "贝", + "304": "贴", + "305": "贸", + "306": "赛", + "307": "超", + "308": "跑", + "309": "距", + "310": "身", + "311": "轨", + "312": "软", + "313": "过", + "314": "迹", + "315": "途", + "316": "速", + "317": "逢", + "318": "道", + "319": "遗", + "320": "遥", + "321": "部", + "322": "酷", + "323": "重", + "324": "野", + "325": "量", + "326": "金", + "327": "钉", + "328": "钢", + "329": "钩", + "330": "铁", + "331": "铆", + "332": "铵", + "333": "银", + "334": "铸", + "335": "镇", + "336": "镜", + "337": "镭", + "338": "长", + "339": "间", + "340": "队", + "341": "防", + "342": "阳", + "343": "阻", + "344": "阿", + "345": "雀", + "346": "雪", + "347": "雷", + "348": "露", + "349": "青", + "350": "面", + "351": "革", + "352": "靴", + "353": "鞋", + "354": "鞲", + "355": "须", + "356": "频", + "357": "风", + "358": "马", + "359": "驭", + "360": "骑", + "361": "高", + "362": "鲸", + "363": "鳞", + "364": "鹰", + "365": "鹿", + "366": "黑", + "367": "默", + "368": "龙" +} \ No newline at end of file diff --git a/models/model_training_starrail.onnx b/models/model_training_starrail.onnx new file mode 100644 index 00000000..0b001b27 --- /dev/null +++ b/models/model_training_starrail.onnx @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6226ad47e62e49c8fabfb09db7439bf8099e77132096ef64aef5ba94961500b3 +size 4630951 diff --git a/src/artifact/internal_relic.rs b/src/artifact/internal_relic.rs new file mode 100644 index 00000000..14dcab89 --- /dev/null +++ b/src/artifact/internal_relic.rs @@ -0,0 +1,276 @@ +use regex::Regex; +use std::hash::{Hash, Hasher}; +use edit_distance; +use log::error; +use strum_macros::Display; + +#[derive(Debug, Hash, Clone, PartialEq, Eq)] +pub enum RelicStatName { + HP, + HPPercentage, + ATK, + ATKPercentage, + DEFPercentage, + SPD, + CRITRate, + CRITDMG, + BreakEffect, + OutgoingHealingBoost, + EnergyRegenerationRate, + EffectHitRate, + PhysicalDMGBoost, + FireDMGBoost, + IceDMGBoost, + LightningDMGBoost, + WindDMGBoost, + QuantumDMGBoost, + ImaginaryDMGBoost, + DEF, + EffectRES, +} + +#[derive(Debug, Hash, Clone, PartialEq, Eq)] +pub enum RelicSlot { + Head, + Hands, + Body, + Feet, + PlanarSphere, + LinkRope, +} + +#[derive(Debug, Hash, Clone, PartialEq, Eq)] +#[derive(Display)] +pub enum RelicSetName { + PasserbyofWanderingCloud, + MusketeerofWildWheat, + KnightofPurityPalace, + HunterofGlacialForest, + ChampionofStreetwiseBoxing, + GuardofWutheringSnow, + FiresmithofLavaForging, + GeniusofBrilliantStars, + BandofSizzlingThunder, + EagleofTwilightLine, + ThiefofShootingMeteor, + WastelanderofBanditryDesert, + SpaceSealingStation, + FleetoftheAgeless, + PanGalacticCommercialEnterprise, + BelobogoftheArchitects, + CelestialDifferentiator, + InertSalsotto, + TaliaKingdomofBanditry, + SprightlyVonwacq, + RutilantArena, + BrokenKeel, + LongevousDisciple, + MessengerTraversingHackerspace, +} + +#[derive(Debug, Clone)] +pub struct RelicStat { + pub name: RelicStatName, + pub value: f64, +} + +#[derive(Debug, Hash, Clone, PartialEq, Eq)] +pub struct InternalRelic { + pub set_name: RelicSetName, + pub slot: RelicSlot, + pub star: u32, + pub level: u32, + pub main_stat: RelicStat, + pub sub_stat_1: Option, + pub sub_stat_2: Option, + pub sub_stat_3: Option, + pub sub_stat_4: Option, + pub equip: Option, +} + +impl Hash for RelicStat { + fn hash(&self, state: &mut H) { + self.name.hash(state); + let v = (self.value * 1000.0) as i32; + v.hash(state); + } +} + +impl PartialEq for RelicStat { + fn eq(&self, other: &Self) -> bool { + if self.name != other.name { + return false; + } + + let v1 = (self.value * 1000.0) as i32; + let v2 = (other.value * 1000.0) as i32; + + v1 == v2 + } +} + +impl Eq for RelicStat {} + +impl RelicStatName { + pub fn from_zh_cn(name: &str, is_percentage: bool) -> Option { + match name { + "生命值" => if is_percentage { Some(RelicStatName::HPPercentage) } else { Some(RelicStatName::HP) }, + "攻击力" => if is_percentage { Some(RelicStatName::ATKPercentage) } else { Some(RelicStatName::ATK) }, + "防御力" => if is_percentage { Some(RelicStatName::DEFPercentage) } else { Some(RelicStatName::DEF) }, + "速度" => Some(RelicStatName::SPD), + "暴击率" => Some(RelicStatName::CRITRate), + "暴击伤害" => Some(RelicStatName::CRITDMG), + "击破特攻" => Some(RelicStatName::BreakEffect), + "治疗量加成" => Some(RelicStatName::OutgoingHealingBoost), + "能量恢复效率" => Some(RelicStatName::EnergyRegenerationRate), + "效果命中" => Some(RelicStatName::EffectHitRate), + "物理属性伤害提高" => Some(RelicStatName::PhysicalDMGBoost), + "火属性伤害提高" => Some(RelicStatName::FireDMGBoost), + "冰属性伤害提高" => Some(RelicStatName::IceDMGBoost), + "雷属性伤害提高" => Some(RelicStatName::LightningDMGBoost), + "风属性伤害提高" => Some(RelicStatName::WindDMGBoost), + "量子属性伤害提高" => Some(RelicStatName::QuantumDMGBoost), + "虚数属性伤害提高" => Some(RelicStatName::ImaginaryDMGBoost), + "效果抵抗" => Some(RelicStatName::EffectRES), + _ => None, + } + } +} + +impl RelicStat { + // e.g "生命值+4,123", "暴击率+10%" + pub fn from_zh_cn_raw(s: &str) -> Option { + let temp: Vec<&str> = s.split("+").collect(); + if temp.len() != 2 { + return None; + } + + let is_percentage = temp[1].contains("%"); + let stat_name = match RelicStatName::from_zh_cn(temp[0], is_percentage) { + Some(v) => v, + None => return None, + }; + + let re = Regex::new("[%,]").unwrap(); + let mut value = match re.replace_all(temp[1], "").parse::() { + Ok(v) => v, + Err(_) => { + error!("stat `{}` parse error", s); + return None; + }, + }; + if is_percentage { + value /= 100.0; + } + + Some(RelicStat { + name: stat_name, + value, + }) + } +} + +pub fn get_real_relic_name_chs(raw: &str) -> Option { + let all_relic_chs = [ + "过客的逢春木簪", "过客的游龙臂鞲", "过客的残绣风衣", "过客的冥途游履", + "快枪手的野穗毡帽", "快枪手的粗革手套", "快枪手的猎风披肩", "快枪手的铆钉马靴", + "圣骑的宽恕盔面", "圣骑的沉默誓环", "圣骑的肃穆胸甲", "圣骑的秩序铁靴", + "雪猎的荒神兜帽", "雪猎的巨蜥手套", "雪猎的冰龙披风", "雪猎的鹿皮软靴", + "拳王的冠军护头", "拳王的重炮拳套", "拳王的贴身护胸", "拳王的弧步战靴", + "铁卫的铸铁面盔", "铁卫的银鳞手甲", "铁卫的旧制军服", "铁卫的白银护胫", + "火匠的黑耀目镜", "火匠的御火戒指", "火匠的阻燃围裙", "火匠的合金义肢", + "天才的超距遥感", "天才的频变捕手", "天才的元域深潜", "天才的引力漫步", + "乐队的偏光墨镜", "乐队的巡演手绳", "乐队的钉刺皮衣", "乐队的铆钉短靴", + "翔鹰的长喙头盔", "翔鹰的鹰击指环", "翔鹰的翼装束带", "翔鹰的绒羽绑带", + "怪盗的千人假面", "怪盗的绘纹手套", "怪盗的纤钢爪钩", "怪盗的流星快靴", + "废土客的呼吸面罩", "废土客的荒漠终端", "废土客的修士长袍", "废土客的动力腿甲", + "「黑塔」的空间站点", "「黑塔」的漫历轨迹", + "罗浮仙舟的天外楼船", "罗浮仙舟的建木枝蔓", + "公司的巨构总部", "公司的贸易航道", + "贝洛伯格的存护堡垒", "贝洛伯格的铁卫防线", + "螺丝星的机械烈阳", "螺丝星的环星孔带", + "萨尔索图的移动城市", "萨尔索图的晨昏界线", + "塔利亚的钉壳小镇", "塔利亚的裸皮电线", + "翁瓦克的诞生之岛", "翁瓦克的环岛海岸", + "泰科铵的镭射球场", "泰科铵的弧光赛道", + "伊须磨洲的残船鲸落", "伊须磨洲的坼裂缆索", + "莳者的复明义眼", "莳者的机巧木手", "莳者的承露羽衣", "莳者的天人丝履", + "信使的全息目镜", "信使的百变义手", "信使的密信挎包", "信使的酷跑板鞋", + ]; + + let mut min_index = 0; + let mut min_dis = edit_distance::edit_distance(raw, all_relic_chs[0]); + let mut same_flag = false; + for (i, &val) in all_relic_chs.iter().enumerate().skip(1) { + let dis = edit_distance::edit_distance(val, raw); + if dis < min_dis { + min_dis = dis; + min_index = i; + same_flag = false; + } else if dis == min_dis { + same_flag = true; + } + } + + if same_flag { + None + } else { + Some(String::from(all_relic_chs[min_index])) + } +} + +impl RelicSetName { + pub fn from_zh_cn(s: &str) -> Option { + // let s = match get_real_relic_name_chs(s) { + // Some(v) => v, + // None => return None, + // }; + // println!("name: {}", s); + match s { + "过客的逢春木簪" | "过客的游龙臂鞲" | "过客的残绣风衣" | "过客的冥途游履" => Some(RelicSetName::PasserbyofWanderingCloud), + "快枪手的野穗毡帽" | "快枪手的粗革手套" | "快枪手的猎风披肩" | "快枪手的铆钉马靴" => Some(RelicSetName::MusketeerofWildWheat), + "圣骑的宽恕盔面" | "圣骑的沉默誓环" | "圣骑的肃穆胸甲" | "圣骑的秩序铁靴" => Some(RelicSetName::KnightofPurityPalace), + "雪猎的荒神兜帽" | "雪猎的巨蜥手套" | "雪猎的冰龙披风" | "雪猎的鹿皮软靴" => Some(RelicSetName::HunterofGlacialForest), + "拳王的冠军护头" | "拳王的重炮拳套" | "拳王的贴身护胸" | "拳王的弧步战靴" => Some(RelicSetName::ChampionofStreetwiseBoxing), + "铁卫的铸铁面盔" | "铁卫的银鳞手甲" | "铁卫的旧制军服" | "铁卫的白银护胫" => Some(RelicSetName::GuardofWutheringSnow), + "火匠的黑耀目镜" | "火匠的御火戒指" | "火匠的阻燃围裙" | "火匠的合金义肢" => Some(RelicSetName::FiresmithofLavaForging), + "天才的超距遥感" | "天才的频变捕手" | "天才的元域深潜" | "天才的引力漫步" => Some(RelicSetName::GeniusofBrilliantStars), + "乐队的偏光墨镜" | "乐队的巡演手绳" | "乐队的钉刺皮衣" | "乐队的铆钉短靴" => Some(RelicSetName::BandofSizzlingThunder), + "翔鹰的长喙头盔" | "翔鹰的鹰击指环" | "翔鹰的翼装束带" | "翔鹰的绒羽绑带" => Some(RelicSetName::EagleofTwilightLine), + "怪盗的千人假面" | "怪盗的绘纹手套" | "怪盗的纤钢爪钩" | "怪盗的流星快靴" => Some(RelicSetName::ThiefofShootingMeteor), + "废土客的呼吸面罩" | "废土客的荒漠终端" | "废土客的修士长袍" | "废土客的动力腿甲" => Some(RelicSetName::WastelanderofBanditryDesert), + "「黑塔」的空间站点" | "「黑塔」的漫历轨迹" => Some(RelicSetName::SpaceSealingStation), + "罗浮仙舟的天外楼船" | "罗浮仙舟的建木枝蔓" => Some(RelicSetName::FleetoftheAgeless), + "公司的巨构总部" | "公司的贸易航道" => Some(RelicSetName::PanGalacticCommercialEnterprise), + "贝洛伯格的存护堡垒" | "贝洛伯格的铁卫防线" => Some(RelicSetName::BelobogoftheArchitects), + "螺丝星的机械烈阳" | "螺丝星的环星孔带" => Some(RelicSetName::CelestialDifferentiator), + "萨尔索图的移动城市" | "萨尔索图的晨昏界线" => Some(RelicSetName::InertSalsotto), + "塔利亚的钉壳小镇" | "塔利亚的裸皮电线" => Some(RelicSetName::TaliaKingdomofBanditry), + "翁瓦克的诞生之岛" | "翁瓦克的环岛海岸" => Some(RelicSetName::FleetoftheAgeless), + "泰科铵的镭射球场" | "泰科铵的弧光赛道" => Some(RelicSetName::RutilantArena), + "伊须磨洲的残船鲸落" | "伊须磨洲的坼裂缆索" => Some(RelicSetName::BrokenKeel), + "莳者的复明义眼" | "莳者的机巧木手" | "莳者的承露羽衣" | "莳者的天人丝履" => Some(RelicSetName::LongevousDisciple), + "信使的全息目镜" | "信使的百变义手" | "信使的密信挎包" | "信使的酷跑板鞋" => Some(RelicSetName::MessengerTraversingHackerspace), + _ => None, + } + } +} + +impl RelicSlot { + pub fn from_zh_cn(s: &str) -> Option { + // let s = match get_real_relic_name_chs(s) { + // Some(v) => v, + // None => return None, + // }; + match s { + "过客的逢春木簪" | "快枪手的野穗毡帽" | "圣骑的宽恕盔面" | "雪猎的荒神兜帽" | "拳王的冠军护头" | "铁卫的铸铁面盔" | "火匠的黑耀目镜" | "天才的超距遥感" | "乐队的偏光墨镜" | "翔鹰的长喙头盔" | "怪盗的千人假面" | "废土客的呼吸面罩" | "莳者的复明义眼" | "信使的全息目镜" => Some(RelicSlot::Head), + "过客的游龙臂鞲" | "快枪手的粗革手套" | "圣骑的沉默誓环" | "雪猎的巨蜥手套" | "拳王的重炮拳套" | "铁卫的银鳞手甲" | "火匠的御火戒指" | "天才的频变捕手" | "乐队的巡演手绳" | "翔鹰的鹰击指环" | "怪盗的绘纹手套" | "废土客的荒漠终端" | "莳者的机巧木手" | "信使的百变义手" => Some(RelicSlot::Hands), + "过客的残绣风衣" | "快枪手的猎风披肩" | "圣骑的肃穆胸甲" | "雪猎的冰龙披风" | "拳王的贴身护胸" | "铁卫的旧制军服" | "火匠的阻燃围裙" | "天才的元域深潜" | "乐队的钉刺皮衣" | "翔鹰的翼装束带" | "怪盗的纤钢爪钩" | "废土客的修士长袍" | "莳者的承露羽衣" | "信使的密信挎包" => Some(RelicSlot::Body), + "过客的冥途游履" | "快枪手的铆钉马靴" | "圣骑的秩序铁靴" | "雪猎的鹿皮软靴" | "拳王的弧步战靴" | "铁卫的白银护胫" | "火匠的合金义肢" | "天才的引力漫步" | "乐队的铆钉短靴" | "翔鹰的绒羽绑带" | "怪盗的流星快靴" | "废土客的动力腿甲" | "莳者的天人丝履" | "信使的酷跑板鞋" => Some(RelicSlot::Feet), + "「黑塔」的空间站点" | "罗浮仙舟的天外楼船" | "公司的巨构总部" | "贝洛伯格的存护堡垒" | "螺丝星的机械烈阳" | "萨尔索图的移动城市" | "塔利亚的钉壳小镇" | "翁瓦克的诞生之岛" | "泰科铵的镭射球场" | "伊须磨洲的残船鲸落" => Some(RelicSlot::PlanarSphere), + "「黑塔」的漫历轨迹" | "罗浮仙舟的建木枝蔓" | "公司的贸易航道" | "贝洛伯格的铁卫防线" | "螺丝星的环星孔带" | "萨尔索图的晨昏界线" | "塔利亚的裸皮电线" | "翁瓦克的环岛海岸" | "泰科铵的弧光赛道" | "伊须磨洲的坼裂缆索" => Some(RelicSlot::LinkRope), + _ => None, + } + } +} + diff --git a/src/artifact/mod.rs b/src/artifact/mod.rs index a474e89f..421697ed 100644 --- a/src/artifact/mod.rs +++ b/src/artifact/mod.rs @@ -1 +1,2 @@ pub mod internal_artifact; +pub mod internal_relic; diff --git a/src/common/mod.rs b/src/common/mod.rs index a0a6ed31..4e4eb833 100644 --- a/src/common/mod.rs +++ b/src/common/mod.rs @@ -59,14 +59,15 @@ impl PixelRectBound { pub fn capture_relative( &self, - info: &ScanInfo, + left: i32, + top: i32, use_pre_process: bool, ) -> Result { let w = self.right - self.left; let h = self.bottom - self.top; let rect = PixelRect { - left: self.left + info.left as i32, - top: self.top + info.top as i32, + left: self.left + left, + top: self.top + top, width: w, height: h, }; @@ -88,12 +89,12 @@ impl PixelRectBound { } } - pub fn capture_relative_image(&self, info: &ScanInfo) -> Result { + pub fn capture_relative_image(&self, left: i32, top: i32) -> Result { let w = self.right - self.left; let h = self.bottom - self.top; let rect = PixelRect { - left: self.left + info.left as i32, - top: self.top + info.top as i32, + left: self.left + left, + top: self.top + top, width: w, height: h, }; diff --git a/src/expo/march7th.rs b/src/expo/march7th.rs new file mode 100644 index 00000000..773dabf3 --- /dev/null +++ b/src/expo/march7th.rs @@ -0,0 +1,219 @@ +use std::convert::From; +use std::fs::File; +use std::io::prelude::*; + +use serde::ser::{Serialize, SerializeMap, Serializer}; + +use crate::artifact::internal_relic::{ + RelicSetName, RelicSlot, RelicStat, RelicStatName, InternalRelic, +}; + + +type March7thRelic = InternalRelic; + +impl RelicStatName { + pub fn to_march7th(&self) -> String { + let temp = match self { + RelicStatName::HP => "hp", + RelicStatName::HPPercentage => "hp_", + RelicStatName::ATK => "atk", + RelicStatName::ATKPercentage => "atk_", + RelicStatName::DEFPercentage => "def_", + RelicStatName::SPD => "spd", + RelicStatName::CRITRate => "critRate", + RelicStatName::CRITDMG => "critDMG", + RelicStatName::BreakEffect => "break", + RelicStatName::OutgoingHealingBoost => "heal", + RelicStatName::EnergyRegenerationRate => "enerRegen", + RelicStatName::EffectHitRate => "eff", + RelicStatName::PhysicalDMGBoost => "physicalDmg", + RelicStatName::FireDMGBoost => "fireDmg", + RelicStatName::IceDMGBoost => "iceDmg", + RelicStatName::LightningDMGBoost => "lightningDmg", + RelicStatName::WindDMGBoost => "windDmg", + RelicStatName::QuantumDMGBoost => "quantumDmg", + RelicStatName::ImaginaryDMGBoost => "imaginaryDmg", + RelicStatName::DEF => "def", + RelicStatName::EffectRES => "effRes", + }; + String::from(temp) + } +} + +impl RelicSetName { + pub fn to_march7th(&self) -> String { + let same = self.to_string(); + let temp = match self { + RelicSetName::PasserbyofWanderingCloud => "PasserbyofWanderingCloud", + RelicSetName::MusketeerofWildWheat => "MusketeerofWildWheat", + RelicSetName::KnightofPurityPalace => "KnightofPurityPalace", + RelicSetName::HunterofGlacialForest => "HunterofGlacialForest", + RelicSetName::ChampionofStreetwiseBoxing => "ChampionofStreetwiseBoxing", + RelicSetName::GuardofWutheringSnow => "GuardofWutheringSnow", + RelicSetName::FiresmithofLavaForging => "FiresmithofLavaForging", + RelicSetName::GeniusofBrilliantStars => "GeniusofBrilliantStars", + RelicSetName::BandofSizzlingThunder => "BandofSizzlingThunder", + RelicSetName::EagleofTwilightLine => "EagleofTwilightLine", + RelicSetName::ThiefofShootingMeteor => "ThiefofShootingMeteor", + RelicSetName::WastelanderofBanditryDesert => "WastelanderofBanditryDesert", + RelicSetName::SpaceSealingStation => "SpaceSealingStation", + RelicSetName::FleetoftheAgeless => "FleetoftheAgeless", + RelicSetName::PanGalacticCommercialEnterprise => "PanGalacticCommercialEnterprise", + RelicSetName::BelobogoftheArchitects => "BelobogoftheArchitects", + RelicSetName::CelestialDifferentiator => "CelestialDifferentiator", + RelicSetName::InertSalsotto => "InertSalsotto", + RelicSetName::TaliaKingdomofBanditry => "TaliaKingdomofBanditry", + RelicSetName::SprightlyVonwacq => "SprightlyVonwacq", + RelicSetName::RutilantArena => "RutilantArena", + RelicSetName::BrokenKeel => "BrokenKeel", + RelicSetName::LongevousDisciple => "LongevousDisciple", + RelicSetName::MessengerTraversingHackerspace => "MessengerTraversingHackerspace", + _ => same.as_str(), + }; + String::from(temp) + } +} + +impl RelicSlot { + pub fn to_march7th(&self) -> String { + let temp = match self { + RelicSlot::Head => "head", + RelicSlot::Hands => "hands", + RelicSlot::Body => "body", + RelicSlot::Feet => "feet", + RelicSlot::PlanarSphere => "planarSphere", + RelicSlot::LinkRope => "linkRope", + }; + String::from(temp) + } +} + +impl Serialize for RelicStat { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let mut root = serializer.serialize_map(Some(2))?; + root.serialize_entry("name", &self.name.to_march7th())?; + root.serialize_entry("value", &self.value)?; + root.end() + } +} + +impl Serialize for March7thRelic { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let mut root = serializer.serialize_map(Some(7))?; + + root.serialize_entry("setName", &self.set_name.to_march7th())?; + root.serialize_entry("position", &self.slot.to_march7th())?; + root.serialize_entry("mainTag", &self.main_stat)?; + + let mut sub_stats: Vec<&RelicStat> = vec![]; + if let Some(ref s) = self.sub_stat_1 { + sub_stats.push(s); + } + if let Some(ref s) = self.sub_stat_2 { + sub_stats.push(s); + } + if let Some(ref s) = self.sub_stat_3 { + sub_stats.push(s); + } + if let Some(ref s) = self.sub_stat_4 { + sub_stats.push(s); + } + // let mut subs = serializer.serialize_seq(Some(sub_stats.len()))?; + // + // for i in sub_stats { + // subs.serialize_element(i); + // } + // subs.end(); + // subs. + + root.serialize_entry("normalTags", &sub_stats)?; + root.serialize_entry("omit", &false)?; + root.serialize_entry("level", &self.level)?; + root.serialize_entry("star", &self.star)?; + root.serialize_entry("equip", &self.equip)?; + // let random_id = thread_rng().gen::(); + // root.serialize_entry("id", &random_id); + + root.end() + } +} + +pub struct March7thFormat<'a> { + version: String, + head: Vec<&'a March7thRelic>, + hands: Vec<&'a March7thRelic>, + body: Vec<&'a March7thRelic>, + feet: Vec<&'a March7thRelic>, + sphere: Vec<&'a March7thRelic>, + rope: Vec<&'a March7thRelic>, +} + +impl<'a> Serialize for March7thFormat<'a> { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let mut root = serializer.serialize_map(Some(6))?; + root.serialize_entry("version", &self.version)?; + root.serialize_entry("head", &self.head)?; + root.serialize_entry("hands", &self.hands)?; + root.serialize_entry("body", &self.body)?; + root.serialize_entry("feet", &self.feet)?; + root.serialize_entry("planarSphere", &self.sphere)?; + root.serialize_entry("linkRope", &self.rope)?; + root.end() + } +} + +impl<'a> March7thFormat<'a> { + pub fn new(results: &Vec) -> March7thFormat { + let mut head: Vec<&March7thRelic> = Vec::new(); + let mut hands: Vec<&March7thRelic> = Vec::new(); + let mut body: Vec<&March7thRelic> = Vec::new(); + let mut feet: Vec<&March7thRelic> = Vec::new(); + let mut head: Vec<&March7thRelic> = Vec::new(); + let mut sphere: Vec<&March7thRelic> = Vec::new(); + let mut rope: Vec<&March7thRelic> = Vec::new(); + + for relic in results.iter() { + match relic.slot { + RelicSlot::Head => head.push(relic), + RelicSlot::Hands => hands.push(relic), + RelicSlot::Body => body.push(relic), + RelicSlot::Feet => feet.push(relic), + RelicSlot::PlanarSphere => sphere.push(relic), + RelicSlot::LinkRope => rope.push(relic), + } + } + + March7thFormat { + head, + hands, + body, + feet, + sphere, + rope, + + version: String::from("1"), + } + } + + pub fn save(&self, path: String) { + let mut file = match File::create(&path) { + Err(why) => panic!("couldn't create {}: {}", path, why), + Ok(file) => file, + }; + let s = serde_json::to_string(&self).unwrap(); + + match file.write_all(s.as_bytes()) { + Err(why) => panic!("couldn't write to {}: {}", path, why), + _ => {}, + } + } +} diff --git a/src/expo/mod.rs b/src/expo/mod.rs index ab4e0b8c..17bcaa13 100644 --- a/src/expo/mod.rs +++ b/src/expo/mod.rs @@ -1,3 +1,4 @@ pub mod good; pub mod mingyu_lab; pub mod mona_uranai; +pub mod march7th; diff --git a/src/inference/inference.rs b/src/inference/inference.rs index bd19e5cf..30f8af7a 100644 --- a/src/inference/inference.rs +++ b/src/inference/inference.rs @@ -14,7 +14,7 @@ pub struct CRNNModel { } impl CRNNModel { - pub fn new(_name: String, _dict_name: String) -> CRNNModel { + pub fn new(name: String, dict_name: String) -> CRNNModel { // let model = tract_onnx::onnx() // .model_for_path(String::from("models/") + name.as_str()).unwrap() // .with_input_fact(0, InferenceFact::dt_shape(f32::datum_type(), tvec!(1, 1, 32, 384))).unwrap() @@ -22,23 +22,49 @@ impl CRNNModel { // .into_runnable().unwrap(); // let mut bytes = include_bytes!("../../models/model_acc100-epoch16.onnx"); let bytes = include_bytes!("../../models/model_training.onnx"); - - let model = tract_onnx::onnx() - .model_for_read(&mut bytes.as_bytes()) - .unwrap() - .with_input_fact( - 0, - InferenceFact::dt_shape(f32::datum_type(), tvec!(1, 1, 32, 384)), - ) - .unwrap() - .into_optimized() - .unwrap() - .into_runnable() - .unwrap(); + let bytes_starrail = include_bytes!("../../models/model_training_starrail.onnx"); + + let model: ModelType; + + if name == "model_training_starrail.onnx" { + model = tract_onnx::onnx() + .model_for_read(&mut bytes_starrail.as_bytes()) + .unwrap() + .with_input_fact( + 0, + InferenceFact::dt_shape(f32::datum_type(), tvec!(1, 1, 32, 384)), + ) + .unwrap() + .into_optimized() + .unwrap() + .into_runnable() + .unwrap(); + } else { + model = tract_onnx::onnx() + .model_for_read(&mut bytes.as_bytes()) + .unwrap() + .with_input_fact( + 0, + InferenceFact::dt_shape(f32::datum_type(), tvec!(1, 1, 32, 384)), + ) + .unwrap() + .into_optimized() + .unwrap() + .into_runnable() + .unwrap(); + } // let content = utils::read_file_to_string(String::from("models/index_2_word.json")); let content = String::from(include_str!("../../models/index_2_word.json")); - let json: Value = serde_json::from_str(content.as_str()).unwrap(); + let content_starrail = String::from(include_str!("../../models/index_2_word_starrail.json")); + + let json: Value; + + if dict_name == "index_2_word_starrail.json" { + json = serde_json::from_str(content_starrail.as_str()).unwrap(); + } else { + json = serde_json::from_str(content.as_str()).unwrap(); + } let mut index_2_word: Vec = Vec::new(); let mut i = 0; diff --git a/src/info/info.rs b/src/info/info.rs index 7a8e2f91..e931b1e2 100644 --- a/src/info/info.rs +++ b/src/info/info.rs @@ -49,152 +49,29 @@ pub struct ScanInfo { } impl ScanInfo { - pub fn from_43_18(width: u32, height: u32, left: i32, top: i32) -> ScanInfo { - WINDOW_43_18.to_scan_info(height as f64, width as f64, left, top) - } - - pub fn from_7_3(width: u32, height: u32, left: i32, top: i32) -> ScanInfo { - WINDOW_7_3.to_scan_info(height as f64, width as f64, left, top) - } - - pub fn from_16_9(width: u32, height: u32, left: i32, top: i32) -> ScanInfo { - WINDOW_16_9.to_scan_info(height as f64, width as f64, left, top) - } - - pub fn from_8_5(width: u32, height: u32, left: i32, top: i32) -> ScanInfo { - WINDOW_8_5.to_scan_info(height as f64, width as f64, left, top) - // let w: u32 = 1440; - // let h: u32 = 900; - // - // let my_get_rect = |rect: (u32, u32, u32, u32)| { - // get_rect(rect, h, w, height, width) - // }; - // - // let info = ScanInfo { - // // panel_height: get_scalar(700.0, w, width), - // // panel_width: get_scalar(410.0, h, height), - // - // title_position: my_get_rect((990, 95, 1240, 125)), - // main_stat_name_position: my_get_rect((990, 194, 1105, 223)), - // main_stat_value_position: my_get_rect((990, 223, 1105, 262)), - // level_position: my_get_rect((993, 323, 1032, 340)), - // panel_position: my_get_rect((969, 90, 1338, 810)), - // - // sub_stat1_position: my_get_rect((1006, 356, 1188, 383)), - // sub_stat2_position: my_get_rect((1006, 385, 1188, 411)), - // sub_stat3_position: my_get_rect((1006, 413, 1188, 439)), - // sub_stat4_position: my_get_rect((1006, 442, 1188, 467)), - // - // equip_position: my_get_rect((1028, 777, 1189, 799)), - // art_count_position: my_get_rect((1173, 25, 1351, 45)), - // - // art_width: get_scalar(92.0, w, width), - // art_height: get_scalar(115.0, h, height), - // art_gap_x: get_scalar(17.0, w, width), - // art_gap_y: get_scalar(17.0, h, height), - // - // art_row: 6, - // art_col: 7, - // - // left_margin: get_scalar(155.0, w, width), - // top_margin: get_scalar(90.0, h, height), - // - // width, - // height, - // left, - // top, - // - // flag_x: get_scalar(312.0, w, width), - // flag_y: get_scalar(87.0, h, height), - // - // star_x: get_scalar(1310.0, w, width), - // star_y: get_scalar(111.0, h, height), - // - // pool_position: my_get_rect((1081, 100, 1092, 408)), - // }; - // - // info - } - - pub fn from_4_3(width: u32, height: u32, left: i32, top: i32) -> ScanInfo { - WINDOW_4_3.to_scan_info(height as f64, width as f64, left, top) - // let w: u32 = 1280; - // let h: u32 = 960; - // - // let my_get_rect = |rect: (u32, u32, u32, u32)| { - // get_rect(rect, h, w, height, width) - // }; - // - // let info = ScanInfo { - // title_position: my_get_rect((880, 85, 1092, 110)), - // main_stat_name_position: my_get_rect((880, 175, 984, 200)), - // main_stat_value_position: my_get_rect((880, 200, 970, 233)), - // level_position: my_get_rect((883, 287, 916, 303)), - // panel_position: my_get_rect((862, 80, 1189, 879)), - // - // sub_stat1_position: my_get_rect((894, 320, 1054, 339)), - // sub_stat2_position: my_get_rect((894, 345, 1054, 365)), - // sub_stat3_position: my_get_rect((894, 373, 1054, 392)), - // sub_stat4_position: my_get_rect((894, 398, 1054, 418)), - // - // equip_position: my_get_rect((913, 850, 1057, 870)), - // art_count_position: my_get_rect((1057, 21, 1204, 41)), - // - // art_width: get_scalar(82.0, w, width), - // art_height: get_scalar(102.0, h, height), - // art_gap_x: get_scalar(15.0, w, width), - // art_gap_y: get_scalar(15.0, h, height), - // - // art_row: 7, - // art_col: 7, - // - // left_margin: get_scalar(138.0, w, width), - // top_margin: get_scalar(80.0, h, height), - // - // width, - // height, - // left, - // top, - // - // flag_x: get_scalar(277.0, w, width), - // flag_y: get_scalar(77.0, h, height), - // - // star_x: get_scalar(1162.0, w, width), - // star_y: get_scalar(100.0, h, height), - // - // pool_position: my_get_rect((959, 95, 974, 365)), - // }; - // - // info - } - - pub fn from_mobile_8_5(width: u32, height: u32, left: i32, top: i32) -> ScanInfo { - WINDOW_MAC_8_5.to_scan_info(height as f64, width as f64, left, top) + pub fn from_pc(width: u32, height: u32, left: i32, top: i32) -> ScanInfo { + if height * 43 == width * 18 { + WINDOW_43_18.to_scan_info(height as f64, width as f64, left, top) + } else if height * 16 == width * 9 { + WINDOW_16_9.to_scan_info(height as f64, width as f64, left, top) + } else if height * 8 == width * 5 { + WINDOW_8_5.to_scan_info(height as f64, width as f64, left, top) + } else if height * 4 == width * 3 { + WINDOW_4_3.to_scan_info(height as f64, width as f64, left, top) + } else if height * 7 == width * 3 { + WINDOW_7_3.to_scan_info(height as f64, width as f64, left, top) + } else { + // 不支持的分辨率 + panic!("不支持的分辨率"); + } } -} - -impl ScanInfo { - pub fn from_rect(rect: &PixelRect) -> Result { - let info: ScanInfo; - if rect.height * 16 == rect.width * 9 { - info = ScanInfo::from_16_9(rect.width as u32, rect.height as u32, rect.left, rect.top); - } else if rect.height * 8 == rect.width * 5 { - info = ScanInfo::from_8_5(rect.width as u32, rect.height as u32, rect.left, rect.top); - } else if rect.height * 4 == rect.width * 3 { - info = ScanInfo::from_4_3(rect.width as u32, rect.height as u32, rect.left, rect.top); - } else if rect.height * 7 == rect.width * 3 { - info = ScanInfo::from_7_3(rect.width as u32, rect.height as u32, rect.left, rect.top); - } else if cfg!(target_os = "macos") { - info = ScanInfo::from_mobile_8_5( - rect.width as u32, - rect.height as u32, - rect.left, - rect.top, - ); + pub fn from_mobile(width: u32, height: u32, left: i32, top: i32) -> ScanInfo { + if (height as i32 * 8 - width as i32 * 5).abs() < 20 { + // 窗口状态下的playcover分辨率长宽无法整除 + WINDOW_MAC_8_5.to_scan_info(height as f64, width as f64, left, top) } else { - return Err(String::from("不支持的分辨率")); + // 不支持的分辨率 + panic!("不支持的分辨率"); } - - Ok(info) } } diff --git a/src/info/info_starrail.rs b/src/info/info_starrail.rs new file mode 100644 index 00000000..a0cf4d39 --- /dev/null +++ b/src/info/info_starrail.rs @@ -0,0 +1,81 @@ +use crate::common::{PixelRect, PixelRectBound}; +use crate::info::window_info_starrail::{ + WINDOW_43_18, WINDOW_16_9, WINDOW_8_5, WINDOW_4_3, WINDOW_7_3, WINDOW_MAC_8_5 +}; + +#[derive(Clone, Debug)] +pub struct ScanInfoStarRail { + // pub panel_height: u32, + // pub panel_width: u32, + + // pub panel_position: PixelRectBound, + pub title_position: PixelRectBound, + pub main_stat_name_position: PixelRectBound, + pub main_stat_value_position: PixelRectBound, + pub level_position: PixelRectBound, + pub panel_position: PixelRectBound, + + pub sub_stat1_name_pos: PixelRectBound, + pub sub_stat1_value_pos: PixelRectBound, + pub sub_stat2_name_pos: PixelRectBound, + pub sub_stat2_value_pos: PixelRectBound, + pub sub_stat3_name_pos: PixelRectBound, + pub sub_stat3_value_pos: PixelRectBound, + pub sub_stat4_name_pos: PixelRectBound, + pub sub_stat4_value_pos: PixelRectBound, + + pub equip_position: PixelRectBound, + pub art_count_position: PixelRectBound, + + pub art_width: u32, + pub art_height: u32, + pub art_gap_x: u32, + pub art_gap_y: u32, + + pub art_row: u32, + pub art_col: u32, + + pub left_margin: u32, + pub top_margin: u32, + + pub width: u32, + pub height: u32, + pub left: i32, + pub top: i32, + + pub flag_x: u32, + pub flag_y: u32, + + pub star_x: u32, + pub star_y: u32, + + pub pool_position: PixelRectBound, +} + +impl ScanInfoStarRail { + pub fn from_pc(width: u32, height: u32, left: i32, top: i32) -> ScanInfoStarRail { + if height * 43 == width * 18 { + WINDOW_43_18.to_scan_info(height as f64, width as f64, left, top) + } else if height * 16 == width * 9 { + WINDOW_16_9.to_scan_info(height as f64, width as f64, left, top) + } else if height * 8 == width * 5 { + WINDOW_8_5.to_scan_info(height as f64, width as f64, left, top) + } else if height * 4 == width * 3 { + WINDOW_4_3.to_scan_info(height as f64, width as f64, left, top) + } else if height * 7 == width * 3 { + WINDOW_7_3.to_scan_info(height as f64, width as f64, left, top) + } else { + // 不支持的分辨率 + panic!("不支持的分辨率"); + } + } + pub fn from_mobile(width: u32, height: u32, left: i32, top: i32) -> ScanInfoStarRail { + if (height as i32 * 8 - width as i32 * 5).abs() < 20 { + // 窗口状态下的playcover分辨率长宽无法整除 + WINDOW_MAC_8_5.to_scan_info(height as f64, width as f64, left, top) + } else { + // 不支持的分辨率 + panic!("不支持的分辨率"); + } + } +} diff --git a/src/info/mod.rs b/src/info/mod.rs index 7f45a009..26878aca 100644 --- a/src/info/mod.rs +++ b/src/info/mod.rs @@ -1,2 +1,4 @@ pub mod info; +pub mod info_starrail; pub mod window_info; +pub mod window_info_starrail; diff --git a/src/info/window_info_starrail.rs b/src/info/window_info_starrail.rs new file mode 100644 index 00000000..ae19539a --- /dev/null +++ b/src/info/window_info_starrail.rs @@ -0,0 +1,335 @@ +use crate::common::PixelRectBound; +use crate::info::info_starrail::ScanInfoStarRail; + +pub struct Rect(f64, f64, f64, f64); // top, right, bottom, left + +pub struct WindowInfoStarRail { + pub width: f64, + pub height: f64, + + pub title_pos: Rect, + pub main_stat_name_pos: Rect, + pub main_stat_value_pos: Rect, + pub level_pos: Rect, + pub panel_pos: Rect, + + pub sub_stat1_name_pos: Rect, + pub sub_stat1_value_pos: Rect, + pub sub_stat2_name_pos: Rect, + pub sub_stat2_value_pos: Rect, + pub sub_stat3_name_pos: Rect, + pub sub_stat3_value_pos: Rect, + pub sub_stat4_name_pos: Rect, + pub sub_stat4_value_pos: Rect, + + pub equip_pos: Rect, + pub art_count_pos: Rect, + + pub art_width: f64, + pub art_height: f64, + pub art_gap_x: f64, + pub art_gap_y: f64, + + pub art_row: usize, + pub art_col: usize, + + pub left_margin: f64, + pub top_margin: f64, + + pub flag_x: f64, + pub flag_y: f64, + + pub star_x: f64, + pub star_y: f64, + + pub pool_pos: Rect, +} + +impl WindowInfoStarRail { + pub fn to_scan_info(&self, h: f64, w: f64, left: i32, top: i32) -> ScanInfoStarRail { + let convert_rect = |rect: &Rect| { + let top = rect.0 / self.height * h; + let right = rect.1 / self.width * w; + let bottom = rect.2 / self.height * h; + let left = rect.3 / self.width * w; + + PixelRectBound { + left: left as i32, + top: top as i32, + right: right as i32, + bottom: bottom as i32, + } + }; + + let convert_x = |x: f64| x / self.width * w; + + let convert_y = |y: f64| y / self.height * h; + + ScanInfoStarRail { + title_position: convert_rect(&self.title_pos), + main_stat_name_position: convert_rect(&self.main_stat_name_pos), + main_stat_value_position: convert_rect(&self.main_stat_value_pos), + level_position: convert_rect(&self.level_pos), + panel_position: convert_rect(&self.panel_pos), + sub_stat1_name_pos: convert_rect(&self.sub_stat1_name_pos), + sub_stat1_value_pos: convert_rect(&self.sub_stat1_value_pos), + sub_stat2_name_pos: convert_rect(&self.sub_stat2_name_pos), + sub_stat2_value_pos: convert_rect(&self.sub_stat2_value_pos), + sub_stat3_name_pos: convert_rect(&self.sub_stat3_name_pos), + sub_stat3_value_pos: convert_rect(&self.sub_stat3_value_pos), + sub_stat4_name_pos: convert_rect(&self.sub_stat4_name_pos), + sub_stat4_value_pos: convert_rect(&self.sub_stat4_value_pos), + equip_position: convert_rect(&self.equip_pos), + art_count_position: convert_rect(&self.art_count_pos), + art_width: convert_x(self.art_width) as u32, + art_height: convert_y(self.art_height) as u32, + art_gap_x: convert_x(self.art_gap_x) as u32, + art_gap_y: convert_y(self.art_gap_y) as u32, + art_row: self.art_row as u32, + art_col: self.art_col as u32, + left_margin: convert_x(self.left_margin) as u32, + top_margin: convert_y(self.top_margin) as u32, + width: w as u32, + height: h as u32, + left, + top, + flag_x: convert_x(self.flag_x) as u32, + flag_y: convert_y(self.flag_y) as u32, + star_x: convert_x(self.star_x) as u32, + star_y: convert_y(self.star_y) as u32, + pool_position: convert_rect(&self.pool_pos), + } + } +} + +pub const WINDOW_43_18: WindowInfoStarRail = WindowInfoStarRail { + width: 3440.0, + height: 1440.0, + + title_pos: Rect(170.0, 3140.0, 220.0, 2560.0), + main_stat_name_pos: Rect(360.0, 2850.0, 400.0, 2560.0), + main_stat_value_pos: Rect(400.0, 2850.0, 460.0, 2560.0), + level_pos: Rect(575.0, 2640.0, 605.0, 2568.0), + panel_pos: Rect(160.0, 3185.0, 1280.0, 2528.0), + + // 凑数用的 + sub_stat1_name_pos: Rect(370.0, 1534.0, 390.0, 1204.0), + sub_stat1_value_pos: Rect(370.0, 1534.0, 390.0, 1204.0), + sub_stat2_name_pos: Rect(402.0, 1534.0, 423.0, 1204.0), + sub_stat2_value_pos: Rect(402.0, 1534.0, 423.0, 1204.0), + sub_stat3_name_pos: Rect(435.0, 1534.0, 456.0, 1204.0), + sub_stat3_value_pos: Rect(435.0, 1534.0, 456.0, 1204.0), + sub_stat4_name_pos: Rect(467.0, 1534.0, 487.0, 1204.0), + sub_stat4_value_pos: Rect(467.0, 1534.0, 487.0, 1204.0), + + equip_pos: Rect(1220.0, 5630.0, 1260.0, 3140.0), + art_count_pos: Rect(50.0, 3185.0, 85.0, 2750.0), + + art_width: 2421.0 - 2257.0, + art_height: 598.0 - 394.0, + art_gap_x: 2257.0 - 2225.0, + art_gap_y: 394.0 - 363.0, + + art_row: 5, + art_col: 11, + + left_margin: 305.0, + top_margin: 161.0, + + flag_x: 580.0, + flag_y: 145.0, + + star_x: 3130.0, + star_y: 200.0, + + pool_pos: Rect(170.0, 2610.0 + 30.0, 900.0, 2610.0), +}; + +pub const WINDOW_7_3: WindowInfoStarRail = WindowInfoStarRail { + width: 2100.0, + height: 900.0, + + title_pos: Rect(106.6, 1800.0, 139.6, 1550.0), + main_stat_name_pos: Rect(224.3, 1690.0, 248.0, 1550.0), + main_stat_value_pos: Rect(248.4, 1690.0, 286.8, 1550.0), + level_pos: Rect(360.0, 1600.0, 378.0, 1557.0), + panel_pos: Rect(100.0, 1941.0, 800.0, 1531.0), + + // 凑数用的 + sub_stat1_name_pos: Rect(370.0, 1534.0, 390.0, 1204.0), + sub_stat1_value_pos: Rect(370.0, 1534.0, 390.0, 1204.0), + sub_stat2_name_pos: Rect(402.0, 1534.0, 423.0, 1204.0), + sub_stat2_value_pos: Rect(402.0, 1534.0, 423.0, 1204.0), + sub_stat3_name_pos: Rect(435.0, 1534.0, 456.0, 1204.0), + sub_stat3_value_pos: Rect(435.0, 1534.0, 456.0, 1204.0), + sub_stat4_name_pos: Rect(467.0, 1534.0, 487.0, 1204.0), + sub_stat4_value_pos: Rect(467.0, 1534.0, 487.0, 1204.0), + + equip_pos: Rect(762.6, 1850.0, 787.8, 1598.0), + art_count_pos: Rect(27.1, 1945.0, 52.9, 1785.0), + + art_width: 1055.0 - 953.0, + art_height: 373.0 - 247.0, + art_gap_x: 953.0 - 933.0, + art_gap_y: 247.0 - 227.0, + + art_row: 5, + art_col: 11, + + left_margin: 166.0, + top_margin: 101.0, + + flag_x: 340.0, + flag_y: 89.8, + + star_x: 1900.0, + star_y: 123.9, + pool_pos: Rect(118.2, 1584.0 + 15.0, 510.3, 1584.0), +}; + +pub const WINDOW_16_9: WindowInfoStarRail = WindowInfoStarRail { + width: 1600.0, + height: 900.0, + + title_pos: Rect(111.0, 1400.0, 132.0, 1169.0), + main_stat_name_pos: Rect(335.0, 1379.0, 355.0, 1207.0), + main_stat_value_pos: Rect(335.0, 1535.0, 355.0, 1465.0), + level_pos: Rect(258.0, 1240.0, 285.0, 1170.0), + panel_pos: Rect(100.0, 1550.0, 800.0, 1150.0), + + sub_stat1_name_pos: Rect(370.0, 1369.0, 390.0, 1204.0), + sub_stat1_value_pos: Rect(370.0, 1534.0, 390.0, 1369.0), + sub_stat2_name_pos: Rect(402.0, 1369.0, 423.0, 1204.0), + sub_stat2_value_pos: Rect(402.0, 1534.0, 423.0, 1369.0), + sub_stat3_name_pos: Rect(435.0, 1369.0, 456.0, 1204.0), + sub_stat3_value_pos: Rect(435.0, 1534.0, 456.0, 1369.0), + sub_stat4_name_pos: Rect(467.0, 1369.0, 487.0, 1204.0), + sub_stat4_value_pos: Rect(467.0, 1534.0, 487.0, 1369.0), + + equip_pos: Rect(762.6, 1389.4, 787.8, 1154.9), + art_count_pos: Rect(813.0, 960.0, 836.0, 753.0), + + art_width: 197.5 - 112.5, + art_height: 379.2 - 295.8, + art_gap_x: 217.5 - 197.5, + art_gap_y: 420.0 - 379.2, + + art_row: 5, + art_col: 9, + + left_margin: 113.0, + top_margin: 172.0, + + flag_x: 271.1, + flag_y: 158.0, + + star_x: 1500.0, + star_y: 208.0, + + pool_pos: Rect(118.2, 1218.7 + 15.0, 510.3, 1218.7), +}; + + +pub const WINDOW_8_5: WindowInfoStarRail = WindowInfoStarRail { + width: 1440.0, + height: 900.0, + title_pos: Rect(96.0, 1268.9, 126.1, 1000.9), + main_stat_name_pos: Rect(201.6, 1128.1, 223.9, 1000.3), + main_stat_value_pos: Rect(225.5, 1128.1, 262.8, 1000.3), + level_pos: Rect(324.0, 1043.0, 340.0, 1006.0), + panel_pos: Rect(90.0, 1350.0, 810.0, 981.0), + // 凑数用的 + sub_stat1_name_pos: Rect(370.0, 1534.0, 390.0, 1204.0), + sub_stat1_value_pos: Rect(370.0, 1534.0, 390.0, 1204.0), + sub_stat2_name_pos: Rect(402.0, 1534.0, 423.0, 1204.0), + sub_stat2_value_pos: Rect(402.0, 1534.0, 423.0, 1204.0), + sub_stat3_name_pos: Rect(435.0, 1534.0, 456.0, 1204.0), + sub_stat3_value_pos: Rect(435.0, 1534.0, 456.0, 1204.0), + sub_stat4_name_pos: Rect(467.0, 1534.0, 487.0, 1204.0), + sub_stat4_value_pos: Rect(467.0, 1534.0, 487.0, 1204.0), + equip_pos: Rect(776.0, 1247.3, 800.6, 1041.3), + art_count_pos: Rect(25.0, 1353.1, 46.8, 1182.8), + art_width: 950.0 - 857.0, + art_height: 204.0 - 91.0, + art_gap_x: 857.0 - 840.0, + art_gap_y: 222.0 - 204.0, + art_row: 6, + art_col: 8, + left_margin: 89.0, + top_margin: 91.0, + flag_x: 245.9, + flag_y: 82.1, + star_x: 1321.3, + star_y: 111.3, + pool_pos: Rect(103.6, 1025.8 + 15.0, 460.7, 1028.5), +}; + + +pub const WINDOW_4_3: WindowInfoStarRail = WindowInfoStarRail { + width: 1280.0, + height: 960.0, + title_pos: Rect(85.0, 1094.8, 111.7, 889.5), + main_stat_name_pos: Rect(181.0, 998.0, 199.8, 889.5), + main_stat_value_pos: Rect(199.8, 998.0, 233.4, 889.5), + level_pos: Rect(288.0, 927.0, 302.0, 894.0), + panel_pos: Rect(80.0, 1200.0, 880.0, 872.0), + // 凑数用的 + sub_stat1_name_pos: Rect(370.0, 1534.0, 390.0, 1204.0), + sub_stat1_value_pos: Rect(370.0, 1534.0, 390.0, 1204.0), + sub_stat2_name_pos: Rect(402.0, 1534.0, 423.0, 1204.0), + sub_stat2_value_pos: Rect(402.0, 1534.0, 423.0, 1204.0), + sub_stat3_name_pos: Rect(435.0, 1534.0, 456.0, 1204.0), + sub_stat3_value_pos: Rect(435.0, 1534.0, 456.0, 1204.0), + sub_stat4_name_pos: Rect(467.0, 1534.0, 487.0, 1204.0), + sub_stat4_value_pos: Rect(467.0, 1534.0, 487.0, 1204.0), + equip_pos: Rect(849.8, 1090.8, 870.1, 924.4), + art_count_pos: Rect(22.9, 1202.3, 41.4, 1058.6), + art_width: 844.0 - 762.0, + art_height: 182.0 - 81.0, + art_gap_x: 762.0 - 747.0, + art_gap_y: 197.0 - 182.0, + art_row: 7, + art_col: 8, + left_margin: 79.0, + top_margin: 81.0, + flag_x: 218.1, + flag_y: 72.1, + star_x: 1175.4, + star_y: 95.8, + pool_pos: Rect(93.2, 912.7 + 15.0, 412.4, 912.7), +}; + + +pub const WINDOW_MAC_8_5: WindowInfoStarRail = WindowInfoStarRail { + width: 1164.0, + height: 755.0 - 28., + title_pos: Rect(122.0 - 28., 1090.0, 157.0 - 28., 770.0), + main_stat_name_pos: Rect(230. - 28., 925., 254. - 28., 765.), + main_stat_value_pos: Rect(253. - 28., 911., 294. - 28., 767.), + level_pos: Rect(353. - 28., 813., 372. - 28., 781.), + panel_pos: Rect(117. - 28., 1127., 666. - 28., 756.), + // 凑数用的 + sub_stat1_name_pos: Rect(370.0, 1534.0, 390.0, 1204.0), + sub_stat1_value_pos: Rect(370.0, 1534.0, 390.0, 1204.0), + sub_stat2_name_pos: Rect(402.0, 1534.0, 423.0, 1204.0), + sub_stat2_value_pos: Rect(402.0, 1534.0, 423.0, 1204.0), + sub_stat3_name_pos: Rect(435.0, 1534.0, 456.0, 1204.0), + sub_stat3_value_pos: Rect(435.0, 1534.0, 456.0, 1204.0), + sub_stat4_name_pos: Rect(467.0, 1534.0, 487.0, 1204.0), + sub_stat4_value_pos: Rect(467.0, 1534.0, 487.0, 1204.0), + equip_pos: Rect(627. - 28., 1090., 659. - 28., 815.), + art_count_pos: Rect(51. - 28., 1076., 80. - 28., 924.), + art_width: 250. - 155., + art_height: 234. - 118., + art_gap_x: 266. - 250., + art_gap_y: 250. - 234., + art_row: 4, + art_col: 5, + left_margin: 155., + top_margin: 118. - 28., + flag_x: 170., //检测颜色出现重复,则判定换行完成 + flag_y: 223. - 28., + star_x: 1060., + star_y: 140. - 28., + pool_pos: Rect(390. - 28., 1010., 504. - 28., 792.), //检测平均颜色是否相同,判断圣遗物有没有切换 +}; diff --git a/src/main.rs b/src/main.rs index 6a748254..4e3dae18 100644 --- a/src/main.rs +++ b/src/main.rs @@ -237,58 +237,21 @@ fn main() { match ui { UI::Desktop => { info!("desktop ui"); - if rect.height * 43 == rect.width * 18 { - info = info::ScanInfo::from_43_18( - rect.width as u32, - rect.height as u32, - rect.left, - rect.top, - ); - } else if rect.height * 16 == rect.width * 9 { - info = info::ScanInfo::from_16_9( - rect.width as u32, - rect.height as u32, - rect.left, - rect.top, - ); - } else if rect.height * 8 == rect.width * 5 { - info = info::ScanInfo::from_8_5( - rect.width as u32, - rect.height as u32, - rect.left, - rect.top, - ); - } else if rect.height * 4 == rect.width * 3 { - info = info::ScanInfo::from_4_3( - rect.width as u32, - rect.height as u32, - rect.left, - rect.top, - ); - } else if rect.height * 7 == rect.width * 3 { - info = info::ScanInfo::from_7_3( - rect.width as u32, - rect.height as u32, - rect.left, - rect.top, - ); - } else { - utils::error_and_quit("不支持的分辨率"); - } + info = info::ScanInfo::from_pc( + rect.width as u32, + rect.height as u32, + rect.left, + rect.top, + ); }, UI::Mobile => { - if (rect.height * 8 - rect.width * 5).abs() < 20 { - // 窗口状态下的playcover分辨率长宽无法整除 - info!("mobile ui 8 / 5"); - info = info::ScanInfo::from_mobile_8_5( - rect.width as u32, - rect.height as u32, - rect.left, - rect.top, - ); - } else { - utils::error_and_quit("不支持的分辨率"); - } + info!("mobile ui"); + info = info::ScanInfo::from_mobile( + rect.width as u32, + rect.height as u32, + rect.left, + rect.top, + ); }, } diff --git a/src/main_starrail.rs b/src/main_starrail.rs new file mode 100644 index 00000000..47b54b28 --- /dev/null +++ b/src/main_starrail.rs @@ -0,0 +1,299 @@ +use std::io::stdin; +use std::path::Path; +use std::time::SystemTime; + +#[cfg(target_os = "macos")] +use yas_scanner::common::utils::get_pid_and_ui; +use yas_scanner::common::{utils, UI}; +use yas_scanner::common::{PixelRect, RawImage}; +use yas_scanner::expo::march7th::March7thFormat; + +use yas_scanner::inference::pre_process::image_to_raw; +use yas_scanner::info::info_starrail; +use yas_scanner::scanner::yas_scanner_starrail::{YasScanner, YasScannerConfig}; + +use clap::{App, Arg}; +use env_logger::Builder; +use image::imageops::grayscale; + +use log::{info, warn, LevelFilter}; + +fn open_local(path: String) -> RawImage { + let img = image::open(path).unwrap(); + let img = grayscale(&img); + let raw_img = image_to_raw(img); + + raw_img +} + +fn main() { + Builder::new().filter_level(LevelFilter::Info).init(); + + #[cfg(windows)] + if !utils::is_admin() { + utils::error_and_quit("请以管理员身份运行该程序") + } + + if let Some(v) = utils::check_update() { + warn!("检测到新版本,请手动更新:{}", v); + } + + let matches = App::new("YAS - 崩坏:星穹铁道遗器导出器") + .version(utils::VERSION) + .author("wormtql <584130248@qq.com>") + .about("Honkai: Star Rail Relic Exporter") + .arg( + Arg::with_name("max-row") + .long("max-row") + .takes_value(true) + .help("最大扫描行数"), + ) + .arg( + Arg::with_name("dump") + .long("dump") + .required(false) + .takes_value(false) + .help("输出模型预测结果、二值化图像和灰度图像,debug专用"), + ) + .arg( + Arg::with_name("capture-only") + .long("capture-only") + .required(false) + .takes_value(false) + .help("只保存截图,不进行扫描,debug专用"), + ) + .arg( + Arg::with_name("min-star") + .long("min-star") + .takes_value(true) + .help("最小星级"), + ) + .arg( + Arg::with_name("min-level") + .long("min-level") + .takes_value(true) + .help("最小等级"), + ) + .arg( + Arg::with_name("max-wait-switch-relic") + .long("max-wait-switch-relic") + .takes_value(true) + .help("切换遗器最大等待时间(ms)"), + ) + .arg( + Arg::with_name("output-dir") + .long("output-dir") + .short("o") + .takes_value(true) + .help("输出目录") + .default_value("."), + ) + .arg( + Arg::with_name("scroll-stop") + .long("scroll-stop") + .takes_value(true) + .help("翻页时滚轮停顿时间(ms)(翻页不正确可以考虑加大该选项,默认为80)"), + ) + .arg( + Arg::with_name("number") + .long("number") + .takes_value(true) + .help("指定遗器数量(在自动识别数量不准确时使用)"), + ) + .arg( + Arg::with_name("verbose") + .long("verbose") + .help("显示详细信息"), + ) + .arg( + Arg::with_name("offset-x") + .long("offset-x") + .takes_value(true) + .help("人为指定横坐标偏移(截图有偏移时可用该选项校正)"), + ) + .arg( + Arg::with_name("offset-y") + .long("offset-y") + .takes_value(true) + .help("人为指定纵坐标偏移(截图有偏移时可用该选项校正)"), + ) + .arg( + Arg::with_name("output-format") + .long("output-format") + .short("f") + .takes_value(true) + .help("输出格式") + .possible_values(&["march7th"]) + .default_value("march7th"), + ) + .arg( + Arg::with_name("cloud-wait-switch-relic") + .long("cloud-wait-switch-relic") + .takes_value(true) + .help("指定云·崩坏:星穹铁道切换遗器等待时间(ms)"), + ) + .get_matches(); + let config = YasScannerConfig::from_match(&matches); + + let rect: PixelRect; + let is_cloud: bool; + let ui: UI; + + #[cfg(windows)] + { + use winapi::um::winuser::{SetForegroundWindow, ShowWindow, SW_RESTORE}; + // use winapi::um::shellscalingapi::{SetProcessDpiAwareness, PROCESS_PER_MONITOR_DPI_AWARE}; + + utils::set_dpi_awareness(); + + let hwnd; + + (hwnd, is_cloud) = utils::find_window_local("崩坏:星穹铁道") + .or_else(|_| utils::find_window_local("Honkai: Star Rail")) + .map(|hwnd| (hwnd, false)) + .unwrap_or_else(|_| { + let Ok(hwnd) = utils::find_window_cloud() else { + utils::error_and_quit("未找到崩坏:星穹铁道窗口,请确认崩坏:星穹铁道已经开启") + }; + (hwnd, true) + }); + + unsafe { + ShowWindow(hwnd, SW_RESTORE); + } + // utils::sleep(1000); + unsafe { + SetForegroundWindow(hwnd); + } + utils::sleep(1000); + + rect = utils::get_client_rect(hwnd).unwrap(); + ui = UI::Desktop; + } + + #[cfg(all(target_os = "linux"))] + { + let window_id = unsafe { + String::from_utf8_unchecked( + std::process::Command::new("sh") + .arg("-c") + .arg(r#" xwininfo|grep "Window id"|cut -d " " -f 4 "#) + .output() + .unwrap() + .stdout, + ) + }; + let window_id = window_id.trim_end_matches("\n"); + + let position_size = unsafe { + String::from_utf8_unchecked( + std::process::Command::new("sh") + .arg("-c") + .arg(&format!(r#" xwininfo -id {window_id}|cut -f 2 -d :|tr -cd "0-9\n"|grep -v "^$"|sed -n "1,2p;5,6p" "#)) + .output() + .unwrap() + .stdout, + ) + }; + + let mut info = position_size.split("\n"); + + let left = info.next().unwrap().parse().unwrap(); + let top = info.next().unwrap().parse().unwrap(); + let width = info.next().unwrap().parse().unwrap(); + let height = info.next().unwrap().parse().unwrap(); + + rect = PixelRect { + left, + top, + width, + height, + }; + is_cloud = false; // todo: detect cloud starrail by title + ui = UI::Desktop; + } + + #[cfg(target_os = "macos")] + { + let (pid, ui_) = get_pid_and_ui(); + let window_title: String; + (rect, window_title) = unsafe { utils::find_window_by_pid(pid).unwrap() }; + info!("Found starrail pid:{}, window name:{}", pid, window_title); + is_cloud = false; // todo: detect cloud starrail by title + ui = ui_; + } + + // rect.scale(1.25); + info!( + "left = {}, top = {}, width = {}, height = {}", + rect.left, rect.top, rect.width, rect.height + ); + + let mut info: info_starrail::ScanInfoStarRail; + + // desktop ui or mobile ui + match ui { + UI::Desktop => { + info!("desktop ui"); + info = info_starrail::ScanInfoStarRail::from_pc( + rect.width as u32, + rect.height as u32, + rect.left, + rect.top, + ); + }, + UI::Mobile => { + info!("mobile ui"); + info = info_starrail::ScanInfoStarRail::from_mobile( + rect.width as u32, + rect.height as u32, + rect.left, + rect.top, + ); + }, + } + + let offset_x = matches + .value_of("offset-x") + .unwrap_or("0") + .parse::() + .unwrap(); + let offset_y = matches + .value_of("offset-y") + .unwrap_or("0") + .parse::() + .unwrap(); + info.left += offset_x; + info.top += offset_y; + + let mut scanner = YasScanner::new(info.clone(), config, is_cloud); + + let now = SystemTime::now(); + #[cfg(target_os = "macos")] + { + info!("初始化完成,请切换到崩坏:星穹铁道窗口,yas将在10s后开始扫描遗器"); + utils::sleep(10000); + } + let results = scanner.start(); + let t = now.elapsed().unwrap().as_secs_f64(); + info!("time: {}s", t); + + let output_dir = Path::new(matches.value_of("output-dir").unwrap()); + match matches.value_of("output-format") { + Some("march7th") => { + let output_filename = output_dir.join("march7th.json"); + let march7th = March7thFormat::new(&results); + march7th.save(String::from(output_filename.to_str().unwrap())); + }, + _ => unreachable!(), + } + // let info = info; + // let img = info.relic_count_position.capture_relative(&info).unwrap(); + + // let mut inference = CRNNModel::new(String::from("model_training.onnx"), String::from("index_2_word.json")); + // let s = inference.inference_string(&img); + // println!("{}", s); + info!("识别结束,请按Enter退出"); + let mut s = String::new(); + stdin().read_line(&mut s).unwrap(); +} diff --git a/src/scanner/mod.rs b/src/scanner/mod.rs index e611946d..903c3b8f 100644 --- a/src/scanner/mod.rs +++ b/src/scanner/mod.rs @@ -1 +1,2 @@ pub mod yas_scanner; +pub mod yas_scanner_starrail; diff --git a/src/scanner/yas_scanner.rs b/src/scanner/yas_scanner.rs index 1ba6fa7e..5b79e05e 100644 --- a/src/scanner/yas_scanner.rs +++ b/src/scanner/yas_scanner.rs @@ -321,7 +321,7 @@ impl YasScanner { let raw_after_pp = self .info .art_count_position - .capture_relative(info, true) + .capture_relative(info.left, info.top, true) .unwrap(); let s = self.model.inference_string(&raw_after_pp); info!("raw count string: {}", s); diff --git a/src/scanner/yas_scanner_starrail.rs b/src/scanner/yas_scanner_starrail.rs new file mode 100644 index 00000000..4d3ce705 --- /dev/null +++ b/src/scanner/yas_scanner_starrail.rs @@ -0,0 +1,946 @@ +use std::collections::HashSet; +use std::convert::From; +use std::fs; + +use std::sync::mpsc; +use std::thread; +use std::time::SystemTime; + +use clap::ArgMatches; +use enigo::*; +use image::{GenericImageView, RgbImage}; +use log::{error, info, warn}; +use tract_onnx::prelude::tract_itertools::Itertools; + +use crate::artifact::internal_relic::{ + RelicSetName, RelicSlot, RelicStat, InternalRelic, +}; +use crate::capture::{self}; +use crate::common::character_name::CHARACTER_NAMES; +use crate::common::color::Color; +#[cfg(target_os = "macos")] +use crate::common::utils::get_pid_and_ui; +use crate::common::{utils, PixelRect, PixelRectBound}; +use crate::inference::inference::CRNNModel; +use crate::inference::pre_process::{pre_process, to_gray, ImageConvExt}; +use crate::info::info_starrail::ScanInfoStarRail; + +// Playcover only, wine should not need this. +#[cfg(all(target_os = "macos", target_arch = "aarch64"))] +use crate::common::utils::mac_scroll; + +pub struct YasScannerConfig { + max_row: u32, + capture_only: bool, + min_star: u32, + min_level: u32, + max_wait_switch_relic: u32, + scroll_stop: u32, + number: u32, + verbose: bool, + dump_mode: bool, + cloud_wait_switch_relic: u32, + // offset_x: i32, + // offset_y: i32, +} + +impl YasScannerConfig { + pub fn from_match(matches: &ArgMatches) -> YasScannerConfig { + YasScannerConfig { + max_row: matches + .value_of("max-row") + .unwrap_or("1000") + .parse::() + .unwrap(), + capture_only: matches.is_present("capture-only"), + dump_mode: matches.is_present("dump"), + min_star: matches + .value_of("min-star") + .unwrap_or("4") + .parse::() + .unwrap(), + min_level: matches + .value_of("min-level") + .unwrap_or("0") + .parse::() + .unwrap(), + max_wait_switch_relic: matches + .value_of("max-wait-switch-relic") + .unwrap_or("800") + .parse::() + .unwrap(), + scroll_stop: matches + .value_of("scroll-stop") + .unwrap_or("80") + .parse::() + .unwrap(), + number: matches + .value_of("number") + .unwrap_or("0") + .parse::() + .unwrap(), + verbose: matches.is_present("verbose"), + cloud_wait_switch_relic: matches + .value_of("cloud-wait-switch-relic") + .unwrap_or("300") + .parse::() + .unwrap(), + // offset_x: matches.value_of("offset-x").unwrap_or("0").parse::().unwrap(), + // offset_y: matches.value_of("offset-y").unwrap_or("0").parse::().unwrap(), + } + } +} + +pub struct YasScanner { + model: CRNNModel, + enigo: Enigo, + + info: ScanInfoStarRail, + config: YasScannerConfig, + + row: u32, + col: u32, + + pool: f64, + + initial_color: Color, + + // for scrolls + scrolled_rows: u32, + avg_scroll_one_row: f64, + + avg_switch_time: f64, + scanned_count: u32, + + is_cloud: bool, +} + +enum ScrollResult { + TLE, + // time limit exceeded + Interrupt, + Success, + Skip, +} + +#[derive(Debug)] +pub struct YasScanResult { + name: String, + main_stat_name: String, + main_stat_value: String, + sub_stat_1: String, + sub_stat_2: String, + sub_stat_3: String, + sub_stat_4: String, + level: String, + equip: String, + star: u32, +} + +impl YasScanResult { + pub fn to_internal_relic(&self) -> Option { + let set_name = RelicSetName::from_zh_cn(&self.name)?; + let slot = RelicSlot::from_zh_cn(&self.name)?; + let star = self.star; + if !self.level.contains("+") { + return None; + } + let level = self + .level + .chars() + .skip(1) + .collect::() + .parse::() + .ok()?; + let main_stat = RelicStat::from_zh_cn_raw( + (self.main_stat_name.clone() + "+" + self.main_stat_value.as_str()).as_str(), + )?; + let sub1 = RelicStat::from_zh_cn_raw(&self.sub_stat_1); + let sub2 = RelicStat::from_zh_cn_raw(&self.sub_stat_2); + let sub3 = RelicStat::from_zh_cn_raw(&self.sub_stat_3); + let sub4 = RelicStat::from_zh_cn_raw(&self.sub_stat_4); + + let equip = None; +/* let equip = if self.equip.contains("已装备") { + //let equip_name = self.equip.clone(); + //equip_name.remove_matches("已装备"); + //let equip_name = &self.equip[..self.equip.len()-9]; + let chars = self.equip.chars().collect_vec(); + let chars2 = &chars[..chars.len() - 3]; + let equip_name = chars2.iter().collect::(); + if CHARACTER_NAMES.contains(equip_name.as_str()) { + Some(equip_name) + } else { + None + } + } else { + None + }; */ + + let relic = InternalRelic { + set_name, + slot, + star, + level, + main_stat, + sub_stat_1: sub1, + sub_stat_2: sub2, + sub_stat_3: sub3, + sub_stat_4: sub4, + equip, + }; + Some(relic) + } +} + +fn calc_pool(row: &Vec) -> f32 { + let len = row.len() / 3; + let mut pool: f32 = 0.0; + + for i in 0..len { + pool += row[i * 3] as f32; + } + // pool /= len as f64; + pool +} + +impl YasScanner { + pub fn new(info: ScanInfoStarRail, config: YasScannerConfig, is_cloud: bool) -> YasScanner { + let row = info.art_row; + let col = info.art_col; + + YasScanner { + model: CRNNModel::new( + String::from("model_training_starrail.onnx"), + String::from("index_2_word_starrail.json"), + ), + enigo: Enigo::new(), + info, + config, + + row, + col, + + pool: -1.0, + initial_color: Color::new(), + scrolled_rows: 0, + avg_scroll_one_row: 0.0, + + avg_switch_time: 0.0, + scanned_count: 0, + + is_cloud, + } + } +} + +impl YasScanner { + fn align_row(&mut self) -> bool { + #[cfg(target_os = "macos")] + let (_, ui) = get_pid_and_ui(); + let mut count = 0; + while count < 10 { + let color = self.get_flag_color(); + if color.is_same(&self.initial_color) { + return true; + } + + #[cfg(windows)] + self.enigo.mouse_scroll_y(-1); + #[cfg(any(target_os = "linux"))] + self.enigo.mouse_scroll_y(1); + #[cfg(target_os = "macos")] + { + match ui { + crate::common::UI::Desktop => { + // mac_scroll(&mut self.enigo, 1); + self.enigo.mouse_scroll_y(-1); + utils::sleep(20); + }, + crate::common::UI::Mobile => { + mac_scroll(&mut self.enigo, 1); + }, + } + } + + utils::sleep(self.config.scroll_stop); + count += 1; + } + + false + } + + /* + pub fn panel_down(&mut self) { + let info = &self.info; + let max_scroll = 20; + let mut count = 0; + self.enigo.mouse_move_to( + info.left + info.star_x as i32, + info.top + info.star_y as i32, + ); + let level_color = Color::from(57, 67, 79); + let mut color = capture::get_color( + info.level_position.left as u32, + info.level_position.bottom as u32, + ); + while !level_color.is_same(&color) && count < max_scroll { + #[cfg(windows)] + self.enigo.mouse_scroll_y(5); + #[cfg(target_os = "linux")] + self.enigo.mouse_scroll_y(-1); + + utils::sleep(self.config.scroll_stop); + color = capture::get_color( + info.level_position.left as u32, + info.level_position.bottom as u32, + ); + count += 1; + } + } + */ + + fn capture_panel(&mut self) -> Result { + let _now = SystemTime::now(); + let w = self.info.panel_position.right - self.info.panel_position.left; + let h = self.info.panel_position.bottom - self.info.panel_position.top; + let rect: PixelRect = PixelRect { + left: self.info.left as i32 + self.info.panel_position.left, + top: self.info.top as i32 + self.info.panel_position.top, + width: w, + height: h, + }; + let u8_arr = capture::capture_absolute(&rect)?; + // info!("capture time: {}ms", now.elapsed().unwrap().as_millis()); + Ok(u8_arr) + } + + fn get_relic_count(&mut self) -> Result { + let count = self.config.number; + if let 0 = count { + let info = &self.info; + let raw_after_pp = self + .info + .art_count_position + .capture_relative(info.left, info.top, true) + .unwrap(); + let s = self.model.inference_string(&raw_after_pp); + info!("raw count string: {}", s); + if s.starts_with("遗器数量") { + let chars = s.chars().collect::>(); + let count_str = (&chars[4..chars.len() - 5]).iter().collect::(); + let count = match count_str.parse::() { + Ok(v) => v, + Err(_) => { + return Err(String::from("无法识别遗器数量")); + } + }; + return Ok(count); + } + Err(String::from("无法识别遗器数量")) + } else { + return Ok(count); + } + } + + fn get_flag_color(&self) -> Color { + let flag_x = self.info.flag_x as i32 + self.info.left; + let flag_y = self.info.flag_y as i32 + self.info.top; + let color = capture::get_color(flag_x as u32, flag_y as u32); + + color + } + + fn get_star(&self) -> u32 { + let color = capture::get_color( + (self.info.star_x as i32 + self.info.left) as u32, + (self.info.star_y as i32 + self.info.top) as u32, + ); + + let color_1 = Color::from(113, 119, 139); // 未核实 + let color_2 = Color::from(42, 143, 114); // 未核实 + let color_3 = Color::from(81, 127, 203); // 未核实 + let color_4 = Color::from(155, 117, 206); + let color_5 = Color::from(194, 159, 112); + + let min_dis: u32 = color_1.dis_2(&color); + let mut star = 1_u32; + if color_2.dis_2(&color) < min_dis { + star = 2; + } + if color_3.dis_2(&color) < min_dis { + star = 3; + } + if color_4.dis_2(&color) < min_dis { + star = 4; + } + if color_5.dis_2(&color) < min_dis { + star = 5; + } + + star + } + + pub fn move_to(&mut self, row: u32, col: u32) { + let info = &self.info; + let left = info.left + + (info.left_margin + (info.art_width + info.art_gap_x) * col + info.art_width / 2) + as i32; + let top = info.top + + (info.top_margin + (info.art_height + info.art_gap_y) * row + info.art_height / 4) + as i32; + self.enigo.mouse_move_to(left as i32, top as i32); + #[cfg(target_os = "macos")] + utils::sleep(20); + } + + fn sample_initial_color(&mut self) { + self.initial_color = self.get_flag_color(); + } + + fn scroll_one_row(&mut self) -> ScrollResult { + #[cfg(target_os = "macos")] + let (_, ui) = get_pid_and_ui(); + let mut state = 0; + let mut count = 0; + let max_scroll = 25; + while count < max_scroll { + if utils::is_rmb_down() { + return ScrollResult::Interrupt; + } + + #[cfg(windows)] + self.enigo.mouse_scroll_y(-5); + #[cfg(any(target_os = "linux"))] + self.enigo.mouse_scroll_y(1); + #[cfg(target_os = "macos")] + { + match ui { + crate::common::UI::Desktop => { + // mac_scroll(&mut self.enigo, 1); + self.enigo.mouse_scroll_y(-1); + utils::sleep(20); + }, + crate::common::UI::Mobile => { + mac_scroll(&mut self.enigo, 1); + }, + } + } + utils::sleep(self.config.scroll_stop); + count += 1; + let color: Color = self.get_flag_color(); + if state == 0 && !color.is_same(&self.initial_color) { + state = 1; + } else if state == 1 && self.initial_color.is_same(&color) { + self.avg_scroll_one_row = (self.avg_scroll_one_row * self.scrolled_rows as f64 + + count as f64) + / (self.scrolled_rows as f64 + 1.0); + info!("avg scroll/row: {}", self.avg_scroll_one_row); + self.scrolled_rows += 1; + return ScrollResult::Success; + } + } + + ScrollResult::TLE + } + + fn scroll_rows(&mut self, count: u32) -> ScrollResult { + #[cfg(target_os = "macos")] + let (_, ui) = get_pid_and_ui(); + if self.scrolled_rows >= 5 { + let scroll = ((self.avg_scroll_one_row * count as f64 - 3.0).round() as u32).max(0); + for _ in 0..scroll { + #[cfg(windows)] + self.enigo.mouse_scroll_y(-1); + #[cfg(target_os = "linux")] + self.enigo.mouse_scroll_y(1); + #[cfg(target_os = "macos")] + { + match ui { + crate::common::UI::Desktop => { + // mac_scroll(&mut self.enigo, 1); + self.enigo.mouse_scroll_y(-1); + utils::sleep(20); + }, + crate::common::UI::Mobile => { + mac_scroll(&mut self.enigo, 1); + }, + } + } + } + utils::sleep(400); + self.align_row(); + return ScrollResult::Skip; + } + + for _ in 0..count { + match self.scroll_one_row() { + ScrollResult::TLE => return ScrollResult::TLE, + ScrollResult::Interrupt => return ScrollResult::Interrupt, + _ => (), + } + } + + ScrollResult::Success + } + + /* + fn start_capture_only(&mut self) { + fs::create_dir("captures"); + let info = &self.info.clone(); + + println!("capture l:{}, t:{}, w:{},h:{}", info.left, info.top, info.width, info.height); + let raw_im_rect = PixelRectBound { + left: info.left as i32, + top: info.top as i32, + right: info.left + info.width as i32, + bottom: info.top + info.height as i32, + }; + + let raw_im = raw_im_rect.capture_relative(info, false).unwrap(); + raw_im.grayscale_to_gray_image().save("captures/raw_im.png"); + println!("Finish capture raw"); + panic!(); + + let count = self.info.art_count_position.capture_relative(info, false).unwrap(); + count.to_gray_image().save("captures/count.png"); + + let convert_rect = |rect: &PixelRectBound| PixelRect { + left: rect.left - info.panel_position.left, + top: rect.top - info.panel_position.top, + width: rect.right - rect.left, + height: rect.bottom - rect.top, + }; + + let panel = self.capture_panel().unwrap(); + let im_title = pre_process(panel.crop_to_raw_img(&convert_rect(&info.title_position))); + if let Some(im) = im_title { + im.to_gray_image().save("captures/title.png").expect("Err"); + } + + let im_main_stat_name = + pre_process(panel.crop_to_raw_img(&convert_rect(&info.main_stat_name_position))); + if let Some(im) = im_main_stat_name { + im.to_gray_image() + .save("captures/main_stat_name.png") + .expect("Err"); + } + + let im_main_stat_value = + pre_process(panel.crop_to_raw_img(&convert_rect(&info.main_stat_value_position))); + if let Some(im) = im_main_stat_value { + im.to_gray_image() + .save("captures/main_stat_value.png") + .expect("Err"); + } + + let im_sub_stat_1 = + pre_process(panel.crop_to_raw_img(&convert_rect(&info.sub_stat1_position))); + if let Some(im) = im_sub_stat_1 { + im.to_gray_image() + .save("captures/sub_stat_1.png") + .expect("Err"); + } + + let im_sub_stat_2 = + pre_process(panel.crop_to_raw_img(&convert_rect(&info.sub_stat2_position))); + if let Some(im) = im_sub_stat_2 { + im.to_gray_image() + .save("captures/sub_stat_2.png") + .expect("Err"); + } + + let im_sub_stat_3 = + pre_process(panel.crop_to_raw_img(&convert_rect(&info.sub_stat3_position))); + if let Some(im) = im_sub_stat_3 { + im.to_gray_image() + .save("captures/sub_stat_3.png") + .expect("Err"); + } + + let im_sub_stat_4 = + pre_process(panel.crop_to_raw_img(&convert_rect(&info.sub_stat4_position))); + if let Some(im) = im_sub_stat_4 { + im.to_gray_image() + .save("captures/sub_stat_4.png") + .expect("Err"); + } + + let im_level = pre_process(panel.crop_to_raw_img(&convert_rect(&info.level_position))); + if let Some(im) = im_level { + im.to_gray_image().save("captures/level.png").expect("Err"); + } + + let im_equip = pre_process(panel.crop_to_raw_img(&convert_rect(&info.equip_position))); + if let Some(im) = im_equip { + im.to_gray_image().save("captures/equip.png").expect("Err"); + } + } + */ + pub fn start(&mut self) -> Vec { + //self.panel_down(); + /* if self.config.capture_only { + self.start_capture_only(); + return Vec::new(); + } */ + + let count = match self.get_relic_count() { + Ok(v) => v, + Err(_) => 1500, + }; + + let total_row = (count + self.col - 1) / self.col; + let last_row_col = if count % self.col == 0 { + self.col + } else { + count % self.col + }; + + // println!("检测到圣遗物数量:{},若无误请按回车,否则输入正确的圣遗物数量:", count); + // let mut s: String = String::new(); + // stdin().read_line(&mut s); + // if s.trim() != "" { + // count = s.trim().parse::().unwrap(); + // } + + info!("detected count: {}", count); + info!("total row: {}", total_row); + info!("last column: {}", last_row_col); + + let (tx, rx) = mpsc::channel::>(); + let info_2 = self.info.clone(); + // v bvvmnvbm + let is_verbose = self.config.verbose; + let is_dump_mode = self.config.dump_mode; + let min_level = self.config.min_level; + let handle = thread::spawn(move || { + let mut results: Vec = Vec::new(); + let model = CRNNModel::new( + String::from("model_training_starrail.onnx"), + String::from("index_2_word_starrail.json"), + ); + let mut error_count = 0; + let mut dup_count = 0; + let mut hash = HashSet::new(); + let mut consecutive_dup_count = 0; + let info = info_2; + + let mut cnt = 0; + if is_dump_mode { + fs::create_dir("dumps").unwrap(); + } + + let convert_rect = |rect: &PixelRectBound| PixelRect { + left: rect.left - info.panel_position.left, + top: rect.top - info.panel_position.top, + width: rect.right - rect.left, + height: rect.bottom - rect.top, + }; + + for i in rx { + let (capture, star) = match i { + Some(v) => v, + None => break, + }; + //info!("raw capture image: width = {}, height = {}", capture.width(), capture.height()); + let _now = SystemTime::now(); + + let model_inference = |pos: &PixelRectBound, + name: &str, + captured_img: &RgbImage, + cnt: i32| + -> String { + let rect = convert_rect(pos); + let raw_img = to_gray(captured_img) + .view( + rect.left as u32, + rect.top as u32, + rect.width as u32, + rect.height as u32, + ) + .to_image(); + //info!("raw_img: width = {}, height = {}", raw_img.width(), raw_img.height()); + + if is_dump_mode { + raw_img + .to_common_grayscale() + .save(format!("dumps/{}_{}.png", name, cnt)) + .expect("Err"); + } + + let processed_img = match pre_process(raw_img) { + Some(im) => im, + None => { + return String::new(); + }, + }; + if is_dump_mode { + processed_img + .to_common_grayscale() + .save(format!("dumps/p_{}_{}.png", name, cnt)) + .expect("Err"); + } + + let inference_result = model.inference_string(&processed_img); + if is_dump_mode { + fs::write(format!("dumps/{}_{}.txt", name, cnt), &inference_result) + .expect("Err"); + } + + inference_result + }; + + let str_title = model_inference(&info.title_position, "title", &capture, cnt); + let str_main_stat_name = model_inference( + &info.main_stat_name_position, + "main_stat_name", + &capture, + cnt, + ); + let str_main_stat_value = model_inference( + &info.main_stat_value_position, + "main_stat_value", + &capture, + cnt, + ); + + let str_sub_stat_1_name = model_inference( + &info.sub_stat1_name_pos, "sub_stat_1_name", &capture, cnt); + let str_sub_stat_1_value = model_inference( + &info.sub_stat1_value_pos, "sub_stat_1_value", &capture, cnt); + let str_sub_stat_2_name = model_inference( + &info.sub_stat2_name_pos, "sub_stat_2_name", &capture, cnt); + let str_sub_stat_2_value = model_inference( + &info.sub_stat2_value_pos, "sub_stat_2_value", &capture, cnt); + let str_sub_stat_3_name = model_inference( + &info.sub_stat3_name_pos, "sub_stat_3_name", &capture, cnt); + let str_sub_stat_3_value = model_inference( + &info.sub_stat3_value_pos, "sub_stat_3_value", &capture, cnt); + let str_sub_stat_4_name = model_inference( + &info.sub_stat4_name_pos, "sub_stat_4_name", &capture, cnt); + let str_sub_stat_4_value = model_inference( + &info.sub_stat4_value_pos, "sub_stat_4_value", &capture, cnt); + + let str_level = model_inference(&info.level_position, "level", &capture, cnt); + // let str_equip = model_inference(&info.equip_position, "equip", &capture, cnt); + + cnt += 1; + + // let predict_time = now.elapsed().unwrap().as_millis(); + // println!("predict time: {}ms", predict_time); + + let result = YasScanResult { + name: str_title, + main_stat_name: str_main_stat_name, + main_stat_value: str_main_stat_value, + sub_stat_1: str_sub_stat_1_name + "+" + &str_sub_stat_1_value, + sub_stat_2: str_sub_stat_2_name + "+" + &str_sub_stat_2_value, + sub_stat_3: str_sub_stat_3_name + "+" + &str_sub_stat_3_value, + sub_stat_4: str_sub_stat_4_name + "+" + &str_sub_stat_4_value, + level: str_level, + equip: String::new(), + star, + }; + if is_verbose { + info!("{:?}", result); + } + // println!("{:?}", result); + let relic = result.to_internal_relic(); + if let Some(a) = relic { + if hash.contains(&a) { + dup_count += 1; + consecutive_dup_count += 1; + warn!("dup relic detected: {:?}", result); + } else { + consecutive_dup_count = 0; + hash.insert(a.clone()); + results.push(a); + } + } else { + error!("wrong detection: {:?}", result); + error_count += 1; + // println!("error parsing results"); + } + if consecutive_dup_count >= info.art_row { + error!("检测到连续多个重复遗器,可能为翻页错误,或者为非背包顶部开始扫描"); + break; + } + } + + info!("error count: {}", error_count); + info!("dup count: {}", dup_count); + + if min_level > 0 { + results + .into_iter() + .filter(|result| result.level >= min_level) + .collect::>() + } else { + results + } + }); + + let mut scanned_row = 0_u32; + let mut scanned_count = 0_u32; + let mut start_row = 0_u32; + self.move_to(0, 0); + #[cfg(target_os = "macos")] + utils::sleep(20); + self.enigo.mouse_click(MouseButton::Left); + utils::sleep(1000); + // self.wait_until_switched(); + self.sample_initial_color(); + + 'outer: while scanned_count < count { + 'row: for row in start_row..self.row { + let c = if scanned_row == total_row - 1 { + last_row_col + } else { + self.col + }; + 'col: for col in 0..c { + // 大于最大数量则退出 + if scanned_count > count { + break 'outer; + } + + // 右键终止 + if utils::is_rmb_down() { + break 'outer; + } + + self.move_to(row, col); + self.enigo.mouse_click(MouseButton::Left); + #[cfg(target_os = "macos")] + utils::sleep(20); + + self.wait_until_switched(); + let capture = self.capture_panel().unwrap(); + let star = self.get_star(); + if star < self.config.min_star { + break 'outer; + } + tx.send(Some((capture, star))).unwrap(); + + scanned_count += 1; + } // end 'col + + scanned_row += 1; + + if scanned_row >= self.config.max_row { + info!("max row reached, quiting..."); + break 'outer; + } + } // end 'row + + let remain = count - scanned_count; + let remain_row = (remain + self.col - 1) / self.col; + let scroll_row = remain_row.min(self.row); + start_row = self.row - scroll_row; + match self.scroll_rows(scroll_row) { + ScrollResult::TLE => { + error!("翻页出现问题"); + break 'outer; + }, + ScrollResult::Interrupt => break 'outer, + _ => (), + } + + utils::sleep(100); + } + + tx.send(None).unwrap(); + + info!("扫描结束,等待识别线程结束,请勿关闭程序"); + let results: Vec = handle.join().unwrap(); + info!("count: {}", results.len()); + results + } + fn wait_until_switched(&mut self) -> bool { + if self.is_cloud { + utils::sleep(self.config.cloud_wait_switch_relic); + return true; + } + let now = SystemTime::now(); + + let mut consecutive_time = 0; + let mut diff_flag = false; + while now.elapsed().unwrap().as_millis() < self.config.max_wait_switch_relic as u128 { + // let pool_start = SystemTime::now(); + let rect = PixelRect { + left: self.info.left as i32 + self.info.pool_position.left, + top: self.info.top as i32 + self.info.pool_position.top, + width: self.info.pool_position.right - self.info.pool_position.left, + height: self.info.pool_position.bottom - self.info.pool_position.top, + }; + let im = capture::capture_absolute(&rect).unwrap(); + let pool = calc_pool(im.as_raw()) as f64; + // info!("pool: {}", pool); + // println!("pool time: {}ms", pool_start.elapsed().unwrap().as_millis()); + + if (pool - self.pool).abs() > 0.000001 { + // info!("pool: {}", pool); + // let raw = RawCaptureImage { + // data: im, + // w: rect.width as u32, + // h: rect.height as u32, + // }; + // let raw = raw.to_raw_image(); + // println!("{:?}", &raw.data[..10]); + // raw.save(&format!("captures/{}.png", rand::thread_rng().gen::())); + + self.pool = pool; + diff_flag = true; + consecutive_time = 0; + // info!("avg switch time: {}ms", self.avg_switch_time); + } else { + if diff_flag { + consecutive_time += 1; + if consecutive_time == 1 { + self.avg_switch_time = (self.avg_switch_time * self.scanned_count as f64 + + now.elapsed().unwrap().as_millis() as f64) + / (self.scanned_count as f64 + 1.0); + self.scanned_count += 1; + return true; + } + } + } + } + + false + } +} + +impl YasScanner { + // pub fn start_from_scratch(config: YasScannerConfig) -> Result, String> { + // set_dpi_awareness(); + // let mut is_cloud = false; + // let hwnd = match find_window_local() { + // Ok(v) => {is_cloud = true; v}, + // Err(s) => { + // match find_window_cloud() { + // Ok(v) => v, + // Err(s) => return Err(String::from("未找到原神窗口")) + // } + // } + // }; + // + // show_window_and_set_foreground(hwnd); + // sleep(1000); + // + // let mut rect = match get_client_rect(hwnd) { + // Ok(v) => v, + // Err(_) => return Err(String::from("未能获取窗口大小")) + // }; + // + // let info = match ScanInfo::from_rect(&rect) { + // Ok(v) => v, + // Err(e) => return Err(e) + // }; + // + // let mut scanner = YasScanner::new(info, config, is_cloud); + // let result = scanner.start(); + // + // Ok(result) + // } +}