├── .gitignore ├── README.md ├── _data └── private │ ├── event_availability_overrides.csv │ ├── gacha_availability_overrides.csv │ └── overrides.csv ├── analytics.py ├── api_endpoints.py ├── app.py ├── cloudflare.txt ├── csvloader.py ├── dispatch.py ├── endpoints.py ├── enums.py ├── models ├── __init__.py ├── base.py └── extra.py ├── requirements.txt ├── starlight ├── __init__.py ├── acquisition.py ├── apiclient.py ├── en.py ├── extra_va_tables.py ├── rijndael.py └── update.py ├── static ├── api.html ├── css │ ├── base.less │ ├── birthday.less │ ├── boxes.less │ ├── main.css │ ├── main.less │ ├── markers.css │ ├── responsive.less │ ├── tables.less │ └── widgets.less ├── dots.html ├── favicon.ico ├── img │ ├── assets │ │ ├── icons │ │ │ ├── allround.png │ │ │ ├── allround@2x.png │ │ │ ├── alternate.png │ │ │ ├── alternate@2x.png │ │ │ ├── balance.png │ │ │ ├── balance@2x.png │ │ │ ├── cboost.png │ │ │ ├── cboost@2x.png │ │ │ ├── cguard.png │ │ │ ├── cguard@2x.png │ │ │ ├── concentrate.png │ │ │ ├── concentrate@2x.png │ │ │ ├── dance.png │ │ │ ├── dance@2x.png │ │ │ ├── encore.png │ │ │ ├── encore@2x.png │ │ │ ├── focus_co.png │ │ │ ├── focus_co@2x.png │ │ │ ├── focus_cu.png │ │ │ ├── focus_cu@2x.png │ │ │ ├── focus_flat.png │ │ │ ├── focus_flat@2x.png │ │ │ ├── focus_pa.png │ │ │ ├── focus_pa@2x.png │ │ │ ├── heal.png │ │ │ ├── heal@2x.png │ │ │ ├── hguard.png │ │ │ ├── hguard@2x.png │ │ │ ├── magic.png │ │ │ ├── magic@2x.png │ │ │ ├── motif.png │ │ │ ├── motif@2x.png │ │ │ ├── mutual.png │ │ │ ├── mutual@2x.png │ │ │ ├── overload.png │ │ │ ├── overload@2x.png │ │ │ ├── plock.png │ │ │ ├── plock@2x.png │ │ │ ├── psb_flick.png │ │ │ ├── psb_flick@2x.png │ │ │ ├── psb_hold.png │ │ │ ├── psb_hold@2x.png │ │ │ ├── psb_slide.png │ │ │ ├── psb_slide@2x.png │ │ │ ├── refrain.png │ │ │ ├── refrain@2x.png │ │ │ ├── scoreup.png │ │ │ ├── scoreup@2x.png │ │ │ ├── skillboost.png │ │ │ ├── skillboost@2x.png │ │ │ ├── sparkle.png │ │ │ ├── sparkle@2x.png │ │ │ ├── symphony.png │ │ │ ├── symphony@2x.png │ │ │ ├── synergy.png │ │ │ ├── synergy@2x.png │ │ │ ├── tuning.png │ │ │ ├── tuning@2x.png │ │ │ ├── visual.png │ │ │ ├── visual@2x.png │ │ │ ├── vocal.png │ │ │ └── vocal@2x.png │ │ ├── msprites_hs │ │ │ ├── balance_sm.png │ │ │ ├── balance_sm@2x.png │ │ │ ├── dance_sm.png │ │ │ ├── dance_sm@2x.png │ │ │ ├── visual_sm.png │ │ │ ├── visual_sm@2x.png │ │ │ ├── vocal_sm.png │ │ │ └── vocal_sm@2x.png │ │ └── msprites_sk │ │ │ ├── allround_sm.png │ │ │ ├── allround_sm@2x.png │ │ │ ├── alternate_sm.png │ │ │ ├── alternate_sm@2x.png │ │ │ ├── cboost_sm.png │ │ │ ├── cboost_sm@2x.png │ │ │ ├── cguard_sm.png │ │ │ ├── cguard_sm@2x.png │ │ │ ├── concentrate_sm.png │ │ │ ├── concentrate_sm@2x.png │ │ │ ├── encore_sm.png │ │ │ ├── encore_sm@2x.png │ │ │ ├── focus_flat_sm.png │ │ │ ├── focus_flat_sm@2x.png │ │ │ ├── focus_sm.png │ │ │ ├── focus_sm@2x.png │ │ │ ├── heal_sm.png │ │ │ ├── heal_sm@2x.png │ │ │ ├── hguard_sm.png │ │ │ ├── hguard_sm@2x.png │ │ │ ├── magic_sm.png │ │ │ ├── magic_sm@2x.png │ │ │ ├── mutual_sm.png │ │ │ ├── mutual_sm@2x.png │ │ │ ├── overload_sm.png │ │ │ ├── overload_sm@2x.png │ │ │ ├── plock_sm.png │ │ │ ├── plock_sm@2x.png │ │ │ ├── psb_flick_sm.png │ │ │ ├── psb_flick_sm@2x.png │ │ │ ├── psb_hold_sm.png │ │ │ ├── psb_hold_sm@2x.png │ │ │ ├── psb_slide_sm.png │ │ │ ├── psb_slide_sm@2x.png │ │ │ ├── refrain_sm.png │ │ │ ├── refrain_sm@2x.png │ │ │ ├── scoreup_sm.png │ │ │ ├── scoreup_sm@2x.png │ │ │ ├── skillboost_sm.png │ │ │ ├── skillboost_sm@2x.png │ │ │ ├── sparkle_sm.png │ │ │ ├── sparkle_sm@2x.png │ │ │ ├── symphony_sm.png │ │ │ ├── symphony_sm@2x.png │ │ │ ├── synergy_sm.png │ │ │ ├── synergy_sm@2x.png │ │ │ ├── tuning_sm.png │ │ │ └── tuning_sm@2x.png │ ├── marker_empty_face.png │ ├── sprites.png │ ├── sprites@2x.png │ ├── sribbons.png │ └── sribbons@2x.png └── js │ ├── home.js │ ├── less.js │ ├── level.js │ ├── modal.js │ ├── sort_table.js │ ├── soundinliner.js │ ├── svex.js │ └── tlinject.js ├── table.py ├── toolchain ├── csvloader.py ├── get_app_ver.py ├── make_contiguous_gacha.py ├── make_event_lookup_table.py ├── models ├── name_finder.py ├── starlight ├── to_roma.py └── update_rich_history.py ├── webui ├── card.html ├── chara.html ├── debug_view_database.html ├── error.html ├── ext_gacha_table.html ├── generictable.html ├── header.html ├── history.html ├── main.html ├── minitable.html ├── partials │ ├── availability_box.html │ ├── card_box.html │ ├── chara_box.html │ ├── footer.html │ ├── frontpage_text.html │ ├── hist_event.html │ ├── hist_gacha.html │ ├── hist_new_ns.html │ ├── new_list_partial.html │ └── va_table_partial.html └── spriteviewer.html └── webutil.py /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .env/ 3 | __pycache__/ 4 | 5 | _gx 6 | static/sync 7 | devmode 8 | analytics.py 9 | toolchain/messy_finder.py 10 | webui/room_sim_main.html 11 | static/room 12 | static/js/rsm.js 13 | static/css/rsm.less 14 | ap.py 15 | fuzzyfinder.py 16 | incertus_key 17 | run_gx_all 18 | _data/transient 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # sparklebox (2.0!) 2 | 3 | https://starlight.kirara.ca/ 4 | 5 | ##### Getting started 6 | 7 | Make a virtualenv (the app requires **at least Python 3.5**, and is tested with 3.7): 8 | 9 | virtualenv --python=python3 .env 10 | source .env/bin/activate 11 | pip install -r requirements.txt 12 | 13 | Grab a copy of ENAMDICT from http://www.csse.monash.edu.au/~jwb/enamdict_doc.html , 14 | then convert it to UTF-8 so name_finder can use it: 15 | 16 | zcat enamdict.gz | iconv -f eucjp -t utf8 > _data/private/enamdictu 17 | 18 | Then configure the environment variables (I suggest saving these to a file as 19 | you go, so you can restore them later). 20 | 21 | # Development mode disables Tornado's internal caching as well as 22 | # HTTPS enforcement. 23 | export DEV=1 24 | export TRANSIENT_DATA_DIR="_data/transient" 25 | 26 | mkdir -p $TRANSIENT_DATA_DIR 27 | # Note the lack of $. This is the name of the variable that has 28 | # the transient data dir. 29 | export TRANSIENT_DIR_POINTER=TRANSIENT_DATA_DIR 30 | export DATABASE_CONNECT='sqlite:///'$TRANSIENT_DATA_DIR'/ss.sqlite3' 31 | export TLABLE_SALT='bang on the keyboard for a random string' 32 | export IMAGE_HOST='https://static.myderesutesite.com' 33 | 34 | If you do not know values for `VC_ACCOUNT`, `VC_AES_KEY`, and `VC_SID_SALT`, 35 | start the app with a version number as the first argument. 36 | 37 | python3 app.py [59234863] 38 | 39 | That's obviously not the correct version number, but you can find it on 40 | [any](https://starlight.kirara.ca/) of the [sites](http://usamin.info/) which 41 | have [them](https://boards.4chan.org/vg/catalog#s=idolm@ster). Alternatively, 42 | you can sniff network traffic using a proxy like Charles. 43 | 44 | Otherwise, set those environment variables and run. 45 | 46 | python3 app.py 47 | 48 | The server will download the current truth automatically, then exit. 49 | You will then be able to run the app. 50 | 51 | ##### Configuration 52 | 53 | The following environment variables are used: 54 | 55 | ``` 56 | $DEV - enables various development features, such as 57 | * /tl_debug, /db/... endpoints 58 | * No caching of templates 59 | * No HTTPS enforcement 60 | * No static file caching 61 | 62 | $TLABLE_SALT - Security salt for translation tokens. It prevents users from 63 | spamming /send_tl endpoint with strings that never occur. 64 | 65 | $DATABASE_CONNECT - Connection string for the translation database. Follows 66 | SQLAlchemy syntax, and you must have the right package installed to talk to 67 | the particular kind of database engine you use. 68 | 69 | $IMAGE_HOST - Prepended to all static content, discussed below. 70 | 71 | $ADDRESS - IP address to bind to. Usually you want 0.0.0.0 to bind all interfaces. 72 | 73 | $PORT - Port to listen on (for HTTP/HTTPS). Defaults to 5000. 74 | 75 | $VC_ACCOUNT - Credentials for automatic updating, in the form user_id:viewer_id:udid. 76 | 77 | $VC_SID_SALT, $VC_AES_KEY - Client secrets used for automatic updating. 78 | 79 | $VC_APP_VER - Game version (not data version), e.g. "1.9.1". Used for automatic updating. 80 | The game will reject version checks with an outdated client, so it's important to 81 | keep this up to date. 82 | 83 | $VC_UNITY_VER - Unity engine version, e.g. "5.4.5p1". 84 | 85 | $TLE_DISABLE_CACHES - Disable local caching of TranslationEngine data. Set this to a 86 | non-blank string if DB query speed doesn't matter (e.g. you use SQLite, or mysqld 87 | is running on the same server as the app) 88 | 89 | $DISABLE_AUTO_UPDATES - Disables the automatic updater even if VC_* are set. 90 | 91 | $TLE_TABLE_PREFIX - Prefix for table names in TranslationSQL. Defaults to 'ss'. 92 | 93 | ``` 94 | 95 | For the `IMAGE_HOST` environment variable, you should use one of these 96 | in most cases: 97 | 98 | - `https://hoshimoriuta.kirara.ca` (the public image server for starlight.kirara.ca) 99 | 100 | The styles are written in Less. If the environment variable DEV is set, 101 | changes to the less files will be rendered live using less.js; 102 | you don't need to run lessc until you're done changing things. 103 | 104 | ##### Build static directory 105 | 106 | GX is no longer part of this repo. Use SBJK to push deltas to cdn going forward. 107 | 108 | ### License 109 | 110 | Unless the file says otherwise, the files in this repo are released 111 | under the BSD license: 112 | 113 | Copyright (c) 2015 - 2016, The Holy Constituency of the Summer Triangle. 114 | All rights reserved. 115 | 116 | Redistribution and use in source and binary forms, with or without 117 | modification, are permitted provided that the following conditions are met: 118 | 119 | * Redistributions of source code must retain the above copyright 120 | notice, this list of conditions and the following disclaimer. 121 | * Redistributions in binary form must reproduce the above copyright 122 | notice, this list of conditions and the following disclaimer in the 123 | documentation and/or other materials provided with the distribution. 124 | * Neither the name of the Holy Constituency of the Summer Triangle nor the 125 | names of its contributors may be used to endorse or promote products 126 | derived from this software without specific prior written permission. 127 | 128 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 129 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 130 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 131 | DISCLAIMED. IN NO EVENT SHALL THE HOLY CONSTITUENCY OF THE SUMMER TRIANGLE 132 | BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 133 | CONSEQUENTIAL DAMAGES 134 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 135 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 136 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 137 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 138 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 139 | -------------------------------------------------------------------------------- /_data/private/event_availability_overrides.csv: -------------------------------------------------------------------------------- 1 | event_id,reward_id 2 | 1003,300109 3 | 1003,300111 4 | # KLab fucked up and assigned the rewards for 5 | # Snow Wings (1004) to Orange Sapphire (1003) instead 6 | 1004,200129 7 | 1004,300135 8 | 1012,300169 9 | 1012,200277 -------------------------------------------------------------------------------- /_data/private/gacha_availability_overrides.csv: -------------------------------------------------------------------------------- 1 | reward_id,limited_flag 2 | 100363,0 -------------------------------------------------------------------------------- /_data/private/overrides.csv: -------------------------------------------------------------------------------- 1 | "chara_id","kanji","kanji_spaced","kana_spaced","conventional" 2 | 120,"太田優","太田 優","おおた ゆう","Ohta Yuu" 3 | 131,"宮本フレデリカ","宮本 フレデリカ","みやもと ふれでりか","Miyamoto Frederica" 4 | 139,"兵藤レナ","兵藤 レナ","ひょうどう れな","Hyodo Rena" 5 | 158,"クラリス","クラリス","くらりす","Clarice" 6 | 190,"八神マキノ","八神 マキノ","やがみ まきの","Yagami Makino" 7 | 191,"ライラ","ライラ","らいら","Layla" 8 | 193,"ヘレン","ヘレン","へれん","Helen" 9 | 227,"アナスタシア","アナスタシア","あなすたしあ","Anastasia" 10 | 273,"ナターリア","ナターリア","なたーりあ","Natalia" 11 | 287,"メアリー・コクラン","メアリー・コクラン","めありー・こくらん","Mary Cochran" 12 | 135,"楊菲菲","楊 菲菲","やお ふぇいふぇい","Yao Feifei" 13 | 214,"白坂小梅","白坂 小梅","しらさか こうめ","Shirasaka Koume" 14 | 164,"乙倉悠貴","乙倉 悠貴","おとくら ゆうき","Otokura Yuuki" 15 | 267,"城ヶ崎美嘉","城ヶ崎 美嘉","じょうがさき みか","Jougasaki Mika" 16 | 268,"城ヶ崎莉嘉","城ヶ崎 莉嘉","じょうがさき りか","Jougasaki Rika" 17 | 156,"栗原ネネ","栗原 ネネ","くりはら ねね","Kurihara Nene" 18 | 202,"ケイト","ケイト","けいと","Kate" 19 | 170,"桐野アヤ","桐野 アヤ","きりの あや","Kirino Aya" 20 | 306,"夢見りあむ","夢見 りあむ","ゆめみ りあむ","Yumemi Riamu" 21 | -------------------------------------------------------------------------------- /analytics.py: -------------------------------------------------------------------------------- 1 | import tornado.httputil 2 | 3 | class Analytics(object): 4 | """ Called by most endpoints. Log, analyze, whatever here. """ 5 | def analyze_request(self, request: tornado.httputil.HTTPServerRequest, 6 | endpoint_class: str, cpar: dict = None): 7 | pass 8 | -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | import locale 2 | locale.setlocale(locale.LC_ALL, "en_US.UTF-8") 3 | 4 | import tornado.httpserver 5 | import tornado.ioloop 6 | import tornado.web 7 | import os 8 | import tornado.options 9 | import json 10 | import ipaddress 11 | import functools 12 | import subprocess 13 | import user_agents 14 | from collections import namedtuple 15 | 16 | import models 17 | import dispatch 18 | import endpoints 19 | import api_endpoints 20 | import enums 21 | import starlight 22 | import analytics 23 | import webutil 24 | from starlight import private_data_path 25 | 26 | def early_init(): 27 | os.chdir(os.path.dirname(os.path.realpath(__file__))) 28 | 29 | if not os.environ.get("DISABLE_HTTPS_ENFORCEMENT", "") and not os.environ.get("DEV", ""): 30 | # production mode: force https usage due to local storage issues 31 | # also we don't want the NSA knowing you play chinese cartoon games 32 | def _swizzle_RequestHandler_prepare(self): 33 | if self.request.protocol != "https": 34 | self.redirect( 35 | "https://{0}{1}".format(self.request.host, self.request.uri)) 36 | tornado.web.RequestHandler.prepare = _swizzle_RequestHandler_prepare 37 | 38 | if os.environ.get("BEHIND_CLOUDFLARE") == "1": 39 | cloudflare_ranges = [] 40 | 41 | with open("cloudflare.txt", "r") as cf: 42 | for line in cf: 43 | cloudflare_ranges.append(ipaddress.ip_network(line.strip())) 44 | 45 | _super_RequestHandler_prepare2 = tornado.web.RequestHandler.prepare 46 | 47 | def _swizzle_RequestHandler_prepare2(self): 48 | for net in cloudflare_ranges: 49 | if ipaddress.ip_address(self.request.remote_ip) in net: 50 | if "CF-Connecting-IP" in self.request.headers: 51 | self.request.remote_ip = self.request.headers[ 52 | "CF-Connecting-IP"] 53 | break 54 | _super_RequestHandler_prepare2(self) 55 | 56 | tornado.web.RequestHandler.prepare = _swizzle_RequestHandler_prepare2 57 | 58 | _super_RequestHandler_prepare3 = tornado.web.RequestHandler.prepare 59 | def _swizzle_RequestHandler_prepare3(self): 60 | self.request.is_low_bandwidth = 0 61 | if "User-Agent" in self.request.headers: 62 | ua = user_agents.parse(self.request.headers["User-Agent"]) 63 | if ua.is_mobile or ua.is_tablet: 64 | self.request.is_low_bandwidth = 1 65 | 66 | _super_RequestHandler_prepare3(self) 67 | tornado.web.RequestHandler.prepare = _swizzle_RequestHandler_prepare3 68 | 69 | def main(): 70 | starlight.init() 71 | early_init() 72 | in_dev_mode = os.environ.get("DEV") 73 | image_server = os.environ.get("IMAGE_HOST", "") 74 | tornado.options.parse_command_line() 75 | application = tornado.web.Application(dispatch.ROUTES, 76 | template_path="webui", 77 | static_path="static", 78 | image_host=image_server, 79 | debug=in_dev_mode, 80 | is_dev=in_dev_mode, 81 | 82 | tle=models.TranslationEngine(starlight), 83 | enums=enums, 84 | starlight=starlight, 85 | tlable=webutil.tlable, 86 | webutil=webutil, 87 | analytics=analytics.Analytics(), 88 | # Change every etag when the server restarts, in case we change what the output looks like. 89 | instance_random=os.urandom(8)) 90 | http_server = tornado.httpserver.HTTPServer(application, xheaders=1) 91 | 92 | addr = os.environ.get("ADDRESS", "0.0.0.0") 93 | port = int(os.environ.get("PORT", 5000)) 94 | 95 | http_server.listen(port, addr) 96 | print("Current APP_VER:", os.environ.get("VC_APP_VER", 97 | "1.9.1 (warning: Truth updates will fail in the future if an accurate VC_APP_VER " 98 | "is not set. Export VC_APP_VER to suppress this warning.)")) 99 | print("Ready.") 100 | tornado.ioloop.IOLoop.current().start() 101 | 102 | if __name__ == "__main__": 103 | main() 104 | -------------------------------------------------------------------------------- /cloudflare.txt: -------------------------------------------------------------------------------- 1 | 103.21.244.0/22 2 | 103.22.200.0/22 3 | 103.31.4.0/22 4 | 104.16.0.0/12 5 | 108.162.192.0/18 6 | 131.0.72.0/22 7 | 141.101.64.0/18 8 | 162.158.0.0/15 9 | 172.64.0.0/13 10 | 173.245.48.0/20 11 | 188.114.96.0/20 12 | 190.93.240.0/20 13 | 197.234.240.0/22 14 | 198.41.128.0/17 15 | 199.27.128.0/21 16 | 2400:cb00::/32 17 | 2405:8100::/32 18 | 2405:b500::/32 19 | 2606:4700::/32 20 | 2803:f800::/32 21 | -------------------------------------------------------------------------------- /csvloader.py: -------------------------------------------------------------------------------- 1 | import csv 2 | from collections import namedtuple 3 | 4 | def clean_value(val): 5 | if val is None: 6 | return None 7 | try: 8 | return int(val) 9 | except ValueError: 10 | return val.replace("\\n", "\n") 11 | 12 | # Return a function taking a tuple that right-pads the tuple to 13 | # `to_n_columns` items with the empty string. 14 | def pad_value_list(to_n_columns): 15 | def apply(to_list): 16 | l = list(to_list) 17 | l += [""] * (to_n_columns - len(l)) 18 | return tuple(l) 19 | return apply 20 | 21 | # Load a database file (equivalent of treasurebox arks) 22 | # For runtime-computed parameters, pass a function into kwargs 23 | # for the attribute name you want, the function will be called 24 | # with the loaded object, and the result added under the kwarg name. 25 | # This is particularly useful for joining multiple DBs with a 26 | # foreign-key relationship. 27 | 28 | # Ex. >>> data = load_db_file("some.csv", myattr=lambda raw: raw.id + 1) 29 | # >>> print(data[0]) 30 | # (id=0, myattr=1) 31 | def load_db_file(file, **kwargs): 32 | class_name = file.split("/")[-1].rsplit(".", 1)[0] + "_t" 33 | 34 | with open(file, "r") as cin: 35 | reader = csv.reader(cin) 36 | fields = next(reader) 37 | raw_field_len = len(fields) 38 | # print(fields) 39 | the_raw_type = namedtuple("_" + class_name, fields) 40 | 41 | keys = list(kwargs.keys()) 42 | for key in keys: 43 | fields.append(key) 44 | 45 | the_type = namedtuple(class_name, fields) 46 | padder = pad_value_list(raw_field_len) 47 | 48 | # exclude empty lines and almost-empty lines (where all values are whitespace) 49 | for val_list in filter(lambda list: len(list) != 0 and any(map(str.split, list)), reader): 50 | # pad partial rows to N columns (issue 7) 51 | val_list = padder(val_list) 52 | temp_obj = the_raw_type(*map(clean_value, val_list)) 53 | try: 54 | extvalues = tuple(kwargs[key](temp_obj) for key in keys) 55 | except Exception: 56 | raise RuntimeError( 57 | "Uncaught exception while filling stage2 data for {0}. Are you missing data?".format(temp_obj)) 58 | yield the_type(*temp_obj + extvalues) 59 | 60 | # Load a database file, and return a dict keyed by the content of the 61 | # key_col parameter. This performs the same runtime-computing as 62 | # load_db_file. 63 | 64 | # Ex. >>> data = load_keyed_db_file("some.csv") 65 | # >>> print(data[0]) 66 | # (id=13, another_column="string") 67 | # >>> print(data) 68 | # {13: (id=13, another_column="string")} 69 | def load_keyed_db_file(file, key_col=0, **kwargs): 70 | ret_dic = {} 71 | tab = load_db_file(file, **kwargs) 72 | for thing in tab: 73 | ret_dic[thing[0]] = thing 74 | 75 | return ret_dic 76 | 77 | if __name__ == "__main__": 78 | import sys 79 | want_keys = sys.argv[2:] 80 | file = sys.argv[1] 81 | for entry in load_db_file(file): 82 | print(*(getattr(entry, key) for key in want_keys)) 83 | -------------------------------------------------------------------------------- /dispatch.py: -------------------------------------------------------------------------------- 1 | import tornado.web 2 | import json 3 | import os 4 | import starlight 5 | import time 6 | try: 7 | from plop.collector import Collector, PlopFormatter 8 | except ImportError: 9 | pass 10 | 11 | ROUTES = [] 12 | 13 | def conditional_route(yes, reason, *regexes): 14 | if yes: 15 | return route(*regexes) 16 | else: 17 | print(reason) 18 | return lambda func: func 19 | 20 | 21 | def route(*regexes): 22 | def wrapper(handler): 23 | for regex in regexes: 24 | ROUTES.append((regex, handler)) 25 | return handler 26 | return wrapper 27 | 28 | 29 | def expose_static_json(path, an_object): 30 | precomputed = json.dumps(an_object) 31 | 32 | class ret(tornado.web.RequestHandler): 33 | def get(self): 34 | self.set_header("Content-Type", "application/json") 35 | self.set_header("Cache-Control", "no-cache") 36 | self.set_header("Expires", "0") 37 | self.write(precomputed) 38 | 39 | route(path)(ret) 40 | 41 | 42 | def dev_mode_only(wrapped): 43 | if os.environ.get("DEV", ""): 44 | return wrapped 45 | 46 | class not_dev_error(tornado.web.RequestHandler): 47 | def get(self, *args): 48 | self.set_status(400) 49 | self.set_header("Content-Type", "text/plain; charset=utf-8") 50 | self.write( 51 | "The requested endpoint is only available in development mode.") 52 | 53 | return not_dev_error 54 | 55 | 56 | class HandlerSyncedWithMaster(tornado.web.RequestHandler): 57 | def prepare(self): 58 | starlight.data.reset_statistics() 59 | self.did_trigger_update = starlight.update.check_version() 60 | 61 | super().prepare() 62 | 63 | if self.get_argument("profile", None) and os.environ.get("ALLOW_PROFILING"): 64 | self.collector = Collector() 65 | self.collector.start() 66 | 67 | def finish(self, *args, **kw): 68 | super().finish(*args, **kw) 69 | 70 | if self.get_argument("profile", None) and os.environ.get("ALLOW_PROFILING"): 71 | self.collector.stop() 72 | formatter = PlopFormatter(max_stacks=9001) 73 | if self.collector.samples_taken: 74 | formatter.store(self.collector, "{0}_{1}.profile".format(self.__class__.__name__, time.time())) 75 | -------------------------------------------------------------------------------- /enums.py: -------------------------------------------------------------------------------- 1 | import models 2 | 3 | def enum(kv): 4 | i = iter(kv) 5 | dic = dict(zip(i, i)) 6 | rev = {v: k for k, v in dic.items()} 7 | 8 | def f(key): 9 | return dic.get(key, "".format(key)) 10 | def _reverse_enum(val): 11 | return rev[val] 12 | f.value_for_description = _reverse_enum 13 | return f 14 | 15 | rarity = enum([ 16 | 1, "Normal", 17 | 2, "Normal+", 18 | 3, "Rare", 19 | 4, "Rare+", 20 | 5, "SR", 21 | 6, "SR+", 22 | 7, "SSR", 23 | 8, "SSR+", 24 | ]) 25 | 26 | attribute = enum([ 27 | 1, "Cute", 28 | 2, "Cool", 29 | 3, "Passion", 30 | 4, "Office", 31 | ]) 32 | 33 | skill_type = enum([ 34 | 1, "Perfect Score Bonus", 35 | 2, "Score Bonus", 36 | 3, "Score Bonus", 37 | 38 | 4, "Combo Bonus", 39 | 40 | 5, "Lesser Perfect Lock", 41 | 6, "Greater Perfect Lock", 42 | 7, "Extreme Perfect Lock", 43 | 8, "Unconditional Perfect Lock", 44 | 45 | 9, "Combo Guard", 46 | 10, "Greater Combo Guard", 47 | 11, "Unconditional Combo Guard", 48 | 49 | 12, "Life Guard", 50 | 13, "Unconditional Healer", 51 | 14, "Overload", 52 | 53 | 15, "Concentration", 54 | 16, "Encore", 55 | 56 | 17, "Healer", 57 | 18, "Healer", 58 | 19, "Healer", 59 | 60 | 20, "Skill Boost", 61 | 62 | 21, "Cute Focus", 63 | 22, "Cool Focus", 64 | 23, "Passion Focus", 65 | 66 | 24, "All-Round", 67 | 25, "Life Sparkle", 68 | 26, "Tricolor Synergy", 69 | 27, "Coordinate", 70 | 28, "Perfect Score Bonus", 71 | 29, "Perfect Score Bonus", 72 | 30, "Perfect Score Bonus", 73 | 31, "Tuning", 74 | 75 | 32, "Cute Ensemble", 76 | 33, "Cool Ensemble", 77 | 34, "Passion Ensemble", 78 | 79 | 35, "Vocal Motif", 80 | 36, "Dance Motif", 81 | 37, "Visual Motif", 82 | 83 | 38, "Tricolor Symphony", 84 | 39, "Alternate", 85 | 40, "Refrain", 86 | 87 | 41, "Cinderella Magic", 88 | 42, "Mutual", 89 | 43, "Overdrive", 90 | 44, "Tricolor Spike", 91 | 92 | 45, "Dominant Harmony", 93 | 46, "Dominant Harmony", 94 | 47, "Dominant Harmony", 95 | 48, "Dominant Harmony", 96 | 49, "Dominant Harmony", 97 | 50, "Dominant Harmony", 98 | 99 | 51, "Cinderella Present", 100 | ]) 101 | 102 | skill_probability = enum([ 103 | 2, "small", 104 | 3, "fair", 105 | 4, "high", 106 | ]) 107 | 108 | skill_length_type = enum([ 109 | 3, "short", 110 | 4, "medium", 111 | 5, "long", 112 | ]) 113 | 114 | lskill_target = enum([ 115 | 1, "all Cute", 116 | 2, "all Cool", 117 | 3, "all Passion", 118 | 4, "all", 119 | ]) 120 | 121 | lskill_effective_target = enum([ 122 | 1, "ca_cute", 123 | 2, "ca_cool", 124 | 3, "ca_passion", 125 | 4, "ca_all", 126 | ]) 127 | 128 | lskill_param = enum([ 129 | 1, "the Vocal appeal", 130 | 2, "the Visual appeal", 131 | 3, "the Dance appeal", 132 | 4, "all appeals", 133 | 5, "the life", 134 | 6, "the skill probability", 135 | ]) 136 | 137 | lskill_effective_param = enum([ 138 | 1, "ce_vocal", 139 | 2, "ce_visual", 140 | 3, "ce_dance", 141 | 4, "ce_anyappeal", 142 | 5, "ce_life", 143 | 6, "ce_skill", 144 | ]) 145 | 146 | api_char_type = enum([ 147 | 1, "cute", 148 | 2, "cool", 149 | 3, "passion", 150 | 4, "office" 151 | ]) 152 | 153 | lskill_target_attr = enum([ 154 | 1, "cute", 155 | 2, "cool", 156 | 3, "passion", 157 | 4, "all", 158 | ]) 159 | 160 | lskill_target_param = enum([ 161 | 1, "vocal", 162 | 2, "visual", 163 | 3, "dance", 164 | 4, "all", 165 | 5, "life", 166 | 6, "skill_probability", 167 | ]) 168 | 169 | skill_class = enum([ 170 | 1, "scoreup", 171 | 2, "scoreup", 172 | 3, "scoreup", 173 | 174 | 4, "cboost", 175 | 176 | 5, "plock", 177 | 6, "plock", 178 | 7, "plock", 179 | 8, "plock", 180 | 181 | 9, "cguard", 182 | 10, "cguard", 183 | 11, "cguard", 184 | 185 | 12, "hguard", 186 | 13, "heal", 187 | 14, "overload", 188 | 189 | 15, "concentrate", 190 | 16, "encore", 191 | 192 | 17, "heal", 193 | 18, "heal", 194 | 19, "heal", 195 | 196 | 20, "skillboost", 197 | 198 | 21, "focus", 199 | 22, "focus", 200 | 23, "focus", 201 | 202 | 24, "allround", 203 | 25, "sparkle", 204 | 26, "synergy", 205 | 27, "focus_flat", 206 | 28, "psb_hold", 207 | 29, "psb_flick", 208 | 30, "psb_slide", 209 | 31, "tuning", 210 | 211 | 32, "skillboost", 212 | 33, "skillboost", 213 | 34, "skillboost", 214 | 35, "motif", 215 | 36, "motif", 216 | 37, "motif", 217 | 218 | 38, "symphony", 219 | 39, "alternate", 220 | 40, "refrain", 221 | 222 | 41, "magic", 223 | 224 | 42, "mutual", 225 | 43, "overdrive", 226 | 44, "spike", 227 | 228 | 45, "dominant", 229 | 46, "dominant", 230 | 47, "dominant", 231 | 48, "dominant", 232 | 49, "dominant", 233 | 50, "dominant", 234 | 235 | 51, "present", 236 | ]) 237 | 238 | stat_dot = enum([ 239 | 1, "visual", 240 | 2, "dance", 241 | 3, "vocal", 242 | 4, "balance", 243 | 5, "balance", 244 | 6, "balance", 245 | 7, "balance", 246 | ]) 247 | 248 | stat_en = enum([ 249 | 1, "This card's highest stat is Visual", 250 | 2, "This card's highest stat is Dance", 251 | 3, "This card's highest stat is Vocal", 252 | 4, "This card's stats are mostly balanced", 253 | 5, "This card's stats are mostly balanced (Visual high)", 254 | 6, "This card's stats are mostly balanced (Dance high)", 255 | 7, "This card's stats are mostly balanced (Vocal high)" 256 | ]) 257 | 258 | floor_rarity = enum([ 259 | 1, "n", 260 | 2, "n", 261 | 3, "r", 262 | 4, "r", 263 | 5, "sr", 264 | 6, "sr", 265 | 7, "ssr", 266 | 8, "ssr", 267 | ]) 268 | 269 | he_event_class = enum([ 270 | models.EVENT_TYPE_TOKEN, "hev_token", 271 | models.EVENT_TYPE_CARAVAN, "hev_caravan", 272 | models.EVENT_TYPE_GROOVE, "hev_groove", 273 | models.EVENT_TYPE_PARTY, "hev_party", 274 | models.EVENT_TYPE_TOUR, "hev_tour", 275 | ]) 276 | 277 | # TODO need enum defs for 278 | # constellation 279 | # blood_type 280 | # hand 281 | # personality 282 | # home_town 283 | -------------------------------------------------------------------------------- /models/base.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | from datetime import datetime 4 | from sqlalchemy.ext.declarative import declarative_base 5 | from sqlalchemy import Column, Integer, String, UnicodeText, LargeBinary, SmallInteger 6 | 7 | def utext(): 8 | # hack 9 | if not os.getenv("DATABASE_CONNECT").startswith("mysql"): 10 | return UnicodeText() 11 | else: 12 | return UnicodeText(collation="utf8_bin") 13 | 14 | TABLE_PREFIX = os.getenv("TLE_TABLE_PREFIX", None) 15 | if TABLE_PREFIX is None: 16 | print("Warning: env variable TLE_TABLE_PREFIX unset. Defaulting to 'ss'. " 17 | "(Set TLE_TABLE_PREFIX to silence this warning.)") 18 | TABLE_PREFIX = "ss" 19 | 20 | Base = declarative_base() 21 | 22 | class TranslationEntry(Base): 23 | __tablename__ = TABLE_PREFIX + "_translation" 24 | 25 | id = Column(Integer, primary_key=True, autoincrement=True) 26 | key = Column(utext()) 27 | english = Column(utext()) 28 | submitter = Column(String(50)) 29 | submit_utc = Column(Integer) 30 | 31 | def __repr__(self): 32 | return "".format(x=self) 33 | 34 | class TranslationCache(Base): 35 | __tablename__ = TABLE_PREFIX + "_translation_cache" 36 | 37 | id = Column(Integer, primary_key=True, autoincrement=True) 38 | key = Column(utext(), index=True) 39 | english = Column(utext()) 40 | 41 | def __repr__(self): 42 | return "".format(x=self) 43 | 44 | class GachaRewardEntry(Base): 45 | __tablename__ = TABLE_PREFIX + "_gacha_available_ex" 46 | 47 | gacha_id = Column(Integer, primary_key=True, nullable=False, autoincrement=False) 48 | step_num = Column(Integer) 49 | reward_id = Column(Integer, primary_key=True, nullable=False, autoincrement=False) 50 | recommend_order = Column(Integer) 51 | limited_flag = Column(Integer, primary_key=True, autoincrement=False) 52 | 53 | class GachaPresenceEntry(Base): 54 | __tablename__ = TABLE_PREFIX + "_gacha_contiguous_presence" 55 | 56 | rowid = Column(Integer, primary_key=True) 57 | card_id = Column(Integer, nullable=False) 58 | gacha_id_first = Column(Integer, nullable=False) 59 | gacha_id_last = Column(Integer, nullable=False) 60 | avail_start = Column(Integer, nullable=False) 61 | avail_end = Column(Integer, nullable=False) 62 | 63 | class EventLookupEntry(Base): 64 | __tablename__ = TABLE_PREFIX + "_event_lookup" 65 | 66 | card_id = Column(Integer, primary_key=True, nullable=False, autoincrement=False) 67 | event_id = Column(Integer, primary_key=True, nullable=False) 68 | acquisition_type = Column(Integer, primary_key=True, nullable=False) 69 | 70 | class GachaLookupEntry(Base): 71 | __tablename__ = TABLE_PREFIX + "_gacha_lookup" 72 | 73 | card_id = Column(Integer, primary_key=True, nullable=False, autoincrement=False) 74 | first_gacha_id = Column(Integer, nullable=False) 75 | last_gacha_id = Column(Integer, nullable=False) 76 | first_available = Column(Integer) 77 | last_available = Column(Integer) 78 | is_limited = Column(Integer, primary_key=True, nullable=False) 79 | 80 | class HistoryEventEntry(Base): 81 | __tablename__ = TABLE_PREFIX + "_history_ex" 82 | 83 | # event type/id 84 | descriptor = Column(Integer, primary_key=True) 85 | extra_type_info = Column(SmallInteger) 86 | # card ids added, as a comma separated list 87 | added_cards = Column(utext()) 88 | event_name = Column(utext()) 89 | 90 | start_time = Column(Integer) 91 | end_time = Column(Integer) 92 | 93 | # the top 4 bits of .descriptor denote the type of history 94 | # event, the others are for the underlying event id, gacha id, 95 | # etc. 96 | 97 | def type(self): 98 | return (self.descriptor & 0xF0000000) >> 28 99 | 100 | # gacha id, event id, etc 101 | def referred_id(self): 102 | return self.descriptor & 0x0FFFFFFF 103 | 104 | def ensure_parsed_changelist(self): 105 | if not hasattr(self, "parsed_changelist"): 106 | if self.added_cards: 107 | self.parsed_changelist = json.loads(self.added_cards) 108 | else: 109 | self.parsed_changelist = {} 110 | 111 | return self.parsed_changelist 112 | 113 | def category_card_list(self, cat): 114 | return self.ensure_parsed_changelist().get(cat, []) 115 | 116 | def card_list(self): 117 | cl = self.ensure_parsed_changelist() 118 | 119 | ret = [] 120 | for k in sorted(cl.keys()): 121 | ret.extend(cl[k]) 122 | 123 | return ret 124 | 125 | def card_list_has_more_than_one_category(self): 126 | return len(self.ensure_parsed_changelist()) > 1 127 | 128 | def card_urlspec(self): 129 | return ",".join( map(str, self.card_list()) ) 130 | 131 | def start_dt_string(self): 132 | return self.start_datetime().strftime("%Y-%m-%d") 133 | 134 | def end_dt_string(self): 135 | return self.end_datetime().strftime("%Y-%m-%d") 136 | 137 | def start_datetime(self): 138 | return datetime.fromtimestamp(self.start_time) 139 | 140 | def end_datetime(self): 141 | return datetime.fromtimestamp(self.end_time) 142 | 143 | def length_in_days(self): 144 | secs = (self.end_time - self.start_time) 145 | return secs / (60 * 60 * 24) 146 | 147 | # -- extra_type_info bitfields for events: 148 | # (these are binary digits, not hex) 149 | # SSAA0000 TTTTTTTT 150 | # SS: Focus stat (for grooves), 0-4 151 | # AA: Attribute (for tokens), 0-4 152 | # TTT: Event type, 0-7 153 | 154 | def event_type(self): 155 | return self.extra_type_info & 0xFF 156 | 157 | # don't use these for now 158 | # def event_attribute(self): 159 | # return (self.extra_type_info & 0x18) >> 3 160 | # 161 | # def groove_stat(self): 162 | # return (self.extra_type_info & 0x60) >> 5 163 | 164 | # -- extra_type_info bitfields for gachas: 165 | # (these are binary digits, not hex) 166 | # 00000000 0000000L 167 | # L: limited?, 0-1 168 | 169 | def gacha_is_limited(self): 170 | return self.extra_type_info & 0x1 171 | -------------------------------------------------------------------------------- /models/extra.py: -------------------------------------------------------------------------------- 1 | from collections import namedtuple 2 | 3 | HISTORY_TYPE_EVENT = 2 4 | HISTORY_TYPE_GACHA = 3 5 | HISTORY_TYPE_ADD_N = 4 6 | HISTORY_TYPE_EVENT_END = 5 7 | HISTORY_TYPE_GACHA_END = 6 8 | 9 | EVENT_TYPE_TOKEN = 0 10 | EVENT_TYPE_CARAVAN = 1 11 | EVENT_TYPE_GROOVE = 2 12 | EVENT_TYPE_PARTY = 3 13 | EVENT_TYPE_TOUR = 4 14 | 15 | EVENT_ATTR_NONE = 0 16 | EVENT_ATTR_CU = 1 17 | EVENT_ATTR_CO = 2 18 | EVENT_ATTR_PA = 3 19 | 20 | EVENT_STAT_NONE = 0 21 | EVENT_STAT_VO = 1 22 | EVENT_STAT_VI = 2 23 | EVENT_STAT_DA = 3 24 | 25 | unknown_gacha_t = namedtuple("unknown_gacha_t", ("name")) 26 | Gap = namedtuple("Gap", ("start", "end")) 27 | 28 | class Availability(object): 29 | """mutable class so we can meld stuff""" 30 | _TYPE_GACHA = 1 31 | _TYPE_EVENT = 2 32 | 33 | def __init__(self, type, name, start, end, gaps, limited): 34 | self.type = type 35 | self.name = name 36 | self.start = start 37 | self.end = end 38 | self.gaps = gaps 39 | self.limited = limited 40 | 41 | def __repr__(self): 42 | return "models.Availability({0})".format( 43 | ", ".join(repr(getattr(self, x)) for x in ["type", "name", "start", "end", "gaps"]) 44 | ) 45 | 46 | def combine_availability(l): 47 | """Take a list of discrete Availability and turn any small lapses <= 3 days 48 | into a Gap on the parent object. 49 | Returns in place because of gacha_availability()""" 50 | if not l: 51 | return 52 | 53 | new_list = [] 54 | prev = l[0] 55 | for availability in l[1:]: 56 | bet = availability.start - prev.end 57 | # max 3 day gap, and both descriptions must be limited/non-limited 58 | if bet.seconds > 0 and bet.days <= 3 and prev.limited == availability.limited: 59 | # prev.gaps.append(Gap(prev.end, availability.start)) 60 | prev.end = availability.end 61 | else: 62 | new_list.append(prev) 63 | prev = availability 64 | new_list.append(prev) 65 | l[:] = new_list 66 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | lz4==2.2.1 2 | msgpack==0.6.2 3 | pytz==2019.2 4 | SQLAlchemy==1.3.8 5 | tornado==6.0.3 6 | user-agents==2.0 7 | pyaes==1.6.1 8 | -------------------------------------------------------------------------------- /starlight/acquisition.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sqlite3 3 | import lz4 4 | import io 5 | from time import time 6 | from email.utils import mktime_tz, parsedate_tz 7 | from collections import namedtuple 8 | from tornado import httpclient 9 | from lz4.block import decompress as lz4_decompress 10 | 11 | DBMANIFEST = "https://asset-starlight-stage.akamaized.net/dl/{0}/manifests" 12 | ASSETBBASEURL = "https://asset-starlight-stage.akamaized.net/dl/resources/AssetBundles" 13 | SOUNDBASEURL = "https://asset-starlight-stage.akamaized.net/dl/resources/Sound" 14 | SQLBASEURL = "https://asset-starlight-stage.akamaized.net/dl/resources/Generic/{1}/{0}" 15 | CACHE = os.path.join(os.path.dirname(__file__), "__manifestloader_cache") 16 | try: 17 | os.makedirs(CACHE, 0o755) 18 | except FileExistsError: 19 | pass 20 | 21 | def extra_acquisition_headers(): 22 | return {"X-Unity-Version": os.environ.get("VC_UNITY_VER", "5.4.5p1")} 23 | 24 | def filename(version, platform, asset_qual, sound_qual): 25 | return "{0}_{1}_{2}_{3}".format(version, platform, asset_qual, sound_qual) 26 | 27 | async def read_manifest(version, platform, asset_qual, sound_qual): 28 | dest_file = os.path.join(CACHE, filename(version, platform, asset_qual, sound_qual)) 29 | if not os.path.exists(dest_file): 30 | path = await acquire_manifest(version, platform, asset_qual, sound_qual, dest_file) 31 | else: 32 | path = dest_file 33 | 34 | if not path: 35 | return None 36 | 37 | return sqlite3.connect(path) 38 | 39 | manifest_selector_t = namedtuple("manifest_selector_t", ("filename", "md5", "platform", "asset_qual", "sound_qual")) 40 | async def acquire_manifest(version, platform, asset_qual, sound_qual, dest_file): 41 | cl = httpclient.AsyncHTTPClient() 42 | meta = "/".join(( DBMANIFEST.format(version), "all_dbmanifest" )) 43 | try: 44 | meta = await cl.fetch(meta, headers=extra_acquisition_headers()) 45 | except Exception as e: 46 | print("acquire_manifest: unhandled error while getting meta:", e) 47 | return None 48 | 49 | m = meta.body.decode("utf8") 50 | mp = map(lambda x: manifest_selector_t(* x.split(",")), filter(bool, m.split("\n"))) 51 | get_file = None 52 | for selector in mp: 53 | if selector.platform == platform and \ 54 | selector.asset_qual == asset_qual and \ 55 | selector.sound_qual == sound_qual: 56 | get_file = selector.filename 57 | break 58 | else: 59 | print("No candidate found for", platform, asset_qual, sound_qual) 60 | return None 61 | 62 | abso = "/".join(( DBMANIFEST.format(version), get_file )) 63 | try: 64 | mani = await cl.fetch(abso, headers=extra_acquisition_headers()) 65 | except Exception as e: 66 | print("acquire_manifest: unhandled error while getting meta:", e) 67 | return None 68 | 69 | buf = mani.buffer.read() 70 | bio = io.BytesIO() 71 | bio.write(buf[4:8]) 72 | bio.write(buf[16:]) 73 | data = lz4_decompress(bio.getvalue()) 74 | with open(dest_file, "wb") as write_db: 75 | write_db.write(data) 76 | return dest_file 77 | 78 | async def get_master(res_ver, to_path): 79 | manifest = await read_manifest(res_ver, "Android", "High", "High") 80 | if not manifest: 81 | return None 82 | 83 | cur = manifest.execute("SELECT hash, attr FROM manifests WHERE name = ?", ("master.mdb",)) 84 | hash, attr = cur.fetchone() 85 | manifest.close() 86 | 87 | url = SQLBASEURL.format(hash, hash[0:2]) 88 | cl = httpclient.AsyncHTTPClient() 89 | 90 | try: 91 | mashttp = await cl.fetch(url, headers=extra_acquisition_headers()) 92 | except Exception as e: 93 | print("get_master: unhandled error while getting master:", e) 94 | return None 95 | 96 | buf = mashttp.buffer.read() 97 | bio = io.BytesIO() 98 | bio.write(buf[4:8]) 99 | bio.write(buf[16:]) 100 | data = lz4_decompress(bio.getvalue()) 101 | with open(to_path, "wb") as write_db: 102 | write_db.write(data) 103 | 104 | mdate = mashttp.headers.get("Last-Modified") 105 | if mdate: 106 | tt = parsedate_tz(mdate) 107 | mtime = mktime_tz(tt) if tt else int(time()) 108 | else: 109 | mtime = int(time.time()) 110 | os.utime(to_path, (-1, mtime)) 111 | return to_path 112 | -------------------------------------------------------------------------------- /starlight/apiclient.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -!- coding: utf-8 -!- 3 | # 4 | # Copyright 2016 Hector Martin 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | 18 | import base64, msgpack, hashlib, random, time, os 19 | from tornado import httpclient 20 | import binascii, pyaes 21 | import json 22 | import traceback 23 | 24 | # laziness 25 | def VIEWER_ID_KEY(): 26 | return os.getenv("VC_AES_KEY", "").encode("ascii") 27 | 28 | def SID_KEY(): 29 | return os.getenv("VC_SID_SALT", "").encode("ascii") 30 | 31 | def decrypt_cbc(s, iv, key): 32 | e = pyaes.Decrypter(pyaes.AESModeOfOperationCBC(key, iv)) 33 | clear = e.feed(s) 34 | clear += e.feed() 35 | return clear 36 | 37 | def encrypt_cbc(s, iv, key): 38 | e = pyaes.Encrypter(pyaes.AESModeOfOperationCBC(key, iv)) 39 | clear = e.feed(s) 40 | clear += e.feed() 41 | return clear 42 | 43 | def is_usable(): 44 | return all([x in os.environ for x in ["VC_ACCOUNT", "VC_AES_KEY", "VC_SID_SALT"]]) \ 45 | and not os.getenv("DISABLE_AUTO_UPDATES", None) 46 | 47 | class ApiClient(object): 48 | BASE = "https://apis.game.starlight-stage.jp" 49 | SHARED_INSTANCE = None 50 | 51 | @classmethod 52 | def shared(cls): 53 | if cls.SHARED_INSTANCE is None: 54 | print("ApiClient.shared is creating the shared instance") 55 | user_id, viewer_id, udid = os.getenv("VC_ACCOUNT", "::").split(":") 56 | the_client = cls(user_id, viewer_id, udid) 57 | cls.SHARED_INSTANCE = the_client 58 | return cls.SHARED_INSTANCE 59 | 60 | def __init__(self, user, viewer_id, udid, res_ver="10013600"): 61 | self.user = user 62 | self.viewer_id = viewer_id 63 | self.udid = udid 64 | self.sid = None 65 | self.res_ver = res_ver 66 | 67 | def lolfuscate(self, s): 68 | return "%04x" % len(s) + "".join( 69 | "%02d" % random.randrange(100) + 70 | chr(ord(c) + 10) + "%d" % random.randrange(10) 71 | for c in s) + "%032d" % random.randrange(10**32) 72 | 73 | def unlolfuscate(self, s): 74 | return "".join(chr(ord(c) - 10) for c in s[6::4][:int(s[:4], 16)]) 75 | 76 | async def call(self, path, args): 77 | vid_iv = b"1111111111111111" 78 | args["viewer_id"] = vid_iv + base64.b64encode( 79 | encrypt_cbc(bytes(self.viewer_id, "ascii"), 80 | vid_iv, 81 | VIEWER_ID_KEY())) 82 | plain = base64.b64encode(msgpack.packb(args)) 83 | # I don't even 84 | key = base64.b64encode(bytes(random.randrange(255) for i in range(32)))[:32] 85 | msg_iv = binascii.unhexlify(self.udid.replace("-","").encode("ascii")) 86 | body = base64.b64encode(encrypt_cbc(plain, msg_iv, key) + key) 87 | sid = self.sid if self.sid else str(self.viewer_id) + self.udid 88 | headers = { 89 | "PARAM": hashlib.sha1(bytes(self.udid + str(self.viewer_id) + path, "ascii") + plain).hexdigest(), 90 | "KEYCHAIN": "", 91 | "USER-ID": self.lolfuscate(str(self.user)), 92 | "CARRIER": "google", 93 | "UDID": self.lolfuscate(self.udid), 94 | "APP-VER": os.environ.get("VC_APP_VER", "1.9.1"), # in case of sent 95 | "RES-VER": str(self.res_ver), 96 | "IP-ADDRESS": "127.0.0.1", 97 | "DEVICE-NAME": "Nexus 42", 98 | "X-Unity-Version": os.environ.get("VC_UNITY_VER", "5.4.5p1"), 99 | "SID": hashlib.md5(bytes(sid, "ascii") + SID_KEY()).hexdigest(), 100 | "GRAPHICS-DEVICE-NAME": "3dfx Voodoo2 (TM)", 101 | "DEVICE-ID": hashlib.md5(b"Totally a real Android").hexdigest(), 102 | "PLATFORM-OS-VERSION": "Android OS 13.3.7 / API-42 (XYZZ1Y/74726f6c6c)", 103 | "DEVICE": "2", 104 | "Content-Type": "application/x-www-form-urlencoded", # lies 105 | "User-Agent": "Dalvik/2.1.0 (Linux; U; Android 13.3.7; Nexus 42 Build/XYZZ1Y)", 106 | } 107 | 108 | req = httpclient.HTTPRequest(self.BASE + path, method="POST", headers=headers, body=body) 109 | try: 110 | response = await httpclient.AsyncHTTPClient().fetch(req) 111 | except httpclient.HTTPError as e: 112 | print("Got HTTP error while talking to API:", e.code) 113 | return (response, None) 114 | except Exception as e: 115 | print("Unexpected error: ", e) 116 | traceback.print_exc() 117 | return (response, None) 118 | 119 | reply = base64.b64decode(response.buffer.read()) 120 | plain = decrypt_cbc(reply[:-32], msg_iv, reply[-32:]).split(b"\0")[0] 121 | msg = msgpack.unpackb(base64.b64decode(plain)) 122 | try: 123 | self.sid = msg["data_headers"]["sid"] 124 | except KeyError: 125 | pass 126 | 127 | return (response, msg) 128 | 129 | async def versioncheck_bare(): 130 | client = ApiClient.shared() 131 | args = { 132 | "campaign_data": "", 133 | "campaign_user": 1337, 134 | "campaign_sign": hashlib.md5(b"All your APIs are belong to us").hexdigest(), 135 | "app_type": 0, 136 | } 137 | return await client.call("/load/check", args) 138 | 139 | async def versioncheck(): 140 | versioncheck_host = os.environ.get("VERSIONCHECK_PROXY") 141 | if versioncheck_host: 142 | path = "/api/v1/versioncheck" 143 | req = httpclient.HTTPRequest(versioncheck_host + path, method="GET") 144 | try: 145 | resp = await httpclient.AsyncHTTPClient().fetch(req) 146 | reply = json.loads(resp.body.decode("utf8")) 147 | if "version" in reply: 148 | return (resp, {b"data_headers": { 149 | b"required_res_ver": str(reply["version"]).encode("ascii") 150 | }}) 151 | else: 152 | return await versioncheck_bare() 153 | except httpclient.HTTPError as e: 154 | print("Got HTTP error while talking to versioncheck proxy:", e.code) 155 | return await versioncheck_bare() 156 | except Exception as e: 157 | print("Unexpected error: ", e) 158 | traceback.print_exc() 159 | return await versioncheck_bare() 160 | else: 161 | return await versioncheck_bare() 162 | 163 | async def gacha_rates(gacha_id): 164 | client = ApiClient.shared() 165 | args = { 166 | "gacha_id": gacha_id, 167 | "timezone": "-07:00:00", 168 | } 169 | return await client.call("/gacha/get_rate", args) 170 | -------------------------------------------------------------------------------- /starlight/extra_va_tables.py: -------------------------------------------------------------------------------- 1 | from . import en 2 | 3 | def char_voices(va_data_t, id): 4 | # title call entry 5 | # use type -4 so it doesn't take the string of USE_TYPE__T_4. 6 | # in endpoints.audio this gets converted back to a positive int. 7 | yield va_data_t(id, 4, 13, 1, "アイドルマスター シンデレラガールズ スターライトステージ!", 414) 8 | # live end; fail 9 | yield va_data_t(id, 2, 1, 1, en.NO_STRING_FMT.format(id, 2, 1), 21) 10 | # live end; full combo 11 | yield va_data_t(id, 2, 2, 1, en.NO_STRING_FMT.format(id, 2, 2), 22) 12 | # live end; score rank S->D 13 | yield va_data_t(id, 2, 3, 1, en.NO_STRING_FMT.format(id, 2, 3), 23) 14 | yield va_data_t(id, 2, 4, 1, en.NO_STRING_FMT.format(id, 2, 4), 24) 15 | yield va_data_t(id, 2, 5, 1, en.NO_STRING_FMT.format(id, 2, 5), 25) 16 | yield va_data_t(id, 2, 6, 1, en.NO_STRING_FMT.format(id, 2, 6), 26) 17 | yield va_data_t(id, 2, 7, 1, en.NO_STRING_FMT.format(id, 2, 7), 27) 18 | # producer lv up 19 | yield va_data_t(id, 2, 8, 1, en.NO_STRING_FMT.format(id, 2, 8), 28) 20 | # name 21 | yield va_data_t(id, 4, 1, 1, en.NO_STRING_FMT.format(id, 4, 1), 41) 22 | yield va_data_t(id, 4, 2, 1, en.NO_STRING_FMT.format(id, 4, 2), 41) 23 | # saying "producer" 24 | yield va_data_t(id, 4, 3, 1, en.NO_STRING_FMT.format(id, 4, 3), 43) 25 | # on lesson 26 | yield va_data_t(id, 4, 4, 1, en.NO_STRING_FMT.format(id, 4, 4), 44) 27 | # on star lesson 28 | yield va_data_t(id, 4, 5, 1, en.NO_STRING_FMT.format(id, 4, 5), 45) 29 | # generic success? 30 | yield va_data_t(id, 4, 6, 1, en.NO_STRING_FMT.format(id, 4, 6), 46) 31 | yield va_data_t(id, 4, 7, 1, en.NO_STRING_FMT.format(id, 4, 7), 46) 32 | yield va_data_t(id, 4, 8, 1, en.NO_STRING_FMT.format(id, 4, 8), 46) 33 | # generic failure? 34 | yield va_data_t(id, 4, 9, 1, en.NO_STRING_FMT.format(id, 4, 9), 49) 35 | yield va_data_t(id, 4, 10, 1, en.NO_STRING_FMT.format(id, 4, 10), 49) 36 | yield va_data_t(id, 4, 11, 1, en.NO_STRING_FMT.format(id, 4, 11), 49) 37 | 38 | def card_voices(va_data_t, id, chain_id): 39 | if id == chain_id: 40 | # live start 41 | yield va_data_t(id, 6, 1, 1, en.NO_STRING_FMT.format(id, 6, 1), 61) 42 | yield va_data_t(id, 6, 2, 1, en.NO_STRING_FMT.format(id, 6, 2), 61) 43 | 44 | # live skill proc 45 | yield va_data_t(id, 6, 3, 1, en.NO_STRING_FMT.format(id, 6, 3), 62) 46 | yield va_data_t(id, 6, 4, 1, en.NO_STRING_FMT.format(id, 6, 4), 62) 47 | yield va_data_t(id, 6, 5, 1, en.NO_STRING_FMT.format(id, 6, 5), 62) 48 | 49 | # live end 50 | yield va_data_t(id, 6, 6, 1, en.NO_STRING_FMT.format(id, 6, 6), 63) 51 | yield va_data_t(id, 6, 7, 1, en.NO_STRING_FMT.format(id, 6, 7), 63) -------------------------------------------------------------------------------- /starlight/update.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | import os 3 | from time import time 4 | from tornado import ioloop 5 | 6 | import starlight 7 | from . import apiclient 8 | from . import acquisition 9 | from threading import Lock 10 | 11 | update_lock = Lock() 12 | last_version_check = 0 13 | 14 | def do_preswitch_tasks(new_db_path, old_db_path): 15 | subprocess.call(["toolchain/name_finder.py", 16 | starlight.private_data_path("enamdictu"), 17 | new_db_path, 18 | starlight.transient_data_path("names.csv")]) 19 | 20 | if not os.getenv("DISABLE_HISTORY_UPDATES", None): 21 | subprocess.call(["toolchain/update_rich_history.py", new_db_path]) 22 | 23 | if old_db_path: 24 | subprocess.call(["toolchain/make_contiguous_gacha.py", old_db_path, new_db_path]) 25 | 26 | async def update_to_res_ver(res_ver): 27 | global last_version_check 28 | mdb_path = starlight.ark_data_path("{0}.mdb".format(res_ver)) 29 | if not os.path.exists(mdb_path): 30 | new_path = await acquisition.get_master(res_ver, 31 | starlight.transient_data_path("{0}.mdb".format(res_ver))) 32 | 33 | last_version_check = time() 34 | 35 | if new_path: 36 | old_path = None 37 | if starlight.data: 38 | old_path = starlight.transient_data_path("{0}.mdb".format(starlight.data.version)) 39 | 40 | try: 41 | do_preswitch_tasks(new_path, old_path) 42 | except Exception as e: 43 | print("do_preswitch_tasks croaked, update aborted.") 44 | raise 45 | 46 | starlight.hand_over_to_version(res_ver) 47 | apiclient.ApiClient.shared().res_ver = str(res_ver) 48 | 49 | async def async_version_check(release): 50 | response, msg = await apiclient.versioncheck() 51 | if not msg: 52 | print("Update check failed.") 53 | release() 54 | return 55 | 56 | res_ver = msg.get(b"data_headers", {}).get(b"required_res_ver", b"-1").decode("utf8") 57 | if not starlight.data or res_ver != starlight.data.version: 58 | if res_ver != "-1": 59 | print("Proceeding with update to version", res_ver, "...") 60 | await update_to_res_ver(res_ver) 61 | else: 62 | print("No required_res_ver, we're either on latest or app is outdated") 63 | 64 | release() 65 | 66 | def _watchdog_check(): 67 | if not update_lock.acquire(blocking=False): 68 | print("warning: update lock was held for more than 5 minutes - forcibly releasing.") 69 | update_lock.release() 70 | else: 71 | update_lock.release() 72 | 73 | ### PUBLIC API STARTS HERE ################################ 74 | ### EVERYTHING ABOVE IS PRIVATE ########################### 75 | 76 | def is_currently_updating(): 77 | return update_lock.locked() 78 | 79 | def check_version(): 80 | """Dispatch a version check if needed, and return whether we did.""" 81 | 82 | global last_version_check 83 | if time() - last_version_check >= 3600 or time() < last_version_check: 84 | if not apiclient.is_usable(): 85 | return False 86 | 87 | if not update_lock.acquire(blocking=False): 88 | return False 89 | 90 | # usually updates happen on the hour so this keeps our 91 | # schedule on the hour too 92 | t = time() 93 | last_version_check = t - (t % 3600) 94 | 95 | loop = ioloop.IOLoop.current() 96 | h = loop.call_later(5 * 60, _watchdog_check) 97 | 98 | def release(): 99 | print("Done, releasing lock.") 100 | loop.remove_timeout(h) 101 | update_lock.release() 102 | 103 | loop.add_callback(async_version_check, release) 104 | return True 105 | return False 106 | -------------------------------------------------------------------------------- /static/api.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 12 | 13 |
14 |

Starlight API

15 | 16 |

The API exports data from the game's internal databases in a nice, webdev friendly JSON format.

17 | 18 |

Endpoints

19 | 20 |

Object Type

21 | 22 |
GET /api/v1/(object type)/(spec)?...
23 | 24 |

Query the API for the objects of object type matching spec.

25 | 26 |

Supported object types

27 | 28 |
    29 |
  • skill_t: Card skill info.
  • 30 |
  • card_t: Card info.
  • 31 |
  • leader_skill_t: Centre skill info.
  • 32 |
  • char_t: Character info.
  • 33 |
34 | 35 |

Specs

36 | 37 |

A spec is a list of IDs separated by commas. IDs are not sequential, and are only valid for their object type. Typically, an ID is an integer.

38 | 39 |

Return value

40 | 41 |

Success:

42 | 43 |
{
 44 |   "result": [
 45 |     ; an array containing each object in the order they
 46 |     ; were requested, or null if it doesn't exist
 47 |     ; for detailed object contents, read the wiki:
 48 |     ; https://github.com/summertriangle-dev/sparklebox/wiki
 49 |   ]
 50 | }
51 | 52 |

Error: (see Errors under Misc.)

53 | 54 |

Query Parameters

55 | 56 |
    57 |
  • stubs=yes|no
    58 | When stubs is present, linked objects (such as card_t.skill) are replaced with a {"ref": url} object, where url is a valid API URL to retrieve the object that would normally be embedded. 59 | {“ref”: “/api/v1/skill_t/123”}
  • 60 |
  • datetime=unix|iso8601
    61 | The format for timestamps returned by the API. 62 | 63 |
      64 |
    • unix: Unix time, i.e. seconds since the epoch as an integer.
    • 65 |
    • iso8601: An ISO 8601 string: “1970-01-01T00:00:00.000Z”
    • 66 |
  • 67 |
68 | 69 |
70 | 71 |

Object List

72 | 73 |
GET /api/v1/list/(object type)?...
74 | 75 |

Return a list of brief descriptions for all objects available using the corresponding object API.

76 | 77 |

Supported object types

78 | 79 |
    80 |
  • card_t: Card info.
  • 81 |
  • char_t: Character info.
  • 82 |
83 | 84 |

Return value

85 | 86 |

Success:

87 | 88 |
{
 89 |   "result": [
 90 |     ; an array containing each object
 91 |     ; for detailed object contents, read the wiki:
 92 |     ; https://github.com/summertriangle-dev/sparklebox/wiki
 93 |   ]
 94 | }
95 | 96 |

Error: This endpoint currently does not have any error conditions.

97 | 98 |

Query Parameters

99 | 100 |
    101 |
  • keys=<NAME>,<NAME>...
    102 | The list of specific keys you want, from the list of available ones. If you don't pass this parameter, you get all keys.
  • 103 |
104 | 105 |
106 | 107 |

Crowd Translations

108 | 109 |
POST /api/v1/read_tl
110 | 
111 |     ["string1", "string2", ..., "stringN"]
112 | 113 |

Query translations for the given strings. This is the same API used by tlinject.js on starlight.kirara.ca.

114 | 115 |

Return value

116 | 117 |

The value returned is a JSON dict mapping the original strings to their translated counterparts if available:

118 | 119 |
{"つぼみ": "Tsubomi"}
120 | 121 |

Errors

122 | 123 |
    124 |
  • 5xx - try again.
  • 125 |
  • 400 - the given string list was not valid JSON, or didn't decode into a list
  • 126 |
127 | 128 |
129 | 130 |

Time

131 | 132 |
GET /api/v1/happening/now
133 |     GET /api/v1/happening/(timestamp)
134 | 135 |

Get time-sensitive information.

136 | 137 |

Information

138 | 139 |
GET /api/v1/info
140 | 
141 | {
142 |   "api_major": 1,
143 |   "api_revision": 2,
144 |   "truth_version": "10017160"
145 | }
146 | 
147 | 148 |

Return a bit of information about this instance of ssdb as a JSON dictionary. The following keys are available:

149 | 150 |
    151 |
  • api_major: The major version of the API as an int. Assume a value of 1 if it doesn't exist.
  • 152 |
  • api_revision: The minor revision of the API as an int. Assume a value of 1 if it doesn't exist. 153 | When new things are added (that don't break the API for existing users), the revision will be bumped.
  • 154 |
  • truth_version: The version game data that ssdb derives data from.
  • 155 |
156 | 157 |

Misc.

158 | 159 |

URLs

160 | 161 |

All URLs returned in API responses should be interpreted as relative to the URL that the response came from.

162 | 163 |

From an API call to /api/v1/card_t/123...

164 | 165 |
    166 |
  • We might return relative paths like ../../images/blah.png, which you would expand to /api/v1/images/blah.png
  • 167 |
  • We might return an absolute path like /static/hoshimoriuta/card/123.png
  • 168 |
  • We might return a fully-qualified URL to another domain like https://static.com/card/123.png
  • 169 |
170 | 171 |

Images

172 | 173 |

Some objects will have image URLs embedded in them. As a general rule, you can hotlink any image returned in an API response. However, if you expect a lot of traffic, you may want to mirror the images to your own server.

174 | 175 |

Do not make any assumptions about the file hierarchy of hoshimoriuta.kirara.ca. I reserve the right to make any change to it at any time, which will probably break your hardcoded image URLs. (Links you find in API responses will remain valid for a reasonable amount of time, so you can still cache them.)

176 | 177 |

Errors

178 | 179 |

If there is an error processing the request, we will return the JSON object

180 | 181 |
 {"error": "Some error message"}
182 | 183 |

and the appropriate HTTP status.

184 | 185 |

Rate limiting

186 | 187 |

There is none currently. If it does exist in the future, we will return an HTTP 429 status with the error object.

188 | 189 |

Attribution

190 | 191 |

You don't have to credit data that comes from the API because it reads from game truth anyway. But if you'd like to, credit the Starlight Database.

192 | 193 |

Bugs and Feature Requests

194 | 195 |

File an issue here if you have any problems with using the API.

196 | 197 | 198 |

Changelog

199 |
    200 |
  • 1.4: 201 |
      202 |
    • Added skill_type_id to skill_t.
    • 203 |
    204 |
  • 205 |
  • 1.2: 206 |
      207 |
    • Added the icon_image_ref key, containing a URL to the object's icon, added to card_t and char_t objects.
      208 | (Note: the images are 124x124 and 88x88 for card_t and char_t, respectively. They may be changed by Namco, so don't rely on this being true always.)
    • 209 |
    210 |
  • 211 |
  • 1.1: 212 |
    • Initial release of the API.
    213 |
  • 214 |
215 |
216 | 217 | -------------------------------------------------------------------------------- /static/css/base.less: -------------------------------------------------------------------------------- 1 | // @link_colour : hsl(221, 72%, 66%); 2 | // @link_visited_colour: hsl(300, 48%, 66%); 3 | // @var_color : rgb(84, 196, 84); 4 | 5 | // @tlinject_bg : #666; 6 | // @tlinject_text: @main_text; 7 | 8 | // @global_header_text : white; 9 | // @global_header_text_fade: darken(white, 20%); 10 | 11 | // @available_colour : lighten(#339933, 40%); 12 | // @unavailable_colour: lighten(#ff0000, 40%); 13 | 14 | * { 15 | -webkit-font-size-adjust: none; 16 | font-size-adjust : none; 17 | box-sizing : border-box; 18 | font-family : system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, 19 | Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; 20 | } 21 | 22 | a { 23 | color: @link_colour; 24 | 25 | &:visited { 26 | color: @link_visited_colour; 27 | } 28 | } 29 | 30 | body { 31 | background-color: @main_bg; 32 | color : @main_text; 33 | } 34 | 35 | footer { 36 | text-align : center; 37 | margin-bottom : env(safe-area-inset-bottom); 38 | padding : 10px; 39 | padding-bottom: 40px; 40 | 41 | .developer { 42 | display: none; 43 | } 44 | } 45 | 46 | .global_header { 47 | color: @global_header_text_fade; 48 | 49 | &>span, 50 | &>a { 51 | display: inline-block; 52 | padding: 6px 6px; 53 | } 54 | 55 | a { 56 | text-decoration: none; 57 | color : @global_header_text; 58 | 59 | &:visited { 60 | color: @global_header_text; 61 | } 62 | 63 | &::after { 64 | content : "»"; 65 | padding-left: 8px; 66 | } 67 | } 68 | } 69 | 70 | .container { 71 | margin : 0 auto; 72 | max-width: @container_max_width; 73 | 74 | &.tight { 75 | font-size: 0; 76 | } 77 | &.negative_box { 78 | padding: 0 8px; 79 | } 80 | } 81 | 82 | // Card icons 83 | .icon { 84 | display : inline-block; 85 | width : 48px; 86 | height : 48px; 87 | border-radius: 4px; 88 | } 89 | 90 | .rel { 91 | position: relative; 92 | } 93 | 94 | span.let, 95 | span.var { 96 | color: @var_color; 97 | } 98 | 99 | span.var { 100 | font-weight: bold; 101 | } 102 | 103 | span.caveat { 104 | font-size: 90%; 105 | } 106 | 107 | .tlable:hover { 108 | background-color: @tlinject_bg !important; 109 | color : @tlinject_text !important; 110 | } 111 | 112 | .crowd_tl_notice { 113 | font-size : 13px; 114 | padding : 6px 10px; 115 | padding-bottom : ~"calc(env(safe-area-inset-bottom) + 6px)"; 116 | color : @main_text; 117 | position : fixed; 118 | bottom : 0; 119 | left : 0; 120 | background-color: fade(@main_bg, 70%); 121 | z-index : 999; 122 | 123 | a { 124 | color: @main_text; 125 | } 126 | } 127 | 128 | td.cb_true { 129 | background-color: @available_colour; 130 | border-color : darken(@available_colour, 10%) !important; 131 | } 132 | 133 | td.cb_false { 134 | background-color: @unavailable_colour; 135 | border-color : darken(@unavailable_colour, 10%) !important; 136 | } 137 | 138 | strong.ga_unavailable { 139 | text-decoration: line-through; 140 | color : @unavailable_colour; 141 | } 142 | 143 | strong.ga_available { 144 | color: @available_colour; 145 | } 146 | 147 | .avlistentry_regular_melded li { 148 | font-size: 80%; 149 | } 150 | 151 | .help { 152 | border-bottom: 1px dashed; 153 | cursor : help; 154 | } 155 | 156 | .page_box { 157 | display: flex; 158 | flex-direction: row; 159 | } 160 | 161 | .flexible_space { 162 | flex-grow: 2; 163 | } 164 | 165 | .align_right { 166 | text-align: right; 167 | } 168 | -------------------------------------------------------------------------------- /static/css/birthday.less: -------------------------------------------------------------------------------- 1 | // @alt_cute_colour : #ff6bec; 2 | // @alt_passion_colour: #ffc047; 3 | 4 | .birthday_banner { 5 | position : relative; 6 | width : 100%; 7 | padding-left: 65px; 8 | padding-top : 12px; 9 | min-height : 58px; 10 | 11 | &.r_Cool { 12 | background-color: @cool_colour; 13 | 14 | a { 15 | color: white; 16 | } 17 | } 18 | 19 | &.r_Cute { 20 | background-color: @alt_cute_colour; 21 | color : black; 22 | 23 | a { 24 | color: black; 25 | } 26 | } 27 | 28 | &.r_Passion { 29 | background-color: @alt_passion_colour; 30 | color : black; 31 | 32 | a { 33 | color: black; 34 | } 35 | } 36 | } 37 | 38 | .stitches-birthday(@x: 0, @y: 0, @width: 0, @height: 0) { 39 | background-position: @x @y; 40 | width : @width; 41 | height : @height; 42 | } 43 | 44 | .ribbon { 45 | background-image : url(../img/sribbons.png?v2); 46 | background-repeat: no-repeat; 47 | display : block; 48 | 49 | position: absolute; 50 | top : 0; 51 | left : 0; 52 | 53 | &.r_Cool { 54 | .stitches-birthday(0, 0, 48px, 48px); 55 | } 56 | 57 | &.r_Cute { 58 | .stitches-birthday(-48px, 0, 48px, 48px); 59 | } 60 | 61 | &.r_Passion { 62 | .stitches-birthday(0, -48px, 48px, 48px); 63 | } 64 | 65 | @media only screen and (-webkit-min-device-pixel-ratio: 1.3), 66 | only screen and (-o-min-device-pixel-ratio: 13/10), 67 | only screen and (min-resolution: 120dpi) { 68 | 69 | background-image: url("../img/sribbons@2x.png?v2") !important; 70 | background-size : 96px 96px !important; 71 | } 72 | } -------------------------------------------------------------------------------- /static/css/boxes.less: -------------------------------------------------------------------------------- 1 | // @name_tag_height : 40px; 2 | // @name_tag_pad : @name_tag_height / 4; 3 | // @name_tag_height_sm: 28px; 4 | // @name_tag_pad_sm : 6px; 5 | 6 | // @box_header_height : 24px; 7 | // @box_header_font_size: 16px; 8 | // @box_header_pad_h : 8px; 9 | // @box_header_pad_v : 5px; 10 | 11 | .box_theme(@main_colour, @accent_colour, @text_colour) { 12 | border-color: @accent_colour; 13 | 14 | &>.header, 15 | .table th, 16 | .table_stub { 17 | background-color: @main_colour; 18 | color : @text_colour; 19 | 20 | .left { 21 | background-color: @accent_colour; 22 | } 23 | 24 | .ext { 25 | border-top-color : @accent_colour; 26 | border-left-color: @accent_colour; 27 | } 28 | } 29 | } 30 | 31 | .box { 32 | margin : 0 5px 10px; 33 | display : inline-block; 34 | border : 1px solid; 35 | vertical-align: top; 36 | 37 | &.col2 { 38 | /* don't know why but it works 39 | * TODO replace it with something sane */ 40 | width: ~"calc(50% - 13px)"; 41 | } 42 | 43 | &.unbordered { 44 | .content { 45 | padding: 0; 46 | } 47 | 48 | margin :0; 49 | border-left :none; 50 | border-right:none; 51 | } 52 | 53 | &>.header { 54 | color : white; 55 | min-height: @box_header_height; 56 | font-size : 0; 57 | 58 | .item { 59 | display : inline-block; 60 | line-height : @box_header_height; 61 | font-size : @box_header_font_size; 62 | vertical-align: bottom; 63 | padding : 0 (@box_header_pad_h / 2); 64 | } 65 | 66 | .left { 67 | padding-left: @box_header_pad_h; 68 | } 69 | 70 | .right::after { 71 | /* force all text to layout with ja line height */ 72 | content : "あ"; 73 | color : transparent; 74 | padding-right: @box_header_pad_h; 75 | } 76 | 77 | .ext { 78 | border-top : (@box_header_height / 2) solid transparent; 79 | border-left : (@box_header_height / 4) solid transparent; 80 | border-right : (@box_header_height / 4) solid transparent; 81 | border-bottom : (@box_header_height / 2) solid transparent; 82 | vertical-align: bottom; 83 | padding : 0; 84 | } 85 | } 86 | 87 | &>.content { 88 | padding : @box_header_pad_v @box_header_pad_h; 89 | background-color: @layer4_bg; 90 | 91 | &>* { 92 | vertical-align: middle; 93 | } 94 | 95 | ul { 96 | margin : 0; 97 | list-style-position: inside; 98 | padding-left : 5px; 99 | } 100 | } 101 | 102 | .table_stub { 103 | padding : 0 10px 0 10px; 104 | text-align: center; 105 | 106 | display : flex; 107 | flex-direction : row; 108 | flex-wrap : wrap; 109 | justify-content: center; 110 | 111 | a { 112 | margin-bottom: 10px; 113 | } 114 | } 115 | } 116 | 117 | .box.history_ev { 118 | margin-bottom: 10px; 119 | 120 | &>.contents { 121 | padding: 6px; 122 | } 123 | } 124 | 125 | .box.Cute { 126 | .box_theme(@cute_colour, darken(@cute_colour, 10%), white); 127 | } 128 | 129 | .box.Cool { 130 | .box_theme(@cool_colour, darken(@cool_colour, 10%), white); 131 | } 132 | 133 | .box.Passion { 134 | .box_theme(@passion_colour, lighten(@passion_colour, 10%), #222); 135 | } 136 | 137 | .box.black { 138 | .box_theme(@layer1_bg, lighten(@layer1_bg, 10%), white); 139 | background: @layer1_bg; 140 | } 141 | 142 | .box.he_new_idols { 143 | .box_theme(@layer1_bg, lighten(#333333, 10%), white); 144 | background: @layer1_bg; 145 | } 146 | 147 | .box.he_event { 148 | .box_theme(@layer1_bg, lighten(#0066ff, 10%), white); 149 | background: @layer1_bg; 150 | } 151 | 152 | .box.he_gacha { 153 | .box_theme(@layer1_bg, #f0ce47, black); 154 | background: @layer1_bg; 155 | 156 | .item.right { 157 | color: @main_text; 158 | } 159 | } 160 | 161 | .box.he_gacha.he_limited { 162 | .box_theme(@layer1_bg, darken(#ffafa1, 2%), black); 163 | background: @layer1_bg; 164 | 165 | .item.right { 166 | color: @main_text; 167 | } 168 | } 169 | 170 | .he_divider { 171 | font-weight : bold; 172 | font-size : 70%; 173 | text-transform: uppercase; 174 | display : block; 175 | margin-bottom : 6px; 176 | } 177 | 178 | .he_divider_line { 179 | border-color: lighten(#333333, 5%); 180 | border-style: solid; 181 | } 182 | 183 | .he_icx_limited { 184 | background-color: #835a5a !important; 185 | } 186 | 187 | /* this entire section is basically a hack to keep things aligned properly 188 | * chances are if you touch something here it will break, so don't do it */ 189 | 190 | @media screen and (min-width:769px) { 191 | .name_tag { 192 | min-height: @name_tag_height; 193 | 194 | .item { 195 | line-height: @name_tag_height; 196 | font-size : (@name_tag_height - (@name_tag_pad * 2)); 197 | padding : 0 @name_tag_pad 0 @name_tag_pad; 198 | } 199 | 200 | .ext { 201 | border-top : (@name_tag_height / 2) solid transparent; 202 | border-left : (@name_tag_height / 4) solid transparent; 203 | border-right : (@name_tag_height / 4) solid transparent; 204 | border-bottom: (@name_tag_height / 2) solid transparent; 205 | } 206 | 207 | .sicon { 208 | vertical-align: text-bottom; 209 | } 210 | } 211 | } 212 | 213 | @media screen and (max-width:768px) { 214 | .name_tag { 215 | min-height: @name_tag_height_sm; 216 | 217 | .item { 218 | line-height: @name_tag_height_sm; 219 | font-size : @name_tag_height_sm - (@name_tag_pad_sm * 2); 220 | padding : 0 @name_tag_pad_sm 0 @name_tag_pad_sm; 221 | } 222 | 223 | .ext { 224 | border-top : (@name_tag_height_sm / 2) solid transparent; 225 | border-left : (@name_tag_height_sm / 4) solid transparent; 226 | border-right : (@name_tag_height_sm / 4) solid transparent; 227 | border-bottom: (@name_tag_height_sm / 2) solid transparent; 228 | } 229 | } 230 | 231 | .sicon { 232 | vertical-align: middle; 233 | } 234 | } 235 | 236 | .name_tag { 237 | color : white; 238 | font-size: 0 !important; 239 | 240 | small { 241 | vertical-align: bottom; 242 | } 243 | 244 | .item.name { 245 | word-wrap: break-word; 246 | } 247 | 248 | .item.name a { 249 | color : white; 250 | text-decoration: none; 251 | 252 | &:hover { 253 | text-decoration: underline; 254 | } 255 | } 256 | 257 | .item { 258 | display : inline-block; 259 | vertical-align: middle; 260 | } 261 | 262 | .name::after { 263 | /* force all-latin text to align with ja line height */ 264 | content: "あ"; 265 | color : transparent; 266 | } 267 | 268 | .ext { 269 | vertical-align: middle; 270 | padding : 0; 271 | } 272 | 273 | &.Cute { 274 | background-color: @cute_colour; 275 | 276 | .title { 277 | background-color: darken(@cute_colour, 10%); 278 | } 279 | 280 | .ext { 281 | border-top-color : darken(@cute_colour, 10%); 282 | border-left-color: darken(@cute_colour, 10%); 283 | } 284 | } 285 | 286 | &.Cool { 287 | background-color: @cool_colour; 288 | 289 | .title { 290 | background-color: darken(@cool_colour, 10%); 291 | } 292 | 293 | .ext { 294 | border-top-color : darken(@cool_colour, 10%); 295 | border-left-color: darken(@cool_colour, 10%); 296 | } 297 | } 298 | 299 | &.Passion { 300 | color: #222; 301 | 302 | .name a { 303 | color: #222; 304 | } 305 | 306 | background-color: @passion_colour; 307 | 308 | .title { 309 | background-color: lighten(@passion_colour, 10%); 310 | } 311 | 312 | .ext { 313 | border-top-color : lighten(@passion_colour, 10%); 314 | border-left-color: lighten(@passion_colour, 10%); 315 | } 316 | } 317 | } 318 | 319 | .name_tag.force_small { 320 | height: @name_tag_height_sm; 321 | 322 | .item { 323 | line-height: @name_tag_height_sm; 324 | font-size : @name_tag_height_sm - (@name_tag_pad_sm * 2); 325 | padding : 0 @name_tag_pad_sm 0 @name_tag_pad_sm; 326 | } 327 | 328 | .ext { 329 | padding : 0; 330 | border-top : (@name_tag_height_sm / 2) solid transparent; 331 | border-left : (@name_tag_height_sm / 4) solid transparent; 332 | border-right : (@name_tag_height_sm / 4) solid transparent; 333 | border-bottom: (@name_tag_height_sm / 2) solid transparent; 334 | } 335 | 336 | .sicon { 337 | vertical-align: middle; 338 | } 339 | } -------------------------------------------------------------------------------- /static/css/main.less: -------------------------------------------------------------------------------- 1 | @main_bg : rgb(42, 46, 54); 2 | @main_text: #eee; 3 | 4 | @layer1_bg: lighten(@main_bg, 5%); 5 | @layer2_bg: lighten(@main_bg, 12%); 6 | @layer3_bg: @layer1_bg; 7 | @layer4_bg: @layer2_bg; 8 | 9 | @layer1_text: @main_text; 10 | @layer2_text: @main_text; 11 | @layer3_text: @main_text; 12 | @layer4_text: @main_text; 13 | 14 | @standard_inset_h: 20px; 15 | @standard_inset_v: 10px; 16 | 17 | @link_colour : hsl(221, 72%, 66%); 18 | @link_visited_colour: hsl(300, 48%, 66%); 19 | @var_color : rgb(84, 196, 84); 20 | 21 | @tlinject_bg : #666; 22 | @tlinject_text: @main_text; 23 | 24 | @global_header_text : white; 25 | @global_header_text_fade: darken(white, 20%); 26 | 27 | @available_colour : hsl(120, 49%, 55%); 28 | @unavailable_colour: hsl(0, 54%, 55%); 29 | 30 | @cute_colour : rgb(251, 7, 116); 31 | @cool_colour : rgb(35, 109, 251); 32 | @passion_colour : rgb(252, 169, 38); 33 | @vocal_colour : #ed5469; 34 | @dance_colour : #3dafdb; 35 | @visual_colour : #ff9a33; 36 | @level_colour : #d3cb53; 37 | @life_colour : #32d48a; 38 | @kizuna_colour : #fa7add; 39 | @alt_cute_colour : #ff6bec; 40 | @alt_passion_colour: #ffc047; 41 | 42 | @cute_colour_text : @cute_colour; 43 | @cool_colour_text : @cool_colour; 44 | @passion_colour_text: @passion_colour; 45 | @vocal_colour_text : @vocal_colour; 46 | @dance_colour_text : @dance_colour; 47 | @visual_colour_text : @visual_colour; 48 | @level_colour_text : @level_colour; 49 | @life_colour_text : @life_colour; 50 | @kizuna_colour_text : @kizuna_colour; 51 | 52 | @card_img_height: 340px; 53 | @card_img_width : 272px; 54 | 55 | @button_text : white; 56 | @button_primary_bg : #007bff; 57 | @button_destructive_bg : #dc3545; 58 | @button_secondary_bg : #484e53; 59 | @button_standard_rounding: 4px; 60 | 61 | @text_field_text: white; 62 | 63 | @stepper_bg : green; 64 | @stepper_text: white; 65 | 66 | @table_base_row_bg : @layer1_bg; 67 | @table_second_row_bg : darken(@layer1_bg, 3%); 68 | @table_base_row_text : @main_text; 69 | @table_second_row_text : @main_text; 70 | @table_base_row_border : darken(@table_base_row_bg, 5%); 71 | @table_sorter_active_criteria_text: #fedba4; 72 | @table_filter_switch_off_bg : #777; 73 | @table_filter_switch_on_bg : #44aa55; 74 | @table_filter_switch_text : @layer2_text; 75 | @large_table_min_width : 1024px; 76 | 77 | @container_max_width: 1280px; 78 | @compact_max_width : 600px; 79 | @large_min_width : 1280px; 80 | 81 | @name_tag_height : 40px; 82 | @name_tag_pad : (@name_tag_height / 4); 83 | @name_tag_height_sm: 28px; 84 | @name_tag_pad_sm : 6px; 85 | 86 | @box_header_height : 24px; 87 | @box_header_font_size: 16px; 88 | @box_header_pad_h : 8px; 89 | @box_header_pad_v : 5px; 90 | 91 | @import "base.less"; 92 | 93 | /* main.html */ 94 | 95 | #search { 96 | font-size: 24px; 97 | padding : @standard_inset_v @standard_inset_h; 98 | } 99 | 100 | #suggestions { 101 | margin: 4px 0; 102 | 103 | &>a { 104 | display : block; 105 | width : 100%; 106 | padding : @standard_inset_v @standard_inset_h; 107 | background-color: @layer2_bg; 108 | color : @main_text; 109 | } 110 | } 111 | 112 | .chara_header { 113 | padding : @standard_inset_v @standard_inset_h; 114 | background-color: @layer1_bg; 115 | 116 | .text_outline { 117 | text-shadow: 1px 0px #000, -1px 0px #000, 0px 1px #000, 0px -1px #000, 118 | 0px 0px 15px #000; 119 | } 120 | 121 | .name_slab { 122 | font-size : 24px; 123 | font-weight: normal; 124 | color : @layer1_text; 125 | .text_outline(); 126 | 127 | .english { 128 | font-weight: normal; 129 | font-style : italic; 130 | } 131 | } 132 | 133 | .pose_sprite_container { 134 | background: center bottom no-repeat; 135 | width : 100%; 136 | height : 100%; 137 | } 138 | } 139 | 140 | .history_date_header { 141 | margin: 15px @box_header_pad_h; 142 | } 143 | 144 | @import "tables.less"; 145 | 146 | /* card_partial */ 147 | 148 | .carcon { 149 | display : -webkit-flex; 150 | display : flex; 151 | background-color: @layer3_bg; 152 | margin-bottom : 10px; 153 | 154 | .card_left { 155 | display : inline-block; 156 | min-width : @card_img_width; 157 | background-color: black; 158 | text-align : center; 159 | font-size : 0; 160 | 161 | img { 162 | height: @card_img_height; 163 | width : @card_img_width; 164 | } 165 | 166 | .card_image_offlink { 167 | background-color: darken(#444, 5%); 168 | text-align : left; 169 | margin : 0; 170 | padding : 8px 8px; 171 | 172 | &:nth-child(3) { 173 | background-color: darken(#444, 10%); 174 | } 175 | 176 | &:nth-child(4) { 177 | background-color: darken(#444, 15%); 178 | } 179 | 180 | a { 181 | color : white; 182 | font-weight : bold; 183 | text-decoration: none; 184 | font-size : 12px; 185 | 186 | &:hover { 187 | text-decoration: underline; 188 | } 189 | } 190 | } 191 | } 192 | 193 | .card_right { 194 | -webkit-flex-grow: 1; 195 | flex-grow : 1; 196 | vertical-align : top; 197 | color : @layer3_text; 198 | 199 | &>* { 200 | font-size: 16px; 201 | } 202 | 203 | .spread_view { 204 | height : 350px; 205 | width : 100%; 206 | background-position: center center; 207 | background-size : cover; 208 | } 209 | 210 | .evolution_setter { 211 | font-size: 80%; 212 | opacity : 0.7; 213 | 214 | &.selected { 215 | opacity: 1.0; 216 | } 217 | } 218 | 219 | &>.content { 220 | padding: 10px 5px 0; 221 | 222 | &.tight { 223 | padding: 0; 224 | } 225 | } 226 | 227 | td { 228 | padding: 3px 6px; 229 | } 230 | 231 | .stats { 232 | width : 100%; 233 | border-left : 0; 234 | border-right : 0; 235 | border-top : 0; 236 | margin : 0; 237 | background-color: @layer4_bg; 238 | } 239 | 240 | .stats_table { 241 | text-align: center; 242 | } 243 | } 244 | 245 | .hang_inside { 246 | position: absolute; 247 | bottom : 0; 248 | left : 5px; 249 | 250 | display : flex; 251 | flex-direction: row; 252 | flex-wrap : wrap; 253 | 254 | a { 255 | margin-bottom: 10px; 256 | } 257 | } 258 | 259 | .card_left .hang_inside { 260 | right: 5px; 261 | left : initial; 262 | } 263 | 264 | .spread_view .sign_view { 265 | position : absolute; 266 | right : 0; 267 | bottom : 0; 268 | max-width : 35%; 269 | max-height: 100%; 270 | } 271 | } 272 | 273 | /* image switcher */ 274 | 275 | .noline { 276 | text-decoration: none; 277 | } 278 | 279 | .iconex_row { 280 | display : flex; 281 | flex-direction: row; 282 | align-items: center; 283 | flex-wrap : wrap; 284 | 285 | &>* { 286 | padding: 0 2px; 287 | } 288 | } 289 | 290 | .profile { 291 | display : inline-flex; 292 | align-items : center; 293 | background : @layer2_bg; 294 | border-radius: 4px; 295 | 296 | .profile_text { 297 | color : white; 298 | text-align: left; 299 | font-size : 80%; 300 | padding : 0 5px; 301 | } 302 | } 303 | 304 | /* many faces */ 305 | 306 | .svx_left { 307 | text-align : center; 308 | background-color: #333; 309 | padding : 10px 20px 0; 310 | } 311 | 312 | .svx_face { 313 | margin : 5px 0 5px 5px; 314 | background-color: #222; 315 | border : 1px solid #000; 316 | } 317 | 318 | @import "boxes.less"; 319 | 320 | @import "birthday.less"; 321 | 322 | @import "responsive.less"; 323 | 324 | @import "widgets.less"; 325 | 326 | .msprites { 327 | position: relative; 328 | 329 | &::before, 330 | &::after { 331 | position: absolute; 332 | left : 1px; 333 | top : 1px; 334 | display : block; 335 | width : 9px; 336 | height : 9px; 337 | content : ""; 338 | } 339 | 340 | &::after { 341 | top: 10px; 342 | } 343 | } 344 | 345 | .sicon { 346 | display: inline-block; 347 | } 348 | 349 | @import (inline) "markers.css"; -------------------------------------------------------------------------------- /static/css/responsive.less: -------------------------------------------------------------------------------- 1 | // @compact_max_width: 600px; 2 | // @large_min_width : 1280px; 3 | 4 | @media screen and (max-width: @compact_max_width) { 5 | body { 6 | margin: 0; 7 | } 8 | 9 | .container.negative_box { 10 | margin: 0 8px; 11 | } 12 | 13 | .carcon { 14 | -webkit-flex-direction: column; 15 | flex-direction : column; 16 | 17 | .card_left { 18 | width: 100%; 19 | } 20 | } 21 | 22 | .stats.box .icon { 23 | display: block; 24 | float : right; 25 | } 26 | 27 | .stats_table tbody { 28 | display: block; 29 | } 30 | 31 | .stats_table tr { 32 | display: inline-block; 33 | } 34 | 35 | .stats_table td, 36 | .stats_table th { 37 | display: block; 38 | padding: 3px 6px; 39 | } 40 | 41 | .stats_table th::before { 42 | content : " "; 43 | font-size: 142%; 44 | } 45 | 46 | .stats_table th { 47 | text-align: left; 48 | } 49 | 50 | .spread_view .sign_view { 51 | top: 0; 52 | } 53 | 54 | .hides_under_mobile { 55 | display: none; 56 | } 57 | } 58 | 59 | @media screen and (min-width: @large_min_width) { 60 | .spread_view { 61 | height: 824px !important; 62 | } 63 | } -------------------------------------------------------------------------------- /static/css/tables.less: -------------------------------------------------------------------------------- 1 | // @table_base_row_bg : @layer1_bg; 2 | // @table_second_row_bg : darken(@layer1_bg, 3%); 3 | // @table_base_row_text : @main_text; 4 | // @table_second_row_text : @main_text; 5 | // @table_base_row_border : darken(@table_base_row_bg, 5%); 6 | // @table_sorter_active_criteria_text: #fedba4; 7 | // @table_filter_switch_off_bg : #777; 8 | // @table_filter_switch_on_bg : #44aa55; 9 | // @table_filter_switch_text : @layer2_text; 10 | // @large_table_min_width : 1024px; 11 | 12 | .table { 13 | background-color: @table_base_row_bg; 14 | color : @table_base_row_text; 15 | font-size : 16px; 16 | border-collapse : collapse; 17 | 18 | height: 100%; 19 | width : 100%; 20 | 21 | td { 22 | border : 1px solid @table_base_row_border; 23 | padding: 3px 6px; 24 | 25 | /* helps with overflow... */ 26 | border-right-width: 0px !important; 27 | } 28 | 29 | tr:nth-child(even) { 30 | background-color: @table_second_row_bg; 31 | color : @table_second_row_text; 32 | } 33 | } 34 | 35 | .table .icon, 36 | .table .profile { 37 | vertical-align: bottom; 38 | } 39 | 40 | th { 41 | font-size : 70%; 42 | text-transform: uppercase; 43 | padding : 3px 6px; 44 | } 45 | 46 | td { 47 | 48 | &.vocal, 49 | &.vocal span { 50 | background-color: @vocal_colour; 51 | color : lighten(saturate(@vocal_colour, 10%), 40%); 52 | border-color : darken(@vocal_colour, 10%); 53 | font-weight : normal; 54 | } 55 | 56 | &.dance, 57 | &.dance span { 58 | background-color: @dance_colour; 59 | color : lighten(saturate(@dance_colour, 10%), 40%); 60 | border-color : darken(@dance_colour, 10%); 61 | font-weight : normal; 62 | } 63 | 64 | &.visual, 65 | &.visual span { 66 | background-color: @visual_colour; 67 | color : darken(saturate(@visual_colour, 10%), 40%); 68 | border-color : darken(@visual_colour, 10%); 69 | font-weight : normal; 70 | } 71 | 72 | &.life, 73 | &.life span { 74 | background-color: @life_colour; 75 | color : darken(saturate(@life_colour, 10%), 40%); 76 | border-color : darken(@life_colour, 10%); 77 | font-weight : normal; 78 | } 79 | 80 | &.max_level, 81 | &.max_level span { 82 | background-color: @level_colour; 83 | color : darken(saturate(@level_colour, 10%), 40%); 84 | font-weight : normal; 85 | } 86 | 87 | &.kizuna, 88 | &.kizuna span { 89 | background-color: @kizuna_colour; 90 | color : lighten(saturate(@kizuna_colour, 10%), 40%); 91 | font-weight : normal; 92 | } 93 | 94 | &.Cute, 95 | &.Cute span, 96 | &.Cute a { 97 | border-color : darken(@cute_colour, 10%); 98 | background-color: @cute_colour; 99 | font-weight : normal; 100 | color : white; 101 | } 102 | 103 | &.Cool, 104 | &.Cool span, 105 | &.Cool a { 106 | border-color : darken(@cool_colour, 10%); 107 | background-color: @cool_colour; 108 | font-weight : normal; 109 | color : white; 110 | } 111 | 112 | &.Passion, 113 | &.Passion span, 114 | &.Passion a { 115 | border-color : lighten(@passion_colour, 10%); 116 | background-color: @passion_colour; 117 | font-weight : normal; 118 | color : black; 119 | } 120 | } 121 | 122 | .Cool_ax { 123 | color: @cool_colour_text; 124 | } 125 | 126 | .Cute_ax { 127 | color: @cute_colour_text; 128 | } 129 | 130 | .Passion_ax { 131 | color: @passion_colour_text; 132 | } 133 | 134 | .vocal_ax { 135 | color: @vocal_colour_text; 136 | } 137 | 138 | .dance_ax { 139 | color: @dance_colour_text; 140 | } 141 | 142 | .visual_ax { 143 | color: @visual_colour_text; 144 | } 145 | 146 | .kizuna_ax { 147 | color: @kizuna_colour_text; 148 | } 149 | 150 | .max_level_ax { 151 | color: @level_colour_text; 152 | } 153 | 154 | .life_ax { 155 | color: @life_colour_text; 156 | } 157 | 158 | .control_row { 159 | background-color: @layer2_bg; 160 | } 161 | 162 | .control_table .control_row, 163 | .control_table { 164 | color: @layer2_text; 165 | } 166 | 167 | .control_row th { 168 | color : @layer2_text; 169 | text-align : left; 170 | padding : 3px 6px; 171 | text-transform: uppercase; 172 | font-size : 80%; 173 | } 174 | 175 | .sort_key { 176 | text-decoration: underline; 177 | 178 | &.in_use { 179 | color: @table_sorter_active_criteria_text; 180 | } 181 | 182 | &[data-sort-reverse="yes"]::before { 183 | content: "▾ "; 184 | } 185 | 186 | &[data-sort-reverse="no"]::before { 187 | content: "▴ "; 188 | } 189 | } 190 | 191 | .toggles_row td { 192 | min-width : 30px; 193 | text-align: center; 194 | 195 | a { 196 | display : block; 197 | width : 100%; 198 | color : @table_filter_switch_text; 199 | text-decoration: none; 200 | } 201 | } 202 | 203 | td.filter_switch { 204 | background-color: @table_filter_switch_off_bg; 205 | 206 | &.enabled { 207 | background-color: @table_filter_switch_on_bg; 208 | } 209 | } 210 | 211 | .contains_large_table { 212 | .table { 213 | min-width: @large_table_min_width; 214 | } 215 | 216 | @media screen and (max-width:@large_table_min_width) { 217 | overflow-x : scroll; 218 | -webkit-overflow-scrolling: touch; 219 | overflow-scrolling : touch; 220 | } 221 | } -------------------------------------------------------------------------------- /static/css/widgets.less: -------------------------------------------------------------------------------- 1 | // @button_text : white; 2 | // @button_primary_bg : #007bff; 3 | // @button_destructive_bg : #dc3545; 4 | // @button_secondary_bg : #484e53; 5 | // @button_standard_rounding: 4px; 6 | 7 | // @text_field_text: white; 8 | 9 | // @stepper_bg : green; 10 | // @stepper_text: white; 11 | 12 | /* **************** modals ****************** */ 13 | 14 | .modal_fixed { 15 | position: fixed; 16 | top : 0; 17 | left : 0; 18 | right : 0; 19 | bottom : 0; 20 | } 21 | 22 | .modal_backdrop { 23 | transition: background 100ms linear; 24 | background: fade(@main_bg, 0%); 25 | z-index : 1000; 26 | 27 | &.on { 28 | background: fade(@main_bg, 70%); 29 | } 30 | } 31 | 32 | .modal_container { 33 | z-index : 1001; 34 | display : flex; 35 | align-items : center; 36 | pointer-events: none; 37 | padding : 16px; 38 | } 39 | 40 | .modal_self { 41 | margin : 0 auto; 42 | max-width : 500px; 43 | background-color: rgb(54, 55, 63); 44 | pointer-events : all; 45 | padding : 16px; 46 | 47 | border-radius: 6px; 48 | box-shadow : inset 0px 1px 1px rgba(255, 255, 255, 0.2), 49 | 0px 8px 20px rgba(0, 0, 0, 0.3); 50 | 51 | transition: all 150ms ease-out; 52 | 53 | &.close { 54 | transform: scale(0.7, 0.7); 55 | opacity : 0; 56 | } 57 | 58 | .text_field { 59 | margin-bottom: 8px; 60 | } 61 | 62 | a.destructive { 63 | color: lighten(@button_destructive_bg, 10%); 64 | font-weight: 600; 65 | text-decoration: none; 66 | 67 | &:hover { 68 | text-decoration: underline; 69 | } 70 | } 71 | } 72 | 73 | .modal_container:not(:last-child) .modal_self { 74 | transform: scale(0.9, 0.9); 75 | } 76 | 77 | .modal_detail_text { 78 | margin: 0; 79 | font-size: 90%; 80 | } 81 | 82 | /*********** controls *************/ 83 | 84 | .button_bevel { 85 | box-shadow: inset 0px 1px 0px rgba(255, 255, 255, 0.2), 86 | 0px 0px 0px 1px rgba(0, 0, 0, 0.3); 87 | } 88 | 89 | .button, 90 | .text_field { 91 | padding : 8px 10px; 92 | border-radius : @button_standard_rounding; 93 | font-size : 16px; 94 | -webkit-appearance: none; 95 | 96 | &:disabled { 97 | opacity: 0.5; 98 | } 99 | } 100 | 101 | .button, 102 | .image_switch { 103 | display : block; 104 | background: @button_secondary_bg; 105 | border : 0px solid black; 106 | color : @button_text; 107 | .button_bevel(); 108 | 109 | border-radius: @button_standard_rounding; 110 | 111 | &.destructive { 112 | background: @button_destructive_bg; 113 | } 114 | 115 | &.primary { 116 | background : @button_primary_bg; 117 | font-weight: bold; 118 | } 119 | 120 | text-decoration:none; 121 | 122 | &:visited { 123 | color: @main_text; 124 | } 125 | 126 | transition: transform 66ms ease-out; 127 | 128 | &:active { 129 | transform: scale(0.90, 0.90); 130 | } 131 | } 132 | 133 | .image_switch { 134 | padding : 4px 8px; 135 | font-size : 13px; 136 | margin : 0 4px; 137 | background-color: fade(@button_secondary_bg, 80%); 138 | } 139 | 140 | .text_field { 141 | width : 100%; 142 | background-color: rgba(255, 255, 255, 0.1); 143 | box-shadow : 0px 1px 0px rgba(255, 255, 255, 0.15), 144 | inset 0px 1px 5px rgba(0, 0, 0, 0.1); 145 | border : 0px solid black; 146 | color : @text_field_text; 147 | font-family: sans-serif; 148 | } 149 | 150 | .button_group { 151 | display : flex; 152 | flex-direction: row-reverse; 153 | margin : 0 -8px; 154 | margin-top : 16px; 155 | 156 | .spacer { 157 | flex-grow: 100; 158 | } 159 | 160 | .button { 161 | margin: 0 8px; 162 | } 163 | } 164 | 165 | .profile { 166 | .button_bevel(); 167 | } 168 | 169 | /*********** stepper *************/ 170 | 171 | .stepper { 172 | font-size : 0; 173 | display : inline-block; 174 | line-height : normal; 175 | background-color: @stepper_bg; 176 | padding : 1px; 177 | margin : 0 4px; 178 | border-radius : @button_standard_rounding; 179 | .button_bevel(); 180 | 181 | input[type="text"] { 182 | padding: 0; 183 | margin : 0; 184 | border : none; 185 | 186 | width : 50px; 187 | line-height: 18px; 188 | font-size : 13px; 189 | text-align : center; 190 | 191 | vertical-align: top; 192 | } 193 | 194 | button { 195 | padding : 0; 196 | margin : 0; 197 | border : none; 198 | background: transparent; 199 | 200 | min-width : 26px; 201 | line-height: 18px; 202 | font-size : 16px; 203 | font-weight: bold; 204 | 205 | color : @stepper_text; 206 | transition: transform 66ms ease-out; 207 | 208 | &:active { 209 | transform: scale(0.90, 0.90); 210 | } 211 | } 212 | } 213 | 214 | .svx_pose_container { 215 | display: inline-block; 216 | } 217 | -------------------------------------------------------------------------------- /static/dots.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 |
15 |

Those dots on card icons...

16 | 17 |

The dot at the top indicates the card's highest stat:

18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 |
Orange: Visual
Blue: Dance
Red: Vocal
Creamy purple: Balanced
36 | 37 |

And the dot below that indicates the card's skill type, if any:

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 |
Blue: perfect lock
Purple with slash: combo protect
Purple with chevron: combo bonus
Red: score bonus
Light green with chevron: healer
Light green with slash: life protect
Yellow: Overload
68 | 69 |

There are larger icons for individual card boxes:

70 |
    71 |
  • high Visual
  • 72 |
  • high Dance
  • 73 |
  • high Vocal
  • 74 |
  • balanced
  • 75 | 76 |
  • Perfect lock
  • 77 |
  • Protects combo
  • 78 |
  • Combo bonus up
  • 79 |
  • Score bonus
  • 80 |
  • Healer
  • 81 |
  • Guard
  • 82 |
  • Overload
  • 83 |
84 | 85 | -------------------------------------------------------------------------------- /static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/summertriangle-dev/sparklebox/c3ecf8a5797915c193f660859c77236389c8182d/static/favicon.ico -------------------------------------------------------------------------------- /static/img/assets/icons/allround.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/summertriangle-dev/sparklebox/c3ecf8a5797915c193f660859c77236389c8182d/static/img/assets/icons/allround.png -------------------------------------------------------------------------------- /static/img/assets/icons/allround@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/summertriangle-dev/sparklebox/c3ecf8a5797915c193f660859c77236389c8182d/static/img/assets/icons/allround@2x.png -------------------------------------------------------------------------------- /static/img/assets/icons/alternate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/summertriangle-dev/sparklebox/c3ecf8a5797915c193f660859c77236389c8182d/static/img/assets/icons/alternate.png -------------------------------------------------------------------------------- /static/img/assets/icons/alternate@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/summertriangle-dev/sparklebox/c3ecf8a5797915c193f660859c77236389c8182d/static/img/assets/icons/alternate@2x.png -------------------------------------------------------------------------------- /static/img/assets/icons/balance.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/summertriangle-dev/sparklebox/c3ecf8a5797915c193f660859c77236389c8182d/static/img/assets/icons/balance.png -------------------------------------------------------------------------------- /static/img/assets/icons/balance@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/summertriangle-dev/sparklebox/c3ecf8a5797915c193f660859c77236389c8182d/static/img/assets/icons/balance@2x.png -------------------------------------------------------------------------------- /static/img/assets/icons/cboost.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/summertriangle-dev/sparklebox/c3ecf8a5797915c193f660859c77236389c8182d/static/img/assets/icons/cboost.png -------------------------------------------------------------------------------- /static/img/assets/icons/cboost@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/summertriangle-dev/sparklebox/c3ecf8a5797915c193f660859c77236389c8182d/static/img/assets/icons/cboost@2x.png -------------------------------------------------------------------------------- /static/img/assets/icons/cguard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/summertriangle-dev/sparklebox/c3ecf8a5797915c193f660859c77236389c8182d/static/img/assets/icons/cguard.png -------------------------------------------------------------------------------- /static/img/assets/icons/cguard@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/summertriangle-dev/sparklebox/c3ecf8a5797915c193f660859c77236389c8182d/static/img/assets/icons/cguard@2x.png -------------------------------------------------------------------------------- /static/img/assets/icons/concentrate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/summertriangle-dev/sparklebox/c3ecf8a5797915c193f660859c77236389c8182d/static/img/assets/icons/concentrate.png -------------------------------------------------------------------------------- /static/img/assets/icons/concentrate@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/summertriangle-dev/sparklebox/c3ecf8a5797915c193f660859c77236389c8182d/static/img/assets/icons/concentrate@2x.png -------------------------------------------------------------------------------- /static/img/assets/icons/dance.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/summertriangle-dev/sparklebox/c3ecf8a5797915c193f660859c77236389c8182d/static/img/assets/icons/dance.png -------------------------------------------------------------------------------- /static/img/assets/icons/dance@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/summertriangle-dev/sparklebox/c3ecf8a5797915c193f660859c77236389c8182d/static/img/assets/icons/dance@2x.png -------------------------------------------------------------------------------- /static/img/assets/icons/encore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/summertriangle-dev/sparklebox/c3ecf8a5797915c193f660859c77236389c8182d/static/img/assets/icons/encore.png -------------------------------------------------------------------------------- /static/img/assets/icons/encore@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/summertriangle-dev/sparklebox/c3ecf8a5797915c193f660859c77236389c8182d/static/img/assets/icons/encore@2x.png -------------------------------------------------------------------------------- /static/img/assets/icons/focus_co.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/summertriangle-dev/sparklebox/c3ecf8a5797915c193f660859c77236389c8182d/static/img/assets/icons/focus_co.png -------------------------------------------------------------------------------- /static/img/assets/icons/focus_co@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/summertriangle-dev/sparklebox/c3ecf8a5797915c193f660859c77236389c8182d/static/img/assets/icons/focus_co@2x.png -------------------------------------------------------------------------------- /static/img/assets/icons/focus_cu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/summertriangle-dev/sparklebox/c3ecf8a5797915c193f660859c77236389c8182d/static/img/assets/icons/focus_cu.png -------------------------------------------------------------------------------- /static/img/assets/icons/focus_cu@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/summertriangle-dev/sparklebox/c3ecf8a5797915c193f660859c77236389c8182d/static/img/assets/icons/focus_cu@2x.png -------------------------------------------------------------------------------- /static/img/assets/icons/focus_flat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/summertriangle-dev/sparklebox/c3ecf8a5797915c193f660859c77236389c8182d/static/img/assets/icons/focus_flat.png -------------------------------------------------------------------------------- /static/img/assets/icons/focus_flat@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/summertriangle-dev/sparklebox/c3ecf8a5797915c193f660859c77236389c8182d/static/img/assets/icons/focus_flat@2x.png -------------------------------------------------------------------------------- /static/img/assets/icons/focus_pa.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/summertriangle-dev/sparklebox/c3ecf8a5797915c193f660859c77236389c8182d/static/img/assets/icons/focus_pa.png -------------------------------------------------------------------------------- /static/img/assets/icons/focus_pa@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/summertriangle-dev/sparklebox/c3ecf8a5797915c193f660859c77236389c8182d/static/img/assets/icons/focus_pa@2x.png -------------------------------------------------------------------------------- /static/img/assets/icons/heal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/summertriangle-dev/sparklebox/c3ecf8a5797915c193f660859c77236389c8182d/static/img/assets/icons/heal.png -------------------------------------------------------------------------------- /static/img/assets/icons/heal@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/summertriangle-dev/sparklebox/c3ecf8a5797915c193f660859c77236389c8182d/static/img/assets/icons/heal@2x.png -------------------------------------------------------------------------------- /static/img/assets/icons/hguard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/summertriangle-dev/sparklebox/c3ecf8a5797915c193f660859c77236389c8182d/static/img/assets/icons/hguard.png -------------------------------------------------------------------------------- /static/img/assets/icons/hguard@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/summertriangle-dev/sparklebox/c3ecf8a5797915c193f660859c77236389c8182d/static/img/assets/icons/hguard@2x.png -------------------------------------------------------------------------------- /static/img/assets/icons/magic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/summertriangle-dev/sparklebox/c3ecf8a5797915c193f660859c77236389c8182d/static/img/assets/icons/magic.png -------------------------------------------------------------------------------- /static/img/assets/icons/magic@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/summertriangle-dev/sparklebox/c3ecf8a5797915c193f660859c77236389c8182d/static/img/assets/icons/magic@2x.png -------------------------------------------------------------------------------- /static/img/assets/icons/motif.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/summertriangle-dev/sparklebox/c3ecf8a5797915c193f660859c77236389c8182d/static/img/assets/icons/motif.png -------------------------------------------------------------------------------- /static/img/assets/icons/motif@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/summertriangle-dev/sparklebox/c3ecf8a5797915c193f660859c77236389c8182d/static/img/assets/icons/motif@2x.png -------------------------------------------------------------------------------- /static/img/assets/icons/mutual.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/summertriangle-dev/sparklebox/c3ecf8a5797915c193f660859c77236389c8182d/static/img/assets/icons/mutual.png -------------------------------------------------------------------------------- /static/img/assets/icons/mutual@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/summertriangle-dev/sparklebox/c3ecf8a5797915c193f660859c77236389c8182d/static/img/assets/icons/mutual@2x.png -------------------------------------------------------------------------------- /static/img/assets/icons/overload.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/summertriangle-dev/sparklebox/c3ecf8a5797915c193f660859c77236389c8182d/static/img/assets/icons/overload.png -------------------------------------------------------------------------------- /static/img/assets/icons/overload@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/summertriangle-dev/sparklebox/c3ecf8a5797915c193f660859c77236389c8182d/static/img/assets/icons/overload@2x.png -------------------------------------------------------------------------------- /static/img/assets/icons/plock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/summertriangle-dev/sparklebox/c3ecf8a5797915c193f660859c77236389c8182d/static/img/assets/icons/plock.png -------------------------------------------------------------------------------- /static/img/assets/icons/plock@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/summertriangle-dev/sparklebox/c3ecf8a5797915c193f660859c77236389c8182d/static/img/assets/icons/plock@2x.png -------------------------------------------------------------------------------- /static/img/assets/icons/psb_flick.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/summertriangle-dev/sparklebox/c3ecf8a5797915c193f660859c77236389c8182d/static/img/assets/icons/psb_flick.png -------------------------------------------------------------------------------- /static/img/assets/icons/psb_flick@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/summertriangle-dev/sparklebox/c3ecf8a5797915c193f660859c77236389c8182d/static/img/assets/icons/psb_flick@2x.png -------------------------------------------------------------------------------- /static/img/assets/icons/psb_hold.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/summertriangle-dev/sparklebox/c3ecf8a5797915c193f660859c77236389c8182d/static/img/assets/icons/psb_hold.png -------------------------------------------------------------------------------- /static/img/assets/icons/psb_hold@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/summertriangle-dev/sparklebox/c3ecf8a5797915c193f660859c77236389c8182d/static/img/assets/icons/psb_hold@2x.png -------------------------------------------------------------------------------- /static/img/assets/icons/psb_slide.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/summertriangle-dev/sparklebox/c3ecf8a5797915c193f660859c77236389c8182d/static/img/assets/icons/psb_slide.png -------------------------------------------------------------------------------- /static/img/assets/icons/psb_slide@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/summertriangle-dev/sparklebox/c3ecf8a5797915c193f660859c77236389c8182d/static/img/assets/icons/psb_slide@2x.png -------------------------------------------------------------------------------- /static/img/assets/icons/refrain.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/summertriangle-dev/sparklebox/c3ecf8a5797915c193f660859c77236389c8182d/static/img/assets/icons/refrain.png -------------------------------------------------------------------------------- /static/img/assets/icons/refrain@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/summertriangle-dev/sparklebox/c3ecf8a5797915c193f660859c77236389c8182d/static/img/assets/icons/refrain@2x.png -------------------------------------------------------------------------------- /static/img/assets/icons/scoreup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/summertriangle-dev/sparklebox/c3ecf8a5797915c193f660859c77236389c8182d/static/img/assets/icons/scoreup.png -------------------------------------------------------------------------------- /static/img/assets/icons/scoreup@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/summertriangle-dev/sparklebox/c3ecf8a5797915c193f660859c77236389c8182d/static/img/assets/icons/scoreup@2x.png -------------------------------------------------------------------------------- /static/img/assets/icons/skillboost.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/summertriangle-dev/sparklebox/c3ecf8a5797915c193f660859c77236389c8182d/static/img/assets/icons/skillboost.png -------------------------------------------------------------------------------- /static/img/assets/icons/skillboost@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/summertriangle-dev/sparklebox/c3ecf8a5797915c193f660859c77236389c8182d/static/img/assets/icons/skillboost@2x.png -------------------------------------------------------------------------------- /static/img/assets/icons/sparkle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/summertriangle-dev/sparklebox/c3ecf8a5797915c193f660859c77236389c8182d/static/img/assets/icons/sparkle.png -------------------------------------------------------------------------------- /static/img/assets/icons/sparkle@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/summertriangle-dev/sparklebox/c3ecf8a5797915c193f660859c77236389c8182d/static/img/assets/icons/sparkle@2x.png -------------------------------------------------------------------------------- /static/img/assets/icons/symphony.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/summertriangle-dev/sparklebox/c3ecf8a5797915c193f660859c77236389c8182d/static/img/assets/icons/symphony.png -------------------------------------------------------------------------------- /static/img/assets/icons/symphony@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/summertriangle-dev/sparklebox/c3ecf8a5797915c193f660859c77236389c8182d/static/img/assets/icons/symphony@2x.png -------------------------------------------------------------------------------- /static/img/assets/icons/synergy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/summertriangle-dev/sparklebox/c3ecf8a5797915c193f660859c77236389c8182d/static/img/assets/icons/synergy.png -------------------------------------------------------------------------------- /static/img/assets/icons/synergy@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/summertriangle-dev/sparklebox/c3ecf8a5797915c193f660859c77236389c8182d/static/img/assets/icons/synergy@2x.png -------------------------------------------------------------------------------- /static/img/assets/icons/tuning.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/summertriangle-dev/sparklebox/c3ecf8a5797915c193f660859c77236389c8182d/static/img/assets/icons/tuning.png -------------------------------------------------------------------------------- /static/img/assets/icons/tuning@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/summertriangle-dev/sparklebox/c3ecf8a5797915c193f660859c77236389c8182d/static/img/assets/icons/tuning@2x.png -------------------------------------------------------------------------------- /static/img/assets/icons/visual.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/summertriangle-dev/sparklebox/c3ecf8a5797915c193f660859c77236389c8182d/static/img/assets/icons/visual.png -------------------------------------------------------------------------------- /static/img/assets/icons/visual@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/summertriangle-dev/sparklebox/c3ecf8a5797915c193f660859c77236389c8182d/static/img/assets/icons/visual@2x.png -------------------------------------------------------------------------------- /static/img/assets/icons/vocal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/summertriangle-dev/sparklebox/c3ecf8a5797915c193f660859c77236389c8182d/static/img/assets/icons/vocal.png -------------------------------------------------------------------------------- /static/img/assets/icons/vocal@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/summertriangle-dev/sparklebox/c3ecf8a5797915c193f660859c77236389c8182d/static/img/assets/icons/vocal@2x.png -------------------------------------------------------------------------------- /static/img/assets/msprites_hs/balance_sm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/summertriangle-dev/sparklebox/c3ecf8a5797915c193f660859c77236389c8182d/static/img/assets/msprites_hs/balance_sm.png -------------------------------------------------------------------------------- /static/img/assets/msprites_hs/balance_sm@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/summertriangle-dev/sparklebox/c3ecf8a5797915c193f660859c77236389c8182d/static/img/assets/msprites_hs/balance_sm@2x.png -------------------------------------------------------------------------------- /static/img/assets/msprites_hs/dance_sm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/summertriangle-dev/sparklebox/c3ecf8a5797915c193f660859c77236389c8182d/static/img/assets/msprites_hs/dance_sm.png -------------------------------------------------------------------------------- /static/img/assets/msprites_hs/dance_sm@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/summertriangle-dev/sparklebox/c3ecf8a5797915c193f660859c77236389c8182d/static/img/assets/msprites_hs/dance_sm@2x.png -------------------------------------------------------------------------------- /static/img/assets/msprites_hs/visual_sm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/summertriangle-dev/sparklebox/c3ecf8a5797915c193f660859c77236389c8182d/static/img/assets/msprites_hs/visual_sm.png -------------------------------------------------------------------------------- /static/img/assets/msprites_hs/visual_sm@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/summertriangle-dev/sparklebox/c3ecf8a5797915c193f660859c77236389c8182d/static/img/assets/msprites_hs/visual_sm@2x.png -------------------------------------------------------------------------------- /static/img/assets/msprites_hs/vocal_sm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/summertriangle-dev/sparklebox/c3ecf8a5797915c193f660859c77236389c8182d/static/img/assets/msprites_hs/vocal_sm.png -------------------------------------------------------------------------------- /static/img/assets/msprites_hs/vocal_sm@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/summertriangle-dev/sparklebox/c3ecf8a5797915c193f660859c77236389c8182d/static/img/assets/msprites_hs/vocal_sm@2x.png -------------------------------------------------------------------------------- /static/img/assets/msprites_sk/allround_sm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/summertriangle-dev/sparklebox/c3ecf8a5797915c193f660859c77236389c8182d/static/img/assets/msprites_sk/allround_sm.png -------------------------------------------------------------------------------- /static/img/assets/msprites_sk/allround_sm@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/summertriangle-dev/sparklebox/c3ecf8a5797915c193f660859c77236389c8182d/static/img/assets/msprites_sk/allround_sm@2x.png -------------------------------------------------------------------------------- /static/img/assets/msprites_sk/alternate_sm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/summertriangle-dev/sparklebox/c3ecf8a5797915c193f660859c77236389c8182d/static/img/assets/msprites_sk/alternate_sm.png -------------------------------------------------------------------------------- /static/img/assets/msprites_sk/alternate_sm@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/summertriangle-dev/sparklebox/c3ecf8a5797915c193f660859c77236389c8182d/static/img/assets/msprites_sk/alternate_sm@2x.png -------------------------------------------------------------------------------- /static/img/assets/msprites_sk/cboost_sm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/summertriangle-dev/sparklebox/c3ecf8a5797915c193f660859c77236389c8182d/static/img/assets/msprites_sk/cboost_sm.png -------------------------------------------------------------------------------- /static/img/assets/msprites_sk/cboost_sm@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/summertriangle-dev/sparklebox/c3ecf8a5797915c193f660859c77236389c8182d/static/img/assets/msprites_sk/cboost_sm@2x.png -------------------------------------------------------------------------------- /static/img/assets/msprites_sk/cguard_sm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/summertriangle-dev/sparklebox/c3ecf8a5797915c193f660859c77236389c8182d/static/img/assets/msprites_sk/cguard_sm.png -------------------------------------------------------------------------------- /static/img/assets/msprites_sk/cguard_sm@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/summertriangle-dev/sparklebox/c3ecf8a5797915c193f660859c77236389c8182d/static/img/assets/msprites_sk/cguard_sm@2x.png -------------------------------------------------------------------------------- /static/img/assets/msprites_sk/concentrate_sm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/summertriangle-dev/sparklebox/c3ecf8a5797915c193f660859c77236389c8182d/static/img/assets/msprites_sk/concentrate_sm.png -------------------------------------------------------------------------------- /static/img/assets/msprites_sk/concentrate_sm@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/summertriangle-dev/sparklebox/c3ecf8a5797915c193f660859c77236389c8182d/static/img/assets/msprites_sk/concentrate_sm@2x.png -------------------------------------------------------------------------------- /static/img/assets/msprites_sk/encore_sm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/summertriangle-dev/sparklebox/c3ecf8a5797915c193f660859c77236389c8182d/static/img/assets/msprites_sk/encore_sm.png -------------------------------------------------------------------------------- /static/img/assets/msprites_sk/encore_sm@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/summertriangle-dev/sparklebox/c3ecf8a5797915c193f660859c77236389c8182d/static/img/assets/msprites_sk/encore_sm@2x.png -------------------------------------------------------------------------------- /static/img/assets/msprites_sk/focus_flat_sm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/summertriangle-dev/sparklebox/c3ecf8a5797915c193f660859c77236389c8182d/static/img/assets/msprites_sk/focus_flat_sm.png -------------------------------------------------------------------------------- /static/img/assets/msprites_sk/focus_flat_sm@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/summertriangle-dev/sparklebox/c3ecf8a5797915c193f660859c77236389c8182d/static/img/assets/msprites_sk/focus_flat_sm@2x.png -------------------------------------------------------------------------------- /static/img/assets/msprites_sk/focus_sm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/summertriangle-dev/sparklebox/c3ecf8a5797915c193f660859c77236389c8182d/static/img/assets/msprites_sk/focus_sm.png -------------------------------------------------------------------------------- /static/img/assets/msprites_sk/focus_sm@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/summertriangle-dev/sparklebox/c3ecf8a5797915c193f660859c77236389c8182d/static/img/assets/msprites_sk/focus_sm@2x.png -------------------------------------------------------------------------------- /static/img/assets/msprites_sk/heal_sm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/summertriangle-dev/sparklebox/c3ecf8a5797915c193f660859c77236389c8182d/static/img/assets/msprites_sk/heal_sm.png -------------------------------------------------------------------------------- /static/img/assets/msprites_sk/heal_sm@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/summertriangle-dev/sparklebox/c3ecf8a5797915c193f660859c77236389c8182d/static/img/assets/msprites_sk/heal_sm@2x.png -------------------------------------------------------------------------------- /static/img/assets/msprites_sk/hguard_sm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/summertriangle-dev/sparklebox/c3ecf8a5797915c193f660859c77236389c8182d/static/img/assets/msprites_sk/hguard_sm.png -------------------------------------------------------------------------------- /static/img/assets/msprites_sk/hguard_sm@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/summertriangle-dev/sparklebox/c3ecf8a5797915c193f660859c77236389c8182d/static/img/assets/msprites_sk/hguard_sm@2x.png -------------------------------------------------------------------------------- /static/img/assets/msprites_sk/magic_sm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/summertriangle-dev/sparklebox/c3ecf8a5797915c193f660859c77236389c8182d/static/img/assets/msprites_sk/magic_sm.png -------------------------------------------------------------------------------- /static/img/assets/msprites_sk/magic_sm@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/summertriangle-dev/sparklebox/c3ecf8a5797915c193f660859c77236389c8182d/static/img/assets/msprites_sk/magic_sm@2x.png -------------------------------------------------------------------------------- /static/img/assets/msprites_sk/mutual_sm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/summertriangle-dev/sparklebox/c3ecf8a5797915c193f660859c77236389c8182d/static/img/assets/msprites_sk/mutual_sm.png -------------------------------------------------------------------------------- /static/img/assets/msprites_sk/mutual_sm@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/summertriangle-dev/sparklebox/c3ecf8a5797915c193f660859c77236389c8182d/static/img/assets/msprites_sk/mutual_sm@2x.png -------------------------------------------------------------------------------- /static/img/assets/msprites_sk/overload_sm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/summertriangle-dev/sparklebox/c3ecf8a5797915c193f660859c77236389c8182d/static/img/assets/msprites_sk/overload_sm.png -------------------------------------------------------------------------------- /static/img/assets/msprites_sk/overload_sm@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/summertriangle-dev/sparklebox/c3ecf8a5797915c193f660859c77236389c8182d/static/img/assets/msprites_sk/overload_sm@2x.png -------------------------------------------------------------------------------- /static/img/assets/msprites_sk/plock_sm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/summertriangle-dev/sparklebox/c3ecf8a5797915c193f660859c77236389c8182d/static/img/assets/msprites_sk/plock_sm.png -------------------------------------------------------------------------------- /static/img/assets/msprites_sk/plock_sm@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/summertriangle-dev/sparklebox/c3ecf8a5797915c193f660859c77236389c8182d/static/img/assets/msprites_sk/plock_sm@2x.png -------------------------------------------------------------------------------- /static/img/assets/msprites_sk/psb_flick_sm.png: -------------------------------------------------------------------------------- 1 | scoreup_sm.png -------------------------------------------------------------------------------- /static/img/assets/msprites_sk/psb_flick_sm@2x.png: -------------------------------------------------------------------------------- 1 | scoreup_sm@2x.png -------------------------------------------------------------------------------- /static/img/assets/msprites_sk/psb_hold_sm.png: -------------------------------------------------------------------------------- 1 | scoreup_sm.png -------------------------------------------------------------------------------- /static/img/assets/msprites_sk/psb_hold_sm@2x.png: -------------------------------------------------------------------------------- 1 | scoreup_sm@2x.png -------------------------------------------------------------------------------- /static/img/assets/msprites_sk/psb_slide_sm.png: -------------------------------------------------------------------------------- 1 | scoreup_sm.png -------------------------------------------------------------------------------- /static/img/assets/msprites_sk/psb_slide_sm@2x.png: -------------------------------------------------------------------------------- 1 | scoreup_sm@2x.png -------------------------------------------------------------------------------- /static/img/assets/msprites_sk/refrain_sm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/summertriangle-dev/sparklebox/c3ecf8a5797915c193f660859c77236389c8182d/static/img/assets/msprites_sk/refrain_sm.png -------------------------------------------------------------------------------- /static/img/assets/msprites_sk/refrain_sm@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/summertriangle-dev/sparklebox/c3ecf8a5797915c193f660859c77236389c8182d/static/img/assets/msprites_sk/refrain_sm@2x.png -------------------------------------------------------------------------------- /static/img/assets/msprites_sk/scoreup_sm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/summertriangle-dev/sparklebox/c3ecf8a5797915c193f660859c77236389c8182d/static/img/assets/msprites_sk/scoreup_sm.png -------------------------------------------------------------------------------- /static/img/assets/msprites_sk/scoreup_sm@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/summertriangle-dev/sparklebox/c3ecf8a5797915c193f660859c77236389c8182d/static/img/assets/msprites_sk/scoreup_sm@2x.png -------------------------------------------------------------------------------- /static/img/assets/msprites_sk/skillboost_sm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/summertriangle-dev/sparklebox/c3ecf8a5797915c193f660859c77236389c8182d/static/img/assets/msprites_sk/skillboost_sm.png -------------------------------------------------------------------------------- /static/img/assets/msprites_sk/skillboost_sm@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/summertriangle-dev/sparklebox/c3ecf8a5797915c193f660859c77236389c8182d/static/img/assets/msprites_sk/skillboost_sm@2x.png -------------------------------------------------------------------------------- /static/img/assets/msprites_sk/sparkle_sm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/summertriangle-dev/sparklebox/c3ecf8a5797915c193f660859c77236389c8182d/static/img/assets/msprites_sk/sparkle_sm.png -------------------------------------------------------------------------------- /static/img/assets/msprites_sk/sparkle_sm@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/summertriangle-dev/sparklebox/c3ecf8a5797915c193f660859c77236389c8182d/static/img/assets/msprites_sk/sparkle_sm@2x.png -------------------------------------------------------------------------------- /static/img/assets/msprites_sk/symphony_sm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/summertriangle-dev/sparklebox/c3ecf8a5797915c193f660859c77236389c8182d/static/img/assets/msprites_sk/symphony_sm.png -------------------------------------------------------------------------------- /static/img/assets/msprites_sk/symphony_sm@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/summertriangle-dev/sparklebox/c3ecf8a5797915c193f660859c77236389c8182d/static/img/assets/msprites_sk/symphony_sm@2x.png -------------------------------------------------------------------------------- /static/img/assets/msprites_sk/synergy_sm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/summertriangle-dev/sparklebox/c3ecf8a5797915c193f660859c77236389c8182d/static/img/assets/msprites_sk/synergy_sm.png -------------------------------------------------------------------------------- /static/img/assets/msprites_sk/synergy_sm@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/summertriangle-dev/sparklebox/c3ecf8a5797915c193f660859c77236389c8182d/static/img/assets/msprites_sk/synergy_sm@2x.png -------------------------------------------------------------------------------- /static/img/assets/msprites_sk/tuning_sm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/summertriangle-dev/sparklebox/c3ecf8a5797915c193f660859c77236389c8182d/static/img/assets/msprites_sk/tuning_sm.png -------------------------------------------------------------------------------- /static/img/assets/msprites_sk/tuning_sm@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/summertriangle-dev/sparklebox/c3ecf8a5797915c193f660859c77236389c8182d/static/img/assets/msprites_sk/tuning_sm@2x.png -------------------------------------------------------------------------------- /static/img/marker_empty_face.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/summertriangle-dev/sparklebox/c3ecf8a5797915c193f660859c77236389c8182d/static/img/marker_empty_face.png -------------------------------------------------------------------------------- /static/img/sprites.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/summertriangle-dev/sparklebox/c3ecf8a5797915c193f660859c77236389c8182d/static/img/sprites.png -------------------------------------------------------------------------------- /static/img/sprites@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/summertriangle-dev/sparklebox/c3ecf8a5797915c193f660859c77236389c8182d/static/img/sprites@2x.png -------------------------------------------------------------------------------- /static/img/sribbons.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/summertriangle-dev/sparklebox/c3ecf8a5797915c193f660859c77236389c8182d/static/img/sribbons.png -------------------------------------------------------------------------------- /static/img/sribbons@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/summertriangle-dev/sparklebox/c3ecf8a5797915c193f660859c77236389c8182d/static/img/sribbons@2x.png -------------------------------------------------------------------------------- /static/js/home.js: -------------------------------------------------------------------------------- 1 | var gis_loading_completion_list = false; 2 | 3 | function load_completion_list_and_call(func) { 4 | if (gis_loading_completion_list) { 5 | return; 6 | } 7 | 8 | gis_loading_completion_list = true; 9 | var xhr = new XMLHttpRequest() 10 | xhr.open("GET", "/suggest", true) 11 | xhr.onreadystatechange = function() { 12 | if (xhr.readyState == 4 && xhr.status == 200) { 13 | window.name_completion_list = JSON.parse(xhr.responseText) 14 | gis_loading_completion_list = false 15 | func() 16 | } 17 | } 18 | xhr.send() 19 | } 20 | 21 | function fuzzyfinder(string, names) { 22 | var suggestions = [] 23 | var pattern = string.split("").join('.*?') 24 | var regex = new RegExp(pattern) 25 | 26 | for (var i = 0; i < names.length; i++) { 27 | var match = regex.exec(names[i]) 28 | 29 | if (match !== null) { 30 | var len = match[0].length 31 | var loc = match.index 32 | 33 | suggestions.push([len, loc, names[i]]) 34 | } 35 | } 36 | 37 | suggestions.sort(function(a, b) { 38 | return a[0] - b[0]; 39 | }); 40 | return suggestions 41 | } 42 | 43 | function suggest(that, text) { 44 | if (!text) { 45 | document.getElementById("suggestions").innerHTML = "" 46 | return 47 | } 48 | 49 | if (window.name_completion_list === undefined) { 50 | load_completion_list_and_call(function() { suggest(that, that.value) }) 51 | return 52 | } 53 | 54 | text = text.toLowerCase() 55 | var found = fuzzyfinder(text, Object.keys(window.name_completion_list)) 56 | document.getElementById("suggestions").innerHTML = "" 57 | 58 | for (var i = 0; i < found.length; i++) { 59 | var n = document.createElement("a"); 60 | 61 | put = "" 62 | aname = window.name_completion_list[found[i][2]][0] 63 | s1 = aname.slice(0, found[i][1]) 64 | s2 = aname.slice(found[i][1], found[i][1] + found[i][0]) 65 | s3 = aname.slice(found[i][1] + found[i][0]) 66 | 67 | n.innerHTML = s1 + put + s2 + "" + s3 68 | n.href = "/char/" + window.name_completion_list[found[i][2]][1] 69 | document.getElementById("suggestions").appendChild(n) 70 | } 71 | } 72 | 73 | // https://stackoverflow.com/questions/10073699/pad-a-number-with-leading-zeros-in-javascript 74 | function pad_digits(number, digits) { 75 | return Array(Math.max(digits - String(number).length + 1, 0)).join(0) + number; 76 | } 77 | 78 | function ec_count(that) { 79 | var expired = 0; 80 | var d = new Date() 81 | var msLeft = (parseFloat(that.getAttribute("data-count-to")) * 1000) - d.getTime() 82 | 83 | if (msLeft < 0) { 84 | expired = 1; 85 | msLeft = -msLeft; 86 | } 87 | 88 | var seconds = msLeft / 1000 89 | var secondsOnly = seconds % 60 90 | var minutes = (seconds - secondsOnly) / 60 91 | var minutesOnly = minutes % 60 92 | var hours = (minutes - minutesOnly) / 60 93 | var hoursOnly = hours % 24 94 | var days = (hours - hoursOnly) / 24 95 | 96 | var s = pad_digits(hoursOnly, 2) + ":" + pad_digits(minutesOnly, 2) + ":" + pad_digits(secondsOnly | 0, 2) 97 | if (days) { 98 | s = days + (days == 1? " day, " : " days, ") + s 99 | } 100 | 101 | if (expired) { 102 | s = "(ended " + s + " ago)"; 103 | } 104 | 105 | that.textContent = s 106 | } 107 | 108 | function event_counter_init() { 109 | if (document.getElementById("event_counter_container")) 110 | document.getElementById("event_counter_container").style.display = "block" 111 | var ec = document.querySelectorAll(".counter"); 112 | if (ec.length) { 113 | setInterval(function() { 114 | for (var i = 0; i < ec.length; i++) { 115 | ec_count(ec[i]); 116 | } 117 | }, 500); 118 | } 119 | } 120 | 121 | // TODO: is there a way to ask the browser for this value instead of hardcoding it here? 122 | JST_MINUTES_LEFT_OF_UTC = -540; 123 | MINUTES_TO_MILLIS = 60 * 1000; 124 | 125 | A_SECOND_BEFORE_MINAMIS_BIRTHDAY = new Date(2016, 6, 26, 7, 59, 59, 0); 126 | ACTUALLY_MINAMIS_BIRTHDAY = new Date(2016, 6, 26, 8, 0, 0, 0); 127 | 128 | function birthday_hider_init() { 129 | var els = document.querySelectorAll(".birthday_banner"); 130 | 131 | // don't even bother trying to trigger this. 132 | // you need to be in PDT and have the server give you the birthday code. 133 | var today; 134 | if (window.location.hash == "#testbirthdaypls") 135 | today = A_SECOND_BEFORE_MINAMIS_BIRTHDAY; 136 | else if (window.location.hash == "#testarealbirthdaypls") 137 | today = ACTUALLY_MINAMIS_BIRTHDAY; 138 | else 139 | today = new Date(); 140 | 141 | // here, we calculate the current JST date 142 | // basically, javascript date arithmetic sucks ass 143 | var minutes_left_of_utc = today.getTimezoneOffset(); 144 | // we're going right, not left 145 | var jst_minute_offset = -(JST_MINUTES_LEFT_OF_UTC - minutes_left_of_utc); 146 | 147 | // console.log("we're " + minutes_left_of_utc + " minutes off UTC"); 148 | // console.log("we need to add " + jst_minute_offset + " min to our time to get to JST"); 149 | 150 | // this date will have the wrong timezone, but we do not care 151 | var jsttoday = new Date(today.getTime() + (jst_minute_offset * MINUTES_TO_MILLIS)); 152 | // console.log("it's " + jsttoday + " in Japan"); 153 | 154 | for (var i = 0; i < els.length; i++) { 155 | var el = els[i]; 156 | var date = el.getAttribute("data-birthday").split("/"); 157 | 158 | if (parseInt(date[0]) == jsttoday.getMonth() + 1 && parseInt(date[1]) == jsttoday.getDate()) { 159 | el.style.display = "block"; 160 | el.querySelector(".where_the_birthday_is").textContent = "in Japan"; 161 | } else if (parseInt(date[0]) == today.getMonth() + 1 && parseInt(date[1]) == today.getDate()) { 162 | el.style.display = "block"; 163 | el.querySelector(".where_the_birthday_is").textContent = "in your time"; 164 | } 165 | } 166 | } -------------------------------------------------------------------------------- /static/js/level.js: -------------------------------------------------------------------------------- 1 | function assign_minmax_if_needed(ae) { 2 | if (!ae.getAttribute("data-max") || !ae.getAttribute("data-min")) { 3 | var vals = ae.textContent.split(".."); 4 | ae.setAttribute("data-min", vals[0]); 5 | ae.setAttribute("data-max", vals[1]); 6 | } 7 | } 8 | 9 | function reassign_vars_for_skill(that) { 10 | var box = that.parentNode.parentNode.parentNode.parentNode; 11 | var elems = box.querySelectorAll(".var"); 12 | var lv = parseInt(that.value) || 0; 13 | for (var i = 0; i < elems.length; i++) { 14 | assign_minmax_if_needed(elems[i]); 15 | 16 | if (lv == 0) { 17 | elems[i].textContent = elems[i].getAttribute("data-min") + ".." + elems[i].getAttribute("data-max"); 18 | continue; 19 | } 20 | 21 | var add = (parseFloat(elems[i].getAttribute("data-max")) - parseFloat(elems[i].getAttribute("data-min"))) * ((lv - 1) / 9) 22 | var fr = parseFloat(elems[i].getAttribute("data-min")) + add; 23 | var trunc = fr | 0; 24 | 25 | if (fr - trunc < 0.001) { 26 | elems[i].textContent = trunc; 27 | } else { 28 | elems[i].textContent = fr.toFixed(2); 29 | } 30 | } 31 | } 32 | 33 | function reassign_vars_for_base(that) { 34 | var box = that.parentNode.parentNode.parentNode.parentNode; 35 | var elems = box.querySelectorAll(".var"); 36 | var lv = parseInt(that.value) || 0; 37 | for (var i = 0; i < elems.length; i++) { 38 | assign_minmax_if_needed(elems[i]); 39 | 40 | if (lv == 0) { 41 | elems[i].textContent = elems[i].getAttribute("data-min") + ".." + elems[i].getAttribute("data-max"); 42 | continue; 43 | } 44 | 45 | var add = (parseFloat(elems[i].getAttribute("data-max")) - parseFloat(elems[i].getAttribute("data-min"))) * ((lv - 1) / (parseInt(that.parentNode.getAttribute("data-stepper-max")) - 1)) 46 | var fr = parseFloat(elems[i].getAttribute("data-min")) + add; 47 | elems[i].textContent = fr | 0; 48 | } 49 | } 50 | 51 | function skill_onchange(that) { 52 | var twaddle = that; 53 | while (!twaddle.hasAttribute("data-stepper-max")) { 54 | twaddle = twaddle.parentNode; 55 | } 56 | 57 | that.value = Math.min(parseInt(twaddle.getAttribute("data-stepper-max")), Math.max(0, parseInt(that.value))) || "All"; 58 | reassign_vars_for_skill(that); 59 | } 60 | 61 | function base_onchange(that) { 62 | var twaddle = that; 63 | while (!twaddle.hasAttribute("data-stepper-max")) { 64 | twaddle = twaddle.parentNode; 65 | } 66 | 67 | that.value = Math.min(parseInt(twaddle.getAttribute("data-stepper-max")), Math.max(0, parseInt(that.value))) || "All"; 68 | reassign_vars_for_base(that); 69 | } 70 | 71 | function stats_step(that, cid, step) { 72 | var targ = that.parentNode.querySelector(".stats_step"); 73 | targ.value = (parseInt(targ.value) || 0) + step; 74 | targ.onchange(targ); 75 | } 76 | 77 | function skill_step(that, cid, step) { 78 | var targ = that.parentNode.querySelector(".skill_step"); 79 | targ.value = (parseInt(targ.value) || 0) + step; 80 | targ.onchange(targ); 81 | } 82 | 83 | /***/ 84 | 85 | function pn(that, nest) { 86 | for (var i = 0; i < nest; i++) { 87 | that = that.parentNode; 88 | } 89 | return that; 90 | } 91 | 92 | function toggle_transform_state(that, owner) { 93 | var chain = owner.getAttribute("data-chain").split(" "); 94 | for (var i = 0; i < chain.length; i++) { 95 | if (chain[i] != owner.getAttribute("data-showing-id")) { 96 | set_stats_visible(owner, chain[i]); 97 | break; 98 | } 99 | } 100 | } 101 | 102 | function set_stats_visible(that, cid) { 103 | var root = that; 104 | 105 | var statboxes = root.querySelectorAll(".stats.box"); 106 | for (var i = 0; i < statboxes.length; i++) { 107 | if (statboxes[i].id == "sb_" + cid) { 108 | statboxes[i].style.display = "inline-block"; 109 | } else { 110 | statboxes[i].style.display = "none"; 111 | } 112 | } 113 | 114 | var cardim = root.querySelector(".card_image"); 115 | if (cardim !== null) { 116 | link = cardim.src.substring(0, cardim.src.lastIndexOf("/")); 117 | cardim.src = link + "/" + cid + ".png" 118 | } else { 119 | cardim = root.querySelector(".spread_view"); 120 | link = cardim.style.backgroundImage.match(/url\("?(.+)\/(.+?).png"?\)/); 121 | cardim.style.backgroundImage = "url(\"" + link[1] + "/" + cid + ".png\")"; 122 | } 123 | 124 | var sprite = root.querySelector(".sprite_link"); 125 | if (sprite) { 126 | sprite.href = "/sprite_go/" + cid + ".png" 127 | } 128 | 129 | var puchi = root.querySelector(".petit_link"); 130 | if (puchi) { 131 | link = puchi.href.substring(0, puchi.href.lastIndexOf("/")); 132 | puchi.href = link + "/" + cid + ".png" 133 | } 134 | 135 | var spread = root.querySelector(".spread_link"); 136 | if (spread) { 137 | link = spread.href.substring(0, spread.href.lastIndexOf("/")); 138 | spread.href = link + "/" + cid + ".png" 139 | } 140 | 141 | root.setAttribute("data-showing-id", cid) 142 | } 143 | 144 | function table(id, kill) { 145 | document.getElementById(id).style.display = 'table'; 146 | kill.parentNode.removeChild(kill); 147 | } 148 | 149 | function load_table(id, htc, va_id, kill) { 150 | kill.parentNode.removeChild(kill); 151 | 152 | var xhr = new XMLHttpRequest(); 153 | xhr.open("POST", "/api/private/va_table", true); 154 | xhr.onreadystatechange = function() { 155 | if (xhr.readyState == 4 && xhr.status == 200) { 156 | var tab = document.getElementById(id); 157 | tab.innerHTML = xhr.responseText; 158 | tab.style.display = 'table'; 159 | 160 | if (tlinject_activate !== undefined) { 161 | tlinject_activate(); 162 | } 163 | if (sound_inliner_init !== undefined) { 164 | sound_inliner_init(); 165 | } 166 | } 167 | } 168 | xhr.send(JSON.stringify({ 169 | has_title_call: htc, 170 | va_ids: va_id, 171 | })) 172 | } 173 | -------------------------------------------------------------------------------- /static/js/modal.js: -------------------------------------------------------------------------------- 1 | function enterModal(onPresent, onExit) { 2 | var backdrop = document.querySelector("#modal_backdrop"); 3 | if (!backdrop) { 4 | backdrop = document.createElement("div"); 5 | backdrop.id = "modal_backdrop"; 6 | backdrop.className = "modal_fixed modal_backdrop"; 7 | document.body.appendChild(backdrop); 8 | 9 | backdrop.addEventListener("click", function() { 10 | if (onExit) { 11 | onExit(); 12 | } 13 | exitModal(); 14 | }, false) 15 | } 16 | 17 | var container = document.createElement("div"); 18 | container.className = "modal_fixed modal_container"; 19 | 20 | var win = document.createElement("div"); 21 | win.className = "modal_self close"; 22 | container.appendChild(win); 23 | 24 | onPresent(win); 25 | document.body.appendChild(container); 26 | 27 | // We need to flush the layout change from inserting the backdrop 28 | // before triggering the transition, or else the animation doesn't 29 | // play. 30 | requestAnimationFrame(function() { 31 | requestAnimationFrame(function() { 32 | backdrop.classList.add("on"); 33 | win.classList.remove("close"); 34 | }) 35 | }) 36 | } 37 | 38 | function exitAllModals() { 39 | var modals = document.querySelectorAll(".modal_container"); 40 | for (var i = 0; i < modals.length; ++i) { 41 | var closeTarget = modals[i]; 42 | closeTarget.querySelector(".modal_self").classList.add("close"); 43 | } 44 | 45 | var backdrop = document.querySelector("#modal_backdrop"); 46 | if (backdrop) { 47 | backdrop.classList.remove("on"); 48 | } 49 | 50 | setTimeout(function() { 51 | for (var i = 0; i < modals.length; ++i) { 52 | var closeTarget = modals[i]; 53 | document.body.removeChild(closeTarget); 54 | } 55 | 56 | var backdrop = document.querySelector("#modal_backdrop"); 57 | if (!backdrop.classList.contains("on")) { 58 | document.body.removeChild(backdrop); 59 | } 60 | }, 100); 61 | } 62 | 63 | function exitModal() { 64 | var modals = document.querySelectorAll(".modal_container"); 65 | if (modals.length > 0) { 66 | var closeTarget = modals[modals.length - 1]; 67 | closeTarget.querySelector(".modal_self").classList.add("close"); 68 | 69 | setTimeout(function() { 70 | var backdrop = document.querySelector("#modal_backdrop"); 71 | if (!backdrop.classList.contains("on")) { 72 | document.body.removeChild(backdrop); 73 | } 74 | 75 | document.body.removeChild(closeTarget); 76 | }, 100); 77 | } 78 | 79 | if (modals.length == 1) { 80 | var backdrop = document.querySelector("#modal_backdrop"); 81 | if (backdrop) { 82 | backdrop.classList.remove("on"); 83 | } 84 | } 85 | } 86 | 87 | function enterSimpleTextModal(text, done) { 88 | var finish = function() { 89 | if (done) { 90 | done(); 91 | } 92 | exitModal(); 93 | } 94 | 95 | enterModal(function(win) { 96 | var textbox = document.createElement("p"); 97 | textbox.style.marginTop = 0; 98 | textbox.textContent = text; 99 | win.appendChild(textbox); 100 | 101 | var bg = document.createElement("div"); 102 | bg.className = "button_group"; 103 | win.appendChild(bg); 104 | 105 | var close = document.createElement("button"); 106 | close.className = "button"; 107 | close.textContent = "Dismiss"; 108 | close.addEventListener("click", finish, false); 109 | bg.appendChild(close); 110 | }, done); 111 | } -------------------------------------------------------------------------------- /static/js/soundinliner.js: -------------------------------------------------------------------------------- 1 | function sound_inliner_inline(e) { 2 | var audio = document.createElement("audio"); 3 | audio.className = "inliner"; 4 | audio.autoplay = "yes"; 5 | audio.controls = "yes"; 6 | audio.volume = 0.5; 7 | var source = document.createElement("source"); 8 | source.src = this.href; 9 | //source.type = "audio/mpeg"; 10 | audio.textContent = "audio?"; 11 | audio.appendChild(source); 12 | 13 | var parent = this.parentNode; 14 | parent.innerHTML = ""; 15 | parent.appendChild(audio); 16 | 17 | e.preventDefault(); 18 | } 19 | 20 | function sound_inliner_explain_no_audio(e) { 21 | enterSimpleTextModal("This line has no audio because it belongs to an unvoiced idol."); 22 | } 23 | 24 | function sound_inliner_init() { 25 | var elements = document.getElementsByClassName("soundinliner-apply"); 26 | for (var i = 0; i < elements.length; ++i) { 27 | //elements[i].setAttribute("data-inliner-href", elements[i].href); 28 | //elements[i].href = "javascript:void(0);"; 29 | elements[i].onclick = sound_inliner_inline; 30 | } 31 | } -------------------------------------------------------------------------------- /static/js/svex.js: -------------------------------------------------------------------------------- 1 | function svx_canvas_save(body, face, file, forcedraw) { 2 | var canvas = document.getElementById("buffer"); 3 | var faceLeft = parseInt(face.style.left); 4 | var faceTop = parseInt(face.style.top); 5 | var allOffsetH = 0; 6 | var allOffsetV = 0; 7 | 8 | if (faceTop < 0) { 9 | allOffsetV = -faceTop; 10 | } 11 | if (faceLeft < 0) { 12 | allOffsetH = -faceTop; 13 | } 14 | 15 | // To horizontally center. 16 | canvas.width = body.naturalWidth + (2 * allOffsetH); 17 | canvas.height = body.naturalHeight + allOffsetV; 18 | 19 | var ctx = canvas.getContext("2d") 20 | ctx.drawImage(body, allOffsetH, allOffsetV); 21 | if (face.style.display != "none" || forcedraw) { 22 | ctx.drawImage(face, faceLeft + allOffsetH, faceTop + allOffsetV); 23 | } 24 | 25 | var a = document.createElement("a"); 26 | a.href = canvas.toDataURL("image/png"); 27 | a.download = file; 28 | a.click(); 29 | } 30 | 31 | function svx_download_img(target) { 32 | var body = document.getElementById("svx__pose_" + target); 33 | var face = document.getElementById("svx__face_" + target); 34 | svx_canvas_save(body, face, CHARA_NAME + " pose " + target + "f" + face.getAttribute("data-face-id") + ".png"); 35 | } 36 | 37 | function svx_apply_face(that) { 38 | var target = that.getAttribute("data-pose-target"); 39 | var overlay = document.querySelector("#svx__face_" + target); 40 | 41 | overlay.src = that.getAttribute("src"); 42 | overlay.setAttribute("data-face-id", that.getAttribute("data-face-id")); 43 | } 44 | 45 | function svx_clear_face(that) { 46 | var target = that.getAttribute("data-pose-target"); 47 | document.querySelector("#svx__face_" + target).style.display = "none"; 48 | } 49 | 50 | function construct_crap_tree(ent) { 51 | var root = document.createElement("div"); 52 | root.innerHTML = document.getElementById("the_template").innerHTML; 53 | 54 | var top = root.querySelector("#template_overlay"); 55 | top.style.display = "none"; 56 | top.style.position = "absolute"; 57 | top.setAttribute("data-rel-position-x", ent.position[0]); 58 | top.setAttribute("data-rel-position-y", ent.position[1]); 59 | top.id = "svx__face_" + ent.id; 60 | 61 | var sub = root.querySelector("#template_img"); 62 | sub.src = ISVR + "/" + ent.id + ".png"; 63 | sub.id = "svx__pose_" + ent.id; 64 | sub.onload = function() { 65 | var scpt = document.createElement("script"); 66 | scpt.src = ISVR + "/" + ent.id + ".json"; 67 | document.body.appendChild(scpt); 68 | } 69 | 70 | var a = root.querySelector("#template_flist"); 71 | a.setAttribute("id", "template_flist" + ent.id); 72 | 73 | var buttonbar = root.querySelector("#template_buttons") 74 | buttonbar.style.display = "flex"; 75 | buttonbar.innerHTML = ( 76 | 'Download current composite').replace(/\%s/g, ent.id); 77 | 78 | return root.children[0]; 79 | } 80 | 81 | function SVX_INIT_FOR_POSE(pid, x, y) { 82 | document.body.querySelector("#main_content").appendChild(construct_crap_tree({ 83 | id: pid, 84 | position: [x, y], 85 | })); 86 | } 87 | 88 | function SVX_APPLY_ADJUSTMENT(pid, x, y, rw, rh) { 89 | var pose = document.getElementById("svx__pose_" + pid); 90 | var face = document.getElementById("svx__face_" + pid); 91 | 92 | var xr = parseInt(face.getAttribute("data-rel-position-x")); 93 | var yr = parseInt(face.getAttribute("data-rel-position-y")); 94 | 95 | // From SVX_APPLY_ADJUSTMENTEX. Apparently the work to support 96 | // explicit pose size was already done in genfacelists, but 97 | // never got used here. 98 | if (rw === undefined || rh === undefined) { 99 | console.debug("Using legacy apply adjustment format"); 100 | rw = 1 << Math.ceil(Math.log2(pose.width - x)); 101 | rh = 1 << Math.ceil(Math.log2(pose.height - y)); 102 | } 103 | 104 | // I forgot why these are specified in 2x units. 105 | face.setAttribute("data-reference-w", 128); 106 | face.setAttribute("data-reference-h", 128); 107 | face.setAttribute("data-position-x", (((rw / 2) - 64) + xr) + x); 108 | face.setAttribute("data-position-y", ((rh - 64) - yr) + y); 109 | face.setAttribute("data-adj-x", x); 110 | face.setAttribute("data-adj-y", y); 111 | } 112 | 113 | function SVX_APPLY_FACE_LIST(pid, list) { 114 | var a = document.querySelector("#template_flist" + pid); 115 | a.removeAttribute("id"); 116 | var face = document.getElementById("svx__face_" + pid); 117 | 118 | face.addEventListener("load", function(event) { 119 | var fce = event.target; 120 | var mov_w = fce.naturalWidth - parseInt(fce.getAttribute("data-reference-w")); 121 | var mov_h = fce.naturalHeight - parseInt(fce.getAttribute("data-reference-h")); 122 | var xo = parseInt(fce.getAttribute("data-position-x")) - (mov_w / 2); 123 | var yo = parseInt(fce.getAttribute("data-position-y")) - (mov_h / 2); 124 | 125 | fce.style.top = (yo) + "px"; 126 | fce.style.left = (xo) + "px"; 127 | fce.style.display = "inline"; 128 | }, false); 129 | 130 | for (var i = 0; i < list.length; i++) { 131 | var img = document.createElement("img"); 132 | img.className = "svx_face"; 133 | img.src = ISVR + "/" + pid + "_" + list[i] + ".png"; 134 | img.setAttribute("data-pose-target", pid); 135 | img.setAttribute("data-face-id", list[i]); 136 | img.setAttribute("onclick", "svx_apply_face(this)"); 137 | a.appendChild(img); 138 | } 139 | 140 | var img = document.createElement("img"); 141 | img.className = "svx_face"; 142 | img.src = MARKER_EMPTY_FACE; 143 | img.setAttribute("data-pose-target", pid); 144 | img.setAttribute("onclick", "svx_clear_face(this)"); 145 | a.appendChild(img); 146 | } 147 | -------------------------------------------------------------------------------- /toolchain/csvloader.py: -------------------------------------------------------------------------------- 1 | ../csvloader.py -------------------------------------------------------------------------------- /toolchain/get_app_ver.py: -------------------------------------------------------------------------------- 1 | import urllib.request 2 | import json 3 | import time 4 | 5 | rand = str(int(time.time())) 6 | store_api = urllib.request.urlopen("https://itunes.apple.com/jp/lookup?id=1016318735&rnd=" + rand) 7 | store_b = store_api.read() 8 | store_j = store_b.decode('utf-8') 9 | store_d = json.loads(store_j) 10 | 11 | results_l = store_d.get("results") 12 | app_ver = results_l[0]['version'] 13 | 14 | print(app_ver) 15 | -------------------------------------------------------------------------------- /toolchain/make_contiguous_gacha.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import sys 3 | import os 4 | 5 | sys.path.insert(0, os.path.realpath(os.path.dirname(__file__) + "/..")) 6 | 7 | import json 8 | import sqlite3 9 | 10 | from datetime import datetime 11 | import locale 12 | locale.setlocale(locale.LC_ALL, "en_US.UTF-8") 13 | 14 | from pytz import timezone, utc 15 | from collections import namedtuple 16 | import models 17 | 18 | from starlight import JST, private_data_path 19 | import csvloader 20 | 21 | overrides = csvloader.load_keyed_db_file(private_data_path("gacha_availability_overrides.csv")) 22 | 23 | gacha_stub_t = namedtuple("gacha_stub_t", ("id", "name", "start_date", "end_date", "type", "subtype")) 24 | 25 | def gacha_ids(f): 26 | gachas = [] 27 | 28 | a = sqlite3.connect(f) 29 | for id, n, ss, es, t, t2 in a.execute("SELECT id, name, start_date, end_date, type, type_detail FROM gacha_data where type = 3 and type_detail = 1"): 30 | ss, es = JST(ss), JST(es) 31 | gachas.append(gacha_stub_t(id, n, ss, es, t, t2)) 32 | 33 | return sorted(gachas, key=lambda x: x.start_date) 34 | 35 | def available(f, g): 36 | a = sqlite3.connect(f) 37 | for x in a.execute( 38 | "SELECT gacha_id, step_num, reward_id, recommend_order, limited_flag FROM gacha_available WHERE gacha_id IN ({0})" 39 | .format(",".join("?" * len(g))), tuple(g)): 40 | if x[2] in overrides: 41 | yield (x[0], x[1], x[2], x[3], overrides[x[2]][1]) 42 | else: 43 | yield x 44 | a.close() 45 | 46 | def main(file1, file2): 47 | if not os.path.exists("./app.py"): 48 | print("You can only run this program with the cwd set to the main code directory.") 49 | 50 | gacha_ids_a = gacha_ids(file1) 51 | gacha_ids_b = gacha_ids(file2) 52 | 53 | ksa, ksb = map(lambda x: set([y.id for y in x]), (gacha_ids_a, gacha_ids_b)) 54 | added = ksb - ksa 55 | 56 | m = models.TranslationSQL() 57 | m.add_reward_tracking_entries(available(file2, added)) 58 | 59 | prev = gacha_ids_b[0] 60 | for gacha in gacha_ids_b[1:]: 61 | if gacha.id not in added: 62 | prev = gacha 63 | continue 64 | 65 | delta = gacha.start_date - prev.end_date 66 | if (delta.days * 86400) + delta.seconds < 10: 67 | m.extend_gacha(prev, gacha) 68 | else: 69 | m.seed_initial(gacha) 70 | prev = gacha 71 | 72 | if __name__ == '__main__': 73 | main(*sys.argv[1:]) 74 | -------------------------------------------------------------------------------- /toolchain/make_event_lookup_table.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import sys 3 | import os 4 | 5 | sys.path.insert(0, os.path.realpath(os.path.dirname(__file__) + "/..")) 6 | 7 | import json 8 | import sqlite3 9 | 10 | from datetime import datetime 11 | import locale 12 | locale.setlocale(locale.LC_ALL, "en_US.UTF-8") 13 | 14 | from pytz import timezone, utc 15 | from collections import namedtuple 16 | import models 17 | from models.base import * 18 | from models.extra import * 19 | 20 | from starlight import JST, private_data_path 21 | import csvloader 22 | 23 | def sync_event_lookup_table(tsql): 24 | with tsql as s: 25 | s.query(EventLookupEntry).delete() 26 | s.commit() 27 | 28 | rows = s.query(HistoryEventEntry).all() 29 | 30 | for h_ent in rows: 31 | print(h_ent) 32 | if h_ent.type() != HISTORY_TYPE_EVENT: 33 | continue 34 | seen = set() 35 | 36 | l = h_ent.category_card_list("progression") 37 | for cid in l: 38 | s.merge(EventLookupEntry(card_id=cid, event_id=h_ent.referred_id(), acquisition_type=1)) 39 | seen.update(l) 40 | 41 | l = h_ent.category_card_list("ranking") 42 | for cid in l: 43 | s.merge(EventLookupEntry(card_id=cid, event_id=h_ent.referred_id(), acquisition_type=2)) 44 | seen.update(l) 45 | 46 | l = h_ent.category_card_list("gacha") 47 | for cid in l: 48 | s.merge(EventLookupEntry(card_id=cid, event_id=h_ent.referred_id(), acquisition_type=3)) 49 | seen.update(l) 50 | 51 | print(seen) 52 | 53 | for any_card in h_ent.card_list(): 54 | if any_card not in seen: 55 | s.merge(EventLookupEntry(card_id=any_card, event_id=h_ent.referred_id(), acquisition_type=0)) 56 | s.commit() 57 | 58 | def sync_gacha_lookup_table(tsql): 59 | with tsql as s: 60 | s.query(GachaLookupEntry).delete() 61 | s.commit() 62 | 63 | first = {} 64 | last = {} 65 | 66 | rows = s.query(HistoryEventEntry).order_by(HistoryEventEntry.start_time).all() 67 | for h_ent in rows: 68 | print(h_ent) 69 | if h_ent.type() != HISTORY_TYPE_GACHA: 70 | continue 71 | seen = set() 72 | 73 | l = h_ent.category_card_list("limited") 74 | for cid in l: 75 | tstart, gstart = first.get(cid, (h_ent.start_time, h_ent.referred_id())) 76 | first[cid] = tstart, gstart 77 | s.merge(GachaLookupEntry(card_id=cid, first_gacha_id=gstart, last_gacha_id=h_ent.referred_id(), 78 | first_available=tstart, last_available=h_ent.end_time, is_limited=1)) 79 | seen.update(l) 80 | 81 | l = h_ent.category_card_list("other") 82 | for cid in l: 83 | tstart, gstart = first.get(cid, (h_ent.start_time, h_ent.referred_id())) 84 | first[cid] = tstart, gstart 85 | s.merge(GachaLookupEntry(card_id=cid, first_gacha_id=gstart, last_gacha_id=h_ent.referred_id(), 86 | first_available=tstart, last_available=h_ent.end_time, is_limited=0)) 87 | seen.update(l) 88 | print(seen) 89 | s.commit() 90 | 91 | def main(): 92 | if not os.path.exists("./app.py"): 93 | print("You can only run this program with the cwd set to the main code directory.") 94 | 95 | m = models.TranslationSQL() 96 | sync_event_lookup_table(m) 97 | sync_gacha_lookup_table(m) 98 | 99 | if __name__ == '__main__': 100 | main() 101 | -------------------------------------------------------------------------------- /toolchain/models: -------------------------------------------------------------------------------- 1 | ../models -------------------------------------------------------------------------------- /toolchain/name_finder.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # coding: utf-8 3 | 4 | # name_finder uses ENAMDICT to split character names. 5 | # It is currently unsure about ~7% of names in the current StarlightStage 6 | # ark (12 Oct 2015.) 7 | # Copyright 2015 The Holy Constituency of the Summer Triangle. 8 | # All rights reserved. 9 | 10 | import locale 11 | locale.setlocale(locale.LC_ALL, "en_US.UTF-8") 12 | 13 | from collections import namedtuple, Counter 14 | import re 15 | import sys 16 | import csvloader 17 | import csv 18 | import to_roma 19 | import sqlite3 20 | from pprint import pprint 21 | 22 | CLASS_FAMILY_NAME = set("sup") 23 | CLASS_GIVEN_NAME = set("mfgu") 24 | AX_PARSE = re.compile(r"/\(([a-z\,]+)\) (.+)/") 25 | FIX_PAT = re.compile(r"([きぎしじずずちぢひびぴみにゆり][ゅょゃ]|[こごそぞとどほぼぽものよろゆ])う") 26 | Word = namedtuple("Word", ["kanji", "kana", "classifier", "roman"]) 27 | _Search = namedtuple("Search", ["kanji", "kana", "real_kanji"]) 28 | 29 | HIRA_FUZZY_COMPARE_TABLE = ["か き く け こ ", 30 | "が ぎ ぐ げ ご ", 31 | "さ し す せ そ ", 32 | "ざ じ ず ぜ ぞ ", 33 | "た ち つ て と ", 34 | "だ ぢ づ で ど ", 35 | "は ひ ふ へ ほ ", 36 | "ば び ぶ べ ぼ ", 37 | "は ひ ふ へ ほ ", 38 | "ぱ ぴ ぷ ぺ ぽ "] 39 | KATA_TABLE = set(map(chr, range(0x30A1, 0x3100))) 40 | 41 | def make_table(table_in): 42 | i = iter(table_in) 43 | for l1, l2 in zip(i, i): 44 | for v, k in zip(l1, l2): 45 | yield (k, v) 46 | 47 | HIRA_FUZZY_COMPARE_TABLE = {k: v for k, v in make_table(HIRA_FUZZY_COMPARE_TABLE)} 48 | 49 | def Search(kanji, kana): 50 | return _Search(re.compile("".join((kanji[0],) + tuple(x + "?" for x in kanji[1:]))), kana, kanji) 51 | 52 | class EnamdictHandle(object): 53 | def __init__(self, file): 54 | self.filename = file 55 | 56 | @staticmethod 57 | def parse_word(line): 58 | piv = line.index("/") 59 | assert piv > 0 60 | 61 | content = line[:piv - 1] 62 | ax = line[piv:] 63 | if "[" in content: 64 | kanji, kana = content.split(" ") 65 | kana = kana[1:-1] 66 | else: 67 | kanji = content 68 | kana = content 69 | 70 | ax_info = AX_PARSE.match(ax) 71 | if not ax_info: 72 | return None 73 | 74 | classifier = set(ax_info.group(1).split(",")) 75 | roman = ax_info.group(2) 76 | return Word(kanji, kana, classifier, roman) 77 | 78 | @staticmethod 79 | def is_word_matching(word, search, classi): 80 | t = search.kanji.match(word.kanji) 81 | if word.classifier & classi and word.kanji in search.real_kanji: 82 | if word.kana and search.kana[:len(word.kana)] == word.kana: 83 | return 1 84 | else: 85 | return 0 86 | return 1 87 | 88 | def find_surname_candidates(self, kanji_str, kana_str): 89 | search = Search(kanji_str, kana_str) 90 | positive_results = [] 91 | with open(self.filename, "r") as dictionary: 92 | t = iter(dictionary) 93 | # discard first line (header line) 94 | next(t) 95 | 96 | has_seen = 0 97 | for line in t: 98 | # make search faster by discarding unwanted result 99 | if line[0] != kanji_str[0]: 100 | if has_seen: 101 | break 102 | continue 103 | has_seen = 1 104 | word = self.parse_word(line.strip()) 105 | if word and self.is_word_matching(word, search, CLASS_FAMILY_NAME): 106 | positive_results.append(word) 107 | 108 | # we should filter on exact kana, since we have it anyway 109 | really_positive_results = [] 110 | for word in positive_results: 111 | if kana_str[:len(word.kana)] == word.kana: 112 | really_positive_results.append(word) 113 | return really_positive_results or positive_results 114 | 115 | def find_given_name_candidates(self, kanji_str, kana_str): 116 | with open(self.filename, "r") as dictionary: 117 | t = iter(dictionary) 118 | # discard first line (header line) 119 | next(t) 120 | 121 | has_seen = 0 122 | has_result = 0 123 | for line in t: 124 | # make search faster by discarding unwanted result 125 | if line[0] != kanji_str[0]: 126 | if has_seen: 127 | break 128 | continue 129 | has_seen = 1 130 | word = self.parse_word(line.strip()) 131 | if word and kanji_str == word.kanji and (word.kana == kana_str or word.kanji == kana_str): 132 | yield word._replace(kana=kana_str) 133 | has_result = 1 134 | 135 | if not has_result: 136 | #print("warning: no given names found! yielding inferred word...") 137 | yield Word(kanji_str, kana_str, set("i"), "") 138 | 139 | def find_name(self, kanji_str, kana_str): 140 | possible = [] 141 | for candy in sorted(self.find_surname_candidates(kanji_str, kana_str), key=lambda x: len(x.kanji), reverse=1): 142 | for given in self.find_given_name_candidates(kanji_str[len(candy.kanji):], kana_str[len(candy.kana):]): 143 | possible.append((candy, given)) 144 | 145 | # if there is a non-inferred given name in there, delete all inferred matches 146 | # this is so we don't provide guesses when real data is available 147 | if not all(map(lambda x: "i" in x[1].classifier, possible)): 148 | return list(filter(lambda x: "i" not in x[1].classifier, possible)) 149 | else: 150 | return possible 151 | 152 | def final_fixups(string): 153 | return FIX_PAT.sub(lambda match: match.group(1), string) 154 | 155 | chara_stub_t = namedtuple("chara_stub_t", ("name", "name_kana")) 156 | def load_from_db(new_db): 157 | a = sqlite3.connect(new_db) 158 | c = a.execute("SELECT chara_id, name, name_kana FROM chara_data") 159 | ret = {} 160 | for r in c: 161 | ret[r[0]] = chara_stub_t(*r[1:]) 162 | a.close() 163 | return ret 164 | 165 | if __name__ == '__main__': 166 | new_db = sys.argv[2] 167 | name_tab = sys.argv[3] 168 | 169 | charas = load_from_db(new_db) 170 | 171 | try: 172 | have_names = csvloader.load_keyed_db_file(name_tab) 173 | except IOError: 174 | have_names = {} 175 | 176 | missing = set(charas.keys()) - set(have_names.keys()) 177 | 178 | for key in sorted(missing): 179 | chara = charas[key] 180 | print("---", chara.name, "----------") 181 | try: 182 | res = EnamdictHandle(sys.argv[1]).find_name(chara.name, chara.name_kana) 183 | except Exception: 184 | print("This input is possibly invalid:", chara.name, chara.name_kana) 185 | res = None 186 | 187 | if not res: 188 | print("warning: No solution found at all") 189 | res = [(Word(chara.name, chara.name_kana, set(), ""),)] 190 | 191 | try: 192 | # get rid of u vowel in certain conditions to stay in line with official romanization... 193 | roma = " ".join(to_roma.consume_hiragana(final_fixups(x.kana)) for x in res[0]) 194 | except ValueError as e: 195 | print("warning: transliteration failed, possibly due to data consistency issues.") 196 | print("exception:", e) 197 | roma = "???" 198 | 199 | for word in res[0]: 200 | if set(word.kanji) < KATA_TABLE: 201 | print("warning: string '{0}' contains katakana - romanization will probably be inaccurate" 202 | .format(word.kanji)) 203 | roma += "?" 204 | 205 | print("ROMAJI: ", roma.title()) 206 | print("KANA_SPACED: ", " ".join(x.kana for x in res[0])) 207 | print("KANJI_SPACED:", " ".join(x.kanji for x in res[0])) 208 | 209 | have_names[key] = (key, chara.name, " ".join(x.kanji for x in res[0]), " ".join(x.kana for x in res[0]), roma.title()) 210 | 211 | f = open(name_tab, "w") 212 | c = csv.writer(f, delimiter=",", quotechar="\"", quoting=csv.QUOTE_NONNUMERIC, lineterminator="\n") 213 | c.writerow(("chara_id", "kanji", "kanji_spaced", "kana_spaced", "conventional")) 214 | 215 | for key in sorted(have_names.keys()): 216 | c.writerow(have_names[key]) 217 | -------------------------------------------------------------------------------- /toolchain/starlight: -------------------------------------------------------------------------------- 1 | ../starlight -------------------------------------------------------------------------------- /toolchain/to_roma.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | import re 3 | 4 | ROMAJI = r"([aiueo]|(?:(?:[kgsztdhbpmnyrfj]{1,2})|sh|ts|dz|ny|jy)(?:[aiueo])|n(?:n)?)" 5 | TL_A = ("a i u e o " 6 | "ka ki ku ke ko " 7 | "ga gi gu ge go " 8 | "sa shi si su se so " 9 | "za zu ze zo " 10 | "ta chi ti tsu tu te to " 11 | "da di de do " 12 | "ha hi fu hu he ho " 13 | "ba bi bu be bo " 14 | "pa pi pu pe po " 15 | "ma mi mu me mo " 16 | "na ni nu ne no " 17 | "ya yu yo " 18 | "ra ri ru re ro " 19 | "wa wo ") 20 | TL_B = ("あ い う え お " 21 | "か き く け こ " 22 | "が ぎ ぐ げ ご " 23 | "さ し し す せ そ " 24 | "ざ ず ぜ ぞ " 25 | "た ち ち つ つ て と " 26 | "だ ぢ で ど " 27 | "は ひ ふ ふ へ ほ " 28 | "ば び ぶ べ ぼ " 29 | "ぱ ぴ ぷ ぺ ぽ " 30 | "ま み む め も " 31 | "な に ぬ ね の " 32 | "や ゆ よ " 33 | "ら り る れ ろ " 34 | "わ を ") 35 | 36 | TL_EXT_A = ("zu dzu du " 37 | "sya syu syo sha shu sho " 38 | "kya kyu kyo " 39 | "ja jya ju jyu jo jyo ji " 40 | "nya nyu nyo n " 41 | "mya myu myo " 42 | "hya hyu hyo " 43 | "bya byu byo " 44 | "pya pyu pyo " 45 | "rya ryu ryo ") 46 | TL_EXT_B = ("づ づ づ " 47 | "しゃ しゅ しょ しゃ しゅ しょ " 48 | "きゃ きゅ きょ " 49 | "じゃ じゃ じゅ じゅ じょ じょ じ " 50 | "にゃ にゅ にょ ん " 51 | "みゃ みゅ みょ " 52 | "ひゃ ひゅ ひょ " 53 | "びゃ びゅ びょ " 54 | "ぴゃ ぴゅ ぴょ " 55 | "りゃ りゅ りょ ") 56 | SMALL_LETTERS = "ぁぃぅぇぉゃゅょゎゕゖー" 57 | 58 | def get_run(table, ind): 59 | ret = [] 60 | while table[ind] != " ": 61 | ret.append(table[ind]) 62 | ind += 1 63 | return "".join(ret) 64 | 65 | def lookup_letter_group(letter_group, tables=((TL_A, TL_B), (TL_EXT_A, TL_EXT_B))): 66 | # special case for extended consonants (e.g. "gakkou") 67 | if len(letter_group) == 3 and letter_group[0] == letter_group[1]: 68 | try: 69 | char = lookup_letter_group(letter_group[1:], ((TL_A, TL_B),)) 70 | except ValueError: 71 | # raise the correct error message 72 | raise ValueError("A transliteration for the letter group '{0}' was not found.".format(letter_group)) 73 | else: 74 | return "っ" + char 75 | 76 | for en_tbl, ja_tbl in tables: 77 | try: 78 | return get_run(ja_tbl, en_tbl.index(letter_group + " ")) 79 | except ValueError: 80 | continue 81 | else: 82 | raise ValueError("A transliteration for the letter group '{0}' was not found.".format(letter_group)) 83 | 84 | def lookup_letter_group2(letter_group, tables=((TL_B, TL_A), (TL_EXT_B, TL_EXT_A))): 85 | # special case for extended consonants (e.g. "gakkou") 86 | if letter_group.startswith("っ"): 87 | try: 88 | char = lookup_letter_group2(letter_group[1:], ((TL_B, TL_A),)) 89 | except ValueError: 90 | # raise the correct error message 91 | raise ValueError("A transliteration for the letter group '{0}' was not found.".format(letter_group)) 92 | else: 93 | return char[0] + char 94 | # for extended vowel too 95 | if letter_group.endswith("ー"): 96 | try: 97 | char = lookup_letter_group2(letter_group[:-1], ((TL_B, TL_A),)) 98 | except ValueError: 99 | # raise the correct error message 100 | raise ValueError("A transliteration for the letter group '{0}' was not found.".format(letter_group)) 101 | else: 102 | return char + char[-1] 103 | 104 | for en_tbl, ja_tbl in tables: 105 | try: 106 | return get_run(ja_tbl, en_tbl.index(letter_group + " ")) 107 | except ValueError: 108 | continue 109 | else: 110 | raise ValueError("A transliteration for the letter group '{0}' was not found.".format(letter_group)) 111 | 112 | def consume_romaji(string): 113 | letter_groups = re.findall(ROMAJI, string) 114 | return "".join(( map(lambda x: lookup_letter_group(x.strip()), filter(bool, letter_groups)) )) 115 | 116 | def _consume_hiragana(string): 117 | groups = [] 118 | mygroup = [] 119 | for char in string: 120 | if char not in SMALL_LETTERS and mygroup != ["っ"]: 121 | if mygroup: 122 | yield "".join(mygroup) 123 | mygroup = [char] 124 | else: 125 | mygroup.append(char) 126 | if mygroup: 127 | yield "".join(mygroup) 128 | def consume_hiragana(string): 129 | return "".join(( map(lambda x: lookup_letter_group2(x.strip()), _consume_hiragana(string)) )) 130 | -------------------------------------------------------------------------------- /webui/card.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {% if is_dev %} 7 | 8 | 9 | {% else %} 10 | 11 | {% end %} 12 | 13 | 14 | 15 | 16 | 17 | 18 | {% if just_one_card %} 19 | Card: {{ enums.rarity(just_one_card.rarity) }} {{ just_one_card.chara.conventional }} (sldb) 20 | {% else %} 21 | {{ len(cards) }} cards (sldb) 22 | {% end %} 23 | 24 | 25 | 26 | {% include header.html %} 27 | 28 | {% if just_one_card %} 29 |
30 | 37 |
38 | {% end %} 39 | 40 | {% for chain in cards %} 41 |
42 | {% set card_id = chain[0].id %} 43 | {% set card = chain[0] %} 44 | {% include partials/card_box.html %} 45 |
46 | {% end %} 47 | 48 | {% include partials/footer.html %} 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /webui/chara.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {% if is_dev %} 7 | 8 | 9 | {% else %} 10 | 11 | {% end %} 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | Idol: {{ chara.conventional }} (sldb) 20 | 21 | 22 | 23 | {% include header.html %} 24 | 25 |
26 | {% include partials/chara_box.html %} 27 |
28 | {% for chain in cards %} 29 |
30 | {% set card_id = chain[0].id %} 31 | {% set card = chain[0] %} 32 | {% include partials/card_box.html %} 33 |
34 | {% end %} 35 | {% include partials/footer.html %} 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /webui/debug_view_database.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {% if is_dev %} 6 | 7 | 8 | {% else %} 9 | 10 | {% end %} 11 | 12 | 13 | 14 | 15 | 16 | {% for key in fields %} 17 | 18 | {% end %} 19 | 20 | {% for obj in data %} 21 | 22 | {% for value in obj %} 23 | 24 | {% end %} 25 | 26 | {% end %} 27 |
{{ key }}
{{ value }}
28 | 29 | 30 | -------------------------------------------------------------------------------- /webui/error.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {% if is_dev %} 6 | 7 | 8 | {% else %} 9 | 10 | {% end %} 11 | 12 | Error 13 | 14 | 15 | 16 | {% include header.html %} 17 | 18 |
19 |
20 |
21 | Error {{ code }} 22 | 23 |
24 |
25 | {{ message }} 26 | {% if traceback %} 27 |
{{ traceback }}
28 | {% end %} 29 |
30 |
31 |
32 | 33 | 34 | -------------------------------------------------------------------------------- /webui/ext_gacha_table.html: -------------------------------------------------------------------------------- 1 | {% extends "generictable.html" %} 2 | 3 | {% block extra_header %} 4 |
5 | {% raw tlable(gacha.name) %} ({{ gacha.id }}) 6 |
7 | {% end %} 8 | 9 | {% block extra_info %} 10 |
    11 |
  • This gacha ends in (JS?).
  • 12 | {% if rates and rates != gacha.rates._REGULAR_RATES %} 13 |
  • This gacha has abnormal rates: R {{ rates.r }}% / SR {{ rates.sr }}% / SSR {{ rates.ssr }}%
  • 14 | {% end %} 15 |
16 | 17 | 18 | {% end %} -------------------------------------------------------------------------------- /webui/generictable.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {% if is_dev %} 7 | 8 | 9 | {% else %} 10 | 11 | {% end %} 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | {{ table_name }} (sldb) 20 | 21 | 22 | 23 | {% include header.html %} 24 | 25 |
26 |
27 | {% block extra_header %} 28 | {% end %} 29 | 30 | {% for filter_ in filters %} 31 | 32 | 33 | 34 | 35 | {% for fopt in filter_.options %} 36 | 37 | {% if fopt.kill_class %} 38 | 39 | {% else %} 40 | 41 | {% end %} 42 | 43 | {% end %} 44 | 45 | 46 | 47 | 48 | {% for fopt in filter_.options %} 49 | 50 | {% if fopt.kill_class %} 51 | 52 | {% else %} 53 | 54 | {% end %} 55 | 56 | {% end %} 57 | 58 |
{{ _(filter_.name) }}X
 {{ _(fopt.name) }}
59 | {% end %} 60 | 61 | 62 | 63 | 64 | 65 | {# yes_symbol and no_symbol are raw html. beware of accidental injections. #} 66 | {% if is_displaying_awake_forms %} 67 | {% set yes_symbol = "X" %} 68 | {% set no_symbol = " " %} 69 | {% else %} 70 | {% set yes_symbol = " " %} 71 | {% set no_symbol = "X" %} 72 | {% end %} 73 | 74 | 77 | 80 | 81 | 82 | 83 | 84 | 85 | 86 |
Awakened? 75 | {% raw yes_symbol %} 76 | 78 | {% raw no_symbol %} 79 |
 YesNo
87 | 88 | {% if show_shortlink %} 89 |

90 | (Short link) 91 |

92 | {% end %} 93 | 94 | {% block extra_info %} 95 | {% end %} 96 | 97 | 98 | Click on an underlined table header to sort the table by that value. 99 | Do note that you should apply filters before using the skill-based sorts, as you can't really compare skill effects. 100 | 101 |
102 | 103 |
104 | 105 | 106 | 107 | {% for cat in categories %} 108 | 109 | {% raw cat.make_headers() %} 110 | 111 | {% end %} 112 | 113 | 114 | 115 | 116 | {% for card in cards %} 117 | 118 | {% for cat in categories %} 119 | 120 | {% raw cat.make_values(card) %} 121 | 122 | {% end %} 123 | 124 | {% end %} 125 | 126 |
127 |
128 |
129 | 130 | {% include partials/footer.html %} 131 | 132 | 133 | 134 | -------------------------------------------------------------------------------- /webui/header.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /webui/history.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {% if is_dev %} 6 | 7 | 8 | {% else %} 9 | 10 | {% end %} 11 | 12 | 13 | 14 | 15 | 16 | History (sldb) 17 | 18 | 19 | 20 | {% include header.html %} 21 | 22 |
23 | {% set prev_header = None %} 24 | {% for history_entry in history %} 25 | 26 | {% set dt = history_entry.start_datetime() %} 27 | {% if (dt.month, dt.year) != prev_header %} 28 |

29 | {{ dt.strftime("%B %Y") }} 30 |

31 | {% set prev_header = (dt.month, dt.year) %} 32 | {% end %} 33 | 34 | {% if history_entry.type() == 2 %} 35 | {% module Template("partials/hist_event.html", history_entry=history_entry, countdown=False) %} 36 | {% elif history_entry.type() == 3 %} 37 | {% module Template("partials/hist_gacha.html", history_entry=history_entry, rates_src={}, countdown=False) %} 38 | {% elif history_entry.type() == 4 %} 39 | {% module Template("partials/hist_new_ns.html", history_entry=history_entry) %} 40 | {% else %} 41 | {# omitted #} 42 | {% end %} 43 | 44 | {% end %} 45 |
46 |
47 | {% if page > 1 %} 48 | Previous page 49 | {% end %} 50 |
51 | Next page 52 |
53 | 54 | {% include partials/footer.html %} 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /webui/main.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {% if is_dev %} 6 | 7 | 8 | {% else %} 9 | 10 | {% end %} 11 | 12 | 13 | 14 | 15 | 16 | 17 | Starlight DB Main 18 | 19 | 20 | 21 | {% include header.html %} 22 | 23 |
24 | 25 |
26 |
27 | 28 |
29 | {% for char in birthdays %} 30 | 37 | {% end %} 38 | 39 |
40 | 41 |
42 |

Current events and gachas:

43 | {% for history_entry in current_history %} 44 | 45 | {% if history_entry.type() == 2 %} 46 | {% module Template("partials/hist_event.html", history_entry=history_entry, countdown=True) %} 47 | {% elif history_entry.type() == 3 %} 48 | {% module Template("partials/hist_gacha.html", history_entry=history_entry, rates_src=live_gacha_rates, countdown=True) %} 49 | {% elif history_entry.type() == 4 %} 50 | {% module Template("partials/hist_new_ns.html", history_entry=history_entry) %} 51 | {% else %} 52 | {# omitted #} 53 | {% end %} 54 | 55 | {% end %} 56 |
57 | 58 |
59 | 60 | {% include "partials/frontpage_text.html" %} 61 | 62 |
63 | 64 |
65 | {% for history_entry in history %} 66 | 67 | {% if history_entry.type() == 2 %} 68 | {% module Template("partials/hist_event.html", history_entry=history_entry, countdown=False) %} 69 | {% elif history_entry.type() == 3 %} 70 | {% module Template("partials/hist_gacha.html", history_entry=history_entry, rates_src=live_gacha_rates, countdown=False) %} 71 | {% elif history_entry.type() == 4 %} 72 | {% module Template("partials/hist_new_ns.html", history_entry=history_entry) %} 73 | {% else %} 74 | {# omitted #} 75 | {% end %} 76 | 77 | {% end %} 78 |
79 | 80 |
81 |

More history

82 |
83 | 84 | {% include partials/footer.html %} 85 | 86 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /webui/minitable.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {% if is_dev %} 7 | 8 | 9 | {% else %} 10 | 11 | {% end %} 12 | 13 | 14 | 15 | 16 | 17 | {{ table_name }} (sldb) 18 | 19 | 20 | 21 | {% include header.html %} 22 | 23 |
24 |
25 |
26 | {{ table_name }} 27 |
28 | 29 | {% block extra_header %} 30 | {% end %} 31 | 32 | {% block extra_info %} 33 | {% end %} 34 |
35 | 36 |
37 | 38 | 39 | 40 | {% for cat in categories %} 41 | {% raw cat.make_headers() %} 42 | {% end %} 43 | 44 | 45 | 46 | 47 | {% for card in cards %} 48 | 49 | {% for cat in categories %} 50 | {% raw cat.make_values(card) %} 51 | {% end %} 52 | 53 | {% end %} 54 | 55 |
56 |
57 |
58 | 59 | {% include partials/footer.html %} 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /webui/partials/availability_box.html: -------------------------------------------------------------------------------- 1 | {% set _a = availability.get(card.id, []) %} 2 | {% if _a %} 3 |
4 |
5 | {{ _("Availability") }} 6 | 7 |
8 |
9 |
    10 | {% for presence in _a %} 11 | 12 | {% if presence.type == presence._TYPE_EVENT %} 13 | 14 |
  • 15 | Event: {% raw tlable(presence.name) %} 16 | ({{ starlight.en.availability_date_range(presence, now) }}) 17 |
  • 18 | 19 | {% elif presence.type == presence._TYPE_GACHA %} 20 | 21 |
  • 23 | 25 | {% if presence.name %} 26 | {% raw tlable(presence.name) %} 27 | {% else %} 28 | Gacha 29 | {% end %} 30 | 31 | 32 | ({{ starlight.en.availability_date_range(presence, now) }}) 33 | 34 | {% if presence.limited %} 35 |
      36 |
    • This card is/was limited during the gacha period.
    • 37 |
    38 | {% end %} 39 |
  • 40 | {% end %} 41 | 42 | {% end %} 43 |
44 |
45 |
46 | {% end %} -------------------------------------------------------------------------------- /webui/partials/card_box.html: -------------------------------------------------------------------------------- 1 |
2 | {% if not use_table %} 3 | {% if not card.has_spread %} 4 | 16 | {% end %} 17 | 18 |
19 |
20 | {% if card.title %} 21 | {% raw tlable(card.title) %} 22 | 23 | {% end %} 24 | 25 | 26 | {% if card.skill %} 27 | 29 | {% end %} 30 | {{ starlight.data.translate_name(card.name_only) }} 31 | [{{ enums.rarity(card.rarity) }}] 32 | 33 |
34 | 35 | {% if card.has_spread %} 36 |
37 | 38 | {% if card.has_sign %} 39 | {{ _( 40 | {% end %} 41 | 47 |
48 | {% end %} 49 | 50 |
51 | {% for _card in chain %} 52 |
54 |
55 | Base stats 56 | 57 |
58 | {{ _("Regular") if _card.evolution_id else _("Transformed") }}: 59 | Lv. 60 |
61 | 62 | 63 | 64 |
65 |
66 |
67 |
68 | {% raw webutil.icon(_card.id) %} 69 | 70 | 71 | 72 | 73 | 75 | 77 | 79 | 80 | 81 | 82 | 83 | 84 |
{{ _card.hp_max }} +{{ _card.bonus_hp }}{{ _card.vocal_min }}..{{ _card.vocal_max }} 74 | +{{ _card.bonus_vocal }}{{ _card.dance_min }}..{{ _card.dance_max }} 76 | +{{ _card.bonus_dance }}{{ _card.visual_min }}..{{ _card.visual_max }} 78 | +{{ _card.bonus_visual }}
LifeVocalDanceVisual
85 | 86 | 87 | 89 | 90 | 91 | 92 | 93 | 94 |
{{ _card.overall_min }}..{{ _card.overall_max }} 88 | +{{ _card.overall_bonus }}{{ _card.rarity_dep.max_love }}{{ _card.rarity_dep.base_max_level }}
Overall AppealMax BondMax Level
95 |
96 |
97 | {% end %} 98 |
99 |
100 | {% include availability_box.html %} 101 | 102 | {% if card.skill %} 103 |
104 |
105 | {{ _("Skill") }} 106 | 107 |
{% raw tlable(card.skill.skill_name) %} 108 |
109 | 110 | 111 | 112 |
113 |
114 |
115 |
116 | ({{ _(enums.skill_type(card.skill.skill_type)) }}) 117 | {% raw starlight.en.describe_skill_html(card.skill) %} 118 |
119 |
120 | {% end %} 121 | 122 | {% if card.lead_skill %} 123 |
124 |
125 | {{ _("Leader Skill") }} 126 | 127 | {% raw tlable(card.lead_skill.name) %} 128 |
129 |
130 | {% raw starlight.en.describe_lead_skill_html(card.lead_skill) %} 131 |
132 |
133 | {% end %} 134 |
135 | 136 |
137 |
138 | {{ _("Lines") }} 139 | 140 |
141 |
142 |
143 | Expand 144 |
145 | 146 |
147 |
148 |
149 | {% else %} 150 | 151 | 154 | {% for key, value in card._asdict().items() %} 155 | 156 | {% end %} 157 |
This table is for human reference. 152 | If you want to access this information programmatically, consider using the API instead. 153 | It will save you the trouble of parsing this table.
{{ key }}{{ value }}
158 | {% end %} 159 |
160 | -------------------------------------------------------------------------------- /webui/partials/chara_box.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | {% for kanji, furi in zip( *map(str.split, (chara.kanji_spaced, chara.kana_spaced)) ) %} 4 | 5 | {{ kanji }} ({{ furi }}) 6 | 7 | {% end %} 8 | 9 | ({{ chara.conventional }}) 10 |
11 |
12 | {% if not use_table %} 13 |
14 |
15 | 20 | 21 |
22 |
23 | {% else %} 24 |
25 | 26 | 29 | {% for key, value in chara._asdict().items() %} 30 | 31 | {% end %} 32 |
This table is for usage by humans. 27 | If you want to access this information programmatically, consider using the API instead. 28 | It will save you the trouble of parsing this table.
{{ key }}{{ value }}
33 |
34 | {% end %} 35 | -------------------------------------------------------------------------------- /webui/partials/footer.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | render time (so far): {{ request.request_time() * 1000 }} (ms)
4 | --- the information below is only useful for devs, please ignore it ---

5 | 6 | truth version {{ starlight.data.version }}, 7 | opened at {{ starlight.data.load_date }}, 8 | app version {{ starlight.display_app_ver() }}
9 | did this page trigger a version check? {{ "yes" if handler.did_trigger_update else "no" }}
10 | did this page generate primes? {{ starlight.data.primed_this }}
11 | {% if starlight.update.is_currently_updating() %} 12 | checking for truth updates... performance may be degraded for a few seconds. 13 | {% end %} 14 |

15 | Report bugs/suggest features on GitHub
16 | The Holy Constituency of the Summer Triangle does not claim ownership of any content on this page. 17 |
-------------------------------------------------------------------------------- /webui/partials/frontpage_text.html: -------------------------------------------------------------------------------- 1 |

Welcome to the Starlight Database. You can change this text by editing webui/partials/frontpage_text.html.

-------------------------------------------------------------------------------- /webui/partials/hist_event.html: -------------------------------------------------------------------------------- 1 | {% import enums %} 2 | {% from webutil import tlable, icon_ex %} 3 |
4 |
5 | Event: {% raw tlable(history_entry.event_name) %} 6 | 7 | 8 | {% if countdown %} 9 | ends in (JS?) 10 | {% end %} 11 | 12 |
13 |
14 |
15 | {% for card in history_entry.category_card_list("event") %} 16 | {% raw icon_ex(card, request.is_low_bandwidth) %} 17 | {% end %} 18 |
19 | 20 | 21 | Batch, 22 | Table 23 | - 24 | Event runs from 25 | {{ history_entry.start_dt_string() }} 26 | to 27 | {{ history_entry.end_dt_string() }} 28 | ({{ "{:.1f}".format(history_entry.length_in_days()) }} days). 29 | 30 |
31 |
-------------------------------------------------------------------------------- /webui/partials/hist_gacha.html: -------------------------------------------------------------------------------- 1 | {% import enums %} 2 | {% from webutil import tlable, icon_ex %} 3 | {% from starlight import gacha_rates_t %} 4 |
5 |
6 | Gacha: {% raw tlable(history_entry.event_name) %} 7 | 8 | 9 | {% if countdown %} 10 | changes in (JS?) 11 | {% end %} 12 | {% set rate = rates_src.get(history_entry.referred_id()) %} 13 | {% if rate and rate != gacha_rates_t._REGULAR_RATES %} 14 | rates: R {{ rate.r }}% / SR {{ rate.sr }}% / SSR {{ rate.ssr }}% 15 | {% end %} 16 | 17 |
18 |
19 | 20 | {% if history_entry.added_cards %} 21 | 22 | {% if history_entry.category_card_list("limited") %} 23 | Limited 24 |
25 | {% for card in history_entry.category_card_list("limited") %} 26 | {% raw icon_ex(card, request.is_low_bandwidth, classes="he_icx_limited") %} 27 | {% end %} 28 |
29 | {% end %} 30 | 31 | {% if history_entry.card_list_has_more_than_one_category() %}
{% end %} 32 |
33 | {% for card in history_entry.category_card_list("other") %} 34 | {% raw icon_ex(card, request.is_low_bandwidth) %} 35 | {% end %} 36 |
37 | 38 | {% end %} 39 | 40 | 41 | {% set has_links = bool(history_entry.added_cards or countdown) %} 42 | {% if has_links %} 43 | 44 | {% if history_entry.added_cards %} 45 | Batch, 46 | {% if countdown %} 47 | Table, 48 | {% else %} 49 | Table 50 | {% end %} 51 | {% end %} 52 | 53 | {% if countdown %} 54 | Full availability list 55 | {% end %} 56 | 57 | - 58 | {% end %} 59 | Gacha available from 60 | {{ history_entry.start_dt_string() }} 61 | to 62 | {{ history_entry.end_dt_string() }} 63 | ({{ "{:.1f}".format(history_entry.length_in_days()) }} days). 64 | 65 |
66 |
-------------------------------------------------------------------------------- /webui/partials/hist_new_ns.html: -------------------------------------------------------------------------------- 1 | {% import enums %} 2 | {% from webutil import tlable, icon_ex %} 3 |
4 |
5 | New idols! 6 | 7 | 8 | {{ history_entry.start_dt_string() }} 9 | 10 |
11 |
12 |
13 | {% for card in history_entry.category_card_list("new") %} 14 | {% raw icon_ex(card, request.is_low_bandwidth) %} 15 | {% end %} 16 |
17 | 18 | 19 | Batch, 20 | Table 21 | 22 |
23 |
-------------------------------------------------------------------------------- /webui/partials/new_list_partial.html: -------------------------------------------------------------------------------- 1 | {% import itertools %} 2 | 3 |
4 | {% set cl = new_list.asdict() %} 5 | {{ new_list.dt_string() }} 6 | 7 | 8 | {% set batch_list = ",".join(map(str, itertools.chain(*(x for _, x in cl.items())))) %} 9 | Batch, Table 10 | 11 |
12 |
13 | {% for key in ["event", "ssr", "sr", "r", "n"] %} 14 | {% if cl[key] %} 15 |
16 | {% raw webutil.icon(key) %} 17 | {% for card in cl[key] %} 18 | {% raw webutil.icon_ex(card, request.is_low_bandwidth) %} 19 | {% end %} 20 |
21 | {% end %} 22 | {% end %} 23 |
24 | -------------------------------------------------------------------------------- /webui/partials/va_table_partial.html: -------------------------------------------------------------------------------- 1 | 2 | {{ _("Usage") }} 3 | {{ _("Text") }} 4 | 5 | {% for current_va_id in va_id %} 6 | {% for id, usage, index, voice, text, override_utype in starlight.data.va_data(current_va_id) %} 7 | 8 | {% raw tlable("USE_TYPE__T_{0}".format(override_utype or usage)) %} 9 | {% raw tlable(text.format("[Producer]")) %} 10 | {% if voice %} 11 | (listen) 12 | {% else %} 13 | 🔇 14 | {% end %} 15 | 16 | {% end %} 17 | {% end %} 18 | -------------------------------------------------------------------------------- /webui/spriteviewer.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {% if is_dev %} 7 | 8 | 9 | {% else %} 10 | 11 | {% end %} 12 | 17 | 18 | Expressions: {{ chara.conventional }} (sldb) 19 | 20 | 21 | 22 | {% include header.html %} 23 | 24 | 40 | 41 |
42 |
43 |
44 |
45 | Go back 46 |
47 |
48 |
49 |
50 | 51 | 56 | 57 | 58 | {% include partials/footer.html %} 59 | 60 | 61 | -------------------------------------------------------------------------------- /webutil.py: -------------------------------------------------------------------------------- 1 | import tornado.escape 2 | import hashlib 3 | import base64 4 | import os 5 | import starlight 6 | import enums 7 | import struct 8 | import hmac 9 | 10 | def tlable_make_assr(text): 11 | salt = os.getenv("TLABLE_SALT").encode("utf8") 12 | return base64.b64encode(hmac.new(salt, text.encode("utf8"), hashlib.sha224).digest()).decode("utf8") 13 | 14 | def tlable(text, write=1): 15 | text = text.replace("\n", " ") 16 | if write: 17 | return """{0}""".format( 18 | tornado.escape.xhtml_escape(text), tlable_make_assr(text)) 19 | else: 20 | return """{0}""".format( 21 | tornado.escape.xhtml_escape(text)) 22 | 23 | def icon(css_class): 24 | return """
""".format(css_class) 25 | 26 | def icon_ex(card_id, is_lowbw=0, collapsible=0, classes=""): 27 | rec = starlight.data.card(card_id) 28 | if not rec: 29 | btext = "(?) bug:{0}".format(card_id) 30 | ish = """
31 |
32 |
Mysterious Kashikoi Person
{btext}
33 |
""".format("hides_under_mobile" if collapsible else "", classes, btext=btext) 34 | return """{ish}""".format(ish=ish) 35 | else: 36 | if not is_lowbw: 37 | link = "/char/{rec.chara_id}#c_{rec.id}_head".format(rec=rec) 38 | else: 39 | link = "/card/{rec.id}".format(rec=rec) 40 | 41 | btext = "({0}) {1}".format(enums.rarity(rec.rarity), tlable(rec.title, write=0) if rec.title_flag else "") 42 | ish = """
43 |
44 |
{0}
{btext}
45 |
""".format(tornado.escape.xhtml_escape(rec.chara.conventional), 46 | enums.stat_dot(rec.best_stat), 47 | enums.skill_class(rec.skill.skill_type) if rec.skill else "none", 48 | "hides_under_mobile" if collapsible else "", 49 | classes, 50 | rec=rec, btext=btext) 51 | return """{ish}""".format(rec=rec, ish=ish, link=link) 52 | 53 | def audio(object_id, use, index): 54 | a = (object_id << 40) | ((use & 0xFF) << 24) | ((index & 0xFF) << 16) | 0x11AB 55 | # make everything 8 bytes long for reasons 56 | a &= 0xFFFFFFFFFFFFFFFF 57 | a ^= 0x1042FC1040200700 58 | basename = hex(a)[2:] 59 | 60 | return "va2/{0}.mp3".format(basename) 61 | 62 | SHORT_MAX_ENCODABLE_ATTR = 3 63 | SHORT_MAX_ENCODABLE_UNIQ = 8191 64 | 65 | def encode_card_id_short(id_): 66 | attr_part = id_ // 100000 67 | 68 | if attr_part > SHORT_MAX_ENCODABLE_ATTR: 69 | return encode_card_id_long(id_) 70 | 71 | uniq_part = id_ % 100000 72 | 73 | if uniq_part > SHORT_MAX_ENCODABLE_UNIQ: 74 | return encode_card_id_long(id_) 75 | 76 | pack = (0b1000000000000000 | 77 | attr_part << 13 | 78 | uniq_part ) 79 | 80 | return struct.pack(">H", pack) 81 | 82 | def encode_card_id_long(id_): 83 | if id_ >= 2 ** 31: 84 | raise ValueError("unencodable id") 85 | 86 | return struct.pack(">I", id_) 87 | 88 | def encode_cardlist(ids): 89 | return base64.urlsafe_b64encode(b"".join(encode_card_id_short(x) for x in ids)).decode("ascii") 90 | 91 | def encode_card_structs(cards): 92 | return encode_cardlist(card.id for card in cards) 93 | 94 | def decode_card_id_short(the_id): 95 | packv, = struct.unpack(">H", the_id) 96 | return (((packv >> 13) & 0b11) * 100000) + (packv & 8191) 97 | 98 | def decode_card_id_long(the_id): 99 | return struct.unpack(">I", the_id)[0] 100 | 101 | def decode_cardlist(id_b): 102 | lastgroup = len(id_b) % 4 103 | if lastgroup == 2: 104 | id_b += "==" 105 | else: 106 | id_b += "=" 107 | 108 | bytea = base64.urlsafe_b64decode(id_b.encode("ascii")) 109 | result = [] 110 | 111 | while bytea: 112 | if bytea[0] & 0x80: 113 | if len(bytea) < 2: 114 | raise ValueError("malformed card list") 115 | 116 | result.append(decode_card_id_short(bytea[:2])) 117 | advance = 2 118 | else: 119 | if len(bytea) < 4: 120 | raise ValueError("malformed card list") 121 | 122 | result.append(decode_card_id_long(bytea[:4])) 123 | advance = 4 124 | 125 | bytea = bytea[advance:] 126 | return result 127 | --------------------------------------------------------------------------------