środa, 25 maja 2016

Aktualizacja danych

W trakcie tworzenie gry może się zdarzyć, że chcemy dodać do programu funkcjonalność której przedtem nie przewidzieliśmy. Zazwyczaj nie sprawia to kłopotów, ale schody zaczynają się wtedy, gdy ta funkcjonalność wymaga innego sposobu zapisu danych o użytkownikach. Z taką sytuacją spotkałem się gdy chciałem, aby gracz miał możliwość nazywania swoich zestawów żetonów. Dotychczas dane każdego zestawu żetonów zawierały tylko 5 linijek danych - numer zestawu oraz 4 stosy kart żetonów (gdzie każdy numer karty był oddzielony spacją) i miały taki format:

set 0
0 1
2 3
4 5 8
6 7 9

Teraz natomiast zależy mi na formacie danych, który zawierałby także informacje o nazwie zestawu, którą nadał jemu graczu:

set 0
Deck 1
0 1
2 3
4 5 8
6 7 9

Zapis danych w nowym formacie nie powinien być problemem, podobnie jak dostosowanie kodu w taki sposób, aby poprawnie ten zapis odczytywał. Problem polega jednak na tym, że po dostosowaniu kodu do nowego formatu danych nie będzie on już w stanie odczytywać dotychczasowych danych użytkowników. Trzeba więc dodać do programu funkcjonalność, która zamieni dane w starym formacie na dane w nowym formacie. Tutaj pojawia się kolejny problem: jeśli aktualizacja danych będzie polegała na dodaniu do pliku paru linijek tekstu, to po kolejnym uruchomieniu skryptu nowe linijki danych powielą się, tworząc coś takiego:

set 0
Deck 1
Deck 1
0 1
2 3
4 5 8
6 7 9

Trzeba więc dodać do skryptu coś, co pozwoli rozpoznać obecną wersję danych.

Porządkowanie danych

Aby gra była w stanie rozpoznać wersję danych, należy dodać na serwer plik przechowujący informację o wersji danych - jeśli według nowej wersji gry dane użytkowników będą przestarzałe, to wszystkie dane zostaną poprawione, a numer wersji uaktualniony. Pojawia się tutaj drobny problem: jeśli informacja o wersji będzie znajdowała się w tym samym folderze co dane użytkowników, to program mógłby mylić ze sobą te pliki. Dlatego więc utwórzmy nowy folder i dodajmy do niego plik tekstowy:

 if (!Directory.Exists (@"C:/TokenBattle/Version")) {
  Directory.CreateDirectory (@"C:/TokenBattle/Version");
  File.WriteAllText (@"C:/TokenBattle/Version/version.txt", "v0.01");
 }

Od teraz przy uruchomieniu gry na serwerze będziemy mogli sprawdzić wersję danych i na jej podstawie określić, czy potrzebne są aktualizacje.

Dla porządku wypadałoby też stworzyć specjalny folder dla danych o użytkownikach:

 Directory.CreateDirectory (@"C:/TokenBattle/Users");

Do tego folderu przeniesiemy wszystkie dane użytkowników. Aby łatwo operować na plikach, potrzebne są nam ich nazwy, a można je pozyskać funkcją Directory.GetFiles (), która zwraca tablicę stringów zawierających ścieżki plików zawartych w folderze którego ścieżkę podamy w argumencie, np.:

 string [] FileNames = Directory.GetFiles (@"C:/TokenBattle");

Jeśli plik użytkownika nazywałby się user1.txt, to ścieżka tego pliku miałaby format C:/TokenBattle\user1.txt. Zależy nam, aby po modyfikacji ścieżka tego pliku brzmiała: C:/TokenBattle/Users\user1.txt, a skoro początek ścieżki zawsze będzie zaczynał się tak samo, to potrzebujemy tylko końcówki tej ścieżki, będącej nazwą pliku. Musimy więc wyciąć ze stringa wszystko co znajduje się przed znakiem \ i dla każdego pliku musimy to wykonać osobno, czyli musimy to wykonać dla wszystkich stringów w tablicy stringów:

 foreach (string fileName in FileNames) {
  string fName = fileName.Remove (0, fileName.IndexOf ("\\") + 1);
 }

Funkcja Remove przyjmuje 2 parametry: pierwszym parametrem jest index znaku od którego chcemy kasować tekst (włącznie), a drugim parametrem jest liczba znaków które chcemy skasować. Pierwszym argumentem będzie więc 0, a drugim argumentem powinna być liczba znaków do znaku \ (włącznie), czyli 21. Teoretycznie moglibyśmy tu podstawić stałą wartość, jednak jest to niewygodne, chociażby dlatego, że możemy się pomylić. Bezpieczniej jest więc użyć funkcji IndexOf, która zwróci indeks wystąpienia wybranego przez nas znaku, którym jest znak \. Znak ten jest na swój sposób wyjątkowy, ponieważ w programowaniu oznacza on rozpoczęcie różnych specjalnych operacji (np. \r\n powoduje utworzenie nowej linii), dlatego ustalono, że jeśli ktoś chce wstawić ten znak jako znak tekstowy, musi go napisać dwukrotnie.

Skoro mamy już wyodrębnioną nazwę pliku, możemy przenieść plik z jednego folderu do drugiego korzystając z funkcji Move:

 File.Move (@"C:/TokenBattle/" + fName, @"C:/TokenBattle/Users/" + fName);

Pierwszym argumentem tej funkcji jest ścieżka pliku który chcemy przenieść, a drugim argumentem jest ścieżka pliku którą chcemy uzyskać.

Modyfikacja danych o użytkownikach

Teraz pozostaje nam tylko zmodyfikować pliki. Aby to zrobić, musimy najpierw wczytać ich zawartość:

 List <string> Lines = new List <string> (File.ReadAllLines (@"C:/TokenBattle/Users/" + fName));

Funkcja File.ReadAllLines zwraca tablicę stringów, które są zawartością pliku o ścieżce podanej w argumencie. Jednak tablice mają pewną wadę: nie można na nich wykonywać wielu złożonych informacji, takich jak np. dodanie stringa w środku tablicy - można go dodać tylko na jej końcu. Skorzystamy więc z list, które oferują większe możliwości niż zwykłe tablice. Użytkownik może mieć zapisane na koncie wiele zestawów żetonów, więc program powinien odnaleźć wszystkie z nich.

 int HandsetNumber = 0;
 while (Lines.IndexOf ("set " + HandsetNumber.ToString ()) != -1) {
  Lines.Insert (Lines.IndexOf ("set " + HandsetNumber.ToString ()) + 1, "Deck " + (HandsetNumber + 1).ToString ());
  HandsetNumber++;
}

Na początku tworzymy tu zmienną, która będzie zapamiętywała numer aktualnie przeglądanego zestawu żetonów. Jeśli w danym pliku znajduje się zestaw z tym numerem, to znaczy że musimy go zmodyfikować, dodając nazwę tego zestawu, a następnie musimy zwiększyć zmienną z numerem i wszystko zapętlamy do momentu, aż w pliku skończą się zestawy. Funkcja IndexOf zwraca na wyjście -1, jeśli w liście nie znajduje się element równy temu podanemu w argumencie, co w naszym przypadku spowoduje przerwanie pętli. Funkcja Insert przyjmuje 2 argumenty - pierwszym argumentem jest numer indeksu na który chcemy dodać nowy element listy (następne elementy listy zostaną odpowiednio przesunięte), a drugim argumentem jest wartość tego elementu listy, czyli w naszym przypadku string. W programowaniu panuje zwyczaj numerowania rzeczy od 0, ale nazwa zestawów żetonów będzie widoczna w grze dla użytkowników, czyli nazwy zestawów wypadałoby numerować od 1. Prócz tego nazwa zestawu w przeciwieństwie do jego poprzedniego numeru będzie mogła być modyfikowana przez użytkowników.

Następnie pozostaje tylko zapisać zmiany:

 File.WriteAllLines (@"C:/TokenBattle/Users/" + fName, Lines.ToArray());

Odpowiednie złożenie tego wszystkiego w całość i dodanie do kodu sprawi, że nasz program będzie mógł zmodyfikować dane, jeśli okażą się nieaktualne.

Brak komentarzy:

Prześlij komentarz