Strona używa cookies (ciasteczek). Dowiedz się więcej o celu ich używania i zmianach ustawień. Korzystając ze strony wyrażasz zgodę na używanie cookies, zgodnie z aktualnymi ustawieniami przeglądarki.    X

Edytor plików Infinity Engine – część 2 — pierwsze szufladkowanie

W poprzedniej części rozszyfrowaliśmy ogólną budowę plików silnika Infinity Engine.

Szybkie przypomnienie:

Założenia wstępne

Przede wszystkim pod lupę weźmie teraz nie plik ITEMS.BIF (zawiera zbyt wiele informacji, ponieważ jest archiwum), tylko pojedynczy plik przedmiotu: BOW01.BIF (wycięty z Baldur's Gate 1 za pomocą NearInfinity).

Opracowanie tego formatu znajdziemy tutaj.

Co będziemy starali się osiągnąć na tym etapie?
Spróbujemy każdą część pliku (zob. opracowanie formatu), czyli: header, extender header oraz feature block wydzielić jako osobny obiekt klasy Block.

Klasa Block będzie posiadała informacje dot. offsetów swojego początku oraz końca w pliku źródłowym, a także wartości poszczególnych pól przypisane do Hashu.

Ostatecznie spróbujemy wykonać konwersję niektórych danych do czytelnego formatu pisząc odpowiednie helpery.

Tworzymy klasę Block

Zgodnie z założeniami wstępnymi klasa Block musi udostępniać atrybuty: początek i koniec, oraz wartości.

class Block attr_reader :start, :end, :values end

Doskonale. Aby utworzyć prawidłowy blok musimy klasie podać źródło (na podstawie którego ma utworzyć nowy blok); offset, od którego ma zacząć oraz wzorzec, na którego podstawie ma odtworzyć pola w danym bloku.

Spróbujmy zacząć od końca, czyli od stworzenia przykładowego wzora, który przekażemy do klasy:

header_pattern = { file_type: ['04', 'word'], file_revision: ['04', 'word'], name_ref_unidentified: ['04'], name_ref_identified: ['04'], used_up_item_file: ['08'], item_attributes: ['04', 'boolean array'], item_type: ['02'], useability: ['04', 'boolean array'], item_animation: ['02', 'word'], min_level: ['02'], min_strength: ['02'], min_strength_bonus: ['02'], min_intelligence: ['02'], min_dexterity: ['02'], min_wisdom: ['02'], min_consitution: ['02'], min_charisma: ['02'], item_price: ['04', 'number'], item_stack: ['02', 'number'], inventory_icon: ['08', 'word'], lore_to_identify: ['02'], ground_icon: ['08', 'word'], item_weight: ['04', 'number'], description_ref_unidentified: ['04'], description_ref_identified: ['04'], description_icon: ['08', 'word'], enchantment: ['04', 'number'], extended_header_offset: ['04', 'number'], extended_header_count: ['02', 'number'], feature_table_offset: ['04', 'number'], zero2: ['02'], feature_block_count: ['02', 'number'] }

Jest to wzorzec nagłówka (header) pliku przedmiotu. Kluczem Hashu jest nazwa pola, a wartością tablica, gdzie pierwsza wartość to wielkość pola, a druga to typ pola (wykorzystywany w późniejszej konwersji). Dla niektórych pól wygodniej jest pozostawić domyślny typ wartości.

Powyższy wzorzec nie jest idealny (osobiście opracowałem ten schemat tylko zgrubsza, żeby działał - przy projekcie zająłem się już kwestią interfejsu i API, żeby widzieć efekty swojej pracy w bardziej przejrzystej formie).

Wróćmy do klasy Block.

W jaki sposób powinno wyglądać odtwarzanie bloku ze źródła? Nasz blok zaczyna się odtwarzać w miejscu podanym jako offset startowy, następnie pobiera tyle bajtów ile wynosi rozmiar pierwszego pola (zdefiniowany we wzorcu), zapisuje te bajty zgodnie z nazwą aktualnego pola, przechodzi do następnego pola i tak dalej pobiera bajty, aż skończy się wzorzec. Wtedy zapisuje wartość offsetu, na którym skończy, i tym samym kończy pracę.

W takim razie, będzie to wyglądało tak:

class Block attr_reader :values, :start, :end def initialize( source, start, pattern ) @start = start @end = nil @values = {} recreate( source, pattern ) self end def recreate( source, pattern ) offset = @start pattern.each do |name, val| size = val[0].to_i(16) type = val[1] # jeśli source to tablica, to poniższe wyrażenie pobiera /size/ elementów, rozpoczynając od elementu o indeksie /offset/ result = source[offset, size] @values[name] = result offset += size end @end = offset end def []( name ) @values[name] end end

Dodatkowo udostępniłem metodę [] dzięki której możemy pobrać wartość pola w taki sam sposób, w jaki wybieramy wartość z tablicy lub hashu (item_block['enchantment']).

Jeśli teraz wykonamy:

header = Block.new( source, 0, header_pattern )

gdzie:
- source to tablica bajtów wczytanego pliku (zob. poprzedni wpis)
- 0 to offset, od którego chcemy zaczynać (header zaczyna się od 0)
- header_pattern to wzorzec headera stworzony wcześniej
otrzymamy dokładnie to, co chcieliśmy: pod atrybutem .values będzie Hash wartości, w którym pola to będzie pola headera, a wartościami będą tablice bajtów.

Konwersja danych

Przygotujemy konwersję tablic bajtów do wybranych przez nas formatów. Rozpoczniemy o rozszerzenie klasy Array (zawsze naszymi danymi będą tablice bajtów) funkcją convert_to

class Array def convert_to( type ) end end

Zastanówmy się, jakie cele konwersji nas interesują:

  • zamiana na słowo (bajt = litera ASCII)
  • zamiana na tablicę bitów (bajt = 8 bitów)
  • zamiana na liczbę (liczby zapisane są w little endian)
  • zamiana na boolean (prawda/fałsz)

Zamiana na słowo:

self.delete_if { |byte| byte == '00' }.join.to_char

Najpierw wyrzucamy wszystkie puste bajty (00) przy pomocy .delete_if, następnie łączymy tablicę w jeden ciąg znaków (.join) i zamieniamy na znaki przy pomocy (.to_char).

Rubysta pewnie wie, .że nie istnieje taka metoda jak .to_char (może jeszcze by miała zgadnąć system liczbowy, w jakim zapisany jest string?). Stwórzmy ją więc:

class String def to_char [self].pack('H*') end end

Zamieniamy string na tablicę, następnie wykorzystujemy funkcję .pack. Zamiana na tablicę jest konieczna, ponieważ funkcja pack działa tylko na tablicach.

Zamiana na tablicę bitów

self.map do |block| boolean_string = block.to_i(16).to_s(2) # fill to correct number of digits (8 digits for sure) boolean_string = "0" * (8 - boolean_string.length) + boolean_string boolean_string.split('') end

Innymi słowy - dla każdego bajtu wykonaj: zamianę na system liczbowy dwójkowy (.to_i(16).to_s(2)), uzupełnij do 8 cyfr (zera od strony MSB znikną podczas konwersji), rozbij 8 bitów w zapisanych w stringu na tablicę

Zamiana na liczbę

self.reverse.join.to_i(16)

Dlaczego odwracamy tablicę? Ponieważ liczba zapisana jest w formacie little endian - oznacza to, że bajty są odwrócone względem logicznego punktu widzenia. Jest to zapis bliższy naszemu codziennemu (powiększanie liczby poprzez dopisywanie na jej końcu cyfer), jednak fałszuje to wyniki konwersji.

Zamiana na boolean

number = self.reverse.join.to_i(16) if number == 0 false else true end

Ponownie musimy bajty zamienić na liczbę i następnie, jeśli liczba jest zerem, to zwrócić fałsz. W innym przypadku - prawdę.

Zapakowane razem...

class String # hex to char def to_char [self].pack('H*') end end class Array def convert_to( type ) case type when 'word' self.delete_if { |block| block == '00' }.join.to_char when 'boolean array' self.map do |block| boolean_string = block.to_i(16).to_s(2) # fill to correct number of digits (8 digits for sure) boolean_string = "0" * (8 - boolean_string.length) + boolean_string boolean_string.split('') end when 'number' self.reverse.join.to_i(16) when 'boolean' number = self.reverse.join.to_i(16) if number == 0 false else true end end end end

I coś takiego dołączamy do naszego kodu jako helpers.rb

Konwersja wewnątrz bloku

Możemy wykorzystać nasze helpery już w trakcie odtwarzania struktury bloku. Wystarczy dorzucić konwersję dla typu podanego we wzorcu;

# class Block def recreate( source, pattern ) offset = @start pattern.each do |name, val| size = val[0].to_i(16) type = val[1] result = source[offset, size] # tutaj linia odpowiadająca za konwersję result = result.convert_to( type ) if type ### @values[name] = result offset += size end @end = offset end

Podsumowanie

Stworzyliśmy elementarną jednostkę naszego programu - Blok. Bloki będziemy w przyszłości wykorzystywać po prostu wszędzie. Przede wszystkim będą służyć jako klasa po której będą w przyszłości dziedziczyć wyspecjalizowane bloki (np. Item::Header lub Chitin::Resource).

Aby w pełni odtworzyć przedmiot, musielibyśmy przygotować także odtwarzanie Extended Headerów (jako dodatku do Headera) oraz Feature Blocków (oddzielnych dla Headerów oraz Extended Headerów).

Taką pracę wykonałem w projekcie IEIE, który jest już sporo do przodu. 

programowanie gry

Komentarze

0 nowych
Nikt nie napisał jeszcze komentarza, możesz być pierwszy!

Gratulacje!

znalezione maszynki:

Twój czas:

Ogól Naczelnego!
Znalazłeś(aś) 10 maszynek Wilkinson Sword
oraz ogoliłeś(aś) naszego naczelnego!
Przejdź do rankingu
Podpowiedź: Przyciśnij lewy przycisk myszki i poruszaj nią, aby ogolić brodę.