├── .gitignore ├── README.md ├── cjedb.json ├── cjedb.proto ├── cjedb_pb2.py ├── cjedb_pb2.pyi └── generator.py /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | __pycache__/ 3 | 4 | /.idea/ 5 | 6 | /master.mdb 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cjedb 2 | 3 | cjedb provides additional data to [EXNOA-CarrotJuicer](https://github.com/CNA-Bld/EXNOA-CarrotJuicer) for additional 4 | functionalities. 5 | 6 | If you are a user of EXNOA-CarrotJuicer, please just 7 | put [cjedb.json](https://github.com/CNA-Bld/cjedb/raw/master/cjedb.json) into `umamusume.exe`'s directory. 8 | 9 | ## Schema 10 | 11 | Schema is defined by [cjedb.proto](cjedb.proto). Run `protoc --python_out=. --pyi_out=. *.proto` to update the generated 12 | Python code. 13 | 14 | Although schema is defined as a protobuf, currently proto3 JSON is used as the data exchange format. 15 | 16 | ## Generate `cjedb.json` 17 | 18 | Run `generator.py --db_path --output `. Both options are optional, and they 19 | default to the files in the working directory. 20 | 21 | The only pip dependencies are `requests` and `protobuf`. 22 | 23 | ## Components 24 | 25 | ### `events` 26 | 27 | This contains data for events that behave differently based on user choices. 28 | 29 | Data comes from [GameWith](https://gamewith.jp/uma-musume/article/show/259587). The generator will automatically fetch a 30 | live version when it runs. 31 | 32 | It attempts to do fuzzy matching whenever possible (with some terrible hacks), and when that doesn't work it will print 33 | a warning message. Known exceptions: 34 | 35 | * General events that are the same across all characters are just ignored (like 追加の自主トレ). Please just recite them with 36 | brain. A full list is in `EXCLUDED_EVENT_NAMES`. 37 | * General events that are different for characters (currently only ダンスレッスン) are hardcoded manually with some safety 38 | checks against `master.mdb`. 39 | * Some character specific events are also excluded. See `PER_CHARA_EXCLUDE_EVENTS`. 40 | * Some events have several copies. See `PERMITTED_DUPLICATED_EVENTS` for details. 41 | * Finally, `KNOWN_OVERRIDES` is the list of events where the name cannot be fuzzy matched, so they got manually mapped. 42 | -------------------------------------------------------------------------------- /cjedb.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | package cjedb; 4 | 5 | message Database { 6 | repeated Event events = 1; 7 | } 8 | 9 | message Event { 10 | optional int32 story_id = 1; 11 | optional string story_name = 3; // Name as presented by upstream. Only available when option --include_name is enabled. 12 | 13 | message Choice { 14 | optional string title = 1; 15 | optional string text = 2; 16 | } 17 | repeated Choice choices = 2; 18 | } 19 | -------------------------------------------------------------------------------- /cjedb_pb2.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by the protocol buffer compiler. DO NOT EDIT! 3 | # source: cjedb.proto 4 | """Generated protocol buffer code.""" 5 | from google.protobuf.internal import builder as _builder 6 | from google.protobuf import descriptor as _descriptor 7 | from google.protobuf import descriptor_pool as _descriptor_pool 8 | from google.protobuf import symbol_database as _symbol_database 9 | # @@protoc_insertion_point(imports) 10 | 11 | _sym_db = _symbol_database.Default() 12 | 13 | 14 | 15 | 16 | DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0b\x63jedb.proto\x12\x05\x63jedb\"(\n\x08\x44\x61tabase\x12\x1c\n\x06\x65vents\x18\x01 \x03(\x0b\x32\x0c.cjedb.Event\"z\n\x05\x45vent\x12\x10\n\x08story_id\x18\x01 \x01(\x05\x12\x12\n\nstory_name\x18\x03 \x01(\t\x12$\n\x07\x63hoices\x18\x02 \x03(\x0b\x32\x13.cjedb.Event.Choice\x1a%\n\x06\x43hoice\x12\r\n\x05title\x18\x01 \x01(\t\x12\x0c\n\x04text\x18\x02 \x01(\t') 17 | 18 | _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) 19 | _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'cjedb_pb2', globals()) 20 | if _descriptor._USE_C_DESCRIPTORS == False: 21 | 22 | DESCRIPTOR._options = None 23 | _DATABASE._serialized_start=22 24 | _DATABASE._serialized_end=62 25 | _EVENT._serialized_start=64 26 | _EVENT._serialized_end=186 27 | _EVENT_CHOICE._serialized_start=149 28 | _EVENT_CHOICE._serialized_end=186 29 | # @@protoc_insertion_point(module_scope) 30 | -------------------------------------------------------------------------------- /cjedb_pb2.pyi: -------------------------------------------------------------------------------- 1 | from google.protobuf.internal import containers as _containers 2 | from google.protobuf import descriptor as _descriptor 3 | from google.protobuf import message as _message 4 | from typing import ClassVar, Iterable, Mapping, Optional, Union 5 | 6 | DESCRIPTOR: _descriptor.FileDescriptor 7 | 8 | class Database(_message.Message): 9 | __slots__ = ["events"] 10 | EVENTS_FIELD_NUMBER: ClassVar[int] 11 | events: _containers.RepeatedCompositeFieldContainer[Event] 12 | def __init__(self, events: Optional[Iterable[Union[Event, Mapping]]] = ...) -> None: ... 13 | 14 | class Event(_message.Message): 15 | __slots__ = ["choices", "story_id", "story_name"] 16 | class Choice(_message.Message): 17 | __slots__ = ["text", "title"] 18 | TEXT_FIELD_NUMBER: ClassVar[int] 19 | TITLE_FIELD_NUMBER: ClassVar[int] 20 | text: str 21 | title: str 22 | def __init__(self, title: Optional[str] = ..., text: Optional[str] = ...) -> None: ... 23 | CHOICES_FIELD_NUMBER: ClassVar[int] 24 | STORY_ID_FIELD_NUMBER: ClassVar[int] 25 | STORY_NAME_FIELD_NUMBER: ClassVar[int] 26 | choices: _containers.RepeatedCompositeFieldContainer[Event.Choice] 27 | story_id: int 28 | story_name: str 29 | def __init__(self, story_id: Optional[int] = ..., story_name: Optional[str] = ..., choices: Optional[Iterable[Union[Event.Choice, Mapping]]] = ...) -> None: ... 30 | -------------------------------------------------------------------------------- /generator.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import ast 3 | import json 4 | import logging 5 | import os 6 | import re 7 | import sqlite3 8 | import unicodedata 9 | from typing import Optional 10 | 11 | import requests 12 | from google.protobuf import json_format 13 | 14 | import cjedb_pb2 15 | 16 | UPSTREAM_DATA_URL = 'https://gamewith-tool.s3-ap-northeast-1.amazonaws.com/uma-musume/male_event_datas.js' 17 | UPSTREAM_DATA_HEADER = '''window.eventDatas['男'] = [''' 18 | UPSTREAM_DATA_FOOTER = '];' 19 | 20 | EXCLUDED_EVENT_CHARA_NAMES = {'共通', 'URA', 'アオハル', 'クライマックス', 'グランドライブ', 'グランドマスターズ', 'プロジェクトL’Arc', 'UAF'} 21 | LOW_PRIORITY_CHARA_NAMES = {'チーム<シリウス>', '玉座に集いし者たち', '祖にして導く者', '刻み続ける者たち'} 22 | 23 | EXCLUDED_EVENT_NAMES = { 24 | '追加の自主トレ', '夏合宿(2年目)にて', '夏合宿(2年目)にて', '初詣', '新年の抱負', 25 | 'お大事に!', '無茶は厳禁!', 26 | 'レース勝利!(1着)', 'レース入着(2~5着)', 'レース敗北(6着以下)', 'レース勝利!', 'レース入着', 'レース敗北', 27 | '今度こそ負けない!', 28 | 'あんし〜ん笹針師、参☆上', 29 | 'チーム<ファースト>の宣戦布告', 'ついに集まったチームメンバー!', # Aoharu only 30 | } 31 | 32 | EVENT_NAME_SUFFIX_TO_REMOVE = {'(お出かけ2)', '(お出かけ3)', '(Rお出かけ3)', '(パリの街にて)', '(その日を信じて)', '(温かなメッセージ)', '(彼の都の思い出は)'} 33 | 34 | PER_CHARA_EXCLUDE_EVENTS = { 35 | ('夏合宿(3年目)終了', 1007), # ゴルシ, wrong event name, but no one else has this choice, and the choice does nothing 36 | ('レース勝利', 1024), # マヤノ, without exclamation mark at the end. Both this and the normal one appear in gallery 37 | ('レース入着(2/4/5着)', 1060), # ナイスネイチャ 38 | ('天皇賞(秋)の後に・空に手を', 1069), # サクラチヨノオー, the choices explain their effect quite well. 39 | 40 | # ゴールドシチー 41 | ('レース勝利!(クラシック10月後半以前1着)', 1040), 42 | ('レース入着(クラシック10月後半以前2~5着)', 1040), 43 | ('レース敗北(クラシック10月後半以前6着以下)', 1040), 44 | ('レース勝利!(クラシック11月前半以降1着)', 1040), 45 | ('レース入着(クラシック11月前半以降2~5着)', 1040), 46 | ('レース敗北(クラシック11月前半以降6着以下)', 1040), 47 | ('レース勝利!(シニア5月前半以降1着)', 1040), 48 | ('レース入着(シニア5月前半以降2~5着)', 1040), 49 | ('レース敗北(シニア5月前半以降6着以下)', 1040), 50 | } 51 | 52 | PERMITTED_DUPLICATED_EVENTS = { 53 | # 理事長. One has choices, the other one doesn't. We don't care and just anyway show. 54 | ('上々の面構えッ!', None): {400001024, 400001037}, 55 | 56 | # ダイワスカーレット. One for ☆2 and one for ☆3. 57 | ('アイツの存在', 1009): {501009115, 501009413}, 58 | 59 | # ゴルドシープ. ☆2 vs ☆3, multiplied by one with choice (宝塚二連覇) vs doesn't. Don't care and anyway show. 60 | ('宝塚記念の後に・キーワード②', 1007): {501007309, 501007310, 501007423, 501007424}, 61 | 62 | # ナリタブライアン. One with choices and one doesn't. Don't care and anyway show. 63 | ('岐', 1016): {501016121, 501016409}, 64 | 65 | # フジキセキ 66 | ('第一幕 スマイル', 1005): {501005113, 501005401}, 67 | 68 | # ファインモーション 69 | ('Who Will Escort Me?', 1022): {501022118, 501022406}, 70 | 71 | # メジロアルダン 72 | ('道、分かたれて', 1071): {501071116, 501071404}, 73 | 74 | # ニシノフラワー. 2 consecutive events with the same name. Upstream groups them as a single event. 75 | ('夜に咲く想い', 1051): {501051524, 501051525}, 76 | 77 | # ケイエスミラクル 78 | ('高架下の捜索', 1093): {501093524, 501093525}, 79 | 80 | # ヴィブロス 81 | ('Sisters♡', 1091): {501091117, 501091118, 501091405, 501091406}, 82 | 83 | # [一天地六に身を任せ]ナカヤマフェスタ 84 | ('デスパレートに輝いて', 1049): {830108003, 830108004}, 85 | 86 | # Aoharu, team name 87 | ('ついに集まったチームメンバー!', None): {400002204, 400002217, 400002444}, 88 | 89 | # Grand Live 90 | ('あなたと私をつなげるライブ', None): {400003202, 400003231}, 91 | 92 | # Grand Masters 93 | ('今を駆ける者たちの祖', None): {400005105, 400005430}, 94 | 95 | # L'Arc 96 | ('With', None): {400006005, 400006404}, 97 | } 98 | 99 | DUPLICATED_EVENTS_DEDUPE = { 100 | # 1061 キングヘイロー, 1019 アグネスデジタル 101 | ('一流の条件', 1061): ({501019116, 501061704}, [501061704]), 102 | 103 | # 1021 タマモクロス, 1024 マヤノトップガン 104 | # For マヤノ, this behaves the same to the normal one and is excluded above by PER_CHARA_EXCLUDE_EVENTS 105 | # For タマ, this is the special one during バ群を怖がる期間 106 | ('レース勝利', 1021): ({501021734, 501024724}, [501021734]), 107 | 108 | # 1077 ナリタトップロード, 30018 [まだ小さな蕾でも]ニシノフラワー 109 | ('私にできること', 1077): ({501077513, 830018001}, [501077513]), 110 | 111 | # 1059 メジロドーベル, 20057 [ふわり、さらり]メジロドーベル, 30180 [この先も!]刻み続ける者たち 112 | ('頼れる先輩', 1059): ({820057001, 830180005}, [820057001]), 113 | } 114 | 115 | KNOWN_OVERRIDES = { 116 | ('秋川理事長のご褒美!', None): 'ついに集まったチームメンバー!', # Aoharu. Manually show the outcome during where the choice happens 117 | 118 | ('女帝vs."帝王"', 1003): '“女帝”vs.“帝王”', 119 | ('支えあいの秘訣', 1004): '支え合いの秘訣', 120 | ('えっアタシのバイト…やばすぎ?', 1007): 'えっアタシのバイト……ヤバすぎ?', 121 | ('挑め、”宿命”', 1008): '挑め、“宿命”', 122 | ('楽しめ!一番!', 1009): '楽しめ! 1番!', 123 | ('女帝と"帝王"', 1018): '“女帝”と“帝王”', 124 | ('女帝と"皇帝"', 1018): '“女帝”と“皇帝”', 125 | ('ラスボスはスペ', 1052): 'ラスボスはスぺ', # ペ in master.mdb is ひらがな... 126 | ('スペの緊急牧場ガイド', 1001): 'スぺの緊急牧場ガイド', # ... and another one... 127 | ('覇王として', 1015): '“覇王”として', 128 | ('麗姿、瞳に焼き付いて', 1018): '麗姿、瞳に焼きついて', 129 | ('すべてはーーーのため', 1038): 'すべては――のため', 130 | ('You’re My Sunshine☆', 1024): 'You\'re My Sunshine☆', 131 | ('With My Whole Heart!', 1024): 'With My Whole Heart!', 132 | ('甦れ!ゴルシ印のソース焼きそば!', 1007): '甦れ! ゴルシ印のソース焼きそば!', 133 | ('08:36/朝寝坊、やばっ', 1040): '08:36/朝寝坊、やばっ', 134 | ('ヒシアマ姐さん奮闘記~問題児編~', 1012): 'ヒシアマ姐さん奮闘記 ~問題児編~', 135 | ('シチースポットを目指して', 1029): '“シチースポット”を目指して', 136 | ('信仰心と親切心が交わる時ーー', 1056): '信仰心と親切心が交わる時――', 137 | ('13:12/昼休み、気合い入れなきゃ', 1040): '13:12/昼休み、気合い入れなきゃ', 138 | ('ヒシアマ姐さん奮闘記~追い込み編~', 1012): 'ヒシアマ姐さん奮闘記 ~追い込み編~', 139 | ('オゥ!トゥナイト・パーティー☆', 1010): 'オゥ! トゥナイト・パーティー☆', 140 | ('皇帝の激励', 1017): '“皇帝”の激励', 141 | ('皇帝の激励', None): '“皇帝”の激励', # From [尊尚親愛]玉座に集いし者たち, but story_id is the same (801017001) 142 | ('#lol #Party! #2nd', 1065): '#lol #Party!! #2nd', 143 | ('検証〜ネコ語は実在するのか?', 1020): '検証~ネコ語は実在するのか?', 144 | ('@DREAM_MAKER', 1005): '@DREAM_MAKER', 145 | ('人生最大の幸運とは', 1005): '人生最大の幸福とは', 146 | ('What a wonderful stage!', 1005): 'What a wonderful stage!', 147 | ('あんしんかばん', 1058): 'あんしんカバン', 148 | ('奏でようWINNING!', 1002): '奏でようWINNING!', 149 | ('推しえて、デジタル先生!', 1019): '“推し”えて、デジタル先生!', 150 | ('あなたの背中を"推し"たくて……', 1019): 'あなたの背中を“推し”たくて……', 151 | ('推しみない愛を推しに!', 1019): '“推し”みない愛を推しに!', 152 | ('Search or Mommy', 1045): 'Search or Mommy', 153 | ('シチーガールの今の気分♪', 1040): '“シチーガール”の今の気分♪', 154 | ('言葉+……', 1033): '言葉+……', 155 | ('”我が弟子”へ', 1072): '“我が弟子”へ', 156 | ('常に、誰かの”師”たれ', 1072): '常に、誰かの“師”たれ', 157 | ('”允許”の重み', 1072): '“允許”の重み', 158 | ('成るか成らぬか”不動心”', 1072): '成るか成らぬか“不動心”', 159 | ('Currens Black', 1038): 'Curren\'s Black', 160 | ('教訓之二:決して撮影を諦めるな', 1010): '教訓之二:決して撮影を諦めるな', 161 | ('チケゾ―配達日記〜蒸気編〜', 1035): 'チケゾー配達日記~蒸気編~', 162 | ('チケゾ―配達日記〜学園編〜', 1035): 'チケゾー配達日記~学園編~', 163 | ('クエスト:撤去作業のお手伝い!', 1050): 'クエスト:撤去作業のお手伝い!', 164 | ('クエスト:演劇部のお手伝い!', 1050): 'クエスト:演劇部のお手伝い!', 165 | ('"シチーガール"になるために', 1029): '“シチーガール”になるために', 166 | ('てきぱき&のびのび', 1100): 'てきぱき&のびのび', 167 | ('悪童、あくなき探究へ', 1043): '悪童、あくなき探求へ', 168 | ('追いつ追われつ(チェイス)は上等', 1094): '“追いつ追われつ”(チェイス)は上等', 169 | ('闘叫(トーキョー)の鬼', 1094): '“闘叫”(トーキョー)の鬼', 170 | ('魔術(マジック)みてぇに', 1094): '“魔術”(マジック)みてぇに', 171 | ('誠心誠意、感謝を込めて', 1063): '誠心誠意、感謝をこめて', 172 | ('”ロマン”を求めて!', 1107): '“ロマン”を求めて!', 173 | ('掴め、ビッグドリーム!', 1107): '掴め、ビッグ・ドリーム!', 174 | ('メジロ’s バックアップ!', 1064): 'メジロ\'s バックアップ!', 175 | ('See Ya! 夢追う友人', 1107): 'See Ya! 夢追う友人', 176 | ('コン・フォーコなアモーレを君に', 1102): 'コン・フオーコなアモーレを君に', 177 | ('アタシだって―—', 1059): 'アタシだって――', 178 | ('Vol.2『脈々と』', 1108): 'Vol.2 『脈々と』', 179 | ('都会で、『おあげんしぇ』!', 1029): '都会で『おあげんしぇ』!', 180 | ('秘密の"れっすん"!', 1029): '秘密の“れっすん”!', 181 | ('巨大ピコーペガサスVSガブガブ大怪獣', 1054): '巨大ビコーペガサスVSガブガブ大怪獣', 182 | ('”最強”と”女帝”', 1108): '“最強”と“女帝”', 183 | ('お疲れさまです……!', 9008): 'お疲れ様です……!', 184 | ('『全力』&『普通』ダイエット!', None): '『全力』&『普通』ダイエット!', 185 | ('あなたと私を繋げるライブ', None): 'あなたと私をつなげるライブ', 186 | } 187 | 188 | 189 | def fetch_gw_upstream(): 190 | r = requests.get(UPSTREAM_DATA_URL) 191 | r.encoding = 'utf-8' 192 | c = r.text 193 | c = c[c.find(UPSTREAM_DATA_HEADER) + len(UPSTREAM_DATA_HEADER) + 1:c.find(UPSTREAM_DATA_FOOTER)] 194 | return ast.literal_eval('[' + c + ']') # A bad hack because Python happens to accept this :( 195 | 196 | 197 | def open_db(path: str) -> sqlite3.Cursor: 198 | connection = sqlite3.connect(path) 199 | return connection.cursor() 200 | 201 | 202 | def read_chara_names(cursor: sqlite3.Cursor) -> dict[str, int]: 203 | cursor.execute("""SELECT "index", text FROM text_data 204 | WHERE category=170""") # Not 6 because of '桐生院葵' 205 | return {row[1]: row[0] for row in cursor.fetchall()} 206 | 207 | 208 | def try_match_event(cursor: sqlite3.Cursor, event_name: str, chara_id: Optional[int], unused_known_overrides: set) \ 209 | -> list[int]: 210 | original_event_name = event_name 211 | # Currently no events use these replaced chars 212 | event_name = event_name.replace('・', '・').replace('~', '~').replace('(', '(').replace(')', ')') 213 | for suffix in EVENT_NAME_SUFFIX_TO_REMOVE: 214 | event_name = event_name.removesuffix(suffix) 215 | 216 | t = (event_name, chara_id) 217 | if t in KNOWN_OVERRIDES: 218 | unused_known_overrides.discard(t) 219 | event_name = KNOWN_OVERRIDES[t] 220 | t = (event_name, chara_id) 221 | 222 | cursor.execute("""SELECT "index" FROM text_data 223 | WHERE category=181 AND text=?""", [event_name]) 224 | possible_story_ids = [row[0] for row in cursor.fetchall()] 225 | 226 | if len(possible_story_ids) == 0: 227 | cursor.execute("""SELECT "index", text FROM text_data 228 | WHERE category=181 AND text LIKE ?""", ['%' + event_name + '%']) 229 | rows = cursor.fetchall() 230 | if len(rows) == 1: 231 | row = rows[0] 232 | if str(row[0]).startswith('50%d' % chara_id) or str(row[0]).startswith('80%d' % chara_id): 233 | # Chara ID matches, just INFO. 234 | logging.info( 235 | "Fuzzily mapped %s for chara %s to %s %s" % (original_event_name, chara_id, row[0], row[1])) 236 | else: 237 | logging.warning( 238 | "Fuzzily mapped %s for chara %s to %s %s" % (original_event_name, chara_id, row[0], row[1])) 239 | return [row[0]] 240 | 241 | logging.warning("Unknown event %s for chara %s" % (original_event_name, chara_id)) 242 | return [] 243 | 244 | if len(possible_story_ids) == 1: 245 | return possible_story_ids 246 | 247 | if event_name == 'ダンスレッスン': 248 | # Just special case this... 249 | story_id = int('50%d506' % chara_id) 250 | if story_id in possible_story_ids: 251 | return [story_id] 252 | 253 | if t in PERMITTED_DUPLICATED_EVENTS: 254 | if set(possible_story_ids) == PERMITTED_DUPLICATED_EVENTS[t]: 255 | return possible_story_ids 256 | 257 | if t in DUPLICATED_EVENTS_DEDUPE: 258 | if set(possible_story_ids) == DUPLICATED_EVENTS_DEDUPE[t][0]: 259 | return DUPLICATED_EVENTS_DEDUPE[t][1] 260 | 261 | logging.warning("More than 1 event for event_name %s for char %s" % (original_event_name, chara_id)) 262 | return [] 263 | 264 | 265 | def match_events(cursor: sqlite3.Cursor, gw_data): 266 | chara_names = read_chara_names(cursor) 267 | 268 | unused_known_overrides = set(KNOWN_OVERRIDES.keys()) 269 | result = {} 270 | low_priority_result = {} 271 | 272 | for row in gw_data: 273 | event_name = unicodedata.normalize('NFC', row['e']) 274 | 275 | event_type = row['c'] # c: chara, s: support card, m: scenario? 276 | if event_type not in {'c', 's', 'm'}: 277 | logging.error('Detected unknown event_type: %s' % row) 278 | 279 | event_chara_name = re.sub(r'\(.+\)', "", row['n']) # remove things like `(新衣装)` 280 | m = re.search('[\u30A0-\u30FF]+', event_chara_name) 281 | if event_chara_name not in EXCLUDED_EVENT_CHARA_NAMES and event_chara_name not in LOW_PRIORITY_CHARA_NAMES: 282 | if m and event_chara_name != '佐岳メイ': 283 | # If it contains some Katakana, just remove all non Katakana chars 284 | event_chara_name = m[0] 285 | if event_chara_name not in chara_names: 286 | logging.warning('Detected unknown event_chara: %s' % row) 287 | chara_id = chara_names.get(event_chara_name) 288 | 289 | if event_name in EXCLUDED_EVENT_NAMES or (event_name, chara_id) in PER_CHARA_EXCLUDE_EVENTS: 290 | continue 291 | 292 | to_update = low_priority_result if event_chara_name in LOW_PRIORITY_CHARA_NAMES else result 293 | 294 | story_ids = try_match_event(cursor, event_name, chara_id, unused_known_overrides) 295 | for story_id in story_ids: 296 | if story_id in to_update: 297 | # Because upstream uses separate entries for support cards R vs SR vs SSR, or different 勝負服 of the same chara. 298 | # For now there is no case where the choices are different than each other, so just ignore. 299 | pass 300 | to_update[story_id] = row 301 | 302 | if len(unused_known_overrides) > 0: 303 | logging.warning('Unused KNOWN_OVERRIDES: %s', unused_known_overrides) 304 | 305 | return low_priority_result | result 306 | 307 | title_formatter = lambda title: title.replace('
L’Arcで発生時:
', '\n') 308 | 309 | text_formatter = lambda text: text.replace('[br]', '\n').replace('
', '\n') 310 | 311 | 312 | def convert_to_proto(events: dict, include_name: bool) -> cjedb_pb2.Database: 313 | db = cjedb_pb2.Database() 314 | for k, v in sorted(events.items()): 315 | e = cjedb_pb2.Event() 316 | e.story_id = k 317 | for choice in v['choices']: 318 | c = cjedb_pb2.Event.Choice() 319 | c.title = title_formatter(choice['n']) 320 | c.text = text_formatter(choice['t']) 321 | e.choices.append(c) 322 | if include_name: 323 | e.story_name = v['e'] 324 | db.events.append(e) 325 | return db 326 | 327 | 328 | def main(): 329 | logging.basicConfig(level=os.environ.get('LOGLEVEL', 'WARNING').upper()) 330 | 331 | parser = argparse.ArgumentParser() 332 | parser.add_argument("--db_path", default="master.mdb") 333 | parser.add_argument("--output", default="cjedb.json") 334 | parser.add_argument("--include_name", action='store_true') 335 | args = parser.parse_args() 336 | 337 | gw_data = fetch_gw_upstream() 338 | cursor = open_db(args.db_path) 339 | 340 | events = match_events(cursor, gw_data) 341 | db = convert_to_proto(events, args.include_name) 342 | 343 | with open(args.output, 'w') as f: 344 | json.dump(json_format.MessageToDict(db), f, ensure_ascii=False, indent=2) 345 | 346 | 347 | if __name__ == '__main__': 348 | main() 349 | --------------------------------------------------------------------------------