diff --git a/font_manipulator.py b/font_manipulator.py new file mode 100644 index 0000000..c8800b9 --- /dev/null +++ b/font_manipulator.py @@ -0,0 +1,221 @@ +# adapted from https://svn.code.sf.net/p/unifraktur/code + +# -*- coding: utf-8 -*- + +import fileinput +import os +import shutil +import ast + +def string_auswerten(string): + try: + string = ast.literal_eval(string) + except ValueError: + pass + return string + +def glyph_nach_datei(glyph): + if glyph.startswith("uni"): + return glyph + ".glyph" + elif glyph == " ": + return "space.glyph" + else: + chars = list(glyph) + i = 0 + while i < len(chars): + if chars[i].isupper(): + chars.insert(i, "_") + i+=1 + i+=1 + return "".join(chars) + ".glyph" + +def stems_zerlegen(stemstring): + ausgabe = [] + for stem in stemstring.split(">"): + if stem.strip(): + ebene = stem.split("<")[0].split() + positionen_roh = stem.split("<")[1].split() + positionen = map(list,zip(*[iter(positionen_roh)]*2)) + ausgabe += [[ebene, positionen]] + return ausgabe + +def stems_vereinen(stemslist): + ausgabe = [] + for stem in [stem for stems in stemslist for stem in stems]: + neu = True + for bestehender_stem in ausgabe: + if stem[0] == bestehender_stem[0]: + bestehender_stem[1] += stem[1] + neu = False + break + if neu: + ausgabe += [stem] + return ausgabe + +def stems_nach_string(stems): + ausgabe = "" + for stem in stems: + ausgabe += " ".join(stem[0]) + ausgabe += "<" + ausgabe += " ".join([komp for position in stem[1] for komp in position]) + ausgabe += "> " + return ausgabe + +class Schrift: + def __init__(self, name, quellordner, zielordner): + self.name = name + self.ordner = zielordner + if self.ordner in os.listdir("."): + raise Exception("Fehler: Zielordner existiert bereits.") + return None + else: + os.mkdir(self.ordner) + + self.props = [] + + for datei in os.listdir(quellordner): + if os.path.isfile(quellordner + "/" + datei): + shutil.copy(quellordner + "/" + datei, self.ordner) + + self.props_oeffnen() + for i,zeile in enumerate(self.props): + for nameString in ["FontName:", "FullName:", "FamilyName:"]: + if zeile.startswith(nameString): + self.props[i] = nameString + " " + self.name + "\n" + + def __del__(self): + self.props_schliessen() + + def props_oeffnen(self): + if not self.props: + with open(self.ordner + "/font.props", "rb") as propsdatei: + self.props = propsdatei.readlines() + + def props_schliessen(self): + if self.props: + with open(self.ordner + "/font.props", "wb") as propsdatei: + propsdatei.writelines(self.props) + + def ersetzen_durch_Kompositglyph(self, ziel, teil1, teil2): + teile = (teil1,teil2) + self._ersetzen_durch_Kompositglyph(ziel, teile) + + def _ersetzen_durch_Kompositglyph(self, ziel, teile): + breiten = [] + encodings = [] + hstems = [] + vstems = [] + dstems = [] + verschiebungen = [] + verschiebungen.append(0) + + self.props_oeffnen() + in_kerningtabelle = True + anzahl_vor = None + kerning_start = None + indizes = [None, None] + j = None + kerning = 0 + for i,zeile in enumerate(self.props): + if zeile.startswith("KernClass2"): + in_kerningtabelle = True + anzahl_vor = int(zeile.split()[1]) + j = anzahl_vor-1 + anzahl_nach = int(zeile.split()[2]) + kerning_start = i + elif in_kerningtabelle: + if zeile.startswith(" "): + if ("{}" in zeile): + if all(indizes): + a = indizes[0] - kerning_start + b = indizes[1] - kerning_start - anzahl_vor + 1 + kerning = int(zeile.split("{}")[a*anzahl_nach+b]) + else: + inhalt = zeile.split()[1:] + if teile[j>0] in inhalt: + inhalt += [ziel] + if teile[j<=0] in inhalt: + indizes[j<=0] = i + neuer_inhalt = " ".join(inhalt) + self.props[i] = " " + str(len(neuer_inhalt)) + " " + neuer_inhalt + "\n" + j -= 1 + continue + + in_kerningtabelle = False + kerning_start = None + anzahl_vor = None + indizes = [None, None] + j = None + + for i,teil in enumerate(teile): + for zeile in open(self.ordner + "/" + glyph_nach_datei(teil), "rb"): + if zeile.startswith("Width:"): + breiten += [int(zeile.split()[1])] + elif zeile.startswith("Encoding:"): + encodings += [zeile.split()[2:]] + elif zeile.startswith("HStem:"): + hstems += [stems_zerlegen(zeile[7:-1])] + elif zeile.startswith("VStem:"): + vstems += [stems_zerlegen(zeile[7:-1])] + elif zeile.startswith("DStem2:"): + dstems += [stems_zerlegen(zeile[8:-1])] + + zieldatei = glyph_nach_datei(ziel) + + first = True + for j,teil in enumerate(teile): + if(first): + first = False + continue + + verschiebung = kerning + r = range(0, j) + for x in r: + verschiebung = verschiebung + breiten[x] + verschiebungen.append(verschiebung) + + if j < len(hstems): + for hstem in hstems[j]: + for position in hstem[1]: + for i in (0,1): + position[i] = str(string_auswerten(position[i]) + verschiebung) + + if j < len(vstems): + for vstem in vstems[j]: + vstem[0][0] = str(string_auswerten(vstem[0][0]) + verschiebung) + + if j < len(dstems): + for dstem in dstems[j]: + for i in (0,2): + dstem[0][i] = str(string_auswerten(dstem[0][i]) + verschiebung) + + for zeile in fileinput.input (self.ordner + "/" + zieldatei, inplace=1): + if ( + zeile.startswith("StartChar:") + or zeile.startswith("Encoding:") + or zeile.startswith("VWidth:") + or zeile.startswith("GlyphClass:") + or zeile.startswith("Flags:") + or zeile.startswith("EndChar") + ): + print zeile, + elif zeile.startswith("Fore"): + print zeile, + for j,teil in enumerate(teile): + print "Refer: %s %s N 1 0 0 1 %s 0 2" % (encodings[j][1], encodings[j][0], verschiebungen[j]) + elif zeile.startswith("Width:"): + print "Width: %i" % (sum(map(int, breiten))+kerning) + elif zeile.startswith("LayerCount:"): + print zeile, + + stemstring = stems_nach_string(stems_vereinen(hstems)) + if stemstring: + print "HStem: " + stems_nach_string(stems_vereinen(hstems)) + + stemstring = stems_nach_string(stems_vereinen(vstems)) + if stemstring: + print "VStem: " + stems_nach_string(stems_vereinen(vstems)) + + stemstring = stems_nach_string(stems_vereinen(dstems)) + if stemstring: + print "DStem2: " + stems_nach_string(stems_vereinen(dstems)) diff --git a/font_mod_character_pairs b/font_mod_character_pairs new file mode 100644 index 0000000..0f47998 --- /dev/null +++ b/font_mod_character_pairs @@ -0,0 +1,2 @@ +Str|ate|Ret|urn|Adm|ira|l C|Cho|ose +Sel|ect \ No newline at end of file diff --git a/font_mod_run.py b/font_mod_run.py new file mode 100644 index 0000000..8672a58 --- /dev/null +++ b/font_mod_run.py @@ -0,0 +1,18 @@ +#! /usr/bin/env python +# -*- coding: utf-8 -*- + +from font_manipulator import * +import itertools + +start_unicode = "0xE000" +unicode_point_int = int(start_unicode, 0) +pairs = [line.rstrip('\n').split("|") for line in open('font_mod_character_pairs')] +pairs = filter(None, list(itertools.chain.from_iterable(pairs))) + +name = "A-OTF-UDShinGoPro-Regular" +M20 = Schrift(name, "../"+name+".sfdir", "../"+name+"_mod.sfdir") + +for pair in pairs: + name = "uni%X" % unicode_point_int + M20._ersetzen_durch_Kompositglyph(name, list(pair)) + unicode_point_int = unicode_point_int + 1 diff --git a/kc_tr_notes.txt b/kc_tr_notes.txt new file mode 100644 index 0000000..756c860 --- /dev/null +++ b/kc_tr_notes.txt @@ -0,0 +1,156 @@ +1. get app and patch +2. decrypt both +3. copy app, then patch into dir "original", overwriting as necessary +4. copy dir "original" to dir "translation", overwrite with contents of translation patch + +next up, unityex unpacking, sadly gameobjects tree view cannot be unpacked from command line, and ui unpack doesn't create nested trees + +first task though should be creating a script to create translation from original using the repo + +the unity asset manipulations tools are not great, so they'll mostly be used manually and then binary patches created, i think (xdelta) + +dds files extracted via conversion from asset files don'd loat in faststone, but irfanview can handle them. support contacted + + +assembly-csharp was unpacked with jetbrains c# decompiler but not yet repackable. binary patch? + +wtf is a monobehavior, is the file format described? found mention that it's a base class of unity objects and the exact type is clarified by the ids. needs more research. + +quest descriptions have space for 4 lines, seem to need newlines as they break below ui elements otherwise, +fit about this much: "Have 2 ships in your main fleet. Mmmmmmm mmm" + +ship greeting texts have space for 4 lines and auto-break, +fit about this much: "mmm mmmmmm mmmmmm mmmmm mmmmmm", 630 px + +font for both appears to be A-OTF-ShinGoPro-Regular.ttf, 18, line leading setting of -5 + + +選択 6 "Select" +提督コマンド 18 "Admiral Command " +戦略へ 9 "Strategy " +決定 6 "Choose" +戻る 6 "Return" + +wrote a script to inject english versions of jp strings via raw string-matching + +wrote a script to inject modified binary files back into .asset files in bulk + + + +=head1 Assembly-CSharp.dll + +Contains mostly strings in UCS-2 LE. But however a FEW strings (enums) are also UTF8. +No idea if the latter show up ingame. + +> "Most tutorials like this, that I've seen, +> always use .NET Reflector with the Reflexil plugin. +> However, I'm going to be using ILSpy, +> which is a free alternative, with the Reflexil plugin." + +https://github.com/icsharpcode/ILSpy/releases +https://github.com/sailro/Reflexil/releases +https://github.com/0xd4d/dnSpy/releases +jetbrains c# decompiler + +None of these that i tried were able to recompile the dll into something that doesn't +crash the vita, so i went the way of binary modding the dll. + +Sadly due to the strings being in UTF16 and binary modding not allowing +changes to the byte length, it's necessary to mod the fonts in order to +show more characters per byte than usually possible. + +A-OTF-ShinGoPro-Regular.ttf in assets 2, used for quest name texts, ship greetings, ... +A-OTF-UDShinGoPro-Regular.ttf in assets 3, used for the popup button guide at the bottom, ... + +online font tester: http://torinak.com/font/lsfont.html + +For modding fonts i use FontForge. When using it there might be +an error window in the taskbar but not actually visible, +click on that and hit esc once. + +Detailed steps from start to finish: + +- put unityex in ../unity_tools +- put the decrypted game in ../kc_original +- run `unpack_original_files.pl` +- open ../kc_original\Media\Managed\Assembly-CSharp.dll in JetBrains dotPeek + - right-click Assembly-CSharp, export to project (might need two clicks (???)) + - ..\kc_original_unpack\Media\Managed\ + - progress bar in bottom right + + + +But first it makes sense to run translate_utf16_binary.pl to regenerate +Assembly-CSharp.dll. It might complain, depending on whether the +translation dictionary has changed, that pairs are missing. Add them +judiciously to the font_mod_character_pairs file until it stops +complaining about that. + +The following steps set up a font to prepare it for injecting character pairs. + +- do this with: + - ..\kc_original_unpack\Media\Unity_Assets_Files\sharedassets2\A-OTF-ShinGoPro-Regular.ttf + - ..\kc_original_unpack\Media\Unity_Assets_Files\sharedassets3\A-OTF-UDShinGoPro-Regular.ttf + - open the ttf file in font forge + - CID > flatten + - save as directory in d:\vita\ + - close + - open font dir in font forge + - save (wait a little) + - close + - open font file in font forge + - TODO: explain how to add a char for "space" + - copy text from unifraktur-maguntia/set_new_glyphs_as_unicode.txt + - ctrl+end, file > execute script, paste, select FF, OK (this'll take a while) + - save (wait a little) + - close + + + +Now we have a directory with the specification of the font that contains +5000+ prepared glyphs in the first "Private Use Area". In the following +steps we make a copy of that and inject multi-character pairs into the +prepared glyphs. + +After that, these steps will take a copy of the earlier prepared directory +and add the glyphs with the character pairs in the pairs file. + +- # cd unifraktur-maguntia +- # python mod_kc_font.py +- open this directory in fontforge: unifraktur-maguntia/A-OTF-UDShinGoPro-Regular.sfdir +- file > generate fonts > opentype (cff) +- close ff + +And these steps inject them into the assets file, +ready for sending to the vita. + +- # cp unifraktur-maguntia/UDShinGoPro-Regular.otf d:\vita\kc_translation_mod_candidate\Media\Unity_Assets_Files\sharedassets3\A-OTF-UDShinGoPro-Regular.ttf +- # cd d:\vita\kc_translation_mod_candidate\Media\ +- # ..\..\unity_tools\UnityEX.exe import sharedassets3.assets +- copy to vita + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/set_new_glyphs_as_unicode.txt b/set_new_glyphs_as_unicode.txt new file mode 100644 index 0000000..53341a8 --- /dev/null +++ b/set_new_glyphs_as_unicode.txt @@ -0,0 +1,22 @@ +start = 0xE000 +chars_to_add = 5822 +# would prefer to add 6400 and use up the entire PUA, +# but UDShinGoPro-Regular.otf already has a char at F6BE + +# copy this so we can add a shape to the new chars so ff will save them +Select("#") +Copy() + +# add the new slots +oldsize = CharCnt() +SetCharCnt(oldsize + chars_to_add) + +# select all the newly added slots +Select(oldsize, oldsize + chars_to_add - 1) + +# add the "e" shape, set the correct unicode value and increase the counter +foreach + Paste() + SetUnicodeValue(start) + start++ +endloop diff --git a/translate_utf16_binary.pl b/translate_utf16_binary.pl new file mode 100644 index 0000000..4f6e645 --- /dev/null +++ b/translate_utf16_binary.pl @@ -0,0 +1,150 @@ +use 5.020; +use strictures 2; +use IO::All -binary; +use List::Util 'sum'; +use Encode qw' decode encode '; +use utf8; + +run(); + +sub get_hits { + my ( $content, $jp_enc ) = @_; + my @hits; + my $pos = 0; + while ( ( my $hit = index $content, $jp_enc, $pos ) != -1 ) { + push @hits, $hit; + $pos = $hit + 1; + } + return @hits; +} + +sub map_str_to_multi_chars { + my ( $tr, %prepared ) = @_; + my @mapped; + my @chars = split //, $tr; + my @pair; + while (@chars) { + push @pair, shift @chars; + next if @pair < 3; + my $combination = join "", @pair; + my $mapped = $prepared{$combination}; + if ( !$mapped and $combination ne lc $combination ) { + say "combination missing: '$combination', lower-casing"; + $combination = lc $combination; + $mapped = $prepared{$combination}; + } + if ( !$mapped ) { + say "combination missing: '$combination'"; + push @mapped, encode "UTF-16LE", shift @pair; + next; + } + push @mapped, $mapped; + @pair = (); + } + push @mapped, shift @pair if @pair; + return encode "UTF-16LE", join "", @mapped; +} + +sub pad_multi_char_w_spaces { + my ( $l_src, $l_tra, $obj ) = @_; + my $diff = ( $l_src - $l_tra ) / 2; + $obj->{tr_mapped} .= encode( "UTF-16LE", " " ) x $diff; + return length $obj->{tr_mapped}; +} + +sub map_tr_to_multi_chars { + my ( $jp, $obj, %prepared ) = @_; + $obj->{tr_mapped} = map_str_to_multi_chars $obj->{tr}, %prepared; + my $l_src = length encode "UTF-16LE", $jp; + my $l_tra = length $obj->{tr_mapped}; + $l_tra = pad_multi_char_w_spaces $l_src, $l_tra, $obj if $l_tra < $l_src; + die "translation '$jp' => '$obj->{tr}' doesn't match lengths: $l_src => $l_tra, probable char count: " . ( $l_src / 2 ) . "\n" + if $l_src != $l_tra; + return; +} + +sub trim_nl { + my ($s) = @_; + $s =~ s/[\r\n]//g; + return $s; +} + +sub run { + $|++; + binmode STDOUT, ":encoding(UTF-8)"; + binmode STDERR, ":encoding(UTF-8)"; + + my %tr = ( + "選択" => { + tr => "Select", + desc => "explains numpad (the skips should be rechecked probably)", + ok => 4562125, + skip => [ + qw( 4487986 4495884 4539834 4539862 4539876 4539906 4548242 4548242 4548404 4550010 4550048 4550486 4552806 + 4562095 4562119 4495298 4563095 4563131 4563513 4564079 4566677 4570003 4595855 4595927 4596751 4596779 + 4596809 4596951 4598553 4598719 4598751 4633240 4698799 4698860 4698896 4699107 4699472 ) + ] + }, + "提督コマンド" => { + tr => "Admiral Cmd", # quick menu? + desc => "usually opens quick menu or changes the buttons to an alternate set", + ok => [ 4518854, 4682604 ] + }, + "戦略へ" => { tr => "Strategy", desc => "leads to fleet strategy management screen", ok => 4518868 }, + "決定" => { tr => "Choose", desc => "select the currently chosen option", ok => 4518802 }, + "戻る" => { tr => "Return", desc => "back out of the current screen", ok => 4518826, skip => [ 4566833, 4682617 ] }, + "らしんばんをまわしてね!" => { desc => "some kind of compass fairy text, using this to find it" }, + "敵艦隊" => { desc => "" }, + "見ゅ" => { desc => "" }, + "見ゆ" => { desc => "" }, + "被弾回避率補正" => { desc => "" }, + ); + + my $unicode = 0xE000; + my @pairs = grep $_, map split( /\|/, $_ ), map trim_nl($_), io("font_mod_character_pairs")->getlines; + my %prepared = map +( $pairs[$_] => chr( $unicode + $_ ) ), 0 .. $#pairs; + + for my $jp ( grep !$tr{$_}{tr}, sort keys %tr ) { + say "no translation for $jp, skipping"; + delete $tr{$jp}; + } + map_tr_to_multi_chars( $_, $tr{$_}, %prepared ) for sort keys %tr; + + my $rel_path = "Media/Managed/Assembly-CSharp.dll"; + my $src = "../kc_original/$rel_path"; + my $tgt = "../kc_translation_mod_candidate/$rel_path"; + + io($tgt)->unlink if -f $tgt; + + my $content = io($src)->all; + + my ( %found, $error ); + for my $jp ( reverse sort { length $a <=> length $b } sort keys %tr ) { + my @hits = get_hits $content, encode "UTF-16LE", $jp; + if ( !@hits ) { + say "no hits at all for $jp"; + next; + } + my %obj = $tr{$jp}->%*; + $obj{$_} = !defined $obj{$_} ? [] : !ref $obj{$_} ? [ $obj{$_} ] : $obj{$_} for qw( ok skip ); + for my $hit (@hits) { + next if grep $hit == $_, $obj{skip}->@*; + if ( !grep $hit == $_, $obj{ok}->@* ) { + my $mod = $hit - ( $hit % 16 ); + my ( $offset, $extract ) = (0); + while ( $offset < 3 ) { + $extract = decode "UTF-16LE", substr $content, $hit - 16 + $offset, 32 + 16; + last if $extract =~ /$jp/; + $offset++; + } + say sprintf "hit $hit %08x %08x for $jp not marked skipped or ok, please verify $jp in >$extract<", $mod, $hit; + next; + } + substr( $content, $hit, length $obj{tr_mapped} ) = $obj{tr_mapped}; + } + } + + io($tgt)->print($content); + say "done"; + return; +} diff --git a/unpack_original_files.pl b/unpack_original_files.pl new file mode 100644 index 0000000..358db29 --- /dev/null +++ b/unpack_original_files.pl @@ -0,0 +1,22 @@ +use 5.010; +use strictures 2; +use IO::All -binary; +use Capture::Tiny 'capture'; + +run(); + +sub run { + my $target = "../kc_original_unpack/Media"; + io($target)->mkpath; + chdir "../kc_original_unpack/Media"; + my @files = qw( sharedassets2.assets sharedassets3.assets ); + io->file("../../kc_original/Media/$_")->copy(".") for @files; + my $unity_ex = io("../../unity_tools/UnityEX.exe")->absolute->pathname; + for my $file (@files) { + my ( $out, $err, $res ) = capture { system qq["$unity_ex" export $file] }; + warn "\n$out" if $out; + warn "\n$err" if $err; + io($file)->unlink; + } + return; +}