diff --git a/docs/python/pythonism.md b/docs/python/pythonism.md new file mode 100644 index 0000000..9b04d07 --- /dev/null +++ b/docs/python/pythonism.md @@ -0,0 +1,48 @@ +# Python-isms that are used often + + +## Find if list has *any* matches + +This is a common problem. Say you have a list of objects with a `parent_id` attribute. +You want to see if there is at least one object in the list where `parent_id` is `117`. + +Python offers a one-liner to do this by abusing the [`next()`](https://docs.python.org/3/library/functions.html#next) +function. + +```python +# my_objects is a list of objects with parent_id +found_match = next(iter([x for x in my_list if x.parent_id == 117])) + +if found_match: + ... +``` + +- `[x for x in my_list if x.parent_id == 117]` is a python list comprehension that +makes a list of all objects matching the `if` statement. +- `iter()` converts the list to an iterator. +- `next()` grabs the next item in the iterator. + +This is an alternative to: + +```python +found_match: bool = False + +for my_object in my_list: + if my_object.parent_id == 117: + found_match = True + break + +if found_match: + ... +``` + +##### Pros + +- One line +- Easy to read for people familiar with this trick + +##### Cons + +- On massive lists, it is better to do the looping method. This is because the list comprehension creates a second + list of **all** matches before calling `next()` on it. +- Looping can be more legible for people unfamiliar with the trick. diff --git a/kirovy/constants/__init__.py b/kirovy/constants/__init__.py index f9684f5..a89f833 100644 --- a/kirovy/constants/__init__.py +++ b/kirovy/constants/__init__.py @@ -10,7 +10,7 @@ CNCNET_INI_SECTION = "CnCNet" CNCNET_INI_MAP_ID_KEY = "ID" -CNCNET_INI_PARENT_ID_KEY = "ParentID" +CNCNET_INI_MAP_PARENT_ID_KEY = "ParentID" class CncnetUserGroup: diff --git a/kirovy/constants/api_codes.py b/kirovy/constants/api_codes.py new file mode 100644 index 0000000..567a181 --- /dev/null +++ b/kirovy/constants/api_codes.py @@ -0,0 +1,10 @@ +import enum + + +class UploadApiCodes(enum.StrEnum): + GAME_SLUG_DOES_NOT_EXIST = "game-slug-does-not-exist" + MISSING_GAME_SLUG = "missing-game-slug" + FILE_TO_LARGE = "file-too-large" + EMPTY_UPLOAD = "where-file" + DUPLICATE_MAP = "duplicate-map" + FILE_EXTENSION_NOT_SUPPORTED = "file-extension-not-supported" diff --git a/kirovy/exception_handler.py b/kirovy/exception_handler.py new file mode 100644 index 0000000..fc6c635 --- /dev/null +++ b/kirovy/exception_handler.py @@ -0,0 +1,29 @@ +from rest_framework import status +from rest_framework.views import exception_handler + +from kirovy.exceptions.view_exceptions import KirovyValidationError +from kirovy.objects import ui_objects +from kirovy.response import KirovyResponse + + +def kirovy_exception_handler(exception: Exception, context) -> KirovyResponse[ui_objects.ErrorResponseData] | None: + """Exception handler to deal with our custom exception types. + + This gets called via the setting ``REST_FRAMEWORK['EXCEPTION_HANDLER']``. + :attr:`kirovy.settings._base.REST_FRAMEWORK` + + .. note:: + + `The DRF docs `_ + + :param exception: + The raised exception. + :param context: + :return: + Returns the ``KirovyResponse`` if the exception is one we defined. + Otherwise, it calls the base DRF exception handler :func:`rest_framework.views.exception_handler`. + """ + if isinstance(exception, KirovyValidationError): + return KirovyResponse(exception.as_error_response_data(), status=status.HTTP_400_BAD_REQUEST) + + return exception_handler(exception, context) diff --git a/kirovy/exceptions/__init__.py b/kirovy/exceptions/__init__.py index 388dfe4..315f91e 100644 --- a/kirovy/exceptions/__init__.py +++ b/kirovy/exceptions/__init__.py @@ -53,6 +53,7 @@ class InvalidMimeType(ValidationError): class InvalidMapFile(ValidationError): """Raised when a map can't be parsed or if it's missing a header.""" + # TODO: Unify with kirovy.exceptions.view_exceptions.KirovyValidationError pass diff --git a/kirovy/exceptions/view_exceptions.py b/kirovy/exceptions/view_exceptions.py new file mode 100644 index 0000000..d7ecd61 --- /dev/null +++ b/kirovy/exceptions/view_exceptions.py @@ -0,0 +1,31 @@ +from rest_framework import status +from rest_framework.exceptions import APIException as _DRFAPIException +from django.utils.translation import gettext_lazy as _ + +from kirovy import typing as _t +from kirovy.objects import ui_objects + + +class KirovyValidationError(_DRFAPIException): + """A custom exception that easily converts to the standard ``ErrorResponseData`` + + See: :class:`kirovy.objects.ui_objects.ErrorResponseData` + + This exception is meant to be used within serializers or views. + """ + + status_code = status.HTTP_400_BAD_REQUEST + default_detail = _("Invalid input.") + default_code = "invalid" + additional: _t.DictStrAny | None = None + code: str | None + detail: str | None + + def __init__(self, detail: str | None = None, code: str | None = None, additional: _t.DictStrAny | None = None): + super().__init__(detail=detail, code=code) + self.code = str(code) if code else self.default_code + self.detail = str(detail) if detail else self.default_detail + self.additional = additional + + def as_error_response_data(self) -> ui_objects.ErrorResponseData: + return ui_objects.ErrorResponseData(message=self.detail, code=self.code, additional=self.additional) diff --git a/kirovy/logging.py b/kirovy/logging.py new file mode 100644 index 0000000..25154af --- /dev/null +++ b/kirovy/logging.py @@ -0,0 +1,15 @@ +from structlog import get_logger +import orjson +import typing as t + + +def default_json_encode_object(value: object) -> str: + json_func: t.Callable[[object], str] | None = getattr(value, "__json__", None) + if json_func and callable(json_func): + return json_func(value) + + stringy: bool = type(value).__str__ is not object.__str__ # Check if this object implements __str__ + if stringy: + return str(value) + + return f"cannot-json-encode--{type(value).__name__}" diff --git a/kirovy/migrations/0010_cncmapfile_hash_sha1_mappreview_hash_sha1.py b/kirovy/migrations/0010_cncmapfile_hash_sha1_mappreview_hash_sha1.py new file mode 100644 index 0000000..65b23c5 --- /dev/null +++ b/kirovy/migrations/0010_cncmapfile_hash_sha1_mappreview_hash_sha1.py @@ -0,0 +1,23 @@ +# Generated by Django 4.2.20 on 2025-03-10 06:47 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("kirovy", "0009_mappreview"), + ] + + operations = [ + migrations.AddField( + model_name="cncmapfile", + name="hash_sha1", + field=models.CharField(max_length=50, null=True), + ), + migrations.AddField( + model_name="mappreview", + name="hash_sha1", + field=models.CharField(max_length=50, null=True), + ), + ] diff --git a/kirovy/models/__init__.py b/kirovy/models/__init__.py index 73da8c9..afdeaed 100644 --- a/kirovy/models/__init__.py +++ b/kirovy/models/__init__.py @@ -1,6 +1,6 @@ import typing -from django.db.models import UUIDField, Model +from django.db.models import Model from .cnc_game import CncGame, CncFileExtension from .cnc_map import CncMap, CncMapFile, MapCategory @@ -10,5 +10,4 @@ class SupportsBan(typing.Protocol): - def set_ban(self, is_banned: bool, banned_by: CncUser) -> None: - ... + def set_ban(self, is_banned: bool, banned_by: CncUser) -> None: ... diff --git a/kirovy/models/cnc_game.py b/kirovy/models/cnc_game.py index 6b814c5..ffc242d 100644 --- a/kirovy/models/cnc_game.py +++ b/kirovy/models/cnc_game.py @@ -17,9 +17,7 @@ def is_valid_extension(extension: str) -> None: Raised for invalid file extension strings. """ if not extension.isalnum(): - raise exceptions.InvalidFileExtension( - f'"{extension}" is not a valid file extension. Must be alpha only.' - ) + raise exceptions.InvalidFileExtension(f'"{extension}" is not a valid file extension. Must be alpha only.') class CncFileExtension(CncNetBaseModel): @@ -45,9 +43,7 @@ class ExtensionTypes(models.TextChoices): IMAGE = "image", "image" """This file extension represents some kind of image uploaded by a user to display on the website.""" - extension = models.CharField( - max_length=32, unique=True, validators=[is_valid_extension], blank=False - ) + extension = models.CharField(max_length=32, unique=True, validators=[is_valid_extension], blank=False) """The actual file extension. Case insensitive but ``.lower()`` will be called all over.""" about = models.CharField(max_length=2048, null=True, blank=False) @@ -61,9 +57,8 @@ class ExtensionTypes(models.TextChoices): ) def save(self, *args, **kwargs): - is_valid_extension( - self.extension - ) # force validator on save instead from a view. + self.extension = self.extension.lower() # Force lowercase + is_valid_extension(self.extension) # force validator on save instead from a view. super().save(*args, **kwargs) @property @@ -94,9 +89,7 @@ class CncGame(CncNetBaseModel): Does not affect temporary uploads via the multiplayer lobby. """ - compatible_with_parent_maps = models.BooleanField( - default=False, null=False, blank=False - ) + compatible_with_parent_maps = models.BooleanField(default=False, null=False, blank=False) """If true then the maps from the parent game work in this game. e.g. RA2 maps work in YR.""" parent_game = models.ForeignKey("self", models.PROTECT, null=True, default=None) diff --git a/kirovy/models/cnc_map.py b/kirovy/models/cnc_map.py index dbb3adc..fc11979 100644 --- a/kirovy/models/cnc_map.py +++ b/kirovy/models/cnc_map.py @@ -16,9 +16,7 @@ class MapCategory(CncNetBaseModel): slug = models.CharField(max_length=16) """Unique slug for URLs, auto-generated from the :attr:`~kirovy.models.cnc_map.MapCategory.name`.""" - def _set_slug_from_name( - self, update_fields: t.Optional[t.List[str]] = None - ) -> t.Optional[t.List[str]]: + def _set_slug_from_name(self, update_fields: t.Optional[t.List[str]] = None) -> t.Optional[t.List[str]]: """Sets ``self.slug`` based on ``self.name``. :param update_fields: @@ -27,9 +25,7 @@ def _set_slug_from_name( The ``update_fields`` for ``.save()``. """ new_slug: str = text_utils.slugify(self.name, allow_unicode=False)[:16] - new_slug = new_slug.rstrip( - "-" - ) # Remove trailing hyphens if the 16th character was unlucky. + new_slug = new_slug.rstrip("-") # Remove trailing hyphens if the 16th character was unlucky. if new_slug != self.slug: self.slug = new_slug @@ -98,9 +94,7 @@ class CncMap(cnc_user.CncNetUserOwnedModel): or searches. Won't have an owner until the client supports logging in. """ - is_reviewed = models.BooleanField( - default=False, help_text="If true, this map was reviewed by a staff member." - ) + is_reviewed = models.BooleanField(default=False, help_text="If true, this map was reviewed by a staff member.") is_banned = models.BooleanField( default=False, @@ -130,10 +124,7 @@ def next_version_number(self) -> int: The current latest version, plus one. """ previous_version: CncMapFile = ( - CncMapFile.objects.filter(cnc_map_id=self.id) - .order_by("-version") - .only("version") - .first() + CncMapFile.objects.filter(cnc_map_id=self.id).order_by("-version").only("version").first() ) if not previous_version: return 1 @@ -168,7 +159,12 @@ def set_ban(self, is_banned: bool, banned_by: cnc_user.CncUser) -> None: class CncMapFile(file_base.CncNetFileBaseModel): - """Represents the actual map file that a Command & Conquer game reads.""" + """Represents the actual map file that a Command & Conquer game reads. + + .. warning:: + + ``name`` is auto-generated for this file subclass. + """ width = models.IntegerField() height = models.IntegerField() @@ -182,9 +178,7 @@ class CncMapFile(file_base.CncNetFileBaseModel): class Meta: constraints = [ - models.UniqueConstraint( - fields=["cnc_map_id", "version"], name="unique_map_version" - ), + models.UniqueConstraint(fields=["cnc_map_id", "version"], name="unique_map_version"), ] def save(self, *args, **kwargs): diff --git a/kirovy/models/file_base.py b/kirovy/models/file_base.py index 7588a93..1fa5d33 100644 --- a/kirovy/models/file_base.py +++ b/kirovy/models/file_base.py @@ -1,17 +1,14 @@ import pathlib -from django.core import validators from django.db import models -from kirovy import typing as t, exceptions +from kirovy import typing as t from kirovy.models.cnc_base_model import CncNetBaseModel from kirovy.models import cnc_game as game_models from kirovy.utils import file_utils -def _generate_upload_to( - instance: "CncNetFileBaseModel", filename: t.Union[str, pathlib.Path] -) -> pathlib.Path: +def _generate_upload_to(instance: "CncNetFileBaseModel", filename: t.Union[str, pathlib.Path]) -> pathlib.Path: """Calls the subclass specific method to generate an upload path. Do **NOT** override this function. Override the ``generate_upload_to`` function on your file model. @@ -47,17 +44,13 @@ class Meta: ) """What type of file extension this object is.""" - ALLOWED_EXTENSION_TYPES: t.Set[str] = set( - game_models.CncFileExtension.ExtensionTypes.values - ) + ALLOWED_EXTENSION_TYPES: t.Set[str] = set(game_models.CncFileExtension.ExtensionTypes.values) """Used to make sure e.g. a ``.mix`` doesn't get uploaded as a ``CncMapFile``. These are checked against :attr:`kirovy.models.cnc_game.CncFileExtension.extension_type`. """ - cnc_game = models.ForeignKey( - game_models.CncGame, models.PROTECT, null=False, blank=False - ) + cnc_game = models.ForeignKey(game_models.CncGame, models.PROTECT, null=False, blank=False) """Which game does this file belong to. Needed for file validation.""" hash_md5 = models.CharField(max_length=32, null=False, blank=False) @@ -66,36 +59,42 @@ class Meta: hash_sha512 = models.CharField(max_length=512, null=False, blank=False) """Used for checking exact file duplicates.""" - def validate_file_extension( - self, file_extension: game_models.CncFileExtension - ) -> None: + hash_sha1 = models.CharField(max_length=50, null=True, blank=False) + """Backwards compatibility with the old CncNetClient.""" + + def validate_file_extension(self, file_extension: game_models.CncFileExtension) -> None: + """Validate that an extension is supported for a game. + + This should probably be done in a serializer, but doing it all the way down in the model is + technically safer to avoid screwing things up in a migration. + """ # Images are allowed for all games. - is_image = ( - self.file_extension.extension_type - == self.file_extension.ExtensionTypes.IMAGE - ) - is_allowed_for_game = ( - file_extension.extension.lower() in self.cnc_game.allowed_extensions_set - ) + is_image = self.file_extension.extension_type == self.file_extension.ExtensionTypes.IMAGE + is_allowed_for_game = file_extension.extension.lower() in self.cnc_game.allowed_extensions_set + + from kirovy.exceptions.view_exceptions import KirovyValidationError + if not is_allowed_for_game and not is_image: - raise validators.ValidationError( - f'"{file_extension.extension}" is not a valid file extension for game "{self.cnc_game.full_name}".' + raise KirovyValidationError( + detail=f'"{file_extension.extension.lower()}" is not a valid file extension for game "{self.cnc_game.full_name}".', + code="file-extension-unsupported-for-game", ) if file_extension.extension_type not in self.ALLOWED_EXTENSION_TYPES: - raise exceptions.InvalidFileExtension( - f'"{file_extension.extension}" is not a valid file extension for this upload type.' + raise KirovyValidationError( + detail=f'"{file_extension.extension}" is not a valid file extension for this upload type.', + code="file-extension-unsupported-for-model", ) def save(self, *args, **kwargs): self.validate_file_extension(self.file_extension) + self.hash_md5 = file_utils.hash_file_md5(self.file) self.hash_sha512 = file_utils.hash_file_sha512(self.file) + self.hash_sha1 = file_utils.hash_file_sha1(self.file) super().save(*args, **kwargs) @staticmethod - def generate_upload_to( - instance: "CncNetFileBaseModel", filename: str - ) -> pathlib.Path: + def generate_upload_to(instance: "CncNetFileBaseModel", filename: str) -> pathlib.Path: """Generate the base upload path. This is where files will go if a class doesn't set its own ``generate_upload_to``. diff --git a/kirovy/objects/ui_objects.py b/kirovy/objects/ui_objects.py index b88dc07..8721786 100644 --- a/kirovy/objects/ui_objects.py +++ b/kirovy/objects/ui_objects.py @@ -53,7 +53,7 @@ class PaginationMetadata(TypedDict): class BaseResponseData(TypedDict): """Basic response from Kirovy to the UI. - Mostly used for post requests where the only side effect is some kind of success or failure message. + Mostly used for post requests where the only response is some kind of success or failure message. """ message: NotRequired[str] @@ -71,10 +71,11 @@ class ListResponseData(BaseResponseData): pagination_metadata: NotRequired[PaginationMetadata] -class ResponseData(BaseResponseData): +class ResultResponseData(BaseResponseData): """Basic response that returns a dictionary in addition to the message from ``BaseResponseData``. - Mostly subclassed for endpoints that return side effects to the UI. + Mostly for endpoints that return object data to the UI. e.g. a ``create`` endpoint returning + the object that was created. """ result: DictStrAny @@ -83,7 +84,14 @@ class ResponseData(BaseResponseData): class ErrorResponseData(BaseResponseData): """Basic response that returns a dictionary of additional data related to an error.""" + code: str + """attr: The same as ``code`` in :class:`rest_framework.exceptions.APIException`. + + This is a string for the UI. The human-readable error should go in ``message``. + """ + additional: NotRequired[DictStrAny] + """attr: Arbitrary data to return to the UI to help user's understand what they did wrong.""" class UiPermissions: @@ -117,9 +125,7 @@ class UiPermissions: """ @classmethod - def render_static( - cls, request: KirovyRequest, view: View - ) -> t.Dict[t.UiPermissionName, bool]: + def render_static(cls, request: KirovyRequest, view: View) -> t.Dict[t.UiPermissionName, bool]: """Create a dictionary of permissions to tell the UI what to display. This **DOES NOT** control the backend permissions, it's just to help the UI know which buttons to show. diff --git a/kirovy/response.py b/kirovy/response.py index 16ec7db..9375030 100644 --- a/kirovy/response.py +++ b/kirovy/response.py @@ -1,13 +1,20 @@ +import typing_extensions from rest_framework.response import Response import kirovy.objects.ui_objects from kirovy import typing as t +DataType = typing_extensions.TypeVar( + "DataType", bound=kirovy.objects.ui_objects.BaseResponseData, default=kirovy.objects.ui_objects.BaseResponseData +) + + +class KirovyResponse(Response, t.Generic[DataType]): + data: DataType -class KirovyResponse(Response): def __init__( self, - data: t.Optional[kirovy.objects.ui_objects.BaseResponseData] = None, + data: DataType = None, status: t.Optional[int] = None, template_name: t.Optional[str] = None, headers: t.Optional[t.DictStrAny] = None, diff --git a/kirovy/serializers/cnc_map_serializers.py b/kirovy/serializers/cnc_map_serializers.py index 030a74a..f9a777a 100644 --- a/kirovy/serializers/cnc_map_serializers.py +++ b/kirovy/serializers/cnc_map_serializers.py @@ -1,6 +1,11 @@ +from kirovy.exceptions.view_exceptions import KirovyValidationError from kirovy.serializers import KirovySerializer, CncNetUserOwnedModelSerializer from rest_framework import serializers -from kirovy.models import cnc_map, CncGame, CncUser, MapCategory +from kirovy import typing as t +from kirovy.models import cnc_map, CncGame, MapCategory, CncFileExtension + + +class MapParserSerializerField(serializers.Field): ... class MapCategorySerializer(KirovySerializer): @@ -19,6 +24,77 @@ def update(self, instance: MapCategory, validated_data: dict) -> MapCategory: return instance +class CncMapFileSerializer(KirovySerializer): + + class Meta: + model = cnc_map.CncMapFile + # We return the ID instead of the whole object. + exclude = ["cnc_game", "cnc_map", "file_extension"] + fields = "__all__" + + width = serializers.IntegerField() + """attr: The map height. + + Extracted using :class:`kirovy.services.cnc_gen_2_services.CncGen2MapParser` + """ + + height = serializers.IntegerField() + """attr: The map height. + + Extracted using :class:`kirovy.services.cnc_gen_2_services.CncGen2MapParser` + """ + + version = serializers.IntegerField(read_only=True) + """attr: The map version. + + Set by :func:`kirovy.models.cnc_map.CncMapFile.save` on creation and cannot be modified. + Allows map authors to version their maps to e.g. update your campaign with fixed scripts. + """ + + cnc_map_id = serializers.PrimaryKeyRelatedField( + source="cnc_map", + queryset=cnc_map.CncMap.objects.all(), + pk_field=serializers.UUIDField(), + ) + + name = serializers.CharField(read_only=True) + """attr: The filename. + + Generated by :func:`kirovy.models.cnc_map.CncMapFile.save` and cannot be customized. + """ + + file = serializers.FileField(use_url=True) + + file_extension_id = serializers.PrimaryKeyRelatedField( + source="file_extension", + queryset=CncFileExtension.objects.filter(extension_type__in=cnc_map.CncMapFile.ALLOWED_EXTENSION_TYPES), + pk_field=serializers.UUIDField(), + ) + + cnc_game_id = serializers.PrimaryKeyRelatedField( + source="cnc_game", + queryset=CncGame.objects.all(), + pk_field=serializers.UUIDField(), + ) + + hash_md5 = serializers.CharField(required=True, allow_blank=False) + hash_sha512 = serializers.CharField(required=True, allow_blank=False) + hash_sha1 = serializers.CharField(required=True, allow_blank=False) + + def create(self, validated_data: t) -> cnc_map.CncMapFile: + map_file = cnc_map.CncMapFile(**validated_data) + map_file.save() + return map_file + + def update(self, instance: cnc_map.CncMapFile, validated_data: t.DictStrAny) -> cnc_map.CncMapFile: + # Map files are not meant to be updated. Only created, or deleted. + raise KirovyValidationError( + detail="Map files cannot be updated. Upload a new version of your map", + code="cannot-update-maps", + additional={"cnc_map_id": str(instance.cnc_map_id)}, + ) + + class CncMapBaseSerializer(CncNetUserOwnedModelSerializer): map_name = serializers.CharField( required=True, @@ -66,5 +142,6 @@ class CncMapBaseSerializer(CncNetUserOwnedModelSerializer): class Meta: model = cnc_map.CncMap + # We return the ID instead of the whole object. exclude = ["cnc_game", "categories"] fields = "__all__" diff --git a/kirovy/services/cnc_gen_2_services.py b/kirovy/services/cnc_gen_2_services.py index 03b47bd..03f8020 100644 --- a/kirovy/services/cnc_gen_2_services.py +++ b/kirovy/services/cnc_gen_2_services.py @@ -23,6 +23,7 @@ class CncGen2MapSections(enum.StrEnum): + # Todo: move to API codes. PREVIEW_PACK = "PreviewPack" PREVIEW = "Preview" HEADER = "Header" @@ -65,9 +66,7 @@ def map_name(self) -> str: :return: The map name or a string saying that the map wasn't found. """ - return self.get( - CncGen2MapSections.BASIC, "Name", fallback=_(self.NAME_NOT_FOUND) - ) + return self.get(CncGen2MapSections.BASIC, "Name", fallback=_(self.NAME_NOT_FOUND)) class CncGen2MapParser: @@ -99,7 +98,7 @@ class ErrorMsg(enum.StrEnum): CORRUPT_MAP = _("Could not parse map file.") MISSING_INI = _("Missing necessary INI sections.") - def __init__(self, uploaded_file: UploadedFile): + def __init__(self, uploaded_file: UploadedFile | File): self.validate_file_type(uploaded_file) self.file = uploaded_file self.ini = MapConfigParser() @@ -222,12 +221,7 @@ def extract_preview(self) -> t.Optional[Image.Image]: # The map preview image size will be ``0,0,width,height``. # We cannot extract the preview if this section is missing, or the Size key value is invalid. - preview_size = [ - int(x) - for x in self.ini.get( - CncGen2MapSections.PREVIEW, "Size", fallback="" - ).split(",") - ] + preview_size = [int(x) for x in self.ini.get(CncGen2MapSections.PREVIEW, "Size", fallback="").split(",")] if len(preview_size) != 4: LOGGER.debug("No preview size") return None @@ -273,14 +267,10 @@ def _decompress_preview_from_base64(self, width: int, height: int) -> io.BytesIO break # Read the compressed size of the block. The size is stored as a two byte integer - block_size_compressed, read_bytes = self._read_16bit_int_le( - compressed_preview, read_bytes, 2 - ) + block_size_compressed, read_bytes = self._read_16bit_int_le(compressed_preview, read_bytes, 2) # Read the uncompressed size of the block. The size is stored as a two byte integer - block_size_uncompressed, read_bytes = self._read_16bit_int_le( - compressed_preview, read_bytes, 2 - ) + block_size_uncompressed, read_bytes = self._read_16bit_int_le(compressed_preview, read_bytes, 2) # If the block sizes are 0 then we reached the end of the actual pixel data. if block_size_compressed == 0 or block_size_uncompressed == 0: @@ -289,16 +279,12 @@ def _decompress_preview_from_base64(self, width: int, height: int) -> io.BytesIO # Reading the expected compressed bytes will exceed the size of the compressed data. # This means the ``Preview.Size`` was wrong, or the preview data is corrupt. projected_read_byte_count = read_bytes + block_size_compressed - compressed_size_exceeds_source = projected_read_byte_count > len( - compressed_preview - ) + compressed_size_exceeds_source = projected_read_byte_count > len(compressed_preview) # The size of the written bytes will exceed the expected uncompressed image size. # This means the ``Preview.Size`` was wrong, or the preview data is corrupt. projected_written_byte_count = written_bytes + block_size_uncompressed - uncompressed_size_exceeds_destination = ( - projected_written_byte_count > decompressed_expected_size - ) + uncompressed_size_exceeds_destination = projected_written_byte_count > decompressed_expected_size error_params = { "decompressed_expected_size": decompressed_expected_size, "projected_decompressed_size": projected_written_byte_count, @@ -315,16 +301,12 @@ def _decompress_preview_from_base64(self, width: int, height: int) -> io.BytesIO ) # Slice off the compressed block of bytes, according to the block header. - compressed_block = compressed_preview[ - read_bytes : read_bytes + block_size_compressed - ] + compressed_block = compressed_preview[read_bytes : read_bytes + block_size_compressed] # Decompress the block. try: # decompress without the header because we manually grabbed the necessary bytes. - uncompressed_block = lzo.decompress( - compressed_block, False, block_size_uncompressed - ) + uncompressed_block = lzo.decompress(compressed_block, False, block_size_uncompressed) except lzo.error as e: raise exceptions.MapPreviewCorrupted( "Could not decompress the preview. Preview data is corrupted in some way.", @@ -343,9 +325,7 @@ def _decompress_preview_from_base64(self, width: int, height: int) -> io.BytesIO return decompressed_preview @staticmethod - def _read_16bit_int_le( - compressed_preview: bytes, start: int, bytes_to_read: int - ) -> t.Tuple[int, int]: + def _read_16bit_int_le(compressed_preview: bytes, start: int, bytes_to_read: int) -> t.Tuple[int, int]: """Read a little-endian 16bit integer from bytes. :param compressed_preview: @@ -364,9 +344,7 @@ def _read_16bit_int_le( ) return read, stop - def _create_preview_bitmap( - self, width: int, height: int, decompressed_preview: io.BytesIO - ) -> Image.Image: + def _create_preview_bitmap(self, width: int, height: int, decompressed_preview: io.BytesIO) -> Image.Image: """Create the pillow image from the decompressed preview bytes. :param width: @@ -380,7 +358,5 @@ def _create_preview_bitmap( """ decompressed_preview.seek(0) # 0, 0 means "start drawing in the top left corner." - img = Image.frombytes( - "RGB", (width, height), decompressed_preview.read(), "raw", "RGB", 0, 0 - ) + img = Image.frombytes("RGB", (width, height), decompressed_preview.read(), "raw", "RGB", 0, 0) return img diff --git a/kirovy/settings/_base.py b/kirovy/settings/_base.py index 71077c9..fa656f7 100644 --- a/kirovy/settings/_base.py +++ b/kirovy/settings/_base.py @@ -11,6 +11,8 @@ """ from pathlib import Path + +from kirovy.utils import file_utils from kirovy.utils.settings_utils import ( get_env_var, secret_key_validator, @@ -31,20 +33,25 @@ ALLOWED_HOSTS = [] +MAX_UPLOADED_FILE_SIZE_MAP = file_utils.ByteSized(mega=25) + # Application definition INSTALLED_APPS = [ "kirovy", + "django.contrib.postgres", # necessary for full-text search and advanced postgres functionality. "django.contrib.admin", "django.contrib.auth", "django.contrib.contenttypes", "django.contrib.sessions", "django.contrib.messages", "django.contrib.staticfiles", - "rest_framework", + "django_filters", # Used for advanced querying on the API. + "rest_framework", # Django REST Framework. ] + MIDDLEWARE = [ "django.middleware.security.SecurityMiddleware", "django.contrib.sessions.middleware.SessionMiddleware", @@ -55,7 +62,7 @@ "django.middleware.clickjacking.XFrameOptionsMiddleware", ] -ROOT_URLCONF = "kirovy.urls" +ROOT_URLCONF = "kirovy.urls" # The file that holds our URL routing. TEMPLATES = [ { @@ -78,8 +85,19 @@ REST_FRAMEWORK = { "DEFAULT_AUTHENTICATION_CLASSES": [ "kirovy.authentication.CncNetAuthentication", - ] + ], + "EXCEPTION_HANDLER": "kirovy.exception_handler.kirovy_exception_handler", } +""" +attr: Define the default authentication backend for endpoints. +Can be overwritten for views, but this is rare. See :class:`kirovy.authentication.CncNetAuthentication`. + +.. warning:: + + Authentication is **not** a permission system. Authentication just checks if a user is logged in or + not. For checking e.g. object permissions, see the module :mod:`kirovy.permissions`. To understand how permissions + are set in Django Rest Framework, see `The DRF docs `_ +""" # Database # https://docs.djangoproject.com/en/4.1/ref/settings/#databases @@ -139,7 +157,7 @@ CNC_MAP_DIRECTORY = "maps" -""":attr: The directory, beneath the game slug, where map files will be stored.""" +"""attr: The directory, beneath the game slug, where map files will be stored.""" ### --------------- SERVING FILES --------------- diff --git a/kirovy/urls.py b/kirovy/urls.py index e7a44b3..ffd18dd 100644 --- a/kirovy/urls.py +++ b/kirovy/urls.py @@ -19,6 +19,7 @@ from django.contrib import admin from django.urls import path, include +import kirovy.views.map_upload_views from kirovy.views import test, cnc_map_views, permission_views, admin_views from kirovy import typing as t @@ -33,9 +34,7 @@ def _get_url_patterns() -> list[path]: [ path("admin/", include(admin_patterns)), path("test/jwt", test.TestJwt.as_view()), - path( - "ui-permissions/", permission_views.ListPermissionForAuthUser.as_view() - ), + path("ui-permissions/", permission_views.ListPermissionForAuthUser.as_view()), path("maps/", include(map_patterns)), # path("users//", ...), # will show which files a user has uploaded. # path("games/", ...), # get games. @@ -50,9 +49,11 @@ def _get_url_patterns() -> list[path]: # path("categories/", ...), # return all categories # path("categories/game//", ...), path("categories/", cnc_map_views.MapCategoryListCreateView.as_view()), - path("upload/", cnc_map_views.MapFileUploadView.as_view()), + path("upload/", kirovy.views.map_upload_views.MapFileUploadView.as_view()), + path("client/upload/", kirovy.views.map_upload_views.CncnetClientMapUploadView.as_view()), path("/", cnc_map_views.MapRetrieveUpdateView.as_view()), path("delete//", cnc_map_views.MapDeleteView.as_view()), + path("search/", cnc_map_views.MapListCreateView.as_view()), # path("img//"), # path("img//", ...), # path("search/") diff --git a/kirovy/utils/file_utils.py b/kirovy/utils/file_utils.py index afcb4ae..884259c 100644 --- a/kirovy/utils/file_utils.py +++ b/kirovy/utils/file_utils.py @@ -1,20 +1,25 @@ import collections import functools import hashlib -from kirovy import typing as t -from django.db.models.fields.files import FieldFile +from django.core.files import File + +from kirovy import typing as t -def hash_file_md5(file: FieldFile, block_size=65536) -> str: +def hash_file_md5(file: File, block_size=65536) -> str: return _hash_file(hashlib.md5(), file, block_size) -def hash_file_sha512(file: FieldFile, block_size=65536) -> str: +def hash_file_sha512(file: File, block_size=65536) -> str: return _hash_file(hashlib.sha512(), file, block_size) -def _hash_file(hasher: "_HASH", file: FieldFile, block_size: int) -> str: +def hash_file_sha1(file: File, block_size=65536) -> str: + return _hash_file(hashlib.sha1(), file, block_size) + + +def _hash_file(hasher: "_HASH", file: File, block_size: int) -> str: file.seek(0) file_contents = file.read() if isinstance(file_contents, str): @@ -68,9 +73,7 @@ def __new__( return self def __str__(self) -> str: - return ", ".join( - [f"{size}{desc}" for desc, size in self.__mapping.items() if size > 0] - ) + return ", ".join([f"{size}{desc}" for desc, size in self.__mapping.items() if size > 0]) @functools.cached_property def __mapping(self) -> t.Dict[str, int]: diff --git a/kirovy/views/admin_views.py b/kirovy/views/admin_views.py index 5812771..31dac44 100644 --- a/kirovy/views/admin_views.py +++ b/kirovy/views/admin_views.py @@ -34,9 +34,7 @@ def post(self, request: KirovyRequest, **kwargs) -> KirovyResponse: data=ui_objects.ErrorResponseData(message="data_failed_validation"), ) - obj = get_object_or_404( - ban_data.get_model().objects.filter(), id=ban_data.object_id - ) + obj = get_object_or_404(ban_data.get_model().objects.filter(), id=ban_data.object_id) try: obj.set_ban(ban_data.is_banned, self.request.user) except exceptions.BanException as e: @@ -47,5 +45,5 @@ def post(self, request: KirovyRequest, **kwargs) -> KirovyResponse: return KirovyResponse( status=status.HTTP_200_OK, - data=ui_objects.ResponseData(message="", result=ban_data.model_dump()), + data=ui_objects.ResultResponseData(message="", result=ban_data.model_dump()), ) diff --git a/kirovy/views/base_views.py b/kirovy/views/base_views.py index 77c5d3b..4f5da90 100644 --- a/kirovy/views/base_views.py +++ b/kirovy/views/base_views.py @@ -1,6 +1,7 @@ """ Base views with common functionality for all API views in Kirovy """ + from rest_framework import ( exceptions as _e, generics as _g, @@ -12,6 +13,7 @@ import kirovy.objects.ui_objects from kirovy import permissions, typing as t +from kirovy.objects import ui_objects from kirovy.request import KirovyRequest from kirovy.response import KirovyResponse from kirovy.serializers import KirovySerializer @@ -23,7 +25,7 @@ class KirovyDefaultPagination(_pagination.LimitOffsetPagination): default_limit = 30 max_limit = 200 - def get_paginated_response(self, results: t.List[t.DictStrAny]) -> Response: + def get_paginated_response(self, results: t.List[t.DictStrAny]) -> KirovyResponse[ui_objects.ListResponseData]: data = kirovy.objects.ui_objects.ListResponseData( results=results, pagination_metadata=kirovy.objects.ui_objects.PaginationMetadata( @@ -33,7 +35,7 @@ def get_paginated_response(self, results: t.List[t.DictStrAny]) -> Response: ), ) - return Response(data, status=status.HTTP_200_OK) + return KirovyResponse(data, status=status.HTTP_200_OK) def get_paginated_response_schema(self, schema): raise NotImplementedError() @@ -50,21 +52,18 @@ class KirovyListCreateView(_g.ListCreateAPIView): _paginator: t.Optional[KirovyDefaultPagination] request: KirovyRequest # Added for type hinting. Populated by DRF ``.setup()`` - def create(self, request: KirovyRequest, *args, **kwargs) -> Response: + def create(self, request: KirovyRequest, *args, **kwargs) -> KirovyResponse[ui_objects.ResultResponseData]: data = request.data - if isinstance(data, dict) and issubclass( - self.get_serializer_class(), KirovySerializer - ): + if isinstance(data, dict) and issubclass(self.get_serializer_class(), KirovySerializer): data["last_modified_by_id"] = request.user.id serializer = self.get_serializer(data=data) serializer.is_valid(raise_exception=True) self.perform_create(serializer) headers = self.get_success_headers(serializer.data) - return Response( - serializer.data, status=status.HTTP_201_CREATED, headers=headers - ) + data = ui_objects.ResultResponseData(result=serializer.data) + return KirovyResponse(data, status=status.HTTP_201_CREATED, headers=headers) - def list(self, request, *args, **kwargs): + def list(self, request, *args, **kwargs) -> KirovyResponse[ui_objects.ListResponseData]: queryset = self.filter_queryset(self.get_queryset()) page = self.paginate_queryset(queryset) @@ -74,9 +73,9 @@ def list(self, request, *args, **kwargs): serializer = self.get_serializer(queryset, many=True) data = kirovy.objects.ui_objects.ListResponseData(results=serializer.data) - return Response(data, status=status.HTTP_200_OK) + return KirovyResponse(data, status=status.HTTP_200_OK) - def get_paginated_response(self, data: t.List[t.DictStrAny]) -> Response: + def get_paginated_response(self, data: t.List[t.DictStrAny]) -> KirovyResponse[ui_objects.ListResponseData]: """ Return a paginated style `Response` object for the given output data. """ @@ -84,7 +83,7 @@ def get_paginated_response(self, data: t.List[t.DictStrAny]) -> Response: @property def paginator(self) -> t.Optional[KirovyDefaultPagination]: - """Just here for typing.""" + """Just here for type hinting.""" return super().paginator @@ -104,7 +103,7 @@ def retrieve(self, request, *args, **kwargs): instance = self.get_object() serializer = self.get_serializer(instance) return KirovyResponse( - kirovy.objects.ui_objects.ResponseData( + kirovy.objects.ui_objects.ResultResponseData( result=serializer.data, ), status=status.HTTP_200_OK, diff --git a/kirovy/views/cnc_map_views.py b/kirovy/views/cnc_map_views.py index 62af31a..81b3bc6 100644 --- a/kirovy/views/cnc_map_views.py +++ b/kirovy/views/cnc_map_views.py @@ -1,30 +1,18 @@ -import io import logging -import pathlib -from django.conf import settings -from django.core.files.uploadedfile import UploadedFile, InMemoryUploadedFile -from django.db.models import Q +from django.db.models import Q, QuerySet from rest_framework import status from rest_framework.exceptions import PermissionDenied -from rest_framework.parsers import MultiPartParser -from rest_framework.views import APIView +from rest_framework.filters import SearchFilter, OrderingFilter +from django_filters import rest_framework as filters -import kirovy.objects.ui_objects -from kirovy import permissions, typing as t, exceptions, constants +from kirovy import permissions from kirovy.models import ( MapCategory, - cnc_map, CncGame, - CncFileExtension, - map_preview, CncMap, ) -from kirovy.request import KirovyRequest -from kirovy.response import KirovyResponse from kirovy.serializers import cnc_map_serializers -from kirovy.services.cnc_gen_2_services import CncGen2MapParser, CncGen2MapSections -from kirovy.utils import file_utils from kirovy.views import base_views @@ -37,11 +25,156 @@ class MapCategoryListCreateView(base_views.KirovyListCreateView): queryset = MapCategory.objects.all() +class MapListFilters(filters.FilterSet): + """The filters for the map list endpoint. + + `Docs on how these work `_ + + For the Many-to-Many filters, like categories, refer to the + `MultipleChoiceFilterDocs _`. + + The TL;DR is that multiple choices are done by specifying the same field multiple times. + + e.g. ``/maps/search/?categories=1&categories=2&categories=3`` + """ + + include_edits = filters.BooleanFilter(field_name="parent_id", method="filter_include_map_edits") + # include_maps_from_sub_games = filters.BooleanFilter( + # field_name="cnc_game__parent_id", method="filter_include_maps_from_sub_games" + # ) + cnc_game = filters.ModelMultipleChoiceFilter( + field_name="cnc_game__id", to_field_name="id", queryset=CncGame.objects.filter(is_visible=True) + ) + # categories = filters.ModelMultipleChoiceFilter(queryset=MapCategory.objects.filter()) + + class Meta: + model = CncMap + fields = ["is_legacy", "is_reviewed", "parent", "categories"] + + def filter_include_map_edits(self, queryset: QuerySet[CncMap], name: str, value: bool) -> QuerySet[CncMap]: + """We will exclude maps that are edits of other maps by default. + + If ``value`` is true, then we will return edits of other maps. + Maps with ``parent_id IS NOT NULL`` are edits of another map. + + See: :attr:`kirovy.models.cnc_map.CncMap.parent`. + + :param queryset: + The queryset that we will modify with our filters. + :param name: + The name of the field. We don't use it, but it's required for the interface. + :param value: + The value from the UI. If ``True``, then we will include map edits. + :return: + The queryset, maybe modified to include map edits. + """ + if not value: + # Was not provided, or set to false. Don't include map edits. + return queryset.exclude(parent_id__isnull=False) + + return queryset + + # TODO: Does anyone even want this behavior? + # def filter_include_maps_from_sub_games( + # self, queryset: QuerySet[CncMap], name: str, value: bool + # ) -> QuerySet[CncMap]: + # """We will exclude maps that are for sub games of the selected games by default. + # + # If ``value`` is true, then we will return maps for sub games. + # e.g. return Yuri's Revenge maps if game is Red Alert 2. + # + # Sub games can also be mods, according to the database, so make sure to set the filter for including mods + # too. + # + # See: :attr:`kirovy.models.cnc_game.CncGame.parent_game`. + # + # :param queryset: + # The queryset that we will modify with our filters. + # :param name: + # The name of the field. We don't use it, but it's required for the interface. + # :param value: + # The value from the UI. If ``True``, then we will include maps for sub games of the game filter. + # :return: + # The queryset, maybe modified to include maps for sub games. + # """ + # specified_games = self.data["cnc_game"] + # if not specified_games: + # # The user didn't specify a game, so don't perform any modifications to the query. + # return queryset + # if not value: + # # User provided games, but does not want to see sub games. + # return queryset.exclude(cnc_game__parent_game_id__isnull=False) + # + # # User wants to see sub games + # return queryset | CncMap.objects.filter(cnc_game__parent_game__in=) + + class MapListCreateView(base_views.KirovyListCreateView): """ The view for maps. """ + def get_queryset(self): + """The default query from which all other map list queries are built. + + By default, maps will be shown if (they are published, and not banned) or if they're a legacy map. + + We only show maps for games that are visible (so we can hide Generals until it's done.) + + .. code-block:: python + + ```CncMap.objects.filter(Q(x=y, z=a) | Q(a=b))``` + + Translates to: + + .. code-block:: sql + + SELECT * FROM cnc_maps WHERE (x=y AND z=a) OR a=b + + """ + base_query = ( + CncMap.objects.filter( + Q(is_banned=False, is_published=True, incomplete_upload=False, is_temporary=False) | Q(is_legacy=True) + ).filter(cnc_game__is_visible=True) + # Prefetch data necessary to the map grid. Pre-fetching avoids hitting the database in a loop. + .select_related("cnc_user", "cnc_game", "parent", "parent__cnc_user") + # Prefetch the categories because they're displayed like tags. + # TODO: Since the category list is going to be somewhat small, + # maybe the UI should just cache them and I return IDs instead of objects? + .prefetch_related("categories") + ) + return base_query + + filter_backends = [ + SearchFilter, + OrderingFilter, + filters.DjangoFilterBackend, + ] + filterset_class = MapListFilters + + search_fields = [ + "@map_name", + "^description", + ] + """ + attr: Fields that can be text searched using query params. + `Django REST Framework docs `_. + `Built-in django search docs `_. + """ + + ordering_fields = [ + "map_name", + "cnc_map_file__created", # For finding maps with new file versions. + "cnc_map_file__width", + "cnc_map_file__height", + ] + """ + attr: The fields we will sort ordering by. + `Docs `_ + """ + + serializer_class = cnc_map_serializers.CncMapBaseSerializer + class MapRetrieveUpdateView(base_views.KirovyRetrieveUpdateView): serializer_class = cnc_map_serializers.CncMapBaseSerializer @@ -71,8 +204,7 @@ def get_queryset(self): # Anyone can view legacy maps, temporary maps (for the cncnet client,) and published maps that aren't banned. queryset = CncMap.objects.filter( - Q(Q(is_published=True) | Q(is_legacy=True) | Q(is_temporary=True)) - & Q(is_banned=False) + Q(Q(is_published=True) | Q(is_legacy=True) | Q(is_temporary=True)) & Q(is_banned=False) ) if self.request.user.is_authenticated: @@ -88,131 +220,5 @@ class MapDeleteView(base_views.KirovyDestroyView): def perform_destroy(self, instance: CncMap): if instance.is_legacy: - raise PermissionDenied( - "cannot-delete-legacy-maps", status.HTTP_403_FORBIDDEN - ) + raise PermissionDenied("cannot-delete-legacy-maps", status.HTTP_403_FORBIDDEN) return super().perform_destroy(instance) - - -class MapFileUploadView(APIView): - parser_classes = [MultiPartParser] - permission_classes = [permissions.CanUpload] - - def post(self, request: KirovyRequest, format=None) -> KirovyResponse: - game = CncGame.objects.get(id=request.data["game_id"]) - uploaded_file: UploadedFile = request.data["file"] - extension = CncFileExtension.objects.get( - extension=pathlib.Path(uploaded_file.name).suffix.lstrip(".") - ) - max_size = file_utils.ByteSized(mega=25) - uploaded_size = file_utils.ByteSized(uploaded_file.size) - - if uploaded_size > max_size: - return KirovyResponse( - kirovy.objects.ui_objects.ErrorResponseData( - message="File too large", - additional={ - "max_bytes": str(max_size), - "your_bytes": str(uploaded_file), - }, - ), - status=status.HTTP_400_BAD_REQUEST, - ) - - try: - # TODO: Finish the map upload. - map_parser = CncGen2MapParser(uploaded_file) - except exceptions.InvalidMapFile as e: - return KirovyResponse( - kirovy.objects.ui_objects.ErrorResponseData(message="Invalid Map File"), - status=status.HTTP_400_BAD_REQUEST, - ) - - parent: t.Optional[cnc_map.CncMap] = None - cnc_map_id: t.Optional[str] = map_parser.ini.get( - constants.CNCNET_INI_SECTION, constants.CNCNET_INI_MAP_ID_KEY, fallback=None - ) - if cnc_map_id: - parent = cnc_map.CncMap.objects.filter(id=cnc_map_id).first() - - new_map = cnc_map.CncMap( - map_name=map_parser.ini.map_name, - cnc_game=game, - is_published=False, - incomplete_upload=True, - cnc_user=request.user, - parent=parent, - ) - new_map.save() - - cnc_net_ini = {constants.CNCNET_INI_MAP_ID_KEY: str(new_map.id)} - if parent: - cnc_net_ini[constants.CNCNET_INI_PARENT_ID_KEY] = str(parent.id) - - map_parser.ini[constants.CNCNET_INI_SECTION] = cnc_net_ini - - # Write the modified ini to the uploaded file before we save it to its final location. - written_ini = io.StringIO() # configparser doesn't like - map_parser.ini.write(written_ini) - written_ini.seek(0) - uploaded_file.seek(0) - uploaded_file.truncate() - uploaded_file.write(written_ini.read().encode("utf8")) - - # Add categories. - non_existing_categories: t.Set[str] = set() - for game_mode in map_parser.ini.categories: - category = MapCategory.objects.filter(name__iexact=game_mode).first() - if not category: - non_existing_categories.add(game_mode) - continue - new_map.categories.add(category) - - if non_existing_categories: - _LOGGER.warning( - "User attempted to upload map with categories that don't exist: non_existing_categories=%s", - non_existing_categories, - ) - - new_map_file = cnc_map.CncMapFile( - width=map_parser.ini.get(CncGen2MapSections.HEADER, "Width"), - height=map_parser.ini.get(CncGen2MapSections.HEADER, "Height"), - cnc_map=new_map, - file=uploaded_file, - file_extension=extension, - cnc_game=new_map.cnc_game, - ) - new_map_file.save() - - extracted_image = map_parser.extract_preview() - extracted_image_url: str = "" - if extracted_image: - image_io = io.BytesIO() - image_extension = CncFileExtension.objects.get(extension="jpg") - extracted_image.save(image_io, format="JPEG", quality=95) - django_image = InMemoryUploadedFile( - image_io, None, "temp.jpg", "image/jpeg", image_io.tell(), None - ) - new_map_preview = map_preview.MapPreview( - is_extracted=True, - cnc_map_file=new_map_file, - file=django_image, - file_extension=image_extension, - ) - new_map_preview.save() - extracted_image_url = new_map_preview.file.url - - # TODO: Actually serialize the return data and include the link to the preview. - # TODO: Should probably convert this to DRF for that step. - return KirovyResponse( - kirovy.objects.ui_objects.ResponseData( - message="File uploaded successfully", - result={ - "cnc_map": new_map.map_name, - "cnc_map_file": new_map_file.file.url, - "cnc_map_id": new_map.id, - "extracted_preview_file": extracted_image_url, - }, - ), - status=status.HTTP_201_CREATED, - ) diff --git a/kirovy/views/map_upload_views.py b/kirovy/views/map_upload_views.py new file mode 100644 index 0000000..47d207a --- /dev/null +++ b/kirovy/views/map_upload_views.py @@ -0,0 +1,343 @@ +import io +import pathlib +from abc import ABCMeta + +from cryptography.utils import cached_property +from django.conf import settings +from django.core.exceptions import ObjectDoesNotExist +from django.core.files.uploadedfile import UploadedFile, InMemoryUploadedFile +from django.db.models import Q, QuerySet +from rest_framework import status, serializers +from rest_framework.parsers import MultiPartParser +from rest_framework.permissions import BasePermission, AllowAny +from rest_framework.views import APIView + +from kirovy import typing as t, permissions, exceptions, constants, logging +from kirovy.constants.api_codes import UploadApiCodes +from kirovy.exceptions.view_exceptions import KirovyValidationError +from kirovy.models import cnc_map, CncGame, CncFileExtension, MapCategory, map_preview +from kirovy.objects.ui_objects import ResultResponseData +from kirovy.request import KirovyRequest +from kirovy.response import KirovyResponse +from kirovy.serializers import cnc_map_serializers +from kirovy.services.cnc_gen_2_services import CncGen2MapParser, CncGen2MapSections +from kirovy.utils import file_utils + + +_LOGGER = logging.get_logger(__name__) + + +class MapHashes(t.NamedTuple): + md5: str + sha1: str + sha512: str + + +class _BaseMapFileUploadView(APIView, metaclass=ABCMeta): + parser_classes = [MultiPartParser] + permission_classes: t.ClassVar[t.Iterable[BasePermission]] + request: KirovyRequest + upload_is_temporary: t.ClassVar[bool] + + def __init__(self, **kwargs): + super().__init__(**kwargs) + has_permission_class_set = getattr(self, "permission_classes", None) + if has_permission_class_set is None: + raise NotImplementedError("Must define permissions to subclass the map uploader.") + has_upload_is_temporary_set = getattr(self, "upload_is_temporary", None) + if has_upload_is_temporary_set is None: + raise NotImplementedError("Must define what this endpoint sets for ``map_file.is_temporary``.") + + def post(self, request: KirovyRequest, format=None) -> KirovyResponse: + # todo: add file version support. + # todo: make validation less trash + uploaded_file: UploadedFile = request.data["file"] + + game_id = self.get_game_id_from_request(request) + extension_id = self.get_extension_id_for_upload(uploaded_file) + self.verify_file_size_is_allowed(uploaded_file) + + map_hashes = self._get_file_hashes(uploaded_file) + self.verify_file_does_not_exist(map_hashes) + map_parser = self.get_map_parser(uploaded_file) + parent_map = self.get_map_parent(map_parser) + + # Make the map that we will attach the map file too. + new_map = cnc_map.CncMap( + map_name=map_parser.ini.map_name, + cnc_game_id=game_id, + is_published=False, + incomplete_upload=True, + cnc_user=request.user, + parent=parent_map, + ) + new_map.save() + + # Set the cncnet map ID in the map file ini. + cnc_net_ini = {constants.CNCNET_INI_MAP_ID_KEY: str(new_map.id)} + if parent_map: + # If the map has a parent, specify that map's parent so that we can properly credit the original creator. + cnc_net_ini[constants.CNCNET_INI_MAP_PARENT_ID_KEY] = str(parent_map.id) + + # write the ID(s) to the cncnet section of the INI. + map_parser.ini[constants.CNCNET_INI_SECTION] = cnc_net_ini + + # Write the modified ini to the uploaded file before we save it to its final location. + written_ini = io.StringIO() # configparser doesn't like strings + map_parser.ini.write(written_ini) + written_ini.seek(0) + uploaded_file.seek(0) + uploaded_file.truncate() + uploaded_file.write(written_ini.read().encode("utf8")) + map_hashes_post_processing = self._get_file_hashes(uploaded_file) + + # Add categories. + # TODO: move above making the new map and put categories in the serializer. + non_existing_categories: t.Set[str] = set() + for game_mode in map_parser.ini.categories: + category = MapCategory.objects.filter(name__iexact=game_mode).first() + if not category: + non_existing_categories.add(game_mode) + continue + new_map.categories.add(category) + + if non_existing_categories: + _LOGGER.warning( + "User attempted to upload map with categories that don't exist: non_existing_categories=%s", + non_existing_categories, + **self.user_log_attrs, + ) + + new_map_file_serializer = cnc_map_serializers.CncMapFileSerializer( + data=dict( + width=map_parser.ini.get(CncGen2MapSections.HEADER, "Width"), + height=map_parser.ini.get(CncGen2MapSections.HEADER, "Height"), + cnc_map_id=new_map.id, + file=uploaded_file, + file_extension_id=extension_id, + cnc_game_id=new_map.cnc_game_id, + hash_md5=map_hashes_post_processing.md5, + hash_sha512=map_hashes_post_processing.sha512, + hash_sha1=map_hashes_post_processing.sha1, + ), + context={"request": self.request}, + ) + new_map_file_serializer.is_valid(raise_exception=True) + new_map_file = new_map_file_serializer.save() + + extracted_image = map_parser.extract_preview() + extracted_image_url: str = "" + if extracted_image: + image_io = io.BytesIO() + image_extension = CncFileExtension.objects.get(extension="jpg") + extracted_image.save(image_io, format="JPEG", quality=95) + django_image = InMemoryUploadedFile(image_io, None, "temp.jpg", "image/jpeg", image_io.tell(), None) + new_map_preview = map_preview.MapPreview( + is_extracted=True, + cnc_map_file=new_map_file, + file=django_image, + file_extension=image_extension, + ) + new_map_preview.save() + extracted_image_url = new_map_preview.file.url + + # TODO: Actually serialize the return data and include the link to the preview. + # TODO: Should probably convert this to DRF for that step. + return KirovyResponse( + ResultResponseData( + message="File uploaded successfully", + result={ + "cnc_map": new_map.map_name, + "cnc_map_file": new_map_file.file.url, + "cnc_map_id": new_map.id, + "extracted_preview_file": extracted_image_url, + }, + ), + status=status.HTTP_201_CREATED, + ) + + def get_map_parser(self, uploaded_file: UploadedFile) -> CncGen2MapParser: + try: + return CncGen2MapParser(uploaded_file) + except exceptions.InvalidMapFile as e: + raise KirovyValidationError(detail=e.message, code=e.code, additional=e.params) + + def get_map_parent(self, map_parser: CncGen2MapParser) -> cnc_map.CncMap | None: + """Determine if this map has a parent or not. + + This exists to make sure original authors are properly credited when someone uploads an edit of their map. + + # TODO: Support uploading new versions of maps. Will need a new permission class called "CanAddNewVersion". + + :param map_parser: + :return: + """ + parent: t.Optional[cnc_map.CncMap] = None + cnc_map_id: t.Optional[str] = map_parser.ini.get( + constants.CNCNET_INI_SECTION, constants.CNCNET_INI_MAP_ID_KEY, fallback=None + ) + if cnc_map_id: + parent = cnc_map.CncMap.objects.filter(id=cnc_map_id).first() + + return parent + + @cached_property + def user_log_attrs(self) -> t.DictStrAny: + # todo move to structlogger + naughty_ip_address = self.request.META.get("HTTP_X_FORWARDED_FOR", "unknown") + user = self.request.user + return { + "ip_address": naughty_ip_address, + "user": f"[{user.cncnet_id}] {user.username}" if user.is_authenticated else "unauthenticated_upload", + } + + @staticmethod + def _get_file_hashes(uploaded_file: UploadedFile) -> MapHashes: + map_hash_sha512 = file_utils.hash_file_sha512(uploaded_file) + map_hash_md5 = file_utils.hash_file_md5(uploaded_file) + map_hash_sha1 = file_utils.hash_file_sha1(uploaded_file) # legacy ban list support + + return MapHashes(md5=map_hash_md5, sha1=map_hash_sha1, sha512=map_hash_sha512) + + def get_game_id_from_request(self, request: KirovyRequest) -> str | None: + """Get the game_id from the request. + + This is a method, rather than a direct lookup, so that the client can use ``game_slug``. + """ + raise NotImplementedError() + + def get_extension_id_for_upload(self, uploaded_file: UploadedFile) -> str: + uploaded_extension = pathlib.Path(uploaded_file.name).suffix.lstrip(".").lower() + # iexact is case insensitive + kirovy_extension = CncFileExtension.objects.filter( + extension__iexact=uploaded_extension, + extension_type__in=cnc_map.CncMapFile.ALLOWED_EXTENSION_TYPES, + ).first() + + if kirovy_extension: + return str(kirovy_extension.id) + + _LOGGER.warning( + "User attempted uploading unknown filetype", + uploaded_extension=uploaded_extension, + **self.user_log_attrs, # todo: the userattrs should be a context tag for structlog. + ) + raise serializers.ValidationError( + detail=f"'{uploaded_extension}' is not a valid map file extension.", + code=UploadApiCodes.FILE_EXTENSION_NOT_SUPPORTED, + ) + + def verify_file_does_not_exist(self, hashes: MapHashes) -> None: + """Check to make sure that a map file doesn't exist. + + We check the overall file because we want to allow e.g. a new version of a map to be uploaded + with fixes to its campaign scripts. + + :param hashes: + The hashes of the uploaded file. + :return: + Nothing + :raises KirovyValidationError: + Raised if a duplicate file exists. + """ + matched_hashes: QuerySet[cnc_map.CncMapFile] = ( + cnc_map.CncMapFile.objects.filter( + Q(hash_md5=hashes.md5) | Q(hash_sha512=hashes.sha512) | Q(hash_sha1=hashes.sha1) + ) + .prefetch_related("cnc_map") + .order_by("created") + .all() + ) + + if not matched_hashes: + return None + + is_banned = next(iter([x for x in matched_hashes if x.cnc_map.is_banned])) + + if is_banned: + log_attrs = { + **self.user_log_attrs, + "map_file_id": is_banned.id, + "map_id": is_banned.cnc_map.id, + } + + _LOGGER.info("attempted_uploading_banned_map_file", **log_attrs) + + raise KirovyValidationError( + detail="This map file already exists", + code=UploadApiCodes.DUPLICATE_MAP, + additional={"existing_map_id": str(matched_hashes[0].cnc_map_id)}, + ) + + def verify_file_size_is_allowed(self, uploaded_file: UploadedFile) -> None: + """Check that the file is small enough, while also not being empty. + + :param uploaded_file: + The file from the API. + :return: + Nothing. No news is good news. + :raises KirovyValidationError: + Raised if the file is too big, or suspiciously small. + """ + uploaded_size = file_utils.ByteSized(uploaded_file.size) + if uploaded_size == file_utils.ByteSized(0): + raise KirovyValidationError( + detail="The uploaded file is empty", + code=UploadApiCodes.EMPTY_UPLOAD, + ) + if uploaded_size > settings.MAX_UPLOADED_FILE_SIZE_MAP: + raise KirovyValidationError( + detail="File too large", + code=UploadApiCodes.FILE_TO_LARGE, + additional={ + "max_bytes": str(settings.MAX_UPLOADED_FILE_SIZE_MAP), + "your_bytes": str(uploaded_file), + }, + ) + + +class MapFileUploadView(_BaseMapFileUploadView): + permission_classes = [permissions.CanUpload] + upload_is_temporary = False + + def get_game_id_from_request(self, request: KirovyRequest) -> str | None: + return request.data.get("game_id") + + +class CncnetClientMapUploadView(_BaseMapFileUploadView): + permission_classes = [AllowAny] + upload_is_temporary = True + + def get_game_id_from_request(self, request: KirovyRequest) -> str | None: + """Get the game ID for a CnCNet client upload. + + The client currently sends a slug in ``request.data["game"]``. The game table has a unique constraint + on :attr:`kirovy.models.cnc_game.CncGame.slug` and the slugs were copied from + `The legacy database `_ + We also added new games, like *Mental Omega*. Those slugs are defined in :file:`kirovy/migrations/0002_add_games.py` + + :param request: + :return: + The ID for the game corresponding to the slug from ``request.data["game"]`` + :raises KirovyValidationError: + Raised if we can't find a game matching the slug. + """ + game_slug = request.data.get("game") # TODO: get game_slug after updating cncnet client. + if not game_slug: + raise KirovyValidationError(detail="Game name must be provided.", code=UploadApiCodes.MISSING_GAME_SLUG) + + try: + game = CncGame.objects.get(slug__iexact=game_slug) + except ObjectDoesNotExist: + _LOGGER.warning( + "client.map_upload: User attempted to upload for game slug that does not exist", + attempted_slug=game_slug, + **self.user_log_attrs, + ) + raise KirovyValidationError( + detail="Game with that name does not exist", + code=UploadApiCodes.GAME_SLUG_DOES_NOT_EXIST, + additional={"attempted_slug": game_slug}, + ) + + return str(game.id) diff --git a/kirovy/views/permission_views.py b/kirovy/views/permission_views.py index 28b1b41..84fa1d4 100644 --- a/kirovy/views/permission_views.py +++ b/kirovy/views/permission_views.py @@ -20,7 +20,7 @@ class ListPermissionForAuthUser(APIView): ] def get(self, request: KirovyRequest, *args, **kwargs) -> KirovyResponse: - data = kirovy.objects.ui_objects.ResponseData( + data = kirovy.objects.ui_objects.ResultResponseData( result=kirovy.objects.ui_objects.UiPermissions.render_static(request, self) ) return KirovyResponse(data=data, status=status.HTTP_200_OK) diff --git a/requirements.txt b/requirements.txt index 0449c97..4937bb4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,3 +9,6 @@ ujson==5.* python-magic>=0.4.27 pydantic>=2.10,<3.0 gunicorn>=22.0.0,<23.0.0 +django-filter==25.1 +orjson>=3.10.15,<4.0 +structlog>=25.1.0,<26.0.0 diff --git a/tests/fixtures/common_fixtures.py b/tests/fixtures/common_fixtures.py index e1f46cb..f28e119 100644 --- a/tests/fixtures/common_fixtures.py +++ b/tests/fixtures/common_fixtures.py @@ -67,6 +67,9 @@ def tmp_media_root(tmp_path, settings): return settings.MEDIA_ROOT +_ClientReturnT = KirovyResponse | FileResponse + + class KirovyClient(Client): """A client wrapper with defaults I prefer. @@ -112,7 +115,7 @@ def set_active_user( self.kirovy_user = kirovy_user self.cncnet_user_info = cncnet_user_info - def request(self, **request) -> KirovyResponse | FileResponse: + def request(self, **request) -> _ClientReturnT: """Wraps request to mock the authenticate method to return our "active" user.""" with mock.patch("kirovy.authentication.CncNetAuthentication.authenticate") as mocked: if not self.kirovy_user: @@ -144,7 +147,7 @@ def post( follow=False, secure=False, **extra, - ): + ) -> _ClientReturnT: """Wraps post to make it default to JSON.""" data = self.__convert_data(data, content_type) @@ -175,7 +178,7 @@ def patch( follow=False, secure=False, **extra, - ): + ) -> _ClientReturnT: """Wraps patch to make it default to JSON.""" data = self.__convert_data(data, content_type) @@ -196,7 +199,7 @@ def put( follow=False, secure=False, **extra, - ): + ) -> _ClientReturnT: """Wraps put to make it default to JSON.""" data = self.__convert_data(data, content_type) @@ -332,7 +335,10 @@ def god(create_kirovy_user) -> CncUser: @pytest.fixture def client_anonymous(create_client) -> KirovyClient: - """Returns a client with a user that isn't signed in.""" + """Returns a client with a user that isn't signed in. + + This also simulates uploads via the CnCNet client. + """ return create_client(AnonymousUser()) diff --git a/tests/fixtures/file_fixtures.py b/tests/fixtures/file_fixtures.py index 0a81d5a..e365145 100644 --- a/tests/fixtures/file_fixtures.py +++ b/tests/fixtures/file_fixtures.py @@ -1,4 +1,6 @@ import pathlib +from collections.abc import Generator +from typing import Any from kirovy import typing as t @@ -16,9 +18,7 @@ def test_data_path() -> pathlib.Path: def load_test_file(test_data_path): """Return a function to load a file from test_data.""" - def _inner( - relative_path: t.Union[str, pathlib.Path], read_mode: str = "rb" - ) -> File: + def _inner(relative_path: t.Union[str, pathlib.Path], read_mode: str = "rb") -> File: """Load a file from test_data. :param relative_path: @@ -34,7 +34,7 @@ def _inner( @pytest.fixture -def file_binary(load_test_file) -> File: +def file_binary(load_test_file) -> Generator[File, Any, None]: """Returns a random binary file.""" file = load_test_file("binary_file.mp3", "rb") yield file @@ -42,7 +42,7 @@ def file_binary(load_test_file) -> File: @pytest.fixture -def file_map_valid(load_test_file) -> File: +def file_map_valid(load_test_file) -> Generator[File, Any, None]: """Returns a valid .map file that was made in Final Alert 2 and has a preview.""" file = load_test_file("test_ra2yr.map") yield file @@ -50,21 +50,21 @@ def file_map_valid(load_test_file) -> File: @pytest.fixture -def file_map_snow(load_test_file) -> File: +def file_map_snow(load_test_file) -> Generator[File, Any, None]: file = load_test_file("non_divisible_by_four.yrm") yield file file.close() @pytest.fixture -def file_map_desert(load_test_file) -> File: +def file_map_desert(load_test_file) -> Generator[File, None, None]: file = load_test_file("desert.map") yield file file.close() @pytest.fixture -def file_map_cant_parse(load_test_file) -> File: +def file_map_cant_parse(load_test_file) -> Generator[File, Any, None]: """Return a text file that is not valid INI.""" file = load_test_file("map_file_cant_parse_as_ini.map") yield file @@ -72,8 +72,16 @@ def file_map_cant_parse(load_test_file) -> File: @pytest.fixture -def file_map_missing_sections(load_test_file) -> File: +def file_map_missing_sections(load_test_file) -> Generator[File, Any, None]: """Return a valid INI file that is missing required map sections.""" file = load_test_file("map_file_missing_sections.map") yield file file.close() + + +@pytest.fixture +def file_map_unfair(load_test_file) -> Generator[File, Any, None]: + """Return a valid map that is very unfair and qualifies as a cheat map.""" + file = load_test_file("totally_fair_map.map") + yield file + file.close() diff --git a/tests/fixtures/map_fixtures.py b/tests/fixtures/map_fixtures.py index 583b743..bd29ea8 100644 --- a/tests/fixtures/map_fixtures.py +++ b/tests/fixtures/map_fixtures.py @@ -1,3 +1,4 @@ +from django.core.files import File from django.db.models import UUIDField from kirovy.models import CncGame, CncUser @@ -5,6 +6,9 @@ from kirovy import typing as t import pytest +from kirovy.services.cnc_gen_2_services import CncGen2MapParser, CncGen2MapSections +from kirovy.utils import file_utils + @pytest.fixture def create_cnc_map_category(db): @@ -37,7 +41,36 @@ def cnc_map_category(create_cnc_map_category) -> MapCategory: @pytest.fixture -def create_cnc_map(db, cnc_map_category, game_yuri, client_user): +def create_cnc_map_file(db, extension_map): + def _inner( + file: File, + cnc_map: CncMap, + ) -> CncMapFile: + map_parser = CncGen2MapParser(file) + map_file = CncMapFile( + width=map_parser.ini.get(CncGen2MapSections.HEADER, "Width"), + height=map_parser.ini.get(CncGen2MapSections.HEADER, "Height"), + file=file, + file_extension=extension_map, + cnc_game_id=cnc_map.cnc_game_id, + hash_md5=file_utils.hash_file_md5(file), + hash_sha512=file_utils.hash_file_sha512(file), + hash_sha1=file_utils.hash_file_sha1(file), + cnc_map_id=cnc_map.id, + ) + + map_file.save() + map_file.refresh_from_db() + # Reset the seek in case other fixtures, or the test, need to use the file. + # This also prevents issues from fixtures and tests using the same file fixture. + file.seek(0) + return map_file + + return _inner + + +@pytest.fixture +def create_cnc_map(db, cnc_map_category, game_yuri, client_user, create_cnc_map_file): """Return a function to create a CncMap object.""" def _inner( @@ -47,6 +80,11 @@ def _inner( map_categories: t.List[MapCategory] = None, user_id: t.Union[UUIDField, str, None, t.NO_VALUE] = t.NO_VALUE, is_legacy: bool = False, + is_published: bool = True, + is_banned: bool = False, + is_reviewed: bool = False, + is_temporary: bool = False, + file: File | None = None, ) -> CncMap: """Create a CncMap object. @@ -62,6 +100,19 @@ def _inner( :param user_id: The user who owns the map. ``None`` is a valid option. Defaults to the user from the :func:`~tests.fixtures.common_fixtures.client_user` fixture. + :param is_legacy: + If true, this is a map copied from the old map database. Has a potential to be garbage, or a holy relic. + :param is_published: + If true, the map author has decided to make their map publicly visible. + :param is_banned: + If true, the map has been banned from all list views. + :param is_reviewed: + If true, a staff member has reviewed this map. + :param is_temporary: + If true, then this map was uploaded by the CnCNet client, and is only visible to the client. + Will only be available through direct links for a limited time. + :param file: + A map file to include. Defaults to ``None`` for the sake of speed. :return: A ``CncMap`` object that can be used to create :class:`kirovy.models.cnc_map.CncMapFile` objects in tests. """ @@ -78,12 +129,19 @@ def _inner( map_name=map_name, is_legacy=is_legacy, cnc_user_id=user_id, + is_published=is_published, + is_banned=is_banned, + is_reviewed=is_reviewed, + is_temporary=is_temporary, ) cnc_map.save() cnc_map.categories.add(*map_categories) cnc_map.refresh_from_db() cnc_map.categories.prefetch_related() + if file: + create_cnc_map_file(file=file, cnc_map=cnc_map) + return cnc_map return _inner @@ -93,3 +151,13 @@ def _inner( def cnc_map(create_cnc_map) -> CncMap: """Convenience wrapper to make a CncMap for a test.""" return create_cnc_map() + + +@pytest.fixture +def banned_cheat_map(create_cnc_map, file_map_unfair) -> CncMap: + """A map cheat map that was uploaded via the CnCNet client, then banned.""" + return create_cnc_map( + is_banned=True, + is_temporary=True, + file=file_map_unfair, + ) diff --git a/tests/models/test_cnc_game.py b/tests/models/test_cnc_game.py index cd061dc..7e4e21a 100644 --- a/tests/models/test_cnc_game.py +++ b/tests/models/test_cnc_game.py @@ -16,7 +16,12 @@ def test_cnc_game_url(db, settings): @pytest.mark.parametrize( "extension,expect_error", - [("mp3", False), ("mix123}", True), (".mix", True), ("YRM", False)], + [ + ("mp3", False), + ("mix123}", True), # Errors because of the bracket. + (".mix", True), # Errors because the period is not alphanumeric. + ("urm", False), + ], ) def test_cnc_extension_validator(db, extension, expect_error): """Test creating extensions and the extension validator.""" diff --git a/tests/models/test_cnc_map.py b/tests/models/test_cnc_map.py index fbfbf70..71012f1 100644 --- a/tests/models/test_cnc_map.py +++ b/tests/models/test_cnc_map.py @@ -1,32 +1,24 @@ import pathlib import pytest -from django.core import validators -from kirovy.models.cnc_map import CncMapFile, CncMap, MapCategory +from kirovy.exceptions.view_exceptions import KirovyValidationError +from kirovy.models.cnc_map import CncMapFile -def test_cnc_map_invalid_file_extension( - game_yuri, extension_map, extension_mix, cnc_map -): +def test_cnc_map_invalid_file_extension(game_yuri, extension_map, extension_mix, cnc_map): """Test creating a map with a non-map file extension is rejected.""" - with pytest.raises(validators.ValidationError) as exc_info: - CncMapFile( - file_extension=extension_mix, cnc_game=game_yuri, cnc_map=cnc_map - ).save() + with pytest.raises(KirovyValidationError) as exc_info: + CncMapFile(file_extension=extension_mix, cnc_game=game_yuri, cnc_map=cnc_map).save() assert extension_mix.extension in str(exc_info.value) -def test_cnc_map_generate_upload_to( - game_yuri, extension_map, file_map_desert, cnc_map, settings -): +def test_cnc_map_generate_upload_to(game_yuri, extension_map, file_map_desert, cnc_map, settings): """Test that we generate the correct upload path for a map file. This test will fail if you alter the initial migrations. """ - settings.CNC_MAP_DIRECTORY = ( - "worlds" # Change default to check that the settings control the upload path. - ) + settings.CNC_MAP_DIRECTORY = "worlds" # Change default to check that the settings control the upload path. expected_path = pathlib.Path( settings.MEDIA_ROOT, "yr", diff --git a/tests/test_data/totally_fair_map.map b/tests/test_data/totally_fair_map.map new file mode 100644 index 0000000..fd8f543 --- /dev/null +++ b/tests/test_data/totally_fair_map.map @@ -0,0 +1,1802 @@ +; Map created with FinalAlert 2(tm) Mission Editor +; Get it at http://www.westwood.com +; note that all comments were truncated + +[Header] +Width=75 +Height=74 +StartX=218 +StartY=46 +Waypoint1=231,61 +Waypoint2=280,103 +Waypoint3=0,0 +Waypoint4=0,0 +Waypoint5=0,0 +Waypoint6=0,0 +Waypoint7=0,0 +Waypoint8=0,0 +NumberStartingPoints=2 + + +[Preview] +Size=0,0,160,80 + +[PreviewPack] +1=AQsAIAagpK2kqLGkqLFAAUsApKixSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQA +2=FIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEAB +3=SABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAU +4=gAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABBpCPiZOQh5WT +5=ikwBSABQAkgADKOnr5mXkJWUjpCOiMvKwydcAEABSABABEgAQAFIAEABSABAAUgAQAFIAE +6=ABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABA +7=AUgAQAFIAEABSABAAUgAQAEnCABYAUgAuAFAAUgAIOQsADNkB1AjIIRkB1wUSABcOLQAoA +8=Eg5y8AkpCKQHZYdkAiSABQI0gAQAEgjWQHrBa0AFAXQAFIAEABSABAAUgAQAFIAEABSABA +9=AUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQA +10=FIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEAB +11=SABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABrHYgpW +12=QHWBmwGlQASABYAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFI +13=AEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSA +14=BAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgA +15=QAFIAEABSABAAUgAQAFIAEABSABAASCxZAe8GrQAQBxAAUgAQAFIAEABSABAAUgAQAFIAE +16=ABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABA +17=AUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQA +18=FIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABIKs0FlgZUBpIAEAB +19=SACgAVQASABYAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAE +20=ABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABA +21=AUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQA +22=FIAEABSABAAUgAQAEgvWQHrBy0AFAdQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEAB +23=SABAAUkAgOAATAFQAkgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQA +24=FIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEAB +25=SABAAUgAQAFIAEABSABAAUgAQAEgw2QHWBywHVQASABYAUgAQAFIAEABSABAAUgAQAFIAE +26=ABSABAAUgAQAFIAOA73AAwXABMBFACSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgA +27=QAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAE +28=ABSABAAUgAQAFIAEABILecHUgbQBxIAEABSABAAUgAoAFUAEgAWAFIAEABSABAAUgAQAFI +29=AEABSABAAUgAQAFIAEABSABAAUgAVDsqLABYAScIACdcAEABSABQBUgAQAFIAEABSABAAU +30=gAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFI +31=AEABSABAAUgAQAFIAEABSABAAaU7lSykQlwCIJPUM1gWUBpIAEABSABAAUgAQAFIAEABSA +32=BAAUgAoAFUAEgAWAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIACdM +33=B6ABJ1wAQAFIAEAESABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAU +34=gAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABIMk0FrwdSB5A +35=AUgAoAFUAEgAWAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSA +36=BQOycsAEABWAGoAEwBSABABEgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABA +37=AUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAEPv7+7xMS8yce+tr +38=WwiIiEjoyFvAIgvcwOrBxYH6ABVABIAFgBSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABA +39=AUgAQAFIAEABSABAAUgAQAFIAEABuDotFABcAkgAQARIAEABSABAAUgAQAFIAEABSABAAU +40=gAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAQAD0M7F +41=3dzW1tfTxsS9xcS/sLCq0dHMXAIgyWQHvCC0AEAfQAFIAEABSABAAUgAQAFIAEABSABAAU +42=gAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIADBwB1ACSABABEgAQAFIAEABSABAAUgA +43=QAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAEnNx +44=bf39lYAVMCxsW/VDwgz2QHrB+0AFAgQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEAB +45=SABAAUgAQAFIAEABSABAAUgAQAFIAEABSABUPCosAFgBqABQAkgAQARIAEABSABAAUgAQA +46=FIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABJ2QHTAFIAFACSABA +47=ASDVZAesH7QAUCBAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQA +48=FIAEABSABAAUgAQAFIAEABSACsOjAUAEAESABABEgAQAFIAEABSABAAUgAQAFIAEABSABA +49=AUgAQAFIAEABSABAAUgAQAFIAEABJGwsIKvcWLwaSBtAAUgAQAFIAEABSABAAUgAQAFIAE +50=ABSABAAUgAQAFIAEABSABAAUgAoAFUAEgAWAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFI +51=AEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIACp8B1ACJzgATAFIAEAESA +52=BAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABNs8OlZSMIXN2lZSOIHt8dlwX +53=SABAGUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAU +54=gAQAFIAEABSACgAVQASABYAUgAQAFIAAikqLGkqLGAgICAgBEAAPMQACAHgICAgKSosaCk +55=rUsApKixSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFACj +56=AIAFwCSABABEgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAEGkpCKlZOKk5CHTAFIAFAC +57=SABAAQmVlIyQjoiVlI7Gxr9YAVACSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQA +58=FIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEAB +59=SABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAU +60=gAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAKABVABIAFgBSABAAUgALfwFJ1wAQAFI +61=AEAESABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAIB58ASAABm +62=QHXCygLlQASABYAUgAQAFIAEABSABUMCcsAEABSABQAkgAQAFIAEABSABAAUgAQAFIAEAB +63=SABAAUgAQwH/AACoAEwBSABQAkgAQAFIAEABSAAgEnwBA5SSjZORiKx2WAdIAFwIQAog7c +64=wOrCJYIqABVABIAFgBSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABA +65=AUgAQAFIAEABSABAAbA7JxQAUAJIAFwCSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAU +66=gAQAFIAEABSABDAY+DbUABSABMAVA7SAAg+WQHSCSgJVQASABYAUgAQAFIAEABSABAAUgA +67=QAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABXDtAAUgATAFIAE +68=ABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSAAJysi/y8i/mJCCmZeQ +69=UAIg/2QHSCRQJrQAoAEqLABIY6gAUAJIAEAESABAAUgAQAFIAEABSABAAUgAQAFIAEABSA +70=BAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgA +71=QAFIAKQRVABMAbQ8A5qRgZSTjSDkBCVMIkAlSABAAUgAQAFIAEABSACgAVQASABYAUgAQA +72=FYKEgATAFUAFgBQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEAB +73=SABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgATBAwCABfAsnFuC +74=eMAEABSABQAkgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEAB +75=SABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAU +76=gAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFI +77=AEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAoAFUAEgAWAFIAEABvCmsASc4AC +78=dQACdcAEABSABABEgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFI +79=AEABSABAAUgAQAFIAEABSABAAUgAQAGoDycUAFACSABcAkgAQAFIAEABSABAAUgAQAFIAE +80=ABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABA +81=AUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQA +82=FIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEAB +83=SABAAUgAQAFIAEABSABAAUgAoAFUAEgAWAFIAEABSABAAUgAJ0gFTAFQAic4AEwBSABQAk +84=gAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFI +85=AEABSABAAUgAQAFIAEABSABAAVQPQAFIAEwBSABAAUgAQAFIAEABSABAAUgAQAFIAEABSA +86=BAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgA +87=QAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAE +88=ABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABA +89=AUgAQAFIAEABSABAAUgAoAFUAEgAWAFIAEABSABAAUgAQAG0KlQASAAnOABAAUgAQARIAE +90=ABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABU +91=DCcIAFACSABQAkgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAE +92=ABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABA +93=AUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQA +94=FIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEAB +95=SABAAUgAQAFIAKABVABIAFgBSABAAUgAQAFIAEABSABAAawurAEnOAAnUAAnXABAAUgAQA +96=RIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAtAkwFABABEgAQARIAEABSABA +97=AUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQA +98=FIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEAB +99=SABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAU +100=gAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAKABVABIAFgBSABA +101=AUgAQAFIAEABSABAAUgAQAFIACf8BUwBUAInOABMAUgAUAJIAEABSABAAUgAQAFIAEABSA +102=BAAUgAQAFIAEABSABAAUgAQAFIAEABSAAnQAFAAVACSABQAkgAQAFIAEABSABAAUgAQAFI +103=AEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSA +104=BAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgA +105=QAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAE +106=ABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAKABVABIAFgBSABAAUgAQAFI +107=AEABSABAAUgAQAFIAEABpC9UAEgAJzgAQAFIAEAESABAAUgAQAFIAEABSABAAUgAQAFIAE +108=ABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABA +109=AUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQA +110=FIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEAB +111=SABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAU +112=gAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSACgAVQASABYAUgAQAFIAEABSABA +113=AUgAQAFIAEABSABAAUgAQAFQO0gArAFYAUABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSA +114=BAAUgATAeoAEwBUAJIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEAB +115=SABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAU +116=gAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFI +117=AEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSA +118=BAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAKABVABIAFgBSABAAUgAQAFIAEAB +119=SABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAU +120=gAuDotFABcAkAESABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgA +121=QAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAE +122=ABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABA +123=AUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQA +124=FIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAKABVABIAFgBSABAAUgAQAFIAEABSABAAUgA +125=QAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAEtcA +126=dQAkgAXAJIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgA +127=QAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAE +128=ABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABA +129=AUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQA +130=FIAEABSABAAUgAQAFIAEABSABAAUgAoAFUAEgAWAFIAEABSABAAUgAQAFIAEABSABAAUgA +131=QAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFUPE +132=ABSABMAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABA +133=AUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQA +134=FIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEAB +135=SABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAU +136=gAQAFIAEABSABAAUgAQAFIAEABSABAAUgAoAFUAEgAWAFIAEABSABAAUgAQAFIAEABSABA +137=AUgAQAFIAAqkqLGkqLGgpK2gpK2kEQAADgsAIAiosaSosaCkraCkrUABSABAAUgAQAFJAI +138=DgAEwBUAJIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgA +139=QAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAE +140=ABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABA +141=AUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQA +142=FIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSACgAVQASABYAUgA +143=QAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgA4DsrHA +144=BcAkAESABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEAB +145=SABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAU +146=gAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFI +147=AEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSA +148=BAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSACgAVQASABYAUgAQAFIAEAB +149=SABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAEGiYeAl5WMk5KNTAFIAFACK2oHgI +150=BQAkgAXAJIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgA +151=QAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAE +152=ABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABA +153=AUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQA +154=FIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAKABVABIAFgBSABAAUgA +155=QAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABBpCPiZOQh5WTikwBSABTAo6OiUABSA +156=BMAVQ8QAFIAEwBSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgA +157=QAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAE +158=ABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABA +159=AUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQA +160=FIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAoAFUAEgA +161=WAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAASpkB1w4qABcAgaUk4yPjol/d2 +162=pMAUgAUAVIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgA +163=QAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAE +164=ABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABA +165=AUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQA +166=FIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAoAFUAEgA +167=WAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABKmQHWAFQAkgAXDsJlZSMkI6IlZSOyM +168=fAJ1wAQAFIAEAESABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgA +169=QAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAE +170=ABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABA +171=AUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQA +172=FIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAoAFUAEgA +173=WAFIAEABSABAAUgAQAFIAEABSABAAUgAQAEzZAdcAidnB8bGv1gBUAVIAEABSABAAUgAQA +174=FIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEAB +175=SABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAU +176=gAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFI +177=AEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSA +178=BAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSACgAVQASABYAUgAQAFIAEAB +179=SABAAUgAQAFIAEMBkpCKUOxIAEwBSABQAkgAQAEgAHhkB0gzoDRUAEgAWAFIAEABSABAAU +180=gAQAFIAEABD7+/u8TEvMnHvra1sIiIhI6Mhb8CmZeQSLFIACAAb8wOSDZANEgAoAFUAEgA +181=WAFIAEABSABAAUgAQAEAA9DOxd3c1tbX08bEvcXEv7CwqtHRzFwCJ2cHy8rDWAFQBUgAQA +182=FIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEAB +183=SABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAU +184=gAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFI +185=AEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSA +186=BAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgA +187=QAFIAKABVABIAFgBSABAAUgAQAG0sQOTkIff39lYAVMCxsW/VDwgAIpkB0g2oDdUAEgAWA +188=FIAEABJ2QHTAFIAFACSABAASAAkGQHSDagN1QASABYASAArmQHrDq0AKQ7MGwsIACWzA6s +189=OkB2YAECo6ynqrNc7FgBSABEAkgAQAEgAKJkB7w4tABAOkABSABAASc8OyAAgWwsvDVINk +190=ABSABAAUgAQAFIAEABSACgAVQASABYASfMDiAAezw7WDRQNUgAQAFIAEABSABAAUgAQAFI +191=AEABSABAAUgAoAFUACdkByAwzA4RAABmCQAgBqSosaCkraCkrUABSwCgpK1IAEABSABAAU +192=gAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFI +193=AEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSA +194=BAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgA +195=QAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAE +196=ABSABAAUgAQAFIAEABSABAAUgAQAFIAEABCZWUjJCOiJWUjsvKw6QCUAJAAUgAQAFIAEAB +197=SABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAU +198=gAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFI +199=AEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSA +200=BAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgA +201=QAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAE +202=ABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAowHGxr+gAaQCIACuLAAgAK40BzCE +203=DjBEACAArsQHIACuNAcwtA4wRAAgAK7EByAArjQHMLQOMEQAIACuxAcgAK40BzC0DjBEAC +204=AArsQHIABdNwf/AAAnCABAMUgAWG1IAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABA +205=AUgAQAFIAEABSABAAUgAQAFIAKABVABIAFgBSABAAUgAQAFIAEABSABAAUgAQAFIAEABSA +206=BAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgA +207=QAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAE +208=ABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABA +209=AUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQA +210=FIAEABSABAAUgAQAG8O6wBWAFAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEAB +211=SABAAUgAQAFIAEABSABAAUgAoAFUAEgAWAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAE +212=ABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABA +213=AUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQA +214=FIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEAB +215=SABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAU +216=gAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFI +217=AEABSABAAUgAQAFIAEABSABAAUgAoAFUAEgAWAFIAEABSABAAUgAQAFIAEABSABAAUgAQA +218=FIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEAB +219=SABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAU +220=gAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFI +221=AEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSA +222=BAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgA +223=QAFIAEABSABAAUgAQAFIAEABSABAAUgAoAFUAEgAWAFIAEABSABAAUgAQAFIAEABSABAAU +224=gAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFI +225=AEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSA +226=BAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgA +227=QAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAE +228=ABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABA +229=AUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAoAFUAEgAWAFIAEABSABAAUgAQAFIAEABSA +230=BAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgA +231=QAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAE +232=ABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABA +233=AUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQA +234=FIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEAB +235=SABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAoAFUAEgAWAFIAEABSABAAUgAQAFIAE +236=ABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABA +237=AUgACKCkraCkraSosaSoEQAA6gwAFgexoKStoKStpKixSAAgADMsACAANlQFQFVAK0gAQA +238=FIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEAB +239=SABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAoAFUAEgAWAFIAEABSABAAUgAQAFIAE +240=ABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABA +241=AUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQA +242=FIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEAB +243=SABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAU +244=gAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFI +245=AEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAoAFUAEgAWAFIAEABSABAAUgAQA +246=FIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEAB +247=SABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAU +248=gAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFI +249=AEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSA +250=BAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgA +251=QAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAoAFUAEgAWAFIAEABSABAAU +252=gAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFI +253=AEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSA +254=BAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgA +255=QAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAE +256=ABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABA +257=AUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAoAFUAEgAWAFIAEABSA +258=BAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgA +259=QAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAE +260=ABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABA +261=AUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQA +262=FIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEAB +263=SABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAoAFUAEgAWAFIAE +264=ABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABA +265=AUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQA +266=FIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEAB +267=SABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAU +268=gAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFI +269=AEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAoAFUAEgAWA +270=FIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEAB +271=SABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAU +272=gAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFI +273=AEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSA +274=BAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgA +275=QAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAoAFUAE +276=gAWAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFI +277=AEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSA +278=BAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgA +279=QAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAE +280=ABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABA +281=AUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAoA +282=FUAEgAWAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgA +283=QAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAE +284=ABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABA +285=AUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQA +286=FIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEAB +287=SABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAU +288=gAoAFUAEgAWAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABA +289=AUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQA +290=FIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEAB +291=SABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAU +292=gAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFI +293=AEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSA +294=BAAUgAQwH///9UAEgAWAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFI +295=AEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSA +296=BAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgA +297=QAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAE +298=ABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABA +299=AUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQAFIAEABSABAAUgAQA +300=FIAEABCaSosaCkraCkraSosREAAA== + +[AITriggerTypesEnable] +043F40BC-G=yes +043F521C-G=yes +043F874C-G=yes +044082BC-G=yes +05CB5B6C-G=yes +05CB830C-G=yes +05CBAEDC-G=yes +05CBC04C-G=yes +05CBC19C-G=yes +05CBC2EC-G=yes +05FC088C-G=yes +05FC0C9C-G=yes +05FC13EC-G=yes +05FC1A8C-G=yes +05FC1C5C-G=yes +05FC333C-G=yes +05FC37CC-G=yes +05FC3C9C-G=yes +05FC50EC-G=yes +05FC747C-G=yes +05FC75CC-G=yes +05FFA7DC-G=yes +05FFB18C-G=yes +06002BFC-G=yes +0611A1EC-G=yes +0611A33C-G=yes +0612B11C-G=yes +0612CD8C-G=yes +0612EAEC-G=yes +0612F46C-G=yes +0612F9AC-G=yes +0616004C-G=yes +06C551AC-G=yes +078F4B2C-G=yes +07C04C7C-G=yes +07C04EDC-G=yes +07C0B14C-G=yes +07C30A5C-G=yes +07C310BC-G=yes +07C3266C-G=yes +07C329FC-G=yes +07C330CC-G=yes +07C33DDC-G=yes +07C4853C-G=yes +08A3EEDC-G=yes +08B902EC-G=yes +08B90B1C-G=yes +08B911BC-G=yes +08B9154C-G=yes +08B9213C-G=yes +08B9305C-G=yes +08B9356C-G=yes +08B940EC-G=yes +08B9423C-G=yes +08B9483C-G=yes +08B9586C-G=yes +08B9641C-G=yes +08B9708C-G=yes +08B9767C-G=yes +08B97C6C-G=yes +0A31295C-G=yes +0A44F17C-G=yes +0A44F2CC-G=yes +0A452C6C-G=yes +0A452E5C-G=yes +0AA845AC-G=yes +0AA846FC-G=yes +0ABC484C-G=yes +0AD2BAEC-G=yes +0C0C7AEC-G=yes +0C0D184C-G=yes +0C0D251C-G=yes +0C1245AC-G=yes +0C1246FC-G=yes +0C32599C-G=yes +0C32D84C-G=yes +0C87B45C-G=yes +0C87B99C-G=yes +0C87E5AC-G=yes +0C8B81BC-G=yes +0C8BC5AC-G=yes +0C8C2AEC-G=yes +0C8C2C3C-G=yes +0C8C430C-G=yes +0C8C4C3C-G=yes +0C8C51BC-G=yes +0C8C56FC-G=yes +0C8C645C-G=yes +0C8C6EDC-G=yes +0C8C71BC-G=yes +0C8E2C3C-G=yes +0C8EE1BC-G=yes +0C8EEAEC-G=yes +0C8EEEDC-G=yes +0C91D45C-G=yes +0C92C1BC-G=yes +0C92C45C-G=yes +0C955AEC-G=yes +0C955C3C-G=yes +0C9561BC-G=yes +0C9565AC-G=yes +0C96F45C-G=yes +0C96F5AC-G=yes +0CA1F84C-G=yes +0CA24D8C-G=yes +0CA2A04C-G=yes +0CA2B06C-G=yes +0CA2FEDC-G=yes +0CA5945C-G=yes +0CA6621C-G=yes +0CA6AEDC-G=yes +0CAD0B2C-G=yes +0CAD0C7C-G=yes +0CAD0DCC-G=yes +0CAD130C-G=yes +0CAD145C-G=yes +0CAD25AC-G=yes +0CAD2AEC-G=yes +0CAD2C3C-G=yes +0CAD5AEC-G=yes +0CADD99C-G=yes +0CADDEDC-G=yes +0CB29EDC-G=yes +0CB2CEDC-G=yes +0CDF899C-G=yes +0CDF8D8C-G=yes +0CDF8EDC-G=yes +0CE2395C-G=yes +0CE23AAC-G=yes +0CE26B5C-G=yes +0CE26CAC-G=yes +0CE2E8DC-G=yes +0CE2EA2C-G=yes +0CE44D0C-G=yes +0D022B1C-G=yes +0D052AEC-G=yes +0D0803CC-G=yes +0D08461C-G=yes +0D0848BC-G=yes +0D084A0C-G=yes +0D084B5C-G=yes +0D0873DC-G=yes +0D087D8C-G=yes +0D0B3B9C-G=yes +0D2769BC-G=yes +0D2C41EC-G=yes +0D2C448C-G=yes +0D2C45DC-G=yes +0D53099C-G=yes +0D53430C-G=yes +0D535EDC-G=yes +0D62145C-G=yes +0D6215AC-G=yes +0D6216FC-G=yes +0D62199C-G=yes +0ECCB2BC-G=yes +0ECCB40C-G=yes +0ECCB55C-G=yes +0ECCB7FC-G=yes +0ECCBC3C-G=yes +0ECCF14C-G=yes +0ECCF29C-G=yes +0ECCF68C-G=yes +0ECCFA7C-G=yes +0ECCFCAC-G=yes + +[Basic] +Name=Totally Fair Map +Percent=0 +GameMode=standard, meatgrind, teamgame +HomeCell=98 +InitTime=10000 +Official=no +EndOfGame=no +FreeRadar=no +MaxPlayer=2 +MinPlayer=2 +SkipScore=no +TrainCrate=no +TruckCrate=no +AltHomeCell=99 +OneTimeOnly=no +CarryOverCap=0 +NewINIFormat=4 +NextScenario= +SkipMapSelect=no +CarryOverMoney=0.000000 +AltNextScenario= +MultiplayerOnly=1 +IceGrowthEnabled=yes +VeinGrowthEnabled=yes +TiberiumGrowthEnabled=yes +IgnoreGlobalAITriggers=no +TiberiumDeathToVisceroid=no + +[CellTags] + +[Chinese] +IQ=0 +Edge=North +Color=SovietLoad +Allies=Chinese +Country=Chinese +Credits=0 +NodeCount=0 +TechLevel=1 +PercentBuilt=0 +PlayerControl=no + +[Europeans] +IQ=0 +Edge=North +Color=AlliedLoad +Allies=Europeans +Country=Europeans +Credits=0 +NodeCount=0 +TechLevel=1 +PercentBuilt=0 +PlayerControl=no + +[Guild1] +IQ=0 +Edge=North +Color=Teal +Allies=Guild1 +Country=Guild1 +Credits=0 +NodeCount=0 +TechLevel=1 +PercentBuilt=0 +PlayerControl=no + +[Guild2] +IQ=0 +Edge=North +Color=Teal +Allies=Guild2 +Country=Guild2 +Credits=0 +NodeCount=0 +TechLevel=1 +PercentBuilt=0 +PlayerControl=no + +[Guild3] +IQ=0 +Edge=North +Color=Teal +Allies=Guild3 +Country=Guild3 +Credits=0 +NodeCount=0 +TechLevel=1 +PercentBuilt=0 +PlayerControl=no + +[Headquaters] +IQ=0 +Edge=North +Color=Purple3 +Allies=Headquaters +Country=Headquaters +Credits=0 +NodeCount=0 +TechLevel=1 +PercentBuilt=0 +PlayerControl=no + +[Houses] +0=UnitedStates +1=Europeans +2=Pacific +3=USSR +4=Latin +5=Chinese +6=PsiCorps +7=ScorpionCell +8=Headquaters +9=Guild1 +10=Guild2 +11=Guild3 +12=Neutral +13=Special + +[IsoMapPack5] +1=5gsAIAlQAAEA//8AAAACAE9MAAH//wAAWQBQTADJAVEoKAABTgADANUCTygpAFAoKQBRKC +2=kAUigrAE0ABPkGTigpAE8oKQBQKCkAUSgpAFIoKQBTKCsATAAF8QlNKCkATigpAE8oKQBQ +3=KCkAUSgpAFIoKQBTKCkAVCgrAEsABukMTCgpAE0oKQBOKCkATygpAFAoKQBRKCkAUigpAF +4=MoKQBUKCkAVSgrAEoAB+EPSygpAEwoKQBNKCkATigpAE8oKQBQKCkAUSgpAFIoKQBTKCkA +5=VCgpAFUoKQBWKCsASQAI+RFKKCkASygpAEwoKQBNKCkATigpAE8oKQBQKCkAUSgpAFIoKQ +6=BTKCkAVCgpAFUoKQBWKCkAVygrAEgACfEUSSgpAEooKQBLKCkATCgpAE0oKQBOKCkATygp +7=AFAoKQBRKCkAUigpAFMoKQBUKCkAVSgpAFYoKQBXKCkAWCgrAEcACukXSCgpAEkoKQBKKC +8=kASygpAEwoKQBNKCkATigpAE8oKQBQKCkAUSgpAFIoKQBTKCkAVCgpAFUoKQBWKCkAVygp +9=AFgoKQBZKCsARgAL4RpHKCkASCgpAEkoKQBKKCkASygpAEwoKQBNKCkATigpAE8oKQBQKC +10=kAUSgpAFIoKQBTKCkAVCgpAFUoKQBWKCkAVygpAFgoKQBZKCkAWigrAEUADPkcRigpAEco +11=KQBIKCkASSgpAEooKQBLKCkATCgpAE0oKQBOKCkATygpAFAoKQBRKCkAUigpAFMoKQBUKC +12=kAVSgpAFYoKQBXKCkAWCgpAFkoKQBaKCkAWygrAEQADfEfRSgpAEYoKQBHKCkASCgpAEko +13=KQBKKCkASygpAEwoKQBNKCkATigpAE8oKQBQKCkAUSgpAFIoKQBTKCkAVCgpAFUoKQBWKC +14=kAVygpAFgoKQBZKCkAWigpAFsoKQBcKCsAQwAO6SJEKCkARSgpAEYoKQBHKCkASCgpAEko +15=KQBKKCkASygpAEwoKQBNKCkATigpAE8oKQBQKCkAUSgpAFIoKQBTKCkAVCgpAFUoKQBWKC +16=kAVygpAFgoKQBZKCkAWigpAFsoKQBcKCkAXSgrAEIAD+ElQygpAEQoKQBFKCkARigpAEco +17=KQBIKCkASSgpAEooKQBLKCkATCgpAE0oKQBOKCkATygpAFAoKQBRKCkAUigpAFMoKQBUKC +18=kAVSgpAFYoKQBXKCkAWCgpAFkoKQBaKCkAWygpAFwoKQBdKCkAXigrAEEAEPknQigpAEMo +19=KQBEKCkARSgpAEYoKQBHKCkASCgpAEkoKQBKKCkASygpAEwoKQBNKCkATigpAE8oKQBQKC +20=kAUSgpAFIoKQBTKCkAVCgpAFUoKQBWKCkAVygpAFgoKQBZKCkAWigpAFsoKQBcKCkAXSgp +21=AF4oKQBfKCsAQAAR8SpBKCkAQigpAEMoKQBEKCkARSgpAEYoKQBHKCkASCgpAEkoKQBKKC +22=kASygpAEwoKQBNKCkATigpAE8oKQBQKCkAUSgpAFIoKQBTKCkAVCgpAFUoKQBWKCkAVygp +23=AFgoKQBZKCkAWigpAFsoKQBcKCkAXSgpAF4oKQBfKCkAYCgrAD8AEuktQCgpAEEoKQBCKC +24=kAQygpAEQoKQBFKCkARigpAEcoKQBIKCkASSgpAEooKQBLKCkATCgpAE0oKQBOKCkATygp +25=AFAoKQBRKCkAUigpAFMoKQBUKCkAVSgpAFYoKQBXKCkAWCgpAFkoKQBaKCkAWygpAFwoKQ +26=BdKCkAXigpAF8oKQBgKCkAYSgrAD4AE+EwPygpAEAoKQBBKCkAQigpAEMoKQBEKCkARSgp +27=AEYoKQBHKCkASCgpAEkoKQBKKCkASygpAEwoKQBNKCkATigpAE8oKQBQKCkAUSgpAFIoKQ +28=BTKCkAVCgpAFUoKQBWKCkAVygpAFgoKQBZKCkAWigpAFsoKQBcKCkAXSgpAF4oKQBfKCkA +29=YCgpAGEoKQBiKCsAPQAU+TI+KCkAPygpAEAoKQBBKCkAQigpAEMoKQBEKCkARSgpAEYoKQ +30=BHKCkASCgpAEkoKQBKKCkASygpAEwoKQBNKCkATigpAE8oKQBQKCkAUSgpAFIoKQBTKCkA +31=VCgpAFUoKQBWKCkAVygpAFgoKQBZKCkAWigpAFsoKQBcKCkAXSgpAF4oKQBfKCkAYCgpAG +32=EoKQBiKCkAYygrADwAFfE1PSgpAD4oKQA/KCkAQCgpAEEoKQBCKCkAQygpAEQoKQBFKCkA +33=RigpAEcoKQBIKCkASSgpAEooKQBLKCkATCgpAE0oKQBOKCkATygpAFAoKQBRKCkAUigpAF +34=MoKQBUKCkAVSgpAFYoKQBXKCkAWCgpAFkoKQBaKCkAWygpAFwoKQBdKCkAXigpAF8oKQBg +35=KCkAYSgpAGIoKQBjKCkAZCgrADsAFuk4PCgpAD0oKQA+KCkAPygpAEAoKQBBKCkAQigpAE +36=MoKQBEKCkARSgpAEYoKQBHKCkASCgpAEkoKQBKKCkASygpAEwoKQBNKCkATigpAE8oKQBQ +37=KCkAUSgpAFIoKQBTKCkAVCgpAFUoKQBWKCkAVygpAFgoKQBZKCkAWigpAFsoKQBcKCkAXS +38=gpAF4oKQBfKCkAYCgpAGEoKQBiKCkAYygpAGQoKQBlKCsAOgAX4Ts7KCkAPCgpAD0oKQA+ +39=KCkAPygpAEAoKQBBKCkAQigpAEMoKQBEKCkARSgpAEYoKQBHKCkASCgpAEkoKQBKKCkASy +40=gpAEwoKQBNKCkATigpAE8oKQBQKCkAUSgpAFIoKQBTKCkAVCgpAFUoKQBWKCkAVygpAFgo +41=KQBZKCkAWigpAFsoKQBcKCkAXSgpAF4oKQBfKCkAYCgpAGEoKQBiKCkAYygpAGQoKQBlKC +42=kAZigrADkAGPk9OigpADsoKQA8KCkAPSgpAD4oKQA/KCkAQCgpAEEoKQBCKCkAQygpAEQo +43=KQBFKCkARigpAEcoKQBIKCkASSgpAEooKQBLKCkATCgpAE0oKQBOKCkATygpAFAoKQBRKC +44=kAUigpAFMoKQBUKCkAVSgpAFYoKQBXKCkAWCgpAFkoKQBaKCkAWygpAFwoKQBdKCkAXigp +45=AF8oKQBgKCkAYSgpAGIoKQBjKCkAZCgpAGUoKQBmKCkAZygrADgAGfFAOSgpADooKQA7KC +46=kAPCgpAD0oKQA+KCkAPygpAEAoKQBBKCkAQigpAEMoKQBEKCkARSgpAEYoKQBHKCkASCgp +47=AEkoKQBKKCkASygpAEwoKQBNKCkATigpAE8oKQBQKCkAUSgpAFIoKQBTKCkAVCgpAFUoKQ +48=BWKCkAVygpAFgoKQBZKCkAWigpAFsoKQBcKCkAXSgpAF4oKQBfKCkAYCgpAGEoKQBiKCkA +49=YygpAGQoKQBlKCkAZigpAGcoKQBoKCsANwAa6UM4KCkAOSgpADooKQA7KCkAPCgpAD0oKQ +50=A+KCkAPygpAEAoKQBBKCkAQigpAEMoKQBEKCkARSgpAEYoKQBHKCkASCgpAEkoKQBKKCkA +51=SygpAEwoKQBNKCkATigpAE8oKQBQKCkAUSgpAFIoKQBTKCkAVCgpAFUoKQBWKCkAVygpAF +52=goKQBZKCkAWigpAFsoKQBcKCkAXSgpAF4oKQBfKCkAYCgpAGEoKQBiKCkAYygpAGQoKQBl +53=KCkAZigpAGcoKQBoKCkAaSgoAAk2ABsAYQUAAAECADdIASWpczgoKQA5KCkAOigpADsoKQ +54=A8KCkAPSgpAD4oKQA/KCkAQCgpAEEoKQBCKCkAQygpAEQoKQBFKCkARigpAEcoKQBIKCkA +55=SSgpAEooKQBLKCkATCgpAE0oKQBOKCkATygpAFAoKQBRKCkAUigpAFMoKQBUKCkAVSgpAF +56=YoKQBXKCkAWCgpAFkoKQBaKCkAWygpAFwoKQBdKCkAXigpAF8oKQBgKCkAYSgpAGIoKQBj +57=KCkAZCgpAGUoKQBmKCkAZygpAGgoKQBpKCkAaigrADUAHJhIAQIGADbJAQNlShwoNBInKQ +58=A5QATdTDooKQA7KCkAPCgpAD0oKQA+KCkAPygpAEAoKQBBKCkAQigpAEMoKAAFRAAcAP// +59=AAARAABBDAAgCAACAEUAHAD//wAASAEBRgAcAMkBRygpAEgoKQBJKCkASigpAEsoKQBMKC +60=kATSgpAE4oKQBPKCkAUCgpAFEoKQBSKCkAUygpAFQoKQBVKCkAVigpAFcoKQBYKCkAWSgp +61=AFooKQBbKCkAXCgpAF0oKQBeKCkAXygpAGAoKQBhKCkAYigpAGMoKQBkKCkAZSgpAGYoKQ +62=BnKCkAaCgpAGkoKQBqKCkAaygoAAI0AB0AJUw1AQAFADVKAWEFTwEGADbIAQEBAgA3SAHJ +63=ODgoKQA5KCkAOigpADsoKQA8KCkAPSgpAD4oKQA/KCkAQCgpAEEoKQBCKCkAQygpAEQoKQ +64=BFKCkARigpAEcoKQBIKCkASSgpAEooKQBLKCkATCgpAE0oKQBOKCkATygpAFAoKQBRKCkA +65=UigpAFMoKQBUKCkAVSgpAFYoKQBXKCkAWCgpAFkoKQBaKCkAWygpAFwoKQBdKCkAXigpAF +66=8oKQBgKCkAYSgpAGIoKQBjKCkAZCgpAGUoKQBmKCkAZygpAGgoKQBpKCkAaigpAGsoKQBs +67=KCsAMwAeUExLAAYANCgpADVIAXVPAnZPHgBpAQN1Tx7hiDhABMFROSgpADooKQA7KCkAPC +68=gpAD0oKQA+KCkAPygpAEAoKQBBKCkAQigpAEMoKQBEKCkARSgpAEYoKQBHKCkASCgpAEko +69=KQBKKCkASygpAEwoKQBNKCkATigpAE8oKQBQKCkAUSgpAFIoKQBTKCkAVCgpAFUoKQBWKC +70=kAVygpAFgoKQBZKCkAWigpAFsoKQBcKCkAXSgpAF4oKQBfKCkAYCgpAGEoKQBiKCkAYygp +71=AGQoKQBlKCkAZigpAGcoKQBoKCkAaSgpAGooKQBrKCkAbCgpAG0oKwAyAB9UUEsABgAzSA +72=GQS2xSJ1UANVQCYVEAbVIfjFKFoh8oTAonKQA5TAWHCAIAOigpADsoKQA8KCkAPSgpAD4o +73=KQA/KCkAQCgpAEEoKQBCKCkAQygpAEQoKQBFKCkARigpAEcoKQBIKCkASSgpAEooKQBLKC +74=kATCgpAE0oKQBOKCkATygpAFAoKQBRKCkAUigpAFMoKQBUKCkAVSgpAFYoKQBXKCkAWCgp +75=AFkoKQBaKCkAWygpAFwoKQBdKCkAXigpAF8oKQBgKCkAYSgpAGIoKQBjKCkAZCgpAGUoKQ +76=BmKCkAZygpAGgoKQBpKCkAaigpAGsoKQBsKCkAbSgpAG4oKAACMQAgAHa98TJIAV1UAEFR +77=M0gBnExlVSCQAlUCNVQCZFWVpyCEVZWnICikCicpADlMBYQIZFUnVQA7KFUAPCgpAD0oKQ +78=A+KCkAPygpAEAoKQBBKCkAQigpAEMoKQBEKCkARSgpAEYoKQBHKCkASCgpAEkoKQBKKCkA +79=SygpAEwoKQBNKCkATigpAE8oKQBQKCkAUSgpAFIoKQBTKCkAVCgpAFUoKQBWKCkAVygpAF +80=goKQBZKCkAWigpAFsoKQBcKCkAXSgpAF4oKQBfKCkAYCgpAGEoKQBiKCkAYygpAGQoKQBl +81=KCkAZigpAGcoKQBoKCkAaSgpAGooKQBrKCkAbCgpAG0oKQBuKCkAbygrADAAIelOMUgBfF +82=cBAQYAMkgBiFVlrSGEAVFWNChVADVJAWGtXTbIAYStJzEBOFQC0VY5KCkAOigpADsoKQA8 +83=KCkAPSgpAD4oKQA/KCkAQCgpAEEoKQBCKCkAQygpAEQoKQBFKCkARigpAEcoKQBIKCkASS +84=gpAEooKQBLKCkATCgpAE0oKQBOKCkATygpAFAoKQBRKCkAUigpAFMoKQBUKCkAVSgpAFYo +85=KQBXKCkAWCgpAFkoKQBaKCkAWygpAFwoKQBdKCkAXigpAF8oKQBgKCkAYSgpAGIoKQBjKC +86=kAZCgpAGUoKQBmKCkAZygpAGgoKQBpKCkAaigpAGsoKQBsKCkAbSgpAG4oKQBvKCkAcCgr +87=AC8AIulZMCgpADFIAXRaXVcySAGMWXVaIoQBVQI0KFUANUgBdFqVsiIoVBYnXQE4QAQnVA +88=snVQA6KFUAOygpADwoKQA9KCkAPigpAD8oKQBAKCkAQSgpAEIoKQBDKCkARCgpAEUoKQBG +89=KCkARygpAEgoKQBJKCkASigpAEsoKQBMKCkATSgpAE4oKQBPKCkAUCgpAFEoKQBSKCkAUy +90=gpAFQoKQBVKCkAVigpAFcoKQBYKCkAWSgpAFooKQBbKCkAXCgpAF0oKQBeKCkAXygpAGAo +91=KQBhKCkAYigpAGMoKQBkKCkAZSgpAGYoKQBnKCkAaCgpAGkoKQBqKCkAaygpAGwoKQBtKC +92=kAbigpAG8oKQBwKCkAcSgrAC4AI+FcLygpADAoKQAxSAFtXQFluCPhXDNUAowBIgQiJ1UA +93=NVUCYeW4I4xdhLgniQE4QAQnrAsnVQA6KFUAOygpADwoKQA9KCkAPigpAD8oKQBAKCkAQS +94=gpAEIoKQBDKCkARCgpAEUoKQBGKCkARygpAEgoKQBJKCkASigpAEsoKQBMKCkATSgpAE4o +95=KQBPKCkAUCgpAFEoKQBSKCkAUygpAFQoKQBVKCkAVigpAFcoKQBYKCkAWSgpAFooKQBbKC +96=kAXCgpAF0oKQBeKCkAXygpAGAoKQBhKCkAYigpAGMoKQBkKCkAZSgpAGYoKQBnKCkAaCgp +97=AGkoKQBqKCkAaygpAGwoKQBtKCkAbigpAG8oKQBwKCkAcSgpAHIoKwAtACT5Xi4oKQAvKC +98=kAMCgpADFIAScFDCSUXkm8M1QCjAFkYCdVADVUAmRglb0khGAjDC4ntQE4QAQnBAwnVQA6 +99=KFUAOygpADwoKQA9KCkAPigpAD8oKQBAKCkAQSgpAEIoKQBDKCkARCgpAEUoKQBGKCkARy +100=gpAEgoKQBJKCkASigpAEsoKQBMKCkATSgpAE4oKQBPKCkAUCgpAFEoKQBSKCkAUygpAFQo +101=KQBVKCkAVigpAFcoKQBYKCkAWSgpAFooKQBbKCkAXCgpAF0oKQBeKCkAXygpAGAoKQBhKC +102=kAYigpAGMoKQBkKCkAZSgpAGYoKQBnKCkAaCgpAGkoKQBqKCkAaygpAGwoKQBtKCkAbigp +103=AG8oKQBwKCkAcSgpAHIoKQBzKCsALAAl8WEtKCkALigpAC8oKQAwKCkAMUgBfGJRYTJIAd +104=FhMygpADQoKQA1SAF8Yk0FNsgBhMMn4QE4VAInXAwnVQA6KFUAOygpADwoKQA9KCkAPigp +105=AD8oKQBAKCkAQSgpAEIoKQBDKCkARCgpAEUoKQBGKCkARygpAEgoKQBJKCkASigpAEsoKQ +106=BMKCkATSgpAE4oKQBPKCkAUCgpAFEoKQBSKCkAUygpAFQoKQBVKCkAVigpAFcoKQBYKCkA +107=WSgpAFooKQBbKCkAXCgpAF0oKQBeKCkAXygpAGAoKQBhKCkAYigpAGMoKQBkKCkAZSgpAG +108=YoKQBnKCkAaCgpAGkoKQBqKCkAaygpAGwoKQBtKCkAbigpAG8oKQBwKCkAcSgpAHIoKQBz +109=KCkAdCgrACsAJulkLCgpAC0oKQAuKCkALygpADAoKQAxSAF1ZQEiHSUmkGVRYTNUAowBdM +110=gnVQA1VAIBYQUAAJXIJigUGScNAjhABCe0DCdVADooVQA7KCkAPCgpAD0oKQA+KCkAPygp +111=AEAoKQBBKCkAQigpAEMoKQBEKCkARSgpAEYoKQBHKCkASCgpAEkoKQBKKCkASygpAEwoKQ +112=BNKCkATigpAE8oKQBQKCkAUSgpAFIoKQBTKCkAVCgpAFUoKQBWKCkAVygpAFgoKQBZKCkA +113=WigpAFsoKQBcKCkAXSgpAF4oKQBfKCkAYCgpAGEoKQBiKCkAYygpAGQoKQBlKCkAZigpAG +114=coKQBoKCkAaSgpAGooKQBrKCkAbCgpAG0oKQBuKCkAbygpAHAoKQBxKCkAcigpAHMoKQB0 +115=KCkAdSgrACoAJ+FnKygpACwoKQAtKCkALigpAC8oKQAwKCkAMUgBJw0NJ5xmjGgnKQA0QA +116=SbAgYANUgBbGhBBDbIAYTOJzkCOFQCJwwNJ1UAOihVADsoKQA8KCkAPSgpAD4oKQA/KCkA +117=QCgpAEEoKQBCKCkAQygpAEQoKQBFKCkARigpAEcoKQBIKCkASSgpAEooKQBLKCkATCgpAE +118=0oKQBOKCkATygpAFAoKQBRKCkAUigpAFMoKQBUKCkAVSgpAFYoKAACVwAnAP8RAAAoDAAg +119=CP8AAAACAFgAJwD/qgFZACcpAFooKQBbKCkAXCgpAF0oKQBeKCkAXygpAGAoKQBhKCkAYi +120=gpAGMoKQBkKCkAZSgpAGYoKQBnKCkAaCgpAGkoKQBqKCkAaygpAGwoKQBtKCkAbigpAG8o +121=KQBwKCkAcSgpAHIoKQBzKCkAdCgpAHUoKQB2KCsAKQAo8SoqKCkAKygpACwoKQAtKCkALi +122=gpAC8oKQAwKCkAMUgBBXYFAAABBgAySAFAOAIAAAYAMygpADQoKQA1SQFhTAUBAgYANsgB +123=AQMCADdIAdk9OCgpADkoKQA6KCkAOygpADwoKQA9KCkAPigpAD8oKQBAKCkAQSgpAEIoKQ +124=BDKCkARCgpAEUoKQBGKCkARygpAEgoKQBJKCkASigpAEsoKQBMKCkATSgpAE4oKQBPKCkA +125=UCgpAFEoKQBSKCkAUygpAFQoKQBVKCkAVigpAFcoKQBYKCkAWSgpAFooKQBbKCkAXCgpAF +126=0oKQBeKCkAXygpAGAoKQBhKCkAYigpAGMoKQBkKCkAZSgpAGYoKQBnKCkAaCgpAGkoKQBq +127=KCkAaygpAGwoKQBtKCkAbigpAG8oKQBwKCkAcSgpAHIoKQBzKCkAdCgpAHUoKQB2KCkAdy +128=grACgAKfFsKSgpACooKQArKCkALCgpAC0oKQAuKCkALygpADAoKQAxSAEnvQ0pmG2cbScp +129=ADRABJsCBgA1SAF8bUEENskBAXxtJ5ECOFQCyW85KCkAOigpADsoKQA8KCkAPSgpAD4oKQ +130=A/KCkAQCgpAEEoKQBCKCkAQygpAEQoKQBFKCkARigpAEcoKQBIKCkASSgpAEooKQBLKCkA +131=TCgpAE0oKQBOKCkATygpAFAoKQBRKCkAUigpAFMoKQBUKCkAVSgpAFYoKQBXKCkAWCgpAF +132=koKQBaKCkAWygpAFwoKQBdKCkAXigpAF8oKQBgKCkAYSgpAGIoKQBjKCkAZCgpAGUoKQBm +133=KCkAZygpAGgoKQBpKCkAaigpAGsoKQBsKCkAbSgpAG4oKQBvKCkAcCgpAHEoKQByKCkAcy +134=gpAHQoKQB1KCkAdigpAHcoKQB4KCsAJwAq6W8oKCkAKSgpACooKQArKCkALCgpAC0oKQAu +135=KCkALygpADAoKQAxSAEnFQ4qmG1RbDNUAo8BBgA0KCkANUgBdHCW3ioAaAGU3ie9AjhABC +136=cUDidVADooVQA7KCkAPCgpAD0oKQA+KCkAPygpAEAoKQBBKCkAQigpAEMoKQBEKCkARSgp +137=AEYoKQBHKCkASCgpAEkoKQBKKCkASygpAEwoKQBNKCkATigpAE8oKQBQKCkAUSgpAFIoKQ +138=BTKCkAVCgpAFUoKQBWKCkAVygpAFgoKQBZKCkAWigpAFsoKQBcKCkAXSgpAF4oKQBfKCkA +139=YCgpAGEoKQBiKCkAYygpAGQoKQBlKCkAZigpAGcoKQBoKCkAaSgpAGooKQBrKCkAbCgpAG +140=0oKQBuKCkAbygpAHAoKQBxKCkAcigpAHMoKQB0KCkAdSgpAHYoKQB3KCkAeCgpAHkoKwAm +141=ACvhcicoKQAoKCkAKSgpACooKQArKCkALCgpAC0oKQAuKCkALygpADAoKQAxSAFsc0FyMk +142=gBwXIzKCkANCgpADVIAWByTQU2yQEBZOQn6QI4VAInbA4nVQA6KFUAOygpADwoKQA9KCkA +143=PigpAD8oKQBAKCkAQSgpAEIoKQBDKCkARCgpAEUoKQBGKCkARygpAEgoKQBJKCkASigpAE +144=soKQBMKCkATSgpAE4oKQBPKCkAUCgpAFEoKQBSKCkAUygpAFQoKQBVKCkAVigpAFcoKQBY +145=KCkAWSgpAFooKQBbKCkAXCgpAF0oKQBeKCkAXygpAGAoKQBhKCkAYigpAGMoKQBkKCkAZS +146=gpAGYoKQBnKCkAaCgpAGkoKQBqKCkAaygpAGwoKQBtKCkAbigpAG8oKQBwKCkAcSgpAHIo +147=KQBzKCkAdCgpAHUoKQB2KCkAdygpAHgoKQB5KCkAeigrACUALPl0JigpACcoKQAoKCkAKS +148=gpACooKQArKCkALCgpAC0oKQAuKCkALygpADAoKQAxSAFldgEiDTksgHZBcjNUAohvdOkn +149=VQA1VQJhJg05LCg0HScVAzhABJsGAgA5KCkAOigpADsoKQA8KCkAPSgpAD4oKQA/KCkAQC +150=gpAEEoKQBCKCkAQygpAEQoKQBFKCkARigpAEcoKQBIKCkASSgpAEooKQBLKCkATCgpAE0o +151=KQBOKCkATygpAFAoKQBRKCkAUigpAFMoKQBUKCkAVSgpAFYoKQBXKCkAWCgpAFkoKQBaKC +152=kAWygpAFwoKQBdKCkAXigpAF8oKQBgKCkAYSgpAGIoKQBjKCkAZCgpAGUoKQBmKCkAZygp +153=AGgoKQBpKCkAaigpAGsoKQBsKCkAbSgpAG4oKQBvKCkAcCgpAHEoKQByKCkAcygpAHQoKQ +154=B1KCkAdigpAHcoKQB4KCkAeSgpAHooKQB7KCsAJAAt8XclKCkAJigpACcoKQAoKCkAKSgp +155=ACooKQArKCkALCgpAC0oKQAuKCkALygpADAoKQAxSAF8eFF3MkgBgHkiLUgthAFVAjQoVQ +156=A1SAF8eFUCNsgBhO8nQQM4VAInHA8nVQA6KFUAOygpADwoKQA9KCkAPigpAD8oKQBAKCkA +157=QSgpAEIoKQBDKCkARCgpAEUoKQBGKCkARygpAEgoKQBJKCkASigpAEsoKQBMKCkATSgpAE +158=4oKQBPKCkAUCgpAFEoKQBSKCkAUygpAFQoKQBVKCkAVigpAFcoKQBYKCkAWSgpAFooKQBb +159=KCkAXCgpAF0oKQBeKCkAXygpAGAoKQBhKCkAYigpAGMoKQBkKCkAZSgpAGYoKQBnKCkAaC +160=gpAGkoKQBqKCkAaygpAGwoKQBtKCkAbigpAG8oKQBwKCkAcSgpAHIoKQBzKCkAdCgpAHUo +161=KQB2KCkAdygpAHgoKQB5KCkAeigpAHsoKQB8KCsAIwAu6XokKCkAJSgpACYoKQAnKCkAKC +162=gpACkoKQAqKCkAKygpACwoKQAtKCkALigpAC8oKQAwKCkAMUgBdXsBdfQu6XozVAKMAXT0 +163=J1UANVUCYfX0LiiUHidtAzhABCd0DydVADooVQA7KCkAPCgpAD0oKQA+KCkAPygpAEAoKQ +164=BBKCkAQigpAEMoKQBEKCkARSgpAEYoKQBHKCkASCgpAEkoKQBKKCkASygpAEwoKQBNKCkA +165=TigpAE8oKQBQKCkAUSgpAFIoKQBTKCkAVCgpAFUoKQBWKCkAVygpAFgoKQBZKCkAWigpAF +166=soKQBcKCkAXSgpAF4oKQBfKCkAYCgpAGEoKQBiKCkAYygpAGQoKQBlKCkAZigpAGcoKQBo +167=KCkAaSgpAGooKQBrKCkAbCgpAG0oKQBuKCkAbygpAHAoKQBxKCkAcigpAHMoKQB0KCkAdS +168=gpAHYoKQB3KCkAeCgpAHkoKQB6KCkAeygpAHwoKQB9KCsAIgAv4X0jKCkAJCgpACUoKQAm +169=KCkAJygpACgoKQApKCkAKigpACsoKQAsKCkALSgpAC4oKQAvKCkAMCgpADFIAWx+VfQySA +170=HBfTMoKQA0KCkANUgBbH5NBTbIAYT6J5kDOFQCJ8wPJ1UAOihVADsoKQA8KCkAPSgpAD4o +171=KQA/KCkAQCgpAEEoKQBCKCkAQygpAEQoKQBFKCkARigpAEcoKQBIKCkASSgpAEooKQBLKC +172=kATCgpAE0oKQBOKCkATygpAFAoKQBRKCkAUigpAFMoKQBUKCkAVSgpAFYoKQBXKCkAWCgp +173=AFkoKQBaKCkAWygpAFwoKQBdKCkAXigpAF8oKQBgKCkAYSgpAGIoKQBjKCkAZCgpAGUoKQ +174=BmKCkAZygpAGgoKQBpKCkAaigpAGsoKQBsKCkAbSgpAG4oKQBvKCkAcCgpAHEoKQByKCkA +175=cygpAHQoKQB1KCkAdigpAHcoKQB4KCkAeSgpAHooKQB7KCkAfCgpAH0oKQB+KCsAIQAw+X +176=8iKCkAIygpACQoKQAlKCkAJigpACcoKQAoKCkAKSgpACooKQArKCkALCgpAC0oKQAuKCkA +177=LygpADAoKQAxSAFlgQF1/zCAgUF9M1QCiHp0/ydVADVVAmH1/zAo9B8nxQM4QASYBiKMPg +178=gwAP//AAAAAgA6ABEAACkMACAIMAD//wAAAAIAOwAnKQA8KCkAPSgpAD4oKQA/KCkAQCgp +179=AEEoKQBCKCkAQygpAEQoKQBFKCkARigpAEcoKQBIKCkASSgpAEooKQBLKCkATCgpAE0oKQ +180=BOKCkATygpAFAoKQBRKCkAUigpAFMoKQBUKCkAVSgpAFYoKQBXKCkAWCgpAFkoKQBaKCkA +181=WygpAFwoKQBdKCkAXigpAF8oKQBgKCkAYSgpAGIoKQBjKCkAZCgpAGUoKQBmKCkAZygpAG +182=goKQBpKCkAaigpAGsoKQBsKCkAbSgpAG4oKQBvKCkAcCgpAHEoKQByKCkAcygpAHQoKQB1 +183=KCkAdigpAHcoKQB4KCkAeSgpAHooKQB7KCkAfCgpAH0oKQB+KCkAfygrACAAMeVgISgpAC +184=IoKQAjKCkAJCgpACUoKQAmKCkAJygpACgoKQApKCkAKigpACsoKQAsKCkALSgpAC4oKQAv +185=KCkAMCgpADFIAQV2BQAAAQYAMkkBAGMABgAzSAGLegYANEgBcAIBAAYANUkBYUwFSQE2yA +186=EBAQIAN0gBjwUCADgoKQA5KCkAOigpADsoKQA8KCkAPSgpAD4oKQA/KCkAQCgpAEEoKQBC +187=KCkAQygpAEQoKQBFKCkARigpAEcoKQBIKCkASSgpAEooKQBLKCkATCgpAE0oKQBOKCkATy +188=gpAFAoKQBRKCkAUigpAFMoKQBUKCkAVSgpAFYoKQBXKCkAWCgpAFkoKQBaKCkAWygpAFwo +189=KQBdKCkAXigpAF8oKQBgKCkAYSgpAGIoKQBjKCkAZCgpAGUoKQBmKCkAZygpAGgoKQBpKC +190=kAaigpAGsoKQBsKCkAbSgpAG4oKQBvKCkAcCgpAHEoKQByKCkAcygpAHQoKQB1KCkAdigp +191=AHcoKQB4KCkAeSgpAHooKQB7KCkAfCgpAH0oKQB+KCkAfygpAIAoKQAfWGzVZSAoKQAhKC +192=kAIigpACMoKQAkKCkAJSgpACYoKQAnKCkAKCgpACkoKQAqKCkAKygpACwoKQAtKCkALigp +193=AC8oKQAwKCkAMUgBdIZFgTJIAd2DMygpADQoKQA1SAF0hgECBgA2yQEDdYYy6aY4VALJIj +194=koKQA6KCkAOygpADwoKQA9KCkAPigpAD8oKQBAKCkAQSgpAEIoKQBDKCkARCgpAEUoKQBG +195=KCkARygpAEgoKQBJKCkASigpAEsoKQBMKCkATSgpAE4oKQBPKCkAUCgpAFEoKQBSKCkAUy +196=gpAFQoKQBVKCkAVigpAFcoKQBYKCkAWSgpAFooKQBbKCkAXCgpAF0oKQBeKCkAXygpAGAo +197=KQBhKCkAYigpAGMoKQBkKCkAZSgpAGYoKQBnKCkAaCgpAGkoKQBqKCkAaygpAGwoKQBtKC +198=kAbigpAG8oKQBwKCkAcSgpAHIoKQBzKCkAdCgpAHUoKQB2KCkAdygpAHgoKQB5KCkAeigp +199=AHsoKQB8KCkAfSgpAH4oKQB/KCkAgCgpAIEoKQAeUPPVZR8oKQAgKCkAISgpACIoKQAjKC +200=kAJCgpACUoKQAmKCkAJygpACgoKQApKCkAKigpACsoKQAsKCkALSgpAC4oKQAvKCkAMCgp +201=ADFIAW2JASIFIjOIiVmKM1QCjAEiBCInVQA1VQJhJgYiMwBoASMFIjMoLBEnKQA5TAXBJT +202=ooKQA7KCkAPCgpAD0oKQA+KCkAPygpAEAoKQBBKCkAQigpAEMoKQBEKCkARSgpAEYoKQBH +203=KCkASCgpAEkoKQBKKCkASygpAEwoKQBNKCkATigpAE8oKQBQKCkAUSgpAFIoKQBTKCkAVC +204=gpAFUoKQBWKCkAVygpAFgoKQBZKCkAWigpAFsoKQBcKCkAXSgpAF4oKQBfKCkAYCgpAGEo +205=KQBiKCkAYygpAGQoKQBlKCkAZigpAGcoKQBoKCkAaSgpAGooKQBrKCkAbCgpAG0oKQBuKC +206=kAbygpAHAoKQBxKCkAcigpAHMoKQB0KCkAdSgpAHYoKQB3KCkAeCgpAHkoKQB6KCkAeygp +207=AHwoKQB9KCkAfigpAH8oKQCAKCkAgSgpAIIoKwAdADTtaB4oKQAfKCkAICgpACEoKQAiKC +208=kAIygpACQoKQAlKCkAJigpACcoKQAoKCkAKSgpACooKQArKCkALCgpAC0oKQAuKCkALygp +209=ADAoKQAxSAEnhRE0lIqEjCcpADRABJgCIo0zNJiKI7QiqAEjtCIndQQ4TAXZijkoKQA6KC +210=kAOygpADwoKQA9KCkAPigpAD8oKQBAKCkAQSgpAEIoKQBDKCkARCgpAEUoKQBGKCkARygp +211=AEgoKQBJKCkASigpAEsoKQBMKCkATSgpAE4oKQBPKCkAUCgpAFEoKQBSKCkAUygpAFQoKQ +212=BVKCkAVigpAFcoKQBYKCkAWSgpAFooKQBbKCkAXCgpAF0oKQBeKCkAXygpAGAoKQBhKCkA +213=YigpAGMoKQBkKCkAZSgpAGYoKQBnKCkAaCgpAGkoKQBqKCkAaygpAGwoKQBtKCkAbigpAG +214=8oKQBwKCkAcSgpAHIoKQBzKCkAdCgpAHUoKQB2KCkAdygpAHgoKQB5KCkAeigpAHsoKQB8 +215=KCkAfSgpAH4oKQB/KCkAgCgpAIEoKQCCKCkAgygrABwANfGNHSgpAB4oKQAfKCkAICgpAC +216=EoKQAiKCkAIygpACQoKQAlKCkAJigpACcoKQAoKCkAKSgpACooKQArKCkALCgpAC0oKQAu +217=KCkALygpADAoKQAxSAF8jlGNMkoBiQUjmUUzygECCkEiNZiOQQQ1VAIiOCNJATbIASNkIy +218=ehBDhUAifcESdVADooVQA7KCkAPCgpAD0oKQA+KCkAPygpAEAoKQBBKCkAQigpAEMoKQBE +219=KCkARSgpAEYoKQBHKCkASCgpAEkoKQBKKCkASygpAEwoKQBNKCkATigpAE8oKQBQKCkAUS +220=gpAFIoKQBTKCkAVCgpAFUoKQBWKCkAVygpAFgoKQBZKCkAWigpAFsoKQBcKCkAXSgpAF4o +221=KQBfKCkAYCgpAGEoKQBiKCkAYygpAGQoKQBlKCkAZigpAGcoKQBoKCkAaSgpAGooKQBrKC +222=kAbCgpAG0oKQBuKCkAbygpAHAoKQBxKCkAcigpAHMoKQB0KCkAdSgpAHYoKQB3KCkAeCgp +223=AHkoKQB6KCkAeygpAHwoKQB9KCkAfigpAH8oKQCAKCkAgSgpAIIoKQCDKCkAhCgrABsANu +224=mQHCgpAB0oKQAeKCkAHygpACAoKQAhKCkAIigpACMoKQAkKCkAJSgpACYoKQAnKCkAKCgp +225=ACkoKQAqKCkAKygpACwoKQAtKCkALigpAC8oKQAwKCkAMUgBaZAEIp41NgBpAQUipFeqAQ +226=YJVJGqAQcJWSQ2KBQkqAEjFCQnzQQ4UAknNBInVQA6KFUAOygpADwoKQA9KCkAPigpAD8o +227=KQBAKCkAQSgpAEIoKQBDKCkARCgpAEUoKQBGKCkARygpAEgoKQBJKCkASigpAEsoKQBMKC +228=kATSgpAE4oKQBPKCkAUCgpAFEoKQBSKCkAUygpAFQoKQBVKCkAVigpAFcoKQBYKCkAWSgp +229=AFooKQBbKCkAXCgpAF0oKQBeKCkAXygpAGAoKQBhKCkAYigpAGMoKQBkKCkAZSgpAGYoKQ +230=BnKCkAaCgpAGkoKQBqKCkAaygpAGwoKQBtKCkAbigpAG8oKQBwKCkAcSgpAHIoKQBzKCkA +231=dCgpAHUoKQB2KCkAdygpAHgoKQB5KCkAeigpAHsoKQB8KCkAfSgpAH4oKQB/KCkAgCgpAI +232=EoKQCCKCkAgygpAIQoKQCFKCsAGgA34ZMbKCkAHCgpAB0oKQAeKCkAHygpACAoKQAhKCkA +233=IigpACMoKQAkKCkAJSgpACYoKQAnKCkAKCgpACkoKQAqKCkAKygpACwoKQAtKCkALigpAC +234=8oKQAwKCkAMUgBYZMIbZQ3jJQBCQcAM9YCCghMlLUCC22UN4yUI8QkqAEjxCQn+QQ4WAYn +235=jBInVQA6KFUAOygpADwoKQA9KCkAPigpAD8oKQBAKCkAQSgpAEIoKQBDKCkARCgpAEUoKQ +236=BGKCkARygpAEgoKQBJKCkASigpAEsoKQBMKCkATSgpAE4oKQBPKCkAUCgpAFEoKQBSKCkA +237=UygpAFQoKQBVKCkAVigpAFcoKAAHWAA3AP//AAAAAhEAACcMACAKAFkANwD//wAAAAIAWi +238=gpAFsoKQBcKCkAXSgpAF4oKQBfKCkAYCgpAGEoKQBiKCkAYygpAGQoKQBlKCkAZigpAGco +239=KQBoKCkAaSgpAGooKQBrKCkAbCgpAG0oKQBuKCkAbygpAHAoKQBxKCkAcigpAHMoKQB0KC +240=kAdSgpAHYoKQB3KCkAeCgpAHkoKQB6KCkAeygpAHwoKQB9KCkAfigpAH8oKQCAKCkAgSgp +241=AIIoKQCDKCkAhCgpAIUoKQCGKCgAAxkAOAD//4U/GigpABsoKQAcKCkAHSgpAB4oKQAfKC +242=kAICgpACEoKQAiKCkAIygpACQoKQAlKCkAJigpACcoKQAoKCkAKSgpACooKQArKCkALCgp +243=AC0oKQAuKCkALygpADAoKQAxSgGKBV8gBgAyyAEBAQcAM8gBAQIIADTIAQEDCQA1SQFhTA +244=UBAgYANsoBAwJIKiclBThUAtEqOSgpADooKQA7KCkAPCgpAD0oKQA+KCkAPygpAEAoKQBB +245=KCkAQigpAEMoKQBEKCkARSgpAEYoKQBHKCkASCgpAEkoKQBKKCkASygpAEwoKQBNKCkATi +246=gpAE8oKQBQKCkAUSgpAFIoKQBTKCkAVCgpAFUoKQBWKCkAVygpAFgoKQBZKCkAWigpAFso +247=KQBcKCkAXSgpAF4oKQBfKCkAYCgpAGEoKQBiKCkAYygpAGQoKQBlKCkAZigpAGcoKQBoKC +248=kAaSgpAGooKQBrKCkAbCgpAG0oKQBuKCkAbygpAHAoKQBxKCkAcigpAHMoKQB0KCkAdSgp +249=AHYoKQB3KCkAeCgpAHkoKQB6KCkAeygpAHwoKQB9KCkAfigpAH8oKQCAKCkAgSgpAIIoKQ +250=CDKCkAhCgpAIUoKQCGKCkAhygrABgAOfGYGSgpABooKQAbKCkAHCgpAB0oKQAeKCkAHygp +251=ACAoKQAhKCkAIigpACMoKQAkKCkAJSgpACYoKQAnKCkAKCgpACkoKQAqKCkAKygpACwoKQ +252=AtKCkALigpAC8oKQAwKCkAMUgBfZkEfpk5AGoBBQZdmTlInEkABlyZJykANUwFfZkAfpk5 +253=AGkBAXyZJ1EFOEAEJzwTJ1UAOihVADsoKQA8KCkAPSgpAD4oKQA/KCkAQCgpAEEoKQBCKC +254=kAQygpAEQoKQBFKCkARigpAEcoKQBIKCkASSgpAEooKQBLKCkATCgpAE0oKQBOKCkATygp +255=AFAoKQBRKCkAUigpAFMoKQBUKCkAVSgpAFYoKQBXKCkAWCgpAFkoKQBaKCkAWygpAFwoKQ +256=BdKCkAXigpAF8oKQBgKCkAYSgpAGIoKQBjKCkAZCgpAGUoKQBmKCkAZygpAGgoKQBpKCkA +257=aigpAGsoKQBsKCkAbSgpAG4oKQBvKCkAcCgpAHEoKQByKCkAcygpAHQoKQB1KCkAdigpAH +258=coKQB4KCkAeSgpAHooKQB7KCkAfCgpAH0oKQB+KCkAfygpAIAoKQCBKCkAgigpAIMoKQCE +259=KCkAhSgpAIYoKQCHKCkAiCgrABcAOumbGCgpABkoKQAaKCkAGygpABwoKQAdKCkAHigpAB +260=8oKQAgKCkAISgpACIoKQAjKCkAJCgpACUoKQAmKCkAJygpACgoKQApKCkAKigpACsoKQAs +261=KCkALSgpAC4oKQAvKCkAMCgpADFIAQJ2BQAAAXWcOpWcCXWcOkicSAB0nCcpADVNBWFMBS +262=PWJjoAaAEj1CYnfQU4QAQnlBMnVQA6KFUAOygpADwoKQA9KCkAPigpAD8oKQBAKCkAQSgp +263=AEIoKQBDKCkARCgpAEUoKQBGKCkARygpAEgoKQBJKCkASigpAEsoKQBMKCkATSgpAE4oKQ +264=BPKCkAUCgpAFEoKQBSKCkAUygpAFQoKQBVKCkAVigpAFcoKQBYKCkAWSgpAFooKQBbKCkA +265=XCgpAF0oKQBeKCkAXygpAGAoKQBhKCkAYigpAGMoKQBkKCkAZSgpAGYoKQBnKCkAaCgpAG +266=koKQBqKCkAaygpAGwoKQBtKCkAbigpAG8oKQBwKCkAcSgpAHIoKQBzKCkAdCgpAHUoKQB2 +267=KCkAdygpAHgoKQB5KCkAeigpAHsoKQB8KCkAfSgpAH4oKQB/KCkAgCgpAIEoKQCCKCkAgy +268=gpAIQoKQCFKCkAhigpAIcoKQCIKCkAiSgrABYAO+GeFygpABgoKQAZKCkAGigpABsoKQAc +269=KCkAHSgpAB4oKQAfKCkAICgpACEoKQAiKCkAIygpACQoKQAlKCkAJigpACcoKQAoKCkAKS +270=gpACooKQArKCkALCgpAC0oKQAuKCkALygpADAoKQAxSAFtnwBtnztUnUgAbJ8nKQA0QARN +271=AgBBBDVIAWCeSQE2yAEjhCcnqQU4VAIn7BMnVQA6KFUAOygpADwoKQA9KCkAPigpAD8oKQ +272=BAKCkAQSgpAEIoKQBDKCkARCgpAEUoKQBGKCkARygpAEgoKQBJKCkASigpAEsoKQBMKCkA +273=TSgpAE4oKQBPKCkAUCgpAFEoKQBSKCkAUygpAFQoKQBVKCkAVigpAFcoKQBYKCkAWSgpAF +274=ooKQBbKCkAXCgpAF0oKQBeKCkAXygpAGAoKQBhKCkAYigpAGMoKQBkKCkAZSgpAGYoKQBn +275=KCkAaCgpAGkoKQBqKCkAaygpAGwoKQBtKCkAbigpAG8oKQBwKCkAcSgpAHIoKQBzKCkAdC +276=gpAHUoKQB2KCkAdygpAHgoKQB5KCkAeigpAHsoKQB8KCkAfSgpAH4oKQB/KCkAgCgpAIEo +277=KQCCKCkAgygpAIQoKQCFKCkAhigpAIcoKQCIKCkAiSgpAIooKwAVADz5oBYoKQAXKCkAGC +278=gpABkoKQAaKCkAGygpABwoKQAdKCkAHigpAB8oKQAgKCkAISgpACIoKQAjKCkAJCgpACUo +279=KQAmKCkAJygpACgoKQApKCkAKigpACsoKQAsKCkALSgpAC4oKQAvKCkAMCgpADFIAWWiAW +280=WiPImfAGSiJykANEAEmwIGADVKAWEFUAEizTs8IzQohKIn1QU4QAQnRBQnVQA6KFUAOygp +281=ADwoKQA9KCkAPigpAD8oKQBAKCkAQSgpAEIoKQBDKCkARCgpAEUoKQBGKCkARygpAEgoKQ +282=BJKCkASigpAEsoKQBMKCkATSgpAE4oKQBPKCkAUCgpAFEoKQBSKCkAUygpAFQoKQBVKCkA +283=VigpAFcoKQBYKCkAWSgpAFooKQBbKCkAXCgpAF0oKQBeKCkAXygpAGAoKQBhKCkAYigpAG +284=MoKQBkKCkAZSgpAGYoKQBnKCkAaCgpAGkoKQBqKCkAaygpAGwoKQBtKCkAbigpAG8oKQBw +285=KCkAcSgpAHIoKQBzKCkAdCgpAHUoKQB2KCkAdygpAHgoKQB5KCkAeigpAHsoKQB8KCkAfS +286=gpAH4oKQB/KCkAgCgpAIEoKQCCKCkAgygpAIQoKQCFKCkAhigpAIcoKQCIKCkAiSgpAIoo +287=KQCLKCsAFAA98aMVKCkAFigpABcoKQAYKCkAGSgpABooKQAbKCkAHCgpAB0oKQAeKCkAHy +288=gpACAoKQAhKCkAIigpACMoKQAkKCkAJSgpACYoKQAnKCkAKCgpACkoKQAqKCkAKygpACwo +289=KQAtKCkALigpAC8oKQAwKCkAMUgBfKRRozJIAVCgIo0oMygpADQoKQA1SAF8pCPVPD2cpC +290=PUPCcBBjhABCecFCdVADooVQA7KCkAPCgpAD0oKQA+KCkAPygpAEAoKQBBKCkAQigpAEMo +291=KQBEKCkARSgpAEYoKQBHKCkASCgpAEkoKQBKKCkASygpAEwoKQBNKCkATigpAE8oKQBQKC +292=kAUSgpAFIoKQBTKCkAVCgpAFUoKQBWKCkAVygpAFgoKQBZKCkAWigpAFsoKQBcKCkAXSgp +293=AF4oKQBfKCkAYCgpAGEoKQBiKCkAYygpAGQoKQBlKCkAZigpAGcoKQBoKCkAaSgpAGooKQ +294=BrKCkAbCgpAG0oKQBuKCkAbygpAHAoKQBxKCkAcigpAHMoKQB0KCkAdSgpAHYoKQB3KCkA +295=eCgpAHkoKQB6KCkAeygpAHwoKQB9KCkAfigpAH8oKQCAKCkAgSgpAIIoKQCDKCkAhCgpAI +296=UoKQCGKCkAhygpAIgoKQCJKCkAiigpAIsoKQCMKCsAEwA+6aYUKCgABBUAPgD//wARAAAS +297=DAAgCAAAAgAWAD4A//8AaAEBFwA+AMkBGCgpABkoKQAaKCkAGygpABwoKQAdKCkAHigpAB +298=8oKQAgKCkAISgpACIoKQAjKCkAJCgpACUoKQAmKCkAJygpACgoKQApKCkAKigpACsoKQAs +299=KCkALSgpAC4oKQAvKCkAMCgpADFIAQV2BQAAAQYAMkgBRCYCAAAGADMoKQA0KCkANUkBYU +300=wFQQQ2yAEBAQIAN0gB3Ss4KCkAOSgpADooKQA7KCkAPCgpAD0oKQA+KCkAPygpAEAoKQBB +301=KCkAQigpAEMoKQBEKCkARSgpAEYoKQBHKCkASCgpAEkoKQBKKCkASygpAEwoKQBNKCkATi +302=gpAE8oKQBQKCkAUSgpAFIoKQBTKCkAVCgpAFUoKQBWKCkAVygpAFgoKQBZKCkAWigpAFso +303=KQBcKCkAXSgpAF4oKQBfKCkAYCgpAGEoKQBiKCkAYygpAGQoKQBlKCkAZigpAGcoKQBoKC +304=kAaSgpAGooKQBrKCkAbCgpAG0oKQBuKCkAbygpAHAoKQBxKCkAcigpAHMoKQB0KCkAdSgp +305=AHYoKQB3KCkAeCgpAHkoKQB6KCkAeygpAHwoKQB9KCkAfigpAH8oKQCAKCkAgSgpAIIoKQ +306=CDKCkAhCgpAIUoKQCGKCkAhygpAIgoKQCJKCkAiigpAIsoKQCMKCkAjSgrABIAP/GjEygp +307=ABQoKQAVKCkAFigpABcoKQAYKCkAGSgpABooKQAbKCkAHCgpAB0oKQAeKCkAHygpACAoKQ +308=AhKCkAIigpACMoKQAkKCkAJSgpACYoKQAnKCkAKCgpACkoKQAqKCkAKygpACwoKQAtKCkA +309=LigpAC8oKQAwKCkAMUgBJ00VP4iqSaYzVAKfpAYANEgBmwIGADVIAWyqAQIGADbJAQNsqi +310=dZBjhUApsGAgA5KCkAOigpADsoKQA8KCkAPSgpAD4oKQA/KCkAQCgpAEEoKQBCKCkAQygp +311=AEQoKQBFKCkARigpAEcoKQBIKCkASSgpAEooKQBLKCkATCgpAE0oKQBOKCkATygpAFAoKQ +312=BRKCkAUigpAFMoKQBUKCkAVSgpAFYoKQBXKCkAWCgpAFkoKQBaKCkAWygpAFwoKQBdKCkA +313=XigpAF8oKQBgKCkAYSgpAGIoKQBjKCkAZCgpAGUoKQBmKCkAZygpAGgoKQBpKCkAaigpAG +314=soKQBsKCkAbSgpAG4oKQBvKCkAcCgpAHEoKQByKCkAcygpAHQoKQB1KCkAdigpAHcoKQB4 +315=KCkAeSgpAHooKQB7KCkAfCgpAH0oKQB+KCkAfygpAIAoKQCBKCkAgigpAIMoKQCEKCkAhS +316=gpAIYoKQCHKCkAiCgpAIkoKQCKKCkAiygpAIwoKQCNKCkAjigrABEAQPmrEigpABMoKQAU +317=KCkAFSgpABYoKQAXKCkAGCgpABkoKQAaKCkAGygpABwoKQAdKCkAHigpAB8oKQAgKCkAIS +318=gpACIoKQAjKCkAJCgpACUoKQAmKCkAJygpACgoKQApKCkAKigpACsoKQAsKCkALSgpAC4o +319=KQAvKCkAMCgpADFIASelFUCIqoSt1C5krSdVADVMBWStQQQ2yAEj9CrPBQIAOFQCJ6QVJ1 +320=UAOihVADsoKQA8KCkAPSgpAD4oKQA/KCkAQCgpAEEoKQBCKCkAQygpAEQoKQBFKCkARigp +321=AEcoKQBIKCkASSgpAEooKQBLKCkATCgpAE0oKQBOKCkATygpAFAoKQBRKCkAUigpAFMoKQ +322=BUKCkAVSgpAFYoKQBXKCkAWCgpAFkoKQBaKCkAWygpAFwoKQBdKCkAXigpAF8oKQBgKCkA +323=YSgpAGIoKQBjKCkAZCgpAGUoKQBmKCkAZygpAGgoKQBpKCkAaigpAGsoKQBsKCkAbSgpAG +324=4oKQBvKCkAcCgpAHEoKQByKCkAcygpAHQoKQB1KCkAdigpAHcoKQB4KCkAeSgpAHooKQB7 +325=KCkAfCgpAH0oKQB+KCkAfygpAIAoKQCBKCkAgigpAIMoKQCEKCkAhSgpAIYoKQCHKCkAiC +326=gpAIkoKQCKKCkAiygpAIwoKQCNKCkAjigpAI8oKwAQAEHxrhEoKQASKCkAEygpABQoKQAV +327=KCkAFigpABcoKQAYKCkAGSgpABooKQAbKCkAHCgpAB0oKQAeKCkAHygpACAoKQAhKCkAIi +328=gpACMoKQAkKCkAJSgpACYoKQAnKCkAKCgpACkoKQAqKCkAKygpACwoKQAtKCkALigpAC8o +329=KQAwKCkAMUgBfK9NqjJIAYCwIvVAQYQBVQI0KFUANUgBfK8jpitBAGgBI6QrJ7EGOEAEJ/ +330=wVJ1UAOihVADsoKQA8KCkAPSgpAD4oKQA/KCkAQCgpAEEoKQBCKCkAQygpAEQoKQBFKCkA +331=RigpAEcoKQBIKCkASSgpAEooKQBLKCkATCgpAE0oKQBOKCkATygpAFAoKQBRKCkAUigpAF +332=MoKQBUKCkAVSgpAFYoKQBXKCkAWCgpAFkoKQBaKCkAWygpAFwoKQBdKCkAXigpAF8oKQBg +333=KCkAYSgpAGIoKQBjKCkAZCgpAGUoKQBmKCkAZygpAGgoKQBpKCkAaigpAGsoKQBsKCkAbS +334=gpAG4oKQBvKCkAcCgpAHEoKQByKCkAcygpAHQoKQB1KCkAdigpAHcoKQB4KCkAeSgpAHoo +335=KQB7KCkAfCgpAH0oKQB+KCkAfygpAIAoKQCBKCkAgigpAIMoKQCEKCkAhSgpAIYoKQCHKC +336=kAiCgpAIkoKQCKKCkAiygpAIwoKQCNKCkAjigpAI8oKQCQKCsADwBC6bEQKCkAESgpABIo +337=KQATKCkAFCgpABUoKQAWKCkAFygpABgoKQAZKCkAGigpABsoKQAcKCkAHSgpAB4oKQAfKC +338=kAICgpACEoKQAiKCkAIygpACQoKQAlKCkAJigpACcoKQAoKCkAKSgpACooKQArKCkALCgp +339=AC0oKQAuKCkALygpADAoKQAxSAF1sgEiTVdC6bEzVAKMASJULCdVADVVAmEmTVdClbIBIv +340=xBJ90GOEAEJ1QWJ1UAOihVADsoKQA8KCkAPSgpAD4oKQA/KCkAQCgpAEEoKQBCKCkAQygp +341=AEQoKQBFKCkARigpAEcoKQBIKCkASSgpAEooKQBLKCkATCgpAE0oKQBOKCkATygpAFAoKQ +342=BRKCkAUigpAFMoKQBUKCkAVSgpAFYoKQBXKCkAWCgpAFkoKQBaKCkAWygpAFwoKQBdKCkA +343=XigpAF8oKQBgKCkAYSgpAGIoKQBjKCkAZCgpAGUoKQBmKCkAZygpAGgoKQBpKCkAaigpAG +344=soKQBsKCkAbSgpAG4oKQBvKCkAcCgpAHEoKQByKCkAcygpAHQoKQB1KCkAdigpAHcoKQB4 +345=KCkAeSgpAHooKQB7KCkAfCgpAH0oKQB+KCkAfygpAIAoKQCBKCkAgigpAIMoKQCEKCkAhS +346=gpAIYoKQCHKCkAiCgpAIkoKQCKKCkAiygpAIwoKQCNKCkAjigpAI8oKQCQKCkAkSgrAA4A +347=Q+G0DygpABAoKQARKCkAEigpABMoKQAUKCkAFSgpABYoKQAXKCkAGCgpABkoKQAaKCkAGy +348=gpABwoKQAdKCkAHigpAB8oKQAgKCkAISgpACIoKQAjKCkAJCgpACUoKQAmKCkAJygpACgo +349=KQApKCkAKigpACsoKQAsKCkALSgpAC4oKQAvKCkAMCgpADFIAW21AG21Q5yzSQEzVAKMAW +350=y1J1UANVQCbLUjBS1DjbUDbLUnCQc4QAQnrBYnVQA6KFUAOygpADwoKQA9KCkAPigpAD8o +351=KQBAKCkAQSgpAEIoKQBDKCkARCgpAEUoKQBGKCkARygpAEgoKQBJKCkASigpAEsoKQBMKC +352=kATSgpAE4oKQBPKCkAUCgpAFEoKQBSKCkAUygpAFQoKQBVKCkAVigpAFcoKQBYKCkAWSgp +353=AFooKQBbKCkAXCgpAF0oKQBeKCkAXygpAGAoKQBhKCkAYigpAGMoKQBkKCkAZSgpAGYoKQ +354=BnKCkAaCgpAGkoKQBqKCkAaygpAGwoKQBtKCkAbigpAG8oKQBwKCkAcSgpAHIoKQBzKCkA +355=dCgpAHUoKQB2KCkAdygpAHgoKQB5KCkAeigpAHsoKQB8KCkAfSgoAAF+AEMAEQAABwwAIA +356=j//wAAAAIAfwBDAMkBgCgpAIEoKQCCKCkAgygpAIQoKQCFKCkAhigpAIcoKQCIKCkAiSgp +357=AIooKQCLKCkAjCgpAI0oKQCOKCkAjygpAJAoKQCRKCkAkigrAA0ARO0bDigpAA8oKQAQKC +358=kAESgpABIoKQATKCkAFCgpABUoKQAWKCkAFygpABgoKQAZKCkAGigpABsoKQAcKCkAHSgp +359=AB4oKQAfKCkAICgpACEoKQAiKCkAIygpACQoKQAlKCkAJigpACcoKQAoKCkAKSgpACooKQ +360=ArKCkALCgpAC0oKQAuKCkALygpADAoKQAxSAEFdgUAAAEGADJIAUxPAgAABgAzKCkANCgp +361=ADVJAWFMBUEENsgBAQECADdIAcVVOCgpADkoKQA6KCkAOygpADwoKQA9KCkAPigpAD8oKQ +362=BAKCkAQSgpAEIoKQBDKCkARCgpAEUoKQBGKCkARygpAEgoKQBJKCkASigpAEsoKQBMKCkA +363=TSgpAE4oKQBPKCkAUCgpAFEoKQBSKCkAUygpAFQoKQBVKCkAVigpAFcoKQBYKCkAWSgpAF +364=ooKQBbKCkAXCgpAF0oKQBeKCkAXygpAGAoKQBhKCkAYigpAGMoKQBkKCkAZSgpAGYoKQBn +365=KCkAaCgpAGkoKQBqKCkAaygpAGwoKQBtKCkAbigpAG8oKQBwKCkAcSgpAHIoKQBzKCkAdC +366=gpAHUoKQB2KCkAdygpAHgoKQB5KCkAeigpAHsoKQB8KCkAfSgpAH4oKQB/KCkAgCgpAIEo +367=KQCCKCkAgygpAIQoKQCFKCkAhigpAIcoKQCIKCkAiSgpAIooKQCLKCkAjCgpAI0oKQCOKC +368=kAjygpAJAoKQCRKCkAkigpAJMoKwAMAEXxuQ0oKQAOKCkADygpABAoKQARKCkAEigpABMo +369=KQAUKCkAFSgpABYoKQAXKCkAGCgpABkoKQAaKCkAGygpABwoKQAdKCkAHigpAB8oKQAgKC +370=kAISgpACIoKQAjKCkAJCgpACUoKQAmKCkAJygpACgoKQApKCkAKigpACsoKQAsKCkALSgp +371=AC4oKQAvKCkAMCgpADFIAXy6TbUySAEnXBfTNQYANChVADVIAXy6AQIGADbJAQN8us8FAg +372=A4VALJvDkoKQA6KCkAOygpADwoKQA9KCkAPigpAD8oKQBAKCkAQSgpAEIoKQBDKCkARCgp +373=AEUoKQBGKCkARygpAEgoKQBJKCkASigpAEsoKQBMKCkATSgpAE4oKQBPKCkAUCgpAFEoKQ +374=BSKCkAUygpAFQoKQBVKCkAVigpAFcoKQBYKCkAWSgpAFooKQBbKCkAXCgpAF0oKQBeKCkA +375=XygpAGAoKQBhKCkAYigpAGMoKQBkKCkAZSgpAGYoKQBnKCkAaCgpAGkoKQBqKCkAaygpAG +376=woKQBtKCkAbigpAG8oKQBwKCkAcSgpAHIoKQBzKCkAdCgpAHUoKQB2KCkAdygpAHgoKQB5 +377=KCkAeigpAHsoKQB8KCkAfSgpAH4oKQB/KCkAgCgpAIEoKQCCKCkAgygpAIQoKQCFKCkAhi +378=gpAIcoKQCIKCkAiSgpAIooKQCLKCkAjCgpAI0oKQCOKCkAjygpAJAoKQCRKCkAkigpAJMo +379=KQCUKCsACwBG6bwMKCkADSgpAA4oKQAPKCkAECgpABEoKQASKCkAEygpABQoKQAVKCkAFi +380=gpABcoKQAYKCkAGSgpABooKQAbKCkAHCgpAB0oKQAeKCkAHygpACAoKQAhKCkAIigpACMo +381=KQAkKCkAJSgpACYoKQAnKCkAKCgpACkoKQAqKCkAKygpACwoKQAtKCkALigpAC8oKQAwKC +382=kAMUgBdb0BIhUvRpC9Qb8zVAKMAXS9J1UANVUCYSYWL0YAaAEjFC8njQc4QAQntBcnVQA6 +383=KFUAOygpADwoKQA9KCkAPigpAD8oKQBAKCkAQSgpAEIoKQBDKCkARCgpAEUoKQBGKCkARy +384=gpAEgoKQBJKCkASigpAEsoKQBMKCkATSgpAE4oKQBPKCkAUCgpAFEoKQBSKCkAUygpAFQo +385=KQBVKCkAVigpAFcoKQBYKCkAWSgpAFooKQBbKCkAXCgpAF0oKQBeKCkAXygpAGAoKQBhKC +386=kAYigpAGMoKQBkKCkAZSgpAGYoKQBnKCkAaCgpAGkoKQBqKCkAaygpAGwoKQBtKCkAbigp +387=AG8oKQBwKCkAcSgpAHIoKQBzKCkAdCgpAHUoKQB2KCkAdygpAHgoKQB5KCkAeigpAHsoKQ +388=B8KCkAfSgpAH4oKQB/KCkAgCgpAIEoKQCCKCkAgygpAIQoKQCFKCkAhigpAIcoKQCIKCkA +389=iSgpAIooKQCLKCkAjCgpAI0oKQCOKCkAjygpAJAoKQCRKCkAkigpAJMoKQCUKCkAlSgrAA +390=oAR+G/CygpAAwoKQANKCkADigpAA8oKQAQKCkAESgpABIoKQATKCkAFCgpABUoKQAWKCkA +391=FygpABgoKQAZKCkAGigpABsoKQAcKCkAHSgpAB4oKQAfKCkAICgpACEoKQAiKCkAIygpAC +392=QoKQAlKCkAJigpACcoKQAoKCkAKSgpACooKQArKCkALCgpAC0oKQAuKCkALygpADAoKQAx +393=SQFQvboySAHBvzMoKQA0KCkANUgBYL8jxS9HjMAjxC8nuQc4QAQnDBgnVQA6KFUAOygpAD +394=woKQA9KCkAPigpAD8oKQBAKCkAQSgpAEIoKQBDKCkARCgpAEUoKQBGKCkARygpAEgoKQBJ +395=KCkASigpAEsoKQBMKCkATSgpAE4oKQBPKCkAUCgpAFEoKQBSKCkAUygpAFQoKQBVKCkAVi +396=gpAFcoKQBYKCkAWSgpAFooKQBbKCkAXCgpAF0oKQBeKCkAXygpAGAoKQBhKCkAYigpAGMo +397=KQBkKCkAZSgpAGYoKQBnKCkAaCgpAGkoKQBqKCkAaygpAGwoKQBtKCkAbigpAG8oKQBwKC +398=kAcSgpAHIoKQBzKCkAdCgpAHUoKQB2KCkAdygpAHgoKQB5KCkAeigpAHsoKQB8KCkAfSgp +399=AH4oKQB/KCkAgCgpAIEoKQCCKCkAgygpAIQoKQCFKCkAhigpAIcoKQCIKCkAiSgpAIooKQ +400=CLKCkAjCgpAI0oKQCOKCkAjygpAJAoKQCRKCkAkigpAJMoKQCUKCkAlSgpAJYoKwAJAEj5 +401=wQooKQALKCkADCgpAA0oKQAOKCkADygpABAoKQARKCkAEigpABMoKQAUKCkAFSgpABYoKQ +402=AXKCkAGCgpABkoKQAaKCkAGygpABwoKQAdKCkAHigpAB8oKQAgKCkAISgpACIoKQAjKCkA +403=JCgpACUoKQAmKCkAJygpACgoKQApKCkAKigpACsoKQAsKCkALSgpAC4oKQAvKCkAMCgpAD +404=FIAWTDAQICADLJAQMij19IAFe9xTRUAmgBAQEGADVJAV+1AjbIASN0MCflBzhUAidkGCdV +405=ADooVQA7KCkAPCgpAD0oKQA+KCkAPygpAEAoKQBBKCkAQigpAEMoKQBEKCkARSgpAEYoKQ +406=BHKCkASCgpAEkoKQBKKCkASygpAEwoKQBNKCkATigpAE8oKQBQKCkAUSgpAFIoKQBTKCkA +407=VCgpAFUoKQBWKCkAVygpAFgoKQBZKCkAWigpAFsoKQBcKCkAXSgpAF4oKQBfKCkAYCgpAG +408=EoKQBiKCkAYygpAGQoKQBlKCkAZigpAGcoKQBoKCkAaSgpAGooKQBrKCkAbCgpAG0oKQBu +409=KCkAbygpAHAoKQBxKCkAcigpAHMoKQB0KCkAdSgpAHYoKQB3KCkAeCgpAHkoKQB6KCkAey +410=gpAHwoKQB9KCkAfigpAH8oKQCAKCkAgSgpAIIoKQCDKCkAhCgpAIUoKQCGKCkAhygpAIgo +411=KQCJKCkAiigpAIsoKQCMKCkAjSgpAI4oKQCPKCkAkCgpAJEoKQCSKCkAkygpAJQoKQCVKC +412=kAligpAJcoKwAIAEnxxAkoKQAKKCkACygpAAwoKQANKCkADigpAA8oKQAQKCkAESgpABIo +413=KQATKCkAFCgpABUoKQAWKCkAFygpABgoKQAZKCkAGigpABsoKQAcKCkAHSgpAB4oKQAfKC +414=kAICgpACEoKQAiKCkAIygoAAkkAEkA//8AAAACACURAADSCwAgCABJAP//AAAAAgAmKCkA +415=JygpACgoKQApKCkAKigpACsoKQAsKCkALSgpAC4oKQAvKCkAMCgpADEoKQAySAEFUAUAAA +416=UCADNJAVdIAQECAgA0yAEBAwIANUkBX7UCNsgBVQI3SAEB//8AAFUYOCgpADkoKQA6KCkA +417=OygpADwoKQA9KCkAPigpAD8oKQBAKCkAQSgpAEIoKQBDKCkARCgpAEUoKQBGKCkARygpAE +418=goKQBJKCkASigpAEsoKQBMKCkATSgpAE4oKQBPKCkAUCgpAFEoKQBSKCkAUygpAFQoKQBV +419=KCkAVigpAFcoKQBYKCkAWSgpAFooKQBbKCkAXCgpAF0oKQBeKCkAXygpAGAoKQBhKCkAYi +420=gpAGMoKQBkKCkAZSgpAGYoKQBnKCkAaCgpAGkoKQBqKCkAaygpAGwoKQBtKCkAbigpAG8o +421=KQBwKCkAcSgpAHIoKQBzKCkAdCgpAHUoKQB2KCkAdygpAHgoKQB5KCkAeigpAHsoKQB8KC +422=kAfSgpAH4oKQB/KCkAgCgpAIEoKQCCKCkAgygpAIQoKQCFKCkAhigpAIcoKQCIKCkAiSgp +423=AIooKQCLKCkAjCgpAI0oKQCOKCkAjygpAJAoKQCRKCkAkigpAJMoKQCUKCkAlSgpAJYoKQ +424=CXKCkAmCgrAAcASu2fCCgpAAkoKQAKKCkACygpAAwoKQANKCkADigpAA8oKQAQKCkAESgp +425=ABIoKQATKCkAFCgpABUoKQAWKCkAFygpABgoKQAZKCkAGigpABsoKQAcKCkAHSgpAB4oKQ +426=AfKCkAICgpACEoKQAiKCkAIygpACQoKQAlKCkAJigpACcoKQAoKCkAKSgpACooKQArKCkA +427=LCgpAC0oKQAuKCkALygpADAoKQAxKCkAMigpADMoKQA0KCkANSgpADYoKQA3KCkAOCgpAD +428=koKQA6KCkAOygpADwoKQA9KCkAPigpAD8oKQBAKCkAQSgpAEIoKQBDKCkARCgpAEUoKQBG +429=KCkARygpAEgoKQBJKCkASigpAEsoKQBMKCkATSgpAE4oKQBPKCkAUCgpAFEoKQBSKCkAUy +430=gpAFQoKQBVKCkAVigpAFcoKQBYKCkAWSgpAFooKQBbKCkAXCgpAF0oKQBeKCkAXygpAGAo +431=KQBhKCkAYigpAGMoKQBkKCkAZSgpAGYoKQBnKCkAaCgpAGkoKQBqKCkAaygpAGwoKQBtKC +432=kAbigpAG8oKQBwKCkAcSgpAHIoKQBzKCkAdCgpAHUoKQB2KCkAdygpAHgoKQB5KCkAeigp +433=AHsoKQB8KCkAfSgpAH4oKQB/KCkAgCgpAIEoKQCCKCkAgygpAIQoKQCFKCkAhigpAIcoKQ +434=CIKCkAiSgpAIooKQCLKCkAjCgpAI0oKQCOKCkAjygpAJAoKQCRKCkAkigpAJMoKQCUKCkA +435=lSgpAJYoKQCXKCkAmCgpAJkoKwAGAEvhygcoKQAIKCkACSgpAAooKQALKCkADCgpAA0oKQ +436=AOKCkADygpABAoKQARKCkAEigpABMoKQAUKCkAFSgpABYoKQAXKCkAGCgpABkoKQAaKCkA +437=GygpABwoKQAdKCkAHigpAB8oKQAgKCkAISgpACIoKQAjKCkAJCgpACUoKQAmKCkAJygpAC +438=goKQApKCkAKigpACsoKQAsKCkALSgpAC4oKQAvKCkAMCgpADEoKQAyKCkAMygpADQoKQA1 +439=KCkANigpADcoKQA4KCkAOSgpADooKQA7KCkAPCgpAD0oKQA+KCkAPygpAEAoKQBBKCkAQi +440=gpAEMoKQBEKCkARSgpAEYoKQBHKCkASCgpAEkoKQBKKCkASygpAEwoKQBNKCkATigpAE8o +441=KQBQKCkAUSgpAFIoKQBTKCkAVCgpAFUoKQBWKCkAVygpAFgoKQBZKCkAWigpAFsoKQBcKC +442=kAXSgpAF4oKQBfKCkAYCgpAGEoKQBiKCkAYygpAGQoKQBlKCkAZigpAGcoKQBoKCkAaSgp +443=AGooKQBrKCkAbCgpAG0oKQBuKCkAbygpAHAoKQBxKCkAcigpAHMoKQB0KCkAdSgpAHYoKQ +444=B3KCkAeCgpAHkoKQB6KCkAeygpAHwoKQB9KCkAfigpAH8oKQCAKCkAgSgpAIIoKQCDKCkA +445=hCgpAIUoKQCGKCkAhygpAIgoKQCJKCkAiigpAIsoKQCMKCkAjSgpAI4oKQCPKCkAkCgpAJ +446=EoKQCSKCkAkygpAJQoKQCVKCkAligpAJcoKQCYKCkAmSgpAJooKwAFAEz5zAYoKQAHKCkA +447=CCgpAAkoKQAKKCkACygpAAwoKQANKCkADigpAA8oKQAQKCkAESgpABIoKQATKCkAFCgpAB +448=UoKQAWKCkAFygpABgoKQAZKCkAGigpABsoKQAcKCkAHSgpAB4oKQAfKCkAICgpACEoKQAi +449=KCkAIygpACQoKQAlKCkAJigpACcoKQAoKCkAKSgpACooKQArKCkALCgpAC0oKQAuKCkALy +450=gpADAoKQAxKCkAMigpADMoKQA0KCkANSgpADYoKQA3KCkAOCgpADkoKQA6KCkAOygpADwo +451=KQA9KCkAPigpAD8oKQBAKCkAQSgpAEIoKQBDKCkARCgpAEUoKQBGKCkARygpAEgoKQBJKC +452=kASigpAEsoKQBMKCkATSgpAE4oKQBPKCkAUCgpAFEoKQBSKCkAUygpAFQoKQBVKCkAVigp +453=AFcoKQBYKCkAWSgpAFooKQBbKCkAXCgpAF0oKQBeKCkAXygpAGAoKQBhKCkAYigpAGMoKQ +454=BkKCkAZSgpAGYoKQBnKCkAaCgpAGkoKQBqKCkAaygpAGwoKQBtKCkAbigpAG8oKQBwKCkA +455=cSgpAHIoKQBzKCkAdCgpAHUoKQB2KCkAdygpAHgoKQB5KCkAeigpAHsoKQB8KCkAfSgpAH +456=4oKQB/KCkAgCgpAIEoKQCCKCkAgygpAIQoKQCFKCkAhigpAIcoKQCIKCkAiSgpAIooKQCL +457=KCkAjCgpAI0oKQCOKCkAjygpAJAoKQCRKCkAkigpAJMoKQCUKCkAlSgpAJYoKQCXKCkAmC +458=gpAJkoKQCaKCkAmygrAAQATfHPBSgpAAYoKQAHKCkACCgpAAkoKQAKKCkACygpAAwoKQAN +459=KCkADigpAA8oKQAQKCkAESgpABIoKQATKCkAFCgpABUoKQAWKCkAFygpABgoKQAZKCkAGi +460=gpABsoKQAcKCkAHSgpAB4oKQAfKCkAICgpACEoKQAiKCkAIygpACQoKQAlKCkAJigpACco +461=KQAoKCkAKSgpACooKQArKCkALCgpAC0oKQAuKCkALygpADAoKQAxKCkAMigpADMoKQA0KC +462=kANSgpADYoKQA3KCkAOCgpADkoKQA6KCkAOygpADwoKQA9KCkAPigpAD8oKQBAKCkAQSgp +463=AEIoKQBDKCkARCgpAEUoKQBGKCkARygpAEgoKQBJKCkASigpAEsoKQBMKCkATSgpAE4oKQ +464=BPKCkAUCgpAFEoKQBSKCkAUygpAFQoKQBVKCkAVigpAFcoKQBYKCkAWSgpAFooKQBbKCkA +465=XCgpAF0oKQBeKCkAXygpAGAoKQBhKCkAYigpAGMoKQBkKCkAZSgpAGYoKQBnKCkAaCgpAG +466=koKQBqKCkAaygpAGwoKQBtKCkAbigpAG8oKQBwKCkAcSgpAHIoKQBzKCkAdCgpAHUoKQB2 +467=KCkAdygpAHgoKQB5KCkAeigpAHsoKQB8KCkAfSgpAH4oKQB/KCkAgCgpAIEoKQCCKCkAgy +468=gpAIQoKQCFKCkAhigpAIcoKQCIKCkAiSgpAIooKQCLKCkAjCgpAI0oKQCOKCkAjygpAJAo +469=KQCRKCkAkigpAJMoKQCUKCkAlSgpAJYoKQCXKCkAmCgpAJkoKQCaKCkAmygpAJwoKwADAE +470=7p0gQoKQAFKCkABigpAAcoKQAIKCkACSgpAAooKQALKCkADCgpAA0oKQAOKCkADygpABAo +471=KQARKCkAEigpABMoKQAUKCkAFSgpABYoKQAXKCkAGCgpABkoKQAaKCkAGygpABwoKQAdKC +472=kAHigoAAYfAE4A//8AAAARAAC9CwAgAAECACAATgD//wAAAAIAIQBOAP//iQEiKCkAIygp +473=ACQoKQAlKCkAJigpACcoKQAoKCkAKSgpACooKQArKCkALCgpAC0oKQAuKCkALygpADAoKQ +474=AxKCkAMigpADMoKQA0KCkANSgpADYoKQA3KCkAOCgpADkoKQA6KCkAOygpADwoKQA9KCkA +475=PigpAD8oKQBAKCkAQSgpAEIoKQBDKCkARCgpAEUoKQBGKCkARygpAEgoKQBJKCkASigpAE +476=soKQBMKCkATSgpAE4oKQBPKCkAUCgpAFEoKQBSKCkAUygpAFQoKQBVKCkAVigpAFcoKQBY +477=KCkAWSgpAFooKQBbKCkAXCgpAF0oKQBeKCkAXygpAGAoKQBhKCkAYigpAGMoKQBkKCkAZS +478=gpAGYoKQBnKCkAaCgpAGkoKQBqKCkAaygpAGwoKQBtKCkAbigpAG8oKQBwKCkAcSgpAHIo +479=KQBzKCkAdCgpAHUoKQB2KCkAdygpAHgoKQB5KCkAeigpAHsoKQB8KCkAfSgpAH4oKQB/KC +480=kAgCgpAIEoKQCCKCkAgygpAIQoKQCFKCkAhigpAIcoKQCIKCkAiSgpAIooKQCLKCkAjCgp +481=AI0oKQCOKCkAjygpAJAoKQCRKCkAkigpAJMoKQCUKCkAlSgpAJYoKQCXKCkAmCgpAJkoKQ +482=CaKCkAmygpAJwoKQCdKCsAAgBP+asDKCkABCgpAAUoKQAGKCkABygpAAgoKQAJKCkACigp +483=AAsoKQAMKCkADSgpAA4oKQAPKCkAECgpABEoKQASKCkAEygpABQoKQAVKCkAFigpABcoKQ +484=AYKCkAGSgpABooKQAbKCkAHCgpAB0oKQAeKCkAHygpACAoKQAhKCkAIigpACMoKQAkKCkA +485=JSgpACYoKQAnKCkAKCgpACkoKQAqKCkAKygpACwoKQAtKCkALigpAC8oKQAwKCkAMSgpAD +486=IoKQAzKCkANCgpADUoKQA2KCkANygpADgoKQA5KCkAOigpADsoKQA8KCkAPSgpAD4oKQA/ +487=KCkAQCgpAEEoKQBCKCkAQygpAEQoKQBFKCkARigpAEcoKQBIKCkASSgpAEooKQBLKCkATC +488=gpAE0oKQBOKCkATygpAFAoKQBRKCkAUigpAFMoKQBUKCkAVSgpAFYoKQBXKCkAWCgpAFko +489=KQBaKCkAWygpAFwoKQBdKCkAXigpAF8oKQBgKCkAYSgpAGIoKQBjKCkAZCgpAGUoKQBmKC +490=kAZygpAGgoKQBpKCkAaigpAGsoKQBsKCkAbSgpAG4oKQBvKCkAcCgpAHEoKQByKCkAcygp +491=AHQoKQB1KCkAdigpAHcoKQB4KCkAeSgpAHooKQB7KCkAfCgpAH0oKQB+KCkAfygpAIAoKQ +492=CBKCkAgigpAIMoKQCEKCkAhSgpAIYoKQCHKCkAiCgpAIkoKQCKKCkAiygpAIwoKQCNKCkA +493=jigpAI8oKQCQKCkAkSgpAJIoKQCTKCkAlCgpAJUoKQCWKCkAlygpAJgoKQCZKCkAmigpAJ +494=soKQCcKCkAnSgpAJ4oKwABAFD51wIoKQADKCkABCgpAAUoKQAGKCkABygpAAgoKQAJKCkA +495=CigpAAsoKQAMKCkADSgpAA4oKQAPKCkAECgpABEoKQASKCkAEygpABQoKQAVKCkAFigpAB +496=coKQAYKCkAGSgpABooKQAbKCkAHCgpAB0oKQAeKCkAHygpACAoKQAhKCkAIigpACMoKQAk +497=KCkAJSgpACYoKQAnKCkAKCgpACkoKQAqKCkAKygpACwoKQAtKCkALigpAC8oKQAwKCkAMS +498=gpADIoKQAzKCkANCgpADUoKQA2KCkANygpADgoKQA5KCkAOigpADsoKQA8KCkAPSgpAD4o +499=KQA/KCkAQCgpAEEoKQBCKCkAQygpAEQoKQBFKCkARigpAEcoKQBIKCkASSgpAEooKQBLKC +500=kATCgpAE0oKQBOKCkATygpAFAoKQBRKCkAUigpAFMoKQBUKCkAVSgpAFYoKQBXKCkAWCgp +501=AFkoKQBaKCkAWygpAFwoKQBdKCkAXigpAF8oKQBgKCkAYSgpAGIoKQBjKCkAZCgpAGUoKQ +502=BmKCkAZygpAGgoKQBpKCkAaigpAGsoKQBsKCkAbSgpAG4oKQBvKCkAcCgpAHEoKQByKCkA +503=cygpAHQoKQB1KCkAdigpAHcoKQB4KCkAeSgpAHooKQB7KCkAfCgpAH0oKQB+KCkAfygpAI +504=AoKQCBKCkAgigpAIMoKQCEKCkAhSgpAIYoKQCHKCkAiCgpAIkoKQCKKCkAiygpAIwoKQCN +505=KCkAjigpAI8oKQCQKCkAkSgpAJIoKQCTKCkAlCgpAJUoKQCWKCkAlygpAJgoKQCZKCkAmi +506=gpAJsoKQCcKCkAnSgpAJ4oKQCfKCsAAgBR8doDKCkABCgpAAUoKQAGKCkABygpAAgoKQAJ +507=KCkACigpAAsoKQAMKCkADSgpAA4oKQAPKCkAECgpABEoKQASKCkAEygpABQoKQAVKCkAFi +508=gpABcoKQAYKCkAGSgpABooKQAbKCkAHCgpAB0oKQAeKCkAHygpACAoKQAhKCkAIigpACMo +509=KQAkKCkAJSgpACYoKQAnKCkAKCgpACkoKQAqKCkAKygpACwoKQAtKCkALigpAC8oKQAwKC +510=kAMSgpADIoKQAzKCkANCgpADUoKQA2KCkANygpADgoKQA5KCkAOigpADsoKQA8KCkAPSgp +511=AD4oKQA/KCkAQCgpAEEoKQBCKCkAQygpAEQoKQBFKCkARigpAEcoKQBIKCkASSgpAEooKQ +512=BLKCkATCgpAE0oKQBOKCkATygpAFAoKQBRKCkAUigpAFMoKQBUKCkAVSgpAFYoKQBXKCkA +513=WCgpAFkoKQBaKCkAWygpAFwoKQBdKCkAXigpAF8oKQBgKCkAYSgpAGIoKQBjKCkAZCgpAG +514=UoKQBmKCkAZygpAGgoKQBpKCkAaigpAGsoKQBsKCkAbSgpAG4oKQBvKCkAcCgpAHEoKQBy +515=KCkAcygpAHQoKQB1KCkAdigpAHcoKQB4KCkAeSgpAHooKQB7KCkAfCgpAH0oKQB+KCkAfy +516=gpAIAoKQCBKCkAgigpAIMoKQCEKCkAhSgpAIYoKQCHKCkAiCgpAIkoKQCKKCkAiygpAIwo +517=KQCNKCkAjigpAI8oKQCQKCkAkSgpAJIoKQCTKCkAlCgpAJUoKQCWKCkAlygpAJgoKQCZKC +518=kAmigpAJsoKQCcKCkAnSgpAJ4oKQCfKCsAAwBS5dkEKCkABSgpAAYoKQAHKCkACCgpAAko +519=KQAKKCkACygpAAwoKQANKCkADigpAA8oKQAQKCkAESgpABIoKQATKCkAFCgpABUoKQAWKC +520=kAFygpABgoKQAZKCkAGigpABsoKQAcKCkAHSgpAB4oKQAfKCkAICgpACEoKQAiKCkAIygp +521=ACQoKQAlKCkAJigpACcoKQAoKCkAKSgpACooKQArKCkALCgpAC0oKQAuKCkALygpADAoKQ +522=AxKCkAMigpADMoKQA0KCkANSgpADYoKQA3KCkAOCgpADkoKQA6KCkAOygpADwoKQA9KCkA +523=PigpAD8oKQBAKCkAQSgpAEIoKQBDKCkARCgpAEUoKQBGKCkARygpAEgoKQBJKCkASigpAE +524=soKQBMKCkATSgpAE4oKQBPKCkAUCgpAFEoKQBSKCkAUygpAFQoKQBVKCkAVigpAFcoKQBY +525=KCkAWSgpAFooKQBbKCkAXCgpAF0oKQBeKCkAXygpAGAoKQBhKCkAYigpAGMoKQBkKCkAZS +526=gpAGYoKQBnKCkAaCgpAGkoKQBqKCkAaygpAGwoKQBtKCkAbigpAG8oKQBwKCkAcSgpAHIo +527=KQBzKCkAdCgpAHUoKQB2KCkAdygpAHgoKQB5KCkAeigpAHsoKQB8KCkAfSgpAH4oKQB/KC +528=kAgCgpAIEoKQCCKCkAgygpAIQoKQCFKCkAhigpAIcoKQCIKCkAiSgpAIooKQCLKCkAjCgp +529=AI0oKQCOKCkAjygpAJAoKQCRKCkAkigoAAOTAFIA//8RAAC+CwAgCAAAAAIAlABSAP//iw +530=GVAFLpAZYoKQCXKCkAmCgpAJkoKQCaKCkAmygpAJwoKQCdKCkAnigrAAQAU/UNBSgpAAYo +531=KQAHKCkACCgpAAkoKQAKKCkACygpAAwoKQANKCkADigpAA8oKQAQKCkAESgpABIoKQATKC +532=kAFCgpABUoKQAWKCkAFygpABgoKQAZKCkAGigpABsoKQAcKCkAHSgpAB4oKQAfKCkAICgp +533=ACEoKQAiKCkAIygpACQoKQAlKCkAJigpACcoKQAoKCkAKSgpACooKQArKCkALCgpAC0oKQ +534=AuKCkALygpADAoKQAxKCkAMigpADMoKQA0KCkANSgpADYoKQA3KCkAOCgpADkoKQA6KCkA +535=OygpADwoKQA9KCkAPigpAD8oKQBAKCkAQSgpAEIoKQBDKCkARCgpAEUoKQBGKCkARygpAE +536=goKQBJKCkASigpAEsoKQBMKCkATSgpAE4oKQBPKCkAUCgpAFEoKQBSKCkAUygpAFQoKQBV +537=KCkAVigpAFcoKQBYKCkAWSgpAFooKQBbKCkAXCgpAF0oKQBeKCkAXygpAGAoKQBhKCkAYi +538=gpAGMoKQBkKCkAZSgpAGYoKQBnKCkAaCgpAGkoKQBqKCkAaygpAGwoKQBtKCkAbigpAG8o +539=KQBwKCkAcSgpAHIoKQBzKCkAdCgpAHUoKQB2KCkAdygpAHgoKQB5KCkAeigpAHsoKQB8KC +540=kAfSgpAH4oKQB/KCkAgCgpAIEoKQCCKCkAgygpAIQoKQCFKCkAhigpAIcoKQCIKCkAiSgp +541=AIooKQCLKCkAjCgpAI0oKQCOKCkAjygpAJAoKQCRKCkAkigpAJMoKQCUKCkAlSgpAJYoKQ +542=CXKCkAmCgpAJkoKQCaKCkAmygpAJwoKQCdKCsABQBU9dMGKCkABygpAAgoKQAJKCkACigp +543=AAsoKQAMKCkADSgpAA4oKQAPKCkAECgpABEoKQASKCkAEygpABQoKQAVKCkAFigpABcoKQ +544=AYKCkAGSgpABooKQAbKCkAHCgpAB0oKQAeKCkAHygpACAoKQAhKCkAIigpACMoKQAkKCkA +545=JSgpACYoKQAnKCkAKCgpACkoKQAqKCkAKygpACwoKQAtKCkALigpAC8oKQAwKCkAMSgpAD +546=IoKQAzKCkANCgpADUoKQA2KCkANygpADgoKQA5KCkAOigpADsoKQA8KCkAPSgpAD4oKQA/ +547=KCkAQCgpAEEoKQBCKCkAQygpAEQoKQBFKCkARigpAEcoKQBIKCkASSgpAEooKQBLKCkATC +548=gpAE0oKQBOKCkATygpAFAoKQBRKCkAUigpAFMoKQBUKCkAVSgpAFYoKQBXKCkAWCgpAFko +549=KQBaKCkAWygpAFwoKQBdKCkAXigpAF8oKQBgKCkAYSgpAGIoKQBjKCkAZCgpAGUoKQBmKC +550=kAZygpAGgoKQBpKCkAaigpAGsoKQBsKCkAbSgpAG4oKQBvKCkAcCgpAHEoKQByKCkAcygp +551=AHQoKQB1KCkAdigpAHcoKQB4KCkAeSgpAHooKQB7KCkAfCgpAH0oKQB+KCkAfygpAIAoKQ +552=CBKCkAgigpAIMoKQCEKCkAhSgpAIYoKQCHKCkAiCgpAIkoKQCKKCkAiygpAIwoKQCNKCkA +553=jigpAI8oKQCQKCkAkSgpAJIoKQCTKCkAlCgpAJUoKQCWKCkAlygpAJgoKQCZKCkAmigpAJ +554=soKQCcKCsABgBV/dAHKCkACCgpAAkoKQAKKCkACygpAAwoKQANKCkADigpAA8oKQAQKCkA +555=ESgpABIoKQATKCkAFCgpABUoKQAWKCkAFygpABgoKQAZKCkAGigpABsoKQAcKCkAHSgpAB +556=4oKQAfKCkAICgpACEoKQAiKCkAIygpACQoKQAlKCkAJigpACcoKQAoKCkAKSgpACooKQAr +557=KCkALCgpAC0oKQAuKCkALygpADAoKQAxKCkAMigpADMoKQA0KCkANSgpADYoKQA3KCkAOC +558=gpADkoKQA6KCkAOygpADwoKQA9KCkAPigpAD8oKQBAKCkAQSgpAEIoKQBDKCkARCgpAEUo +559=KQBGKCkARygpAEgoKQBJKCkASigpAEsoKQBMKCkATSgpAE4oKQBPKCkAUCgpAFEoKQBSKC +560=kAUygpAFQoKQBVKCkAVigpAFcoKQBYKCkAWSgpAFooKQBbKCkAXCgpAF0oKQBeKCkAXygp +561=AGAoKQBhKCkAYigpAGMoKQBkKCkAZSgpAGYoKQBnKCkAaCgpAGkoKQBqKCkAaygpAGwoKQ +562=BtKCkAbigpAG8oKQBwKCkAcSgpAHIoKQBzKCkAdCgpAHUoKQB2KCkAdygpAHgoKQB5KCkA +563=eigpAHsoKQB8KCkAfSgpAH4oKQB/KCkAgCgpAIEoKQCCKCkAgygpAIQoKQCFKCkAhigpAI +564=coKQCIKCkAiSgpAIooKQCLKCkAjCgpAI0oKQCOKCkAjygpAJAoKQCRKCkAkigpAJMoKQCU +565=KCkAlSgpAJYoKQCXKCkAmCgpAJkoKQCaKCkAmygrAAcAVuXOCCgpAAkoKQAKKCkACygpAA +566=woKQANKCkADigpAA8oKQAQKCkAESgpABIoKQATKCkAFCgpABUoKQAWKCkAFygpABgoKQAZ +567=KCkAGigpABsoKQAcKCkAHSgpAB4oKQAfKCkAICgpACEoKQAiKCkAIygpACQoKQAlKCkAJi +568=gpACcoKQAoKCkAKSgpACooKQArKCkALCgpAC0oKQAuKCkALygpADAoKQAxKCkAMigpADMo +569=KQA0KCkANSgpADYoKQA3KCkAOCgpADkoKQA6KCkAOygpADwoKQA9KCkAPigpAD8oKQBAKC +570=kAQSgpAEIoKQBDKCkARCgpAEUoKQBGKCkARygpAEgoKQBJKCkASigpAEsoKQBMKCkATSgp +571=AE4oKQBPKCkAUCgpAFEoKQBSKCkAUygpAFQoKQBVKCkAVigpAFcoKQBYKCkAWSgpAFooKQ +572=BbKCkAXCgpAF0oKQBeKCkAXygpAGAoKQBhKCkAYigpAGMoKQBkKCkAZSgpAGYoKQBnKCkA +573=aCgpAGkoKQBqKCkAaygpAGwoKQBtKCkAbigpAG8oKQBwKCkAcSgpAHIoKQBzKCkAdCgpAH +574=UoKQB2KCkAdygpAHgoKQB5KCkAeigpAHsoKQB8KCkAfSgpAH4oKQB/KCkAgCgpAIEoKQCC +575=KCkAgygpAIQoKQCFKCkAhigpAIcoKQCIKCkAiSgpAIooKQCLKCkAjCgpAI0oKQCOKCkAjy +576=gpAJAoKQCRKCkAkigpAJMoKQCUKCkAlSgpAJYoKQCXKCkAmCgpAJkoKQCaKCsACABX7csJ +577=KCkACigpAAsoKQAMKCkADSgpAA4oKQAPKCkAECgpABEoKQASKCkAEygpABQoKQAVKCkAFi +578=gpABcoKQAYKCkAGSgpABooKQAbKCkAHCgpAB0oKQAeKCkAHygpACAoKQAhKCkAIigpACMo +579=KQAkKCkAJSgpACYoKQAnKCkAKCgpACkoKQAqKCkAKygpACwoKQAtKCkALigpAC8oKQAwKC +580=kAMSgpADIoKQAzKCkANCgpADUoKQA2KCkANygpADgoKQA5KCkAOigpADsoKQA8KCkAPSgp +581=AD4oKQA/KCkAQCgpAEEoKQBCKCkAQygpAEQoKQBFKCkARigpAEcoKQBIKCkASSgpAEooKQ +582=BLKCkATCgpAE0oKQBOKCkATygpAFAoKQBRKCkAUigpAFMoKQBUKCkAVSgpAFYoKQBXKCkA +583=WCgpAFkoKQBaKCkAWygpAFwoKQBdKCkAXigpAF8oKQBgKCkAYSgpAGIoKQBjKCkAZCgpAG +584=UoKQBmKCkAZygpAGgoKQBpKCkAaigpAGsoKQBsKCkAbSgpAG4oKQBvKCkAcCgpAHEoKQBy +585=KCkAcygpAHQoKQB1KCkAdigpAHcoKQB4KCkAeSgpAHooKQB7KCkAfCgpAH0oKQB+KCkAfy +586=gpAIAoKQCBKCkAgigpAIMoKQCEKCkAhSgpAIYoKQCHKCgAC4gAVwD//wAAAAIAiQBXEQAA +587=vQsAIAgA//8AAAACAIoAV+kBiygpAIwoKQCNKCkAjigpAI8oKQCQKCkAkSgpAJIoKQCTKC +588=kAlCgpAJUoKQCWKCkAlygpAJgoKQCZKCsACQBY/RUKKCkACygpAAwoKQANKCkADigpAA8o +589=KQAQKCkAESgpABIoKQATKCkAFCgpABUoKQAWKCkAFygpABgoKQAZKCkAGigpABsoKQAcKC +590=kAHSgpAB4oKQAfKCkAICgpACEoKQAiKCkAIygpACQoKQAlKCkAJigpACcoKQAoKCkAKSgp +591=ACooKQArKCkALCgpAC0oKQAuKCkALygpADAoKQAxKCkAMigpADMoKQA0KCkANSgpADYoKQ +592=A3KCkAOCgpADkoKQA6KCkAOygpADwoKQA9KCkAPigpAD8oKQBAKCkAQSgpAEIoKQBDKCkA +593=RCgpAEUoKQBGKCkARygpAEgoKQBJKCkASigpAEsoKQBMKCkATSgpAE4oKQBPKCkAUCgpAF +594=EoKQBSKCkAUygpAFQoKQBVKCkAVigpAFcoKQBYKCkAWSgpAFooKQBbKCkAXCgpAF0oKQBe +595=KCkAXygpAGAoKQBhKCkAYigpAGMoKQBkKCkAZSgpAGYoKQBnKCkAaCgpAGkoKQBqKCkAay +596=gpAGwoKQBtKCkAbigpAG8oKQBwKCkAcSgpAHIoKQBzKCkAdCgpAHUoKQB2KCkAdygpAHgo +597=KQB5KCkAeigpAHsoKQB8KCkAfSgpAH4oKQB/KCkAgCgpAIEoKQCCKCkAgygpAIQoKQCFKC +598=kAhigpAIcoKQCIKCkAiSgpAIooKQCLKCkAjCgpAI0oKQCOKCkAjygpAJAoKQCRKCkAkigp +599=AJMoKQCUKCkAlSgpAJYoKQCXKCkAmCgoAAEKAFkAyd0LKCkADCgpAA0oKQAOKCkADygpAB +600=AoKQARKCkAEigpABMoKQAUKCkAFSgpABYoKQAXKCkAGCgpABkoKQAaKCkAGygpABwoKQAd +601=KCkAHigpAB8oKQAgKCkAISgpACIoKQAjKCkAJCgpACUoKQAmKCkAJygpACgoKQApKCkAKi +602=gpACsoKQAsKCkALSgpAC4oKQAvKCkAMCgpADEoKQAyKCkAMygpADQoKQA1KCkANigpADco +603=KQA4KCkAOSgpADooKQA7KCkAPCgpAD0oKQA+KCkAPygpAEAoKQBBKCkAQigpAEMoKQBEKC +604=kARSgpAEYoKQBHKCkASCgpAEkoKQBKKCkASygpAEwoKQBNKCkATigpAE8oKQBQKCkAUSgp +605=AFIoKQBTKCkAVCgpAFUoKQBWKCkAVygpAFgoKQBZKCkAWigpAFsoKQBcKCkAXSgpAF4oKQ +606=BfKCkAYCgpAGEoKQBiKCkAYygpAGQoKQBlKCkAZigpAGcoKQBoKCkAaSgpAGooKQBrKCkA +607=bCgpAG0oKQBuKCkAbygpAHAoKQBxKCkAcigpAHMoKQB0KCkAdSgpAHYoKQB3KCkAeCgpAH +608=koKQB6KCkAeygpAHwoKQB9KCkAfigpAH8oKQCAKCkAgSgpAIIoKQCDKCkAhCgpAIUoKQCG +609=KCkAhygpAIgoKQCJKCkAiigpAIsoKQCMKCkAjSgpAI4oKQCPKCkAkCgpAJEoKQCSKCkAky +610=gpAJQoKQCVKCkAligpAJcoKwALAFrlwwwoKQANKCkADigpAA8oKQAQKCkAESgpABIoKQAT +611=KCkAFCgpABUoKQAWKCkAFygpABgoKQAZKCkAGigpABsoKQAcKCkAHSgpAB4oKQAfKCkAIC +612=gpACEoKQAiKCkAIygpACQoKQAlKCkAJigpACcoKQAoKCkAKSgpACooKQArKCkALCgpAC0o +613=KQAuKCkALygpADAoKQAxKCkAMigpADMoKQA0KCkANSgpADYoKQA3KCkAOCgpADkoKQA6KC +614=kAOygpADwoKQA9KCkAPigpAD8oKQBAKCkAQSgpAEIoKQBDKCkARCgpAEUoKQBGKCkARygp +615=AEgoKQBJKCkASigpAEsoKQBMKCkATSgpAE4oKQBPKCkAUCgpAFEoKQBSKCkAUygpAFQoKQ +616=BVKCkAVigpAFcoKQBYKCkAWSgpAFooKQBbKCkAXCgpAF0oKQBeKCkAXygpAGAoKQBhKCkA +617=YigpAGMoKQBkKCkAZSgpAGYoKQBnKCkAaCgpAGkoKQBqKCkAaygpAGwoKQBtKCkAbigpAG +618=8oKQBwKCkAcSgpAHIoKQBzKCkAdCgpAHUoKQB2KCkAdygpAHgoKQB5KCkAeigpAHsoKQB8 +619=KCkAfSgpAH4oKQB/KCkAgCgpAIEoKQCCKCkAgygpAIQoKQCFKCkAhigpAIcoKQCIKCkAiS +620=gpAIooKQCLKCkAjCgpAI0oKQCOKCkAjygpAJAoKQCRKCkAkigpAJMoKQCUKCkAlSgpAJYo +621=KwAMAFvtwA0oKQAOKCkADygpABAoKQARKCkAEigpABMoKQAUKCkAFSgpABYoKQAXKCkAGC +622=gpABkoKQAaKCkAGygpABwoKQAdKCkAHigpAB8oKQAgKCkAISgpACIoKQAjKCkAJCgpACUo +623=KQAmKCkAJygpACgoKQApKCkAKigpACsoKQAsKCkALSgpAC4oKQAvKCkAMCgpADEoKQAyKC +624=kAMygpADQoKQA1KCkANigpADcoKQA4KCkAOSgpADooKQA7KCkAPCgpAD0oKQA+KCkAPygp +625=AEAoKQBBKCkAQigpAEMoKQBEKCkARSgpAEYoKQBHKCkASCgpAEkoKQBKKCkASygpAEwoKQ +626=BNKCkATigpAE8oKQBQKCkAUSgpAFIoKQBTKCkAVCgpAFUoKQBWKCkAVygpAFgoKQBZKCkA +627=WigpAFsoKQBcKCkAXSgpAF4oKQBfKCkAYCgpAGEoKQBiKCkAYygpAGQoKQBlKCkAZigpAG +628=coKQBoKCkAaSgpAGooKQBrKCkAbCgpAG0oKQBuKCkAbygpAHAoKQBxKCkAcigpAHMoKQB0 +629=KCkAdSgpAHYoKQB3KCkAeCgpAHkoKQB6KCkAeygpAHwoKQB9KCkAfigpAH8oKQCAKCkAgS +630=gpAIIoKQCDKCkAhCgpAIUoKQCGKCkAhygpAIgoKQCJKCkAiigpAIsoKQCMKCkAjSgpAI4o +631=KQCPKCkAkCgpAJEoKQCSKCkAkygpAJQoKQCVKCsADQBc9b0OKCkADygpABAoKQARKCkAEi +632=gpABMoKQAUKCkAFSgpABYoKQAXKCkAGCgpABkoKQAaKCkAGygpABwoKQAdKCkAHigpAB8o +633=KQAgKCkAISgpACIoKQAjKCkAJCgpACUoKQAmKCkAJygpACgoKQApKCkAKigpACsoKQAsKC +634=kALSgpAC4oKQAvKCkAMCgpADEoKQAyKCkAMygpADQoKQA1KCkANigpADcoKQA4KCkAOSgp +635=ADooKQA7KCkAPCgpAD0oKQA+KCkAPygpAEAoKQBBKCkAQigpAEMoKQBEKCkARSgpAEYoKQ +636=BHKCkASCgpAEkoKQBKKCkASygpAEwoKQBNKCkATigpAE8oKQBQKCkAUSgpAFIoKQBTKCkA +637=VCgpAFUoKQBWKCkAVygpAFgoKQBZKCkAWigpAFsoKQBcKCkAXSgpAF4oKQBfKCkAYCgpAG +638=EoKQBiKCkAYygpAGQoKQBlKCkAZigpAGcoKQBoKCkAaSgpAGooKQBrKCkAbCgpAG0oKQBu +639=KCkAbygpAHAoKQBxKCkAcigpAHMoKQB0KCkAdSgpAHYoKQB3KCkAeCgpAHkoKQB6KCkAey +640=gpAHwoKQB9KCkAfigpAH8oKQCAKCkAgSgpAIIoKQCDKCkAhCgpAIUoKQCGKCkAhygpAIgo +641=KQCJKCkAiigpAIsoKQCMKCkAjSgpAI4oKQCPKCkAkCgpAJEoKQCSKCkAkygpAJQoKwAOAF +642=39ug8oKQAQKCkAESgpABIoKQATKCkAFCgpABUoKQAWKCkAFygpABgoKQAZKCkAGigpABso +643=KQAcKCkAHSgpAB4oKQAfKCkAICgpACEoKQAiKCkAIygpACQoKQAlKCkAJigpACcoKQAoKC +644=gACCkAXQD//wAAAAIAEQAAvwsAIAkqAF0A//8AAAACACsoKQAsKCkALSgpAC4oKQAvKCkA +645=MCgpADEoKQAyKCkAMygpADQoKQA1KCkANigpADcoKQA4KCkAOSgpADooKQA7KCkAPCgpAD +646=0oKQA+KCkAPygpAEAoKQBBKCkAQigpAEMoKQBEKCkARSgpAEYoKQBHKCkASCgpAEkoKQBK +647=KCkASygpAEwoKQBNKCkATigpAE8oKQBQKCkAUSgpAFIoKQBTKCkAVCgpAFUoKQBWKCkAVy +648=gpAFgoKQBZKCkAWigpAFsoKQBcKCkAXSgpAF4oKQBfKCkAYCgpAGEoKQBiKCkAYygpAGQo +649=KQBlKCkAZigpAGcoKQBoKCkAaSgpAGooKQBrKCkAbCgpAG0oKQBuKCkAbygpAHAoKQBxKC +650=kAcigpAHMoKQB0KCkAdSgpAHYoKQB3KCkAeCgpAHkoKQB6KCkAeygpAHwoKQB9KCkAfigp +651=AH8oKQCAKCkAgSgpAIIoKQCDKCkAhCgpAIUoKQCGKCkAhygpAIgoKQCJKCkAiigpAIsoKQ +652=CMKCkAjSgpAI4oKQCPKCkAkCgpAJEoKQCSKCkAkygoAAUPAF4A//8AAFWRECgpABEoKQAS +653=KCkAEygpABQoKQAVKCkAFigpABcoKQAYKCkAGSgpABooKQAbKCkAHCgpAB0oKQAeKCkAHy +654=gpACAoKQAhKCkAIigpACMoKQAkKCkAJSgpACYoKQAnKCkAKCgpACkoKQAqKCkAKygpACwo +655=KQAtKCkALigpAC8oKQAwKCkAMSgpADIoKQAzKCkANCgpADUoKQA2KCkANygpADgoKQA5KC +656=kAOigpADsoKQA8KCkAPSgpAD4oKQA/KCkAQCgpAEEoKQBCKCkAQygpAEQoKQBFKCkARigp +657=AEcoKQBIKCkASSgpAEooKQBLKCkATCgpAE0oKQBOKCkATygpAFAoKQBRKCkAUigpAFMoKQ +658=BUKCkAVSgpAFYoKQBXKCkAWCgpAFkoKQBaKCkAWygpAFwoKQBdKCkAXigpAF8oKQBgKCkA +659=YSgpAGIoKQBjKCkAZCgpAGUoKQBmKCkAZygpAGgoKQBpKCkAaigpAGsoKQBsKCkAbSgpAG +660=4oKQBvKCkAcCgpAHEoKQByKCkAcygpAHQoKQB1KCkAdigpAHcoKQB4KCkAeSgpAHooKQB7 +661=KCkAfCgpAH0oKQB+KCkAfygpAIAoKQCBKCkAgigpAIMoKQCEKCkAhSgpAIYoKQCHKCkAiC +662=gpAIkoKQCKKCkAiygpAIwoKQCNKCkAjigpAI8oKQCQKCkAkSgpAJIoKwAQAF/ttREoKQAS +663=KCkAEygpABQoKQAVKCkAFigpABcoKQAYKCkAGSgpABooKQAbKCkAHCgpAB0oKQAeKCkAHy +664=gpACAoKQAhKCkAIigpACMoKQAkKCkAJSgpACYoKQAnKCkAKCgpACkoKQAqKCkAKygpACwo +665=KQAtKCkALigpAC8oKQAwKCkAMSgpADIoKQAzKCkANCgpADUoKQA2KCkANygpADgoKQA5KC +666=kAOigpADsoKQA8KCkAPSgpAD4oKQA/KCkAQCgpAEEoKQBCKCkAQygpAEQoKQBFKCkARigp +667=AEcoKQBIKCkASSgpAEooKQBLKCkATCgpAE0oKQBOKCkATygpAFAoKQBRKCkAUigpAFMoKQ +668=BUKCkAVSgpAFYoKQBXKCkAWCgpAFkoKQBaKCkAWygpAFwoKQBdKCkAXigpAF8oKQBgKCkA +669=YSgpAGIoKQBjKCkAZCgpAGUoKQBmKCkAZygpAGgoKQBpKCkAaigpAGsoKQBsKCkAbSgpAG +670=4oKQBvKCkAcCgpAHEoKQByKCkAcygpAHQoKQB1KCkAdigpAHcoKQB4KCkAeSgpAHooKQB7 +671=KCkAfCgpAH0oKQB+KCkAfygpAIAoKQCBKCkAgigpAIMoKQCEKCkAhSgpAIYoKQCHKCkAiC +672=gpAIkoKQCKKCkAiygpAIwoKQCNKCkAjigpAI8oKQCQKCkAkSgrABEAYPWyEigpABMoKQAU +673=KCkAFSgpABYoKQAXKCkAGCgpABkoKQAaKCkAGygpABwoKQAdKCkAHigpAB8oKQAgKCkAIS +674=gpACIoKQAjKCkAJCgpACUoKQAmKCkAJygpACgoKQApKCkAKigpACsoKQAsKCkALSgpAC4o +675=KQAvKCkAMCgpADEoKQAyKCkAMygpADQoKQA1KCkANigpADcoKQA4KCkAOSgpADooKQA7KC +676=kAPCgpAD0oKQA+KCkAPygpAEAoKQBBKCkAQigpAEMoKQBEKCkARSgpAEYoKQBHKCkASCgp +677=AEkoKQBKKCkASygpAEwoKQBNKCkATigpAE8oKQBQKCkAUSgpAFIoKQBTKCkAVCgpAFUoKQ +678=BWKCkAVygpAFgoKQBZKCkAWigpAFsoKQBcKCkAXSgpAF4oKQBfKCkAYCgpAGEoKQBiKCkA +679=YygpAGQoKQBlKCkAZigpAGcoKQBoKCkAaSgpAGooKQBrKCkAbCgpAG0oKQBuKCkAbygpAH +680=AoKQBxKCkAcigpAHMoKQB0KCkAdSgpAHYoKQB3KCkAeCgpAHkoKQB6KCkAeygpAHwoKQB9 +681=KCkAfigpAH8oKQCAKCkAgSgpAIIoKQCDKCkAhCgpAIUoKQCGKCkAhygpAIgoKQCJKCkAii +682=gpAIsoKQCMKCkAjSgpAI4oKQCPKCkAkCgrABIAYf2vEygpABQoKQAVKCkAFigpABcoKQAY +683=KCkAGSgpABooKQAbKCkAHCgpAB0oKQAeKCkAHygpACAoKQAhKCkAIigpACMoKQAkKCkAJS +684=gpACYoKQAnKCkAKCgpACkoKQAqKCkAKygpACwoKQAtKCkALigpAC8oKQAwKCkAMSgpADIo +685=KQAzKCkANCgpADUoKQA2KCkANygpADgoKQA5KCkAOigpADsoKQA8KCkAPSgpAD4oKQA/KC +686=kAQCgpAEEoKQBCKCkAQygpAEQoKQBFKCkARigpAEcoKQBIKCkASSgpAEooKQBLKCkATCgp +687=AE0oKQBOKCkATygpAFAoKQBRKCkAUigpAFMoKQBUKCkAVSgpAFYoKQBXKCkAWCgpAFkoKQ +688=BaKCkAWygpAFwoKQBdKCkAXigpAF8oKQBgKCkAYSgpAGIoKQBjKCkAZCgpAGUoKQBmKCkA +689=ZygpAGgoKQBpKCkAaigpAGsoKQBsKCkAbSgpAG4oKQBvKCkAcCgpAHEoKQByKCkAcygpAH +690=QoKQB1KCkAdigpAHcoKQB4KCkAeSgpAHooKQB7KCkAfCgpAH0oKQB+KCkAfygpAIAoKQCB +691=KCkAgigpAIMoKQCEKCkAhSgpAIYoKQCHKCkAiCgpAIkoKQCKKCkAiygpAIwoKQCNKCkAji +692=gpAI8oKwATAGLlrRQoKQAVKCkAFigpABcoKQAYKCkAGSgpABooKQAbKCkAHCgpAB0oKQAe +693=KCkAHygpACAoKQAhKCkAIigpACMoKQAkKCkAJSgpACYoKQAnKCkAKCgpACkoKQAqKCkAKy +694=gpACwoKQAtKCkALigpAC8oKQAwKCkAMSgpADIoKQAzKCkANCgpADUoKQA2KCkANygpADgo +695=KQA5KCkAOigpADsoKQA8KCkAPSgpAD4oKQA/KCkAQCgpAEEoKQBCKCkAQygpAEQoKQBFKC +696=kARigpAEcoKQBIKCkASSgpAEooKQBLKCkATCgpAE0oKQBOKCkATygpAFAoKQBRKCkAUigp +697=AFMoKQBUKCkAVSgpAFYoKQBXKCkAWCgpAFkoKQBaKCkAWygpAFwoKQBdKCkAXigpAF8oKQ +698=BgKCkAYSgpAGIoKQBjKCkAZCgpAGUoKQBmKCkAZygpAGgoKQBpKCkAaigpAGsoKQBsKCkA +699=bSgpAG4oKQBvKCkAcCgpAHEoKQByKCkAcygpAHQoKQB1KCkAdigpAHcoKQB4KCkAeSgpAH +700=ooKQB7KCkAfCgpAH0oKQB+KCkAfygpAIAoKQCBKCkAgigpAIMoKQCEKCkAhSgpAIYoKQCH +701=KCkAiCgpAIkoKQCKKCkAiygpAIwoKAAFjQBiAP//AAARAADlCwAgCAACAI4AYgD//wAASA +702=ECFABjAP+pARUoKQAWKCkAFygpABgoKQAZKCkAGigpABsoKQAcKCkAHSgpAB4oKQAfKCkA +703=ICgpACEoKQAiKCkAIygpACQoKQAlKCkAJigpACcoKQAoKCkAKSgpACooKQArKCkALCgpAC +704=0oKQAuKCkALygpADAoKQAxKCkAMigpADMoKQA0KCkANSgpADYoKQA3KCkAOCgpADkoKQA6 +705=KCkAOygpADwoKQA9KCkAPigpAD8oKQBAKCkAQSgpAEIoKQBDKCkARCgpAEUoKQBGKCkARy +706=gpAEgoKQBJKCkASigpAEsoKQBMKCkATSgpAE4oKQBPKCkAUCgpAFEoKQBSKCkAUygpAFQo +707=KQBVKCkAVigpAFcoKQBYKCkAWSgpAFooKQBbKCkAXCgpAF0oKQBeKCkAXygpAGAoKQBhKC +708=kAYigpAGMoKQBkKCkAZSgpAGYoKQBnKCkAaCgpAGkoKQBqKCkAaygpAGwoKQBtKCkAbigp +709=AG8oKQBwKCkAcSgpAHIoKQBzKCkAdCgpAHUoKQB2KCkAdygpAHgoKQB5KCkAeigpAHsoKQ +710=B8KCkAfSgpAH4oKQB/KCkAgCgpAIEoKQCCKCkAgygpAIQoKQCFKCkAhigpAIcoKQCIKCkA +711=iSgpAIooKQCLKCkAjCgpAI0oKwAVAGT1pxYoKQAXKCkAGCgpABkoKQAaKCkAGygpABwoKQ +712=AdKCkAHigpAB8oKQAgKCkAISgpACIoKQAjKCkAJCgpACUoKQAmKCkAJygpACgoKQApKCkA +713=KigpACsoKQAsKCkALSgpAC4oKQAvKCkAMCgpADEoKQAyKCkAMygpADQoKQA1KCkANigpAD +714=coKQA4KCkAOSgpADooKQA7KCkAPCgpAD0oKQA+KCkAPygpAEAoKQBBKCkAQigpAEMoKQBE +715=KCkARSgpAEYoKQBHKCkASCgpAEkoKQBKKCkASygpAEwoKQBNKCkATigpAE8oKQBQKCkAUS +716=gpAFIoKQBTKCkAVCgpAFUoKQBWKCkAVygpAFgoKQBZKCkAWigpAFsoKQBcKCkAXSgpAF4o +717=KQBfKCkAYCgpAGEoKQBiKCkAYygpAGQoKQBlKCkAZigpAGcoKQBoKCkAaSgpAGooKQBrKC +718=kAbCgpAG0oKQBuKCkAbygpAHAoKQBxKCkAcigpAHMoKQB0KCkAdSgpAHYoKQB3KCkAeCgp +719=AHkoKQB6KCkAeygpAHwoKQB9KCkAfigpAH8oKQCAKCkAgSgpAIIoKQCDKCkAhCgpAIUoKQ +720=CGKCkAhygpAIgoKQCJKCkAiigpAIsoKQCMKCsAFgBl/aQXKCkAGCgpABkoKQAaKCkAGygp +721=ABwoKQAdKCkAHigpAB8oKQAgKCkAISgpACIoKQAjKCkAJCgpACUoKQAmKCkAJygpACgoKQ +722=ApKCkAKigpACsoKQAsKCkALSgpAC4oKQAvKCkAMCgpADEoKQAyKCkAMygpADQoKQA1KCkA +723=NigpADcoKQA4KCkAOSgpADooKQA7KCkAPCgpAD0oKQA+KCkAPygpAEAoKQBBKCkAQigpAE +724=MoKQBEKCkARSgpAEYoKQBHKCkASCgpAEkoKQBKKCkASygpAEwoKQBNKCkATigpAE8oKQBQ +725=KCkAUSgpAFIoKQBTKCkAVCgpAFUoKQBWKCkAVygpAFgoKQBZKCkAWigpAFsoKQBcKCkAXS +726=gpAF4oKQBfKCkAYCgpAGEoKQBiKCkAYygpAGQoKQBlKCkAZigpAGcoKQBoKCkAaSgpAGoo +727=KQBrKCkAbCgpAG0oKQBuKCkAbygpAHAoKQBxKCkAcigpAHMoKQB0KCkAdSgpAHYoKQB3KC +728=kAeCgpAHkoKQB6KCkAeygpAHwoKQB9KCkAfigpAH8oKQCAKCkAgSgpAIIoKQCDKCkAhCgp +729=AIUoKQCGKCkAhygpAIgoKQCJKCkAiigpAIsoKwAXAGblohgoKQAZKCkAGigpABsoKQAcKC +730=kAHSgpAB4oKQAfKCkAICgpACEoKQAiKCkAIygpACQoKQAlKCkAJigpACcoKQAoKCkAKSgp +731=ACooKQArKCkALCgpAC0oKQAuKCkALygpADAoKQAxKCkAMigpADMoKQA0KCkANSgpADYoKQ +732=A3KCkAOCgpADkoKQA6KCkAOygpADwoKQA9KCkAPigpAD8oKQBAKCkAQSgpAEIoKQBDKCkA +733=RCgpAEUoKQBGKCkARygpAEgoKQBJKCkASigpAEsoKQBMKCkATSgpAE4oKQBPKCkAUCgpAF +734=EoKQBSKCkAUygpAFQoKQBVKCkAVigpAFcoKQBYKCkAWSgpAFooKQBbKCkAXCgpAF0oKQBe +735=KCkAXygpAGAoKQBhKCkAYigpAGMoKQBkKCkAZSgpAGYoKQBnKCkAaCgpAGkoKQBqKCkAay +736=gpAGwoKQBtKCkAbigpAG8oKQBwKCkAcSgpAHIoKQBzKCkAdCgpAHUoKQB2KCkAdygpAHgo +737=KQB5KCkAeigpAHsoKQB8KCkAfSgpAH4oKQB/KCkAgCgpAIEoKQCCKCkAgygpAIQoKQCFKC +738=kAhigpAIcoKQCIKCkAiSgpAIooKwAYAGftnxkoKQAaKCkAGygpABwoKQAdKCkAHigpAB8o +739=KQAgKCkAISgpACIoKQAjKCkAJCgpACUoKQAmKCkAJygpACgoKQApKCkAKigpACsoKQAsKC +740=kALSgpAC4oKQAvKCkAMCgpADEoKQAyKCkAMygpADQoKQA1KCkANigpADcoKQA4KCkAOSgp +741=ADooKQA7KCkAPCgpAD0oKQA+KCkAPygpAEAoKQBBKCkAQigpAEMoKQBEKCkARSgpAEYoKQ +742=BHKCkASCgpAEkoKQBKKCkASygpAEwoKQBNKCkATigpAE8oKQBQKCkAUSgpAFIoKQBTKCkA +743=VCgpAFUoKQBWKCkAVygpAFgoKQBZKCkAWigpAFsoKQBcKCkAXSgpAF4oKQBfKCkAYCgpAG +744=EoKQBiKCkAYygpAGQoKQBlKCkAZigpAGcoKQBoKCkAaSgpAGooKQBrKCkAbCgpAG0oKQBu +745=KCkAbygpAHAoKQBxKCkAcigpAHMoKQB0KCkAdSgpAHYoKQB3KCkAeCgpAHkoKQB6KCkAey +746=gpAHwoKQB9KCkAfigpAH8oKQCAKCkAgSgpAIIoKQCDKCkAhCgpAIUoKQCGKCkAhygpAIgo +747=KQCJKCsAGQBo9ZwaKCkAGygpABwoKQAdKCkAHigpAB8oKQAgKCkAISgpACIoKQAjKCkAJC +748=gpACUoKQAmKCkAJygpACgoKQApKCkAKigpACsoKQAsKCkALSgpAC4oKQAvKCkAMCgpADFI +749=AQVuBQAAAAYAMsgBAQEGADNJAW21AjRJAWipATXIAQEBAgA2SAElYWo3KCkAOCgpADkoKQ +750=A6KCkAOygpADwoKQA9KCkAPigpAD8oKQBAKCkAQSgpAEIoKQBDKCkARCgpAEUoKQBGKCkA +751=RygpAEgoKQBJKCkASigpAEsoKQBMKCkATSgpAE4oKQBPKCkAUCgpAFEoKQBSKCkAUygpAF +752=QoKQBVKCkAVigpAFcoKQBYKCkAWSgpAFooKQBbKCkAXCgpAF0oKQBeKCkAXygpAGAoKQBh +753=KCkAYigpAGMoKQBkKCkAZSgpAGYoKQBnKCkAaCgpAGkoKQBqKCkAaygpAGwoKQBtKCkAbi +754=gpAG8oKQBwKCkAcSgpAHIoKQBzKCkAdCgpAHUoKQB2KCkAdygpAHgoKQB5KCkAeigpAHso +755=KQB8KCkAfSgpAH4oKQB/KCkAgCgpAIEoKQCCKCkAgygpAIQoKQCFKCkAhigpAIcoKQCIKC +756=gAARoAaQDBchsoKQAcKCkAHSgpAB4oKQAfKCkAICgpACEoKQAiKCkAIygpACQoKQAlKCkA +757=JigpACcoKQAoKCkAKSgpACooKQArKCkALCgpAC0oKQAuKCkALygpADAoKQAxSAFxmAJxmG +758=lQmUgAcJgnKQA0QARMAnWcNUgBfpkEBlCYVFJImwEFAgA3VALZJzgoKQA5KCkAOigpADso +759=KQA8KCkAPSgpAD4oKQA/KCkAQCgpAEEoKQBCKCgAAkMAaQD/EQAAHQwAIAj/AAAAAgBEAG +760=kA/6oBRQAnKQBGKCkARygpAEgoKQBJKCkASigpAEsoKQBMKCkATSgpAE4oKQBPKCkAUCgp +761=AFEoKQBSKCkAUygpAFQoKQBVKCkAVigpAFcoKQBYKCkAWSgpAFooKQBbKCkAXCgpAF0oKQ +762=BeKCkAXygpAGAoKQBhKCkAYigpAGMoKQBkKCkAZSgpAGYoKQBnKCkAaCgpAGkoKQBqKCkA +763=aygpAGwoKQBtKCkAbigpAG8oKQBwKCkAcSgpAHIoKQBzKCkAdCgpAHUoKQB2KCkAdygpAH +764=goKQB5KCkAeigpAHsoKQB8KCkAfSgpAH4oKQB/KCkAgCgpAIEoKQCCKCkAgygpAIQoKQCF +765=KCkAhigpAIcoKwAbAGrtXRwoKQAdKCkAHigpAB8oKQAgKCkAISgpACIoKQAjKCkAJCgpAC +766=UoKQAmKCkAJygpACgoKQApKCkAKigpACsoKQAsKCkALSgpAC4oKQAvKCkAMCgpADFKAXYF +767=Q30GADJIAUABAgAABgAzKCkANCgpADVJAWGtBTbIAQEBAgA3SAHdgzgoKQA5KCkAOigpAD +768=soKQA8KCkAPSgpAD4oKQA/KCkAQCgpAEEoKQBCKCkAQygpAEQoKQBFKCkARigpAEcoKQBI +769=KCkASSgpAEooKQBLKCkATCgpAE0oKQBOKCkATygpAFAoKQBRKCkAUigpAFMoKQBUKCkAVS +770=gpAFYoKQBXKCkAWCgpAFkoKQBaKCkAWygpAFwoKQBdKCkAXigpAF8oKQBgKCkAYSgpAGIo +771=KQBjKCkAZCgpAGUoKQBmKCkAZygpAGgoKQBpKCkAaigpAGsoKQBsKCkAbSgpAG4oKQBvKC +772=kAcCgpAHEoKQByKCkAcygpAHQoKQB1KCkAdigpAHcoKQB4KCkAeSgpAHooKQB7KCkAfCgp +773=AH0oKQB+KCkAfygpAIAoKQCBKCkAgigpAIMoKQCEKCkAhSgpAIYoKwAcAGvtlB0oKQAeKC +774=kAHygpACAoKQAhKCkAIigpACMoKQAkKCkAJSgpACYoKQAnKCkAKCgpACkoKQAqKCkAKygp +775=ACwoKQAtKCkALigpAC8oKQAwKCkAMUgBYZMBYZNrnJKAk9MfBgA0QASbAgYANUkBYUCTAQ +776=IGADbJAQNgk88FAgA4VALNlDkoKQA6KCkAOygpADwoKQA9KCkAPigpAD8oKQBAKCkAQSgp +777=AEIoKQBDKCkARCgpAEUoKQBGKCkARygpAEgoKQBJKCkASigpAEsoKQBMKCkATSgpAE4oKQ +778=BPKCkAUCgpAFEoKQBSKCkAUygpAFQoKQBVKCkAVigpAFcoKQBYKCkAWSgpAFooKQBbKCkA +779=XCgpAF0oKQBeKCkAXygpAGAoKQBhKCkAYigpAGMoKQBkKCkAZSgpAGYoKQBnKCkAaCgpAG +780=koKQBqKCkAaygpAGwoKQBtKCkAbigpAG8oKQBwKCkAcSgpAHIoKQBzKCkAdCgpAHUoKQB2 +781=KCkAdygpAHgoKQB5KCkAeigpAHsoKQB8KCkAfSgpAH4oKQB/KCkAgCgpAIEoKQCCKCkAgy +782=gpAIQoKQCFKCsAHQBs9ZEeKCkAHygpACAoKQAhKCkAIigpACMoKQAkKCkAJSgpACYoKQAn +783=KCkAKCgpACkoKQAqKCkAKygpACwoKQAtKCkALigpAC8oKQAwKCkAMUgBaJBdjjJIAdGNMy +784=gpADQoKQA1SAFokE0FNsgBI2wkJ3UEOFQCJwgSJ1UAOihVADsoKQA8KCkAPSgpAD4oKQA/ +785=KCkAQCgpAEEoKQBCKCkAQygpAEQoKQBFKCkARigpAEcoKQBIKCkASSgpAEooKQBLKCkATC +786=gpAE0oKQBOKCkATygpAFAoKQBRKCkAUigpAFMoKQBUKCkAVSgpAFYoKQBXKCkAWCgpAFko +787=KQBaKCkAWygpAFwoKQBdKCkAXigpAF8oKQBgKCkAYSgpAGIoKQBjKCkAZCgpAGUoKQBmKC +788=kAZygpAGgoKQBpKCkAaigpAGsoKQBsKCkAbSgpAG4oKQBvKCkAcCgpAHEoKQByKCkAcygp +789=AHQoKQB1KCkAdigpAHcoKQB4KCkAeSgpAHooKQB7KCkAfCgpAH0oKQB+KCkAfygpAIAoKQ +790=CBKCkAgigpAIMoKQCEKCsAHgBt/Y4fKCkAICgpACEoKQAiKCkAIygpACQoKQAlKCkAJigp +791=ACcoKQAoKCkAKSgpACooKQArKCkALCgpAC0oKQAuKCkALygpADAoKQAxSAFxjQEivSNtjI +792=1NiTNUAowBIrwjJ1UANVUCYSa+I20AaAEjvCMnSQQ4QAQnsBEnVQA6KFUAOygpADwoKQA9 +793=KCkAPigpAD8oKQBAKCkAQSgpAEIoKQBDKCkARCgpAEUoKQBGKCkARygpAEgoKQBJKCkASi +794=gpAEsoKQBMKCkATSgpAE4oKQBPKCkAUCgpAFEoKQBSKCkAUygpAFQoKQBVKCkAVigpAFco +795=KQBYKCkAWSgpAFooKQBbKCkAXCgpAF0oKQBeKCkAXygpAGAoKQBhKCkAYigpAGMoKQBkKC +796=kAZSgpAGYoKQBnKCkAaCgpAGkoKQBqKCkAaygpAGwoKQBtKCkAbigpAG8oKQBwKCkAcSgp +797=AHIoKQBzKCkAdCgpAHUoKQB2KCkAdygpAHgoKQB5KCkAeigpAHsoKQB8KCkAfSgpAH4oKQ +798=B/KCkAgCgpAIEoKQCCKCkAgygrAB8AbuWMICgpACEoKQAiKCkAIygpACQoKQAlKCkAJigp +799=ACcoKQAoKCkAKSgpACooKQArKCkALCgpAC0oKQAuKCkALygpADAoKQAxSAF4ik2JMkgBzY +800=kzSAGcg3mKbpACQQQ1VAJsiUkBNsgBIwwjJx0EOFQCmwYCADkoKQA6KCkAOygpADwoKQA9 +801=KCkAPigpAD8oKQBAKCkAQSgpAEIoKQBDKCkARCgpAEUoKQBGKCkARygpAEgoKQBJKCkASi +802=gpAEsoKQBMKCkATSgpAE4oKQBPKCkAUCgpAFEoKQBSKCkAUygpAFQoKQBVKCkAVigpAFco +803=KQBYKCkAWSgpAFooKQBbKCkAXCgpAF0oKQBeKCkAXygpAGAoKQBhKCkAYigpAGMoKQBkKC +804=kAZSgpAGYoKQBnKCkAaCgpAGkoKQBqKCkAaygpAGwoKQBtKCkAbigpAG8oKQBwKCkAcSgp +805=AHIoKQBzKCkAdCgpAHUoKQB2KCkAdygpAHgoKQB5KCkAeigpAHsoKQB8KCkAfSgpAH4oKQ +806=B/KCkAgCgpAIEoKQCCKCsAIABv7YkhKCkAIigpACMoKQAkKCkAJSgpACYoKQAnKCkAKCgp +807=ACkoKQAqKCkAKygpACwoKQAtKCkALigpAC8oKQAwKCkAMUgBYYgBIl0ib+mFM1QCjAFgiC +808=dVADVVAmEmXSJvKFwiJ/EDOEAEJwARJ1UAOihVADsoKQA8KCkAPSgpAD4oKQA/KCkAQCgp +809=AEEoKQBCKCkAQygpAEQoKQBFKCkARigpAEcoKQBIKCkASSgpAEooKQBLKCkATCgpAE0oKQ +810=BOKCkATygpAFAoKQBRKCkAUigpAFMoKQBUKCkAVSgpAFYoKQBXKCkAWCgpAFkoKQBaKCkA +811=WygpAFwoKQBdKCkAXigpAF8oKQBgKCkAYSgpAGIoKQBjKCkAZCgpAGUoKQBmKCkAZygpAG +812=goKQBpKCkAaigpAGsoKQBsKCkAbSgpAG4oKQBvKCkAcCgpAHEoKQByKCkAcygpAHQoKQB1 +813=KCkAdigpAHcoKQB4KCkAeSgpAHooKQB7KCkAfCgpAH0oKQB+KCkAfygpAIAoKQCBKCsAIQ +814=Bw9YYiKCkAIygpACQoKQAlKCkAJigpACcoKQAoKCkAKSgpACooKQArKCkALCgpAC0oKQAu +815=KCkALygpADAoKQAxSAFphQBphXCYg0kBM1QCjAFohSdVADVUAmiFQQQ2yAEjrCEnxQM4VA +816=InqBAnVQA6KFUAOygpADwoKQA9KCkAPigpAD8oKQBAKCkAQSgpAEIoKQBDKCkARCgpAEUo +817=KQBGKCkARygpAEgoKQBJKCkASigpAEsoKQBMKCkATSgpAE4oKQBPKCkAUCgpAFEoKQBSKC +818=kAUygpAFQoKQBVKCkAVigpAFcoKQBYKCkAWSgoAApaAHAA//8AAAACAFsAEQAAQwwAIAhw +819=AP//AAAAAgBcACcpAF0oKQBeKCkAXygpAGAoKQBhKCkAYigpAGMoKQBkKCkAZSgpAGYoKQ +820=BnKCkAaCgpAGkoKQBqKCkAaygpAGwoKQBtKCkAbigpAG8oKQBwKCkAcSgpAHIoKQBzKCkA +821=dCgpAHUoKQB2KCkAdygpAHgoKQB5KCkAeigpAHsoKQB8KCkAfSgpAH4oKQB/KCkAgCgrAC +822=IAceU0IygpACQoKQAlKCkAJigpACcoKQAoKCkAKSgpACooKQArKCkALCgpAC0oKQAuKCkA +823=LygpADAoKQAxSAEFdgUAAAEGADJJAQBjAAYAM0gBk0sGADRIAXACAQAGADVJAWFMBUkBNs +824=gBAQECADdIAY8FAgA4KCkAOSgpADooKQA7KCkAPCgpAD0oKQA+KCkAPygpAEAoKQBBKCkA +825=QigpAEMoKQBEKCkARSgpAEYoKQBHKCkASCgpAEkoKQBKKCkASygpAEwoKQBNKCkATigpAE +826=8oKQBQKCkAUSgpAFIoKQBTKCkAVCgpAFUoKQBWKCkAVygpAFgoKQBZKCkAWigpAFsoKQBc +827=KCkAXSgpAF4oKQBfKCkAYCgpAGEoKQBiKCkAYygpAGQoKQBlKCkAZigpAGcoKQBoKCkAaS +828=gpAGooKQBrKCkAbCgpAG0oKQBuKCkAbygpAHAoKQBxKCkAcigpAHMoKQB0KCkAdSgpAHYo +829=KQB3KCkAeCgpAHkoKQB6KCkAeygpAHwoKQB9KCkAfigpAH8oKwAjAHLlgSQoKQAlKCkAJi +830=gpACcoKQAoKCkAKSgpACooKQArKCkALCgpAC0oKQAuKCkALygpADAoKQAxSAF4f0l6MkgB +831=wX0zSAGIenl/cpACQQQ1VAIn+g9yAGgBmH8nbQM4QASbBgIAOSgpADooKQA7KCkAPCgpAD +832=0oKQA+KCkAPygpAEAoKQBBKCkAQigpAEMoKQBEKCkARSgpAEYoKQBHKCkASCgpAEkoKQBK +833=KCkASygpAEwoKQBNKCkATigpAE8oKQBQKCkAUSgpAFIoKQBTKCkAVCgpAFUoKQBWKCkAVy +834=gpAFgoKQBZKCkAWigpAFsoKQBcKCkAXSgpAF4oKQBfKCkAYCgpAGEoKQBiKCkAYygpAGQo +835=KQBlKCkAZigpAGcoKQBoKCkAaSgpAGooKQBrKCkAbCgpAG0oKQBuKCkAbygpAHAoKQBxKC +836=kAcigpAHMoKQB0KCkAdSgpAHYoKQB3KCkAeCgpAHkoKQB6KCkAeygpAHwoKQB9KCkAfigr +837=ACQAc+1+JSgpACYoKQAnKCkAKCgpACkoKQAqKCkAKygpACwoKQAtKCkALigpAC8oKQAwKC +838=kAMUgBJ6MPcwCJWfgBfPyqAQIKXfxzKKEPc5R7TX42WQZhbAV8/CdBAzhUAiegDydVADoo +839=VQA7KCkAPCgpAD0oKQA+KCkAPygpAEAoKQBBKCkAQigpAEMoKQBEKCkARSgpAEYoKQBHKC +840=kASCgpAEkoKQBKKCkASygpAEwoKQBNKCkATigpAE8oKQBQKCkAUSgpAFIoKQBTKCkAVCgp +841=AFUoKQBWKCkAVygpAFgoKQBZKCkAWigpAFsoKQBcKCkAXSgpAF4oKQBfKCkAYCgpAGEoKQ +842=BiKCkAYygpAGQoKQBlKCkAZigpAGcoKQBoKCkAaSgpAGooKQBrKCkAbCgpAG0oKQBuKCkA +843=bygpAHAoKQBxKCkAcigpAHMoKQB0KCkAdSgpAHYoKQB3KCkAeCgpAHkoKQB6KCkAeygpAH +844=woKQB9KCsAJQB09XsmKCkAJygpACgoKQApKCkAKigpACsoKQAsKCkALSgpAC4oKQAvKCkA +845=MCgpADFIAX14BCLpLnSJegVoeqoBBglIeqgBAQcJADVMBXx4iXp0lHuM9ycVAzhABCdIDy +846=dVADooVQA7KCkAPCgpAD0oKQA+KCkAPygpAEAoKQBBKCkAQigpAEMoKQBEKCkARSgpAEYo +847=KQBHKCkASCgpAEkoKQBKKCkASygpAEwoKQBNKCkATigpAE8oKQBQKCkAUSgpAFIoKQBTKC +848=kAVCgpAFUoKQBWKCkAVygpAFgoKQBZKCkAWigpAFsoKQBcKCkAXSgpAF4oKQBfKCkAYCgp +849=AGEoKQBiKCkAYygpAGQoKQBlKCkAZigpAGcoKQBoKCkAaSgpAGooKQBrKCkAbCgpAG0oKQ +850=BuKCkAbygpAHAoKQBxKCkAcigpAHMoKQB0KCkAdSgpAHYoKQB3KCkAeCgpAHkoKQB6KCkA +851=eygpAHwoKwAmAHX9eCcoKQAoKCkAKSgpACooKQArKCkALCgpAC0oKQAuKCkALygpADAoKQ +852=AxSAFxdwhxd3WQdwEJBwAz1gIKCFB3tQILcXd1hHaQd6gBkHcn6QI4WAYn8A4nVQA6KFUA +853=OygpADwoKQA9KCkAPigpAD8oKQBAKCkAQSgpAEIoKQBDKCkARCgpAEUoKQBGKCkARygpAE +854=goKQBJKCkASigpAEsoKQBMKCkATSgpAE4oKQBPKCkAUCgpAFEoKQBSKCkAUygpAFQoKQBV +855=KCkAVigpAFcoKQBYKCkAWSgpAFooKQBbKCkAXCgpAF0oKQBeKCkAXygpAGAoKQBhKCkAYi +856=gpAGMoKQBkKCkAZSgpAGYoKQBnKCkAaCgpAGkoKQBqKCkAaygpAGwoKQBtKCkAbigpAG8o +857=KQBwKCkAcSgpAHIoKQBzKCkAdCgpAHUoKQB2KCkAdygpAHgoKQB5KCkAeigpAHsoKwAnAH +858=bldigoKQApKCkAKigpACsoKQAsKCkALSgpAC4oKQAvKCkAMCgpADFIAQGKBQAASW8yyQEB +859=enR2AHUCAnh0qQEDeXR2KJgOqAGYdCe9AjhECCeYDidVADooVQA7KCkAPCgpAD0oKQA+KC +860=kAPygpAEAoKQBBKCkAQigpAEMoKQBEKCkARSgpAEYoKQBHKCkASCgpAEkoKQBKKCkASygp +861=AEwoKQBNKCkATigpAE8oKQBQKCkAUSgpAFIoKQBTKCkAVCgpAFUoKQBWKCkAVygpAFgoKQ +862=BZKCkAWigpAFsoKQBcKCkAXSgpAF4oKQBfKCkAYCgpAGEoKQBiKCkAYygpAGQoKQBlKCkA +863=ZigpAGcoKQBoKCkAaSgpAGooKQBrKCkAbCgpAG0oKQBuKCkAbygpAHAoKQBxKCkAcigpAH +864=MoKQB0KCkAdSgpAHYoKQB3KCkAeCgpAHkoKQB6KCsAKAB37XMpKCkAKigpACsoKQAsKCkA +865=LSgpAC4oKQAvKCkAMCgpADFIAWhvI9Erd5RwI9Erd0x0SAAiwEonKQA1TAUn0St3jHOAci +866=eRAjhABCdADidVADooVQA7KCkAPCgpAD0oKQA+KCkAPygpAEAoKQBBKCkAQigpAEMoKQBE +867=KCkARSgpAEYoKQBHKCkASCgpAEkoKQBKKCkASygpAEwoKQBNKCkATigpAE8oKQBQKCkAUS +868=gpAFIoKQBTKCkAVCgpAFUoKQBWKCkAVygpAFgoKQBZKCkAWigpAFsoKQBcKCkAXSgpAF4o +869=KQBfKCkAYCgpAGEoKQBiKCkAYygpAGQoKQBlKCkAZigpAGcoKQBoKCkAaSgpAGooKQBrKC +870=kAbCgpAG0oKQBuKCkAbygpAHAoKQBxKCkAcigpAHMoKQB0KCkAdSgpAHYoKQB3KCkAeCgp +871=AHkoKwApAHj1cCooKQArKCkALCgpAC0oKQAuKCkALygpADAoKQAxSQF2TeEBIskqeIlvCS +872=K9OXhcbkgAaG8nKQA1TQVhTAVd5jbIAYhvJ2UCOFQCJ+gNJ1UAOihVADsoKQA8KCkAPSgp +873=AD4oKQA/KCkAQCgpAEEoKQBCKCkAQygpAEQoKQBFKCkARigpAEcoKQBIKCkASSgpAEooKQ +874=BLKCkATCgpAE0oKQBOKCkATygpAFAoKQBRKCkAUigpAFMoKQBUKCkAVSgpAFYoKQBXKCkA +875=WCgpAFkoKQBaKCkAWygpAFwoKQBdKCkAXigpAF8oKQBgKCkAYSgpAGIoKQBjKCkAZCgpAG +876=UoKQBmKCkAZygpAGgoKQBpKCkAaigpAGsoKQBsKCkAbSgpAG4oKQBvKCkAcCgpAHEoKQBy +877=KCkAcygpAHQoKQB1KCkAdigpAHcoKQB4KCsAKgB5/W0rKCkALCgpAC0oKQAuKCkALygpAD +878=AoKQAxSAFwbEFnMkgBXGoi7SkzSAGUZQkGADQAeQAAAAAAAAYRAAAoDAAgCgA1AHkAYQUA +879=AAAGADbIAQEBAgA3SAEF//8AAAACADgoKQA5KCkAOigpADsoKQA8KCkAPSgpAD4oKQA/KC +880=kAQCgpAEEoKQBCKCkAQygpAEQoKQBFKCkARigpAEcoKQBIKCkASSgpAEooKQBLKCkATCgp +881=AE0oKQBOKCkATygpAFAoKQBRKCkAUigpAFMoKQBUKCkAVSgpAFYoKQBXKCkAWCgpAFkoKQ +882=BaKCkAWygpAFwoKQBdKCkAXigpAF8oKQBgKCkAYSgpAGIoKQBjKCkAZCgpAGUoKQBmKCkA +883=ZygpAGgoKQBpKCkAaigpAGsoKQBsKCkAbSgpAG4oKQBvKCkAcCgpAHEoKQByKCkAcygpAH +884=QoKQB1KCkAdigpAHcoKAABKwB6AMlZLCgpAC0oKQAuKCkALygpADAoKQAxSAEFdgUAAAEG +885=ADJIAVRidWUzKCkANCgpADVJAWFMBZppegBoAZhpJw0COEAE2RE5KCkAOigpADsoKQA8KC +886=kAPSgpAD4oKQA/KCkAQCgpAEEoKQBCKCkAQygpAEQoKQBFKCkARigpAEcoKQBIKCkASSgp +887=AEooKQBLKCkATCgpAE0oKQBOKCkATygpAFAoKQBRKCkAUigpAFMoKQBUKCkAVSgpAFYoKQ +888=BXKCkAWCgpAFkoKQBaKCkAWygpAFwoKQBdKCkAXigpAF8oKQBgKCkAYSgpAGIoKQBjKCkA +889=ZCgpAGUoKQBmKCkAZygpAGgoKQBpKCkAaigpAGsoKQBsKCkAbSgpAG4oKQBvKCkAcCgpAH +890=EoKQByKCkAcygpAHQoKQB1KCkAdigrACwAe+1oLSgpAC4oKQAvKCkAMCgpADFIAWBnUWEy +891=SAEn4Qx7hAFVAjQoVQA1SAF0ZVUCNsgBgGcn4QE4VAIn4AwnVQA6KFUAOygpADwoKQA9KC +892=kAPigpAD8oKQBAKCkAQSgpAEIoKQBDKCkARCgpAEUoKQBGKCkARygpAEgoKQBJKCkASigp +893=AEsoKQBMKCkATSgpAE4oKQBPKCkAUCgpAFEoKQBSKCkAUygpAFQoKQBVKCkAVigpAFcoKQ +894=BYKCkAWSgpAFooKQBbKCkAXCgpAF0oKQBeKCkAXygpAGAoKQBhKCkAYigpAGMoKQBkKCkA +895=ZSgpAGYoKQBnKCkAaCgpAGkoKQBqKCkAaygpAGwoKQBtKCkAbigpAG8oKQBwKCkAcSgpAH +896=IoKQBzKCkAdCgpAHUoKwAtAHz1ZS4oKQAvKCkAMCgpADFIAWlkAW3LfP1iM1QCj10GADRI +897=AZsCBgA1SQFh7ct8KGwZJ7UBOEAEmwYCADkoKQA6KCkAOygpADwoKQA9KCkAPigpAD8oKQ +898=BAKCkAQSgpAEIoKQBDKCkARCgpAEUoKQBGKCkARygpAEgoKQBJKCkASigpAEsoKQBMKCkA +899=TSgpAE4oKQBPKCkAUCgpAFEoKQBSKCkAUygpAFQoKQBVKCkAVigpAFcoKQBYKCkAWSgpAF +900=ooKQBbKCkAXCgpAF0oKQBeKCkAXygpAGAoKQBhKCkAYigpAGMoKQBkKCkAZSgpAGYoKQBn +901=KCkAaCgpAGkoKQBqKCkAaygpAGwoKQBtKCkAbigpAG8oKQBwKCkAcSgpAHIoKQBzKCkAdC +902=grAC4Aff1iLygpADAoKQAxSAFwYU3AMkgB2V4zKCkANCgpADVIAScxDH0oMAwniQE4QAQn +903=MAwnVQA6KFUAOygpADwoKQA9KCkAPigpAD8oKQBAKCkAQSgpAEIoKQBDKCkARCgpAEUoKQ +904=BGKCkARygpAEgoKQBJKCkASigpAEsoKQBMKCkATSgpAE4oKQBPKCkAUCgpAFEoKQBSKCkA +905=UygpAFQoKQBVKCkAVigpAFcoKQBYKCkAWSgpAFooKQBbKCkAXCgpAF0oKQBeKCkAXygpAG +906=AoKQBhKCkAYigpAGMoKQBkKCkAZSgpAGYoKQBnKCkAaCgpAGkoKQBqKCkAaygpAGwoKQBt +907=KCkAbigpAG8oKQBwKCkAcSgpAHIoKQBzKCsALwB+5WAwKCkAMUgBJ9kLfpReRWAzVAKMAW +908=zAJ1UANVQCJ9kLfijYCyddAThABCfYCydVADooVQA7KCkAPCgpAD0oKQA+KCkAPygpAEAo +909=KQBBKCkAQigpAEMoKQBEKCkARSgpAEYoKQBHKCkASCgpAEkoKQBKKCkASygpAEwoKQBNKC +910=kATigpAE8oKQBQKCkAUSgpAFIoKQBTKCkAVCgpAFUoKQBWKCkAVygpAFgoKQBZKCkAWigp +911=AFsoKQBcKCkAXSgpAF4oKQBfKCkAYCgpAGEoKQBiKCkAYygpAGQoKQBlKCkAZigpAGcoKQ +912=BoKCkAaSgpAGooKQBrKCkAbCgpAG0oKQBuKCkAbygpAHAoKQBxKCkAcigrADAAf+1dMUgB +913=J4ELf5BagFzABGBcJ1UANUwFYVwCIj1Kf4FcAyI8SswFIjxKJykAOUwFzV06KCkAOygpAD +914=woKQA9KCkAPigpAD8oKQBAKCkAQSgpAEIoKQBDKCkARCgpAEUoKQBGKCkARygpAEgoKQBJ +915=KCkASigpAEsoKQBMKCkATSgpAE4oKQBPKCkAUCgpAFEoKQBSKCkAUygpAFQoKQBVKCkAVi +916=gpAFcoKQBYKCkAWSgpAFooKQBbKCkAXCgpAF0oKQBeKCkAXygpAGAoKQBhKCkAYigpAGMo +917=KQBkKCkAZSgpAGYoKQBnKCkAaCgpAGkoKQBqKCkAaygpAGwoKQBtKCkAbigpAG8oKQBwKC +918=kAcSgoAAExAIAAaVkBIr0ugCgoCycpADRABJgCIr0ugJxXTV02VAIBYQUAACNJO4D9YjhU +919=At1XOSgpADooKQA7KCkAPCgpAD0oKQA+KCkAPygpAEAoKQBBKCkAQigpAEMoKQBEKCkARS +920=gpAEYoKQBHKCkASCgpAEkoKQBKKCkASygpAEwoKQBNKCkATigpAE8oKQBQKCkAUSgpAFIo +921=KQBTKCkAVCgpAFUoKQBWKCkAVygpAFgoKQBZKCkAWigpAFsoKQBcKCkAXSgpAF4oKQBfKC +922=kAYCgpAGEoKQBiKCkAYygpAGQoKQBlKCkAZigpAGcoKQBoKCkAaSgpAGooKQBrKCkAbCgp +923=AG0oKQBuKCkAbygpAHAoKwAyAIGUU01SMygpADQoKQA1SAFkVUEENsgBkVaBKNAKJykAOU +924=AE3Vc6KCkAOygpADwoKQA9KCkAPigpAD8oKQBAKCkAQSgpAEIoKQBDKCkARCgpAEUoKQBG +925=KCkARygpAEgoKQBJKCkASigpAEsoKQBMKCkATSgpAE4oKQBPKCkAUCgpAFEoKQBSKCkAUy +926=gpAFQoKQBVKCkAVigpAFcoKQBYKCkAWSgpAFooKQBbKCkAXCgpAF0oKQBeKCkAXygpAGAo +927=KQBhKCkAYigpAGMoKQBkKCkAZSgpAGYoKQBnKCkAaCgpAGkoKQBqKCkAaygpAGwoKQBtKC +928=kAbigpAG8oKwAzAILlVTQoKQA1SAEneQqCmKuZU4IoeAonKQA5TAUneAonVQA7KFUAPCgp +929=AD0oKQA+KCkAPygpAEAoKQBBKCkAQigpAEMoKQBEKCkARSgpAEYoKQBHKCkASCgpAEkoKQ +930=BKKCkASygpAEwoKQBNKCkATigpAE8oKQBQKCkAUSgpAFIoKQBTKCkAVCgpAFUoKQBWKCkA +931=VygpAFgoKQBZKCkAWigpAFsoKQBcKCkAXSgpAF4oKQBfKCkAYCgpAGEoKQBiKCkAYygpAG +932=QoKQBlKCkAZigpAGcoKQBoKCkAaSgpAGooKQBrKCkAbCgpAG0oKQBuKCsANACD7VI1SAEn +933=IQqDKCEKgyggCicpADlMBScgCidVADsoVQA8KCkAPSgpAD4oKQA/KCkAQCgpAEEoKQBCKC +934=kAQygpAEQoKQBFKCkARigpAEcoKQBIKCkASSgpAEooKQBLKCkATCgpAE0oKQBOKCkATygp +935=AFAoKQBRKCkAUigpAFMoKQBUKCkAVSgpAFYoKQBXKCkAWCgpAFkoKQBaKCkAWygpAFwoKQ +936=BdKCkAXigpAF8oKQBgKCkAYSgpAGIoKQBjKCkAZCgpAGUoKQBmKCkAZygpAGgoKQBpKCkA +937=aigpAGsoKQBsKCkAbSgrADUAhJxMiPOoAY2fhCjICScpADlMBSfICSdVADsoVAAEPACEAP +938=//ABEAANILACAIAAACAD0AhAD//wBoAQE+AIQAyQE/KCkAQCgpAEEoKQBCKCkAQygpAEQo +939=KQBFKCkARigpAEcoKQBIKCkASSgpAEooKQBLKCkATCgpAE0oKQBOKCkATygpAFAoKQBRKC +940=kAUigpAFMoKQBUKCkAVSgpAFYoKQBXKCkAWCgpAFkoKQBaKCkAWygpAFwoKQBdKCkAXigp +941=AF8oKQBgKCkAYSgpAGIoKQBjKCkAZCgpAGUoKQBmKCkAZygpAGgoKQBpKCkAaigpAGsoKQ +942=BsKCgACTYAhQBhBQAAAwIAN0gB3UE4KCkAOSgpADooKQA7KCkAPCgpAD0oKQA+KCkAPygp +943=AEAoKQBBKCkAQigpAEMoKQBEKCkARSgpAEYoKQBHKCkASCgpAEkoKQBKKCkASygpAEwoKQ +944=BNKCkATigpAE8oKQBQKCkAUSgpAFIoKQBTKCkAVCgpAFUoKQBWKCkAVygpAFgoKQBZKCkA +945=WigpAFsoKQBcKCkAXSgpAF4oKQBfKCkAYCgpAGEoKQBiKCkAYygpAGQoKQBlKCkAZigpAG +946=coKQBoKCkAaSgpAGooKQBrKCsANwCG+Yo4KCkAOSgpADooKQA7KCkAPCgpAD0oKQA+KCkA +947=PygpAEAoKQBBKCkAQigpAEMoKQBEKCkARSgpAEYoKQBHKCkASCgpAEkoKQBKKCkASygpAE +948=woKQBNKCkATigpAE8oKQBQKCkAUSgpAFIoKQBTKCkAVCgpAFUoKQBWKCkAVygpAFgoKQBZ +949=KCkAWigpAFsoKQBcKCkAXSgpAF4oKQBfKCkAYCgpAGEoKQBiKCkAYygpAGQoKQBlKCkAZi +950=gpAGcoKQBoKCkAaSgpAGooKwA4AIftRzkoKQA6KCkAOygpADwoKQA9KCkAPigpAD8oKQBA +951=KCkAQSgpAEIoKQBDKCkARCgpAEUoKQBGKCkARygpAEgoKQBJKCkASigpAEsoKQBMKCkATS +952=gpAE4oKQBPKCkAUCgpAFEoKQBSKCkAUygpAFQoKQBVKCkAVigpAFcoKQBYKCkAWSgpAFoo +953=KQBbKCkAXCgpAF0oKQBeKCkAXygpAGAoKQBhKCkAYigpAGMoKQBkKCkAZSgpAGYoKQBnKC +954=kAaCgpAGkoKwA5AIj1RDooKQA7KCkAPCgpAD0oKQA+KCkAPygpAEAoKQBBKCkAQigpAEMo +955=KQBEKCkARSgpAEYoKQBHKCkASCgpAEkoKQBKKCkASygpAEwoKQBNKCkATigpAE8oKQBQKC +956=kAUSgpAFIoKQBTKCkAVCgpAFUoKQBWKCkAVygpAFgoKQBZKCkAWigpAFsoKQBcKCkAXSgp +957=AF4oKQBfKCkAYCgpAGEoKQBiKCkAYygpAGQoKQBlKCkAZigpAGcoKQBoKCsAOgCJ/UE7KC +958=kAPCgpAD0oKQA+KCkAPygpAEAoKQBBKCkAQigpAEMoKQBEKCkARSgpAEYoKQBHKCkASCgp +959=AEkoKQBKKCkASygpAEwoKQBNKCkATigpAE8oKQBQKCkAUSgpAFIoKQBTKCkAVCgpAFUoKQ +960=BWKCkAVygpAFgoKQBZKCkAWigpAFsoKQBcKCkAXSgpAF4oKQBfKCkAYCgpAGEoKQBiKCkA +961=YygpAGQoKQBlKCkAZigpAGcoKwA7AIrlPzwoKQA9KCkAPigpAD8oKQBAKCkAQSgpAEIoKQ +962=BDKCkARCgpAEUoKQBGKCkARygpAEgoKQBJKCkASigpAEsoKQBMKCkATSgpAE4oKQBPKCkA +963=UCgpAFEoKQBSKCkAUygpAFQoKQBVKCkAVigpAFcoKQBYKCkAWSgpAFooKQBbKCkAXCgpAF +964=0oKQBeKCkAXygpAGAoKQBhKCkAYigpAGMoKQBkKCkAZSgpAGYoKwA8AIvtPD0oKQA+KCkA +965=PygpAEAoKQBBKCkAQigpAEMoKQBEKCkARSgpAEYoKQBHKCkASCgpAEkoKQBKKCkASygpAE +966=woKQBNKCkATigpAE8oKQBQKCkAUSgpAFIoKQBTKCkAVCgpAFUoKQBWKCkAVygpAFgoKQBZ +967=KCkAWigpAFsoKQBcKCkAXSgpAF4oKQBfKCkAYCgpAGEoKQBiKCkAYygpAGQoKQBlKCsAPQ +968=CM9Tk+KCkAPygpAEAoKQBBKCkAQigpAEMoKQBEKCkARSgpAEYoKQBHKCkASCgpAEkoKQBK +969=KCkASygpAEwoKQBNKCkATigpAE8oKQBQKCkAUSgpAFIoKQBTKCkAVCgpAFUoKQBWKCkAVy +970=gpAFgoKQBZKCkAWigpAFsoKQBcKCkAXSgpAF4oKQBfKCkAYCgpAGEoKQBiKCkAYygpAGQo +971=KwA+AI39Nj8oKQBAKCkAQSgpAEIoKQBDKCkARCgpAEUoKQBGKCkARygpAEgoKQBJKCkASi +972=gpAEsoKQBMKCkATSgpAE4oKQBPKCkAUCgpAFEoKQBSKCkAUygpAFQoKQBVKCkAVigpAFco +973=KQBYKCkAWSgpAFooKQBbKCkAXCgpAF0oKQBeKCkAXygpAGAoKQBhKCkAYigpAGMoKwA/AI +974=7lNEAoKQBBKCkAQigpAEMoKQBEKCkARSgpAEYoKQBHKCkASCgpAEkoKQBKKCkASygpAEwo +975=KQBNKCkATigpAE8oKQBQKCkAUSgpAFIoKQBTKCkAVCgpAFUoKQBWKCkAVygpAFgoKQBZKC +976=kAWigpAFsoKQBcKCkAXSgpAF4oKQBfKCkAYCgpAGEoKQBiKCsAQACP7TFBKCkAQigpAEMo +977=KQBEKCkARSgpAEYoKQBHKCkASCgpAEkoKQBKKCkASygpAEwoKQBNKCkATigpAE8oKQBQKC +978=kAUSgpAFIoKQBTKCkAVCgpAFUoKQBWKCkAVygpAFgoKQBZKCkAWigpAFsoKQBcKCkAXSgp +979=AF4oKQBfKCkAYCgpAGEoKwBBAJD1LkIoKQBDKCkARCgpAEUoKQBGKCkARygpAEgoKQBJKC +980=kASigpAEsoKQBMKCkATSgpAE4oKQBPKCkAUCgpAFEoKQBSKCkAUygpAFQoKQBVKCkAVigp +981=AFcoKQBYKCkAWSgpAFooKQBbKCkAXCgpAF0oKQBeKCkAXygpAGAoKwBCAJH9K0MoKQBEKC +982=kARSgpAEYoKQBHKCkASCgpAEkoKQBKKCkASygpAEwoKQBNKCkATigpAE8oKQBQKCkAUSgp +983=AFIoKQBTKCkAVCgpAFUoKQBWKCkAVygpAFgoKQBZKCkAWigpAFsoKQBcKCkAXSgpAF4oKQ +984=BfKCsAQwCS5SlEKCkARSgpAEYoKQBHKCkASCgpAEkoKQBKKCkASygpAEwoKQBNKCkATigp +985=AE8oKQBQKCkAUSgpAFIoKQBTKCkAVCgpAFUoKQBWKCkAVygpAFgoKQBZKCkAWigpAFsoKQ +986=BcKCkAXSgpAF4oKwBEAJPtJkUoKQBGKCkARygpAEgoKQBJKCkASigpAEsoKQBMKCkATSgp +987=AE4oKQBPKCkAUCgpAFEoKQBSKCkAUygpAFQoKQBVKCkAVigpAFcoKQBYKCkAWSgpAFooKQ +988=BbKCkAXCgpAF0oKwBFAJT1I0YoKQBHKCkASCgpAEkoKQBKKCkASygpAEwoKQBNKCkATigp +989=AE8oKQBQKCkAUSgpAFIoKQBTKCkAVCgpAFUoKQBWKCkAVygpAFgoKQBZKCkAWigpAFsoKQ +990=BcKCsARgCV/SBHKCkASCgpAEkoKQBKKCkASygpAEwoKQBNKCkATigpAE8oKQBQKCkAUSgp +991=AFIoKQBTKCkAVCgpAFUoKQBWKCkAVygpAFgoKQBZKCkAWigpAFsoKwBHAJblHkgoKQBJKC +992=kASigpAEsoKQBMKCkATSgpAE4oKQBPKCkAUCgpAFEoKQBSKCkAUygpAFQoKQBVKCkAVigp +993=AFcoKQBYKCkAWSgpAFooKwBIAJftG0koKQBKKCkASygpAEwoKQBNKCkATigpAE8oKQBQKC +994=kAUSgpAFIoKQBTKCkAVCgpAFUoKQBWKCkAVygpAFgoKQBZKCsASQCY9RhKKCkASygpAEwo +995=KQBNKCkATigpAE8oKQBQKCoAUQAnXQFSKCkAUygpAFQoKAABVQCYABEAAAgBkAII//8AAA +996=ACAFYAmADJAVcoKQBYKCsASgCZ4QRLKCkATCgpAE0oKQBOKCkATygpAFAoKQBRKCkAUigp +997=AFMoKQBUKCkAVSgpAFYoKQBXKCsASwCa5RNMKCkATSgpAE4oKQBPKCkAUCgpAFEoKQBSKC +998=kAUygpAFQoKQBVKCkAVigrAEwAm+0QTSgpAE4oKQBPKCkAUCgpAFEoKQBSKCkAUygpAFQo +999=KQBVKCsATQCc9Q1OKCkATygpAFAoKQBRKCkAUigpAFMoKQBUKCsATgCd/QpPKCkAUCgpAF +1000=EoKQBSKCkAUygrAE8AnuUIUCgpAFEoKQBSKCsAUACf7AUIUQCfAP//AAAAAgARAAA= + +[Latin] +IQ=0 +Edge=North +Color=SovietLoad +Allies=Latin +Country=Latin +Credits=0 +NodeCount=0 +TechLevel=1 +PercentBuilt=0 +PlayerControl=no + +[Lighting] +Red=1.000000 +Blue=1.000000 +Green=1.000000 +Level=0.000000 +Ground=0.000000 +IonRed=1.000000 +Ambient=1.000000 +IonBlue=1.000000 +IonGreen=1.000000 +IonLevel=0.000000 +IonGround=0.000000 +IonAmbient=1.000000 +DominatorRed=0.850000 +DominatorBlue=0.300000 +DominatorGreen=0.200000 +DominatorLevel=0.000000 +DominatorGround=0.000000 +DominatorAmbient=1.500000 +DominatorAmbientChangeRate=0.009000 + +[Map] +Size=0,0,80,80 +Theater=LUNAR +LocalSize=2,4,76,74 + +[Neutral] +IQ=0 +Edge=North +Color=Grey +Allies=Neutral +Country=Neutral +Credits=0 +NodeCount=0 +TechLevel=1 +PercentBuilt=0 +PlayerControl=no + +[OverlayDataPack] +1=BQAAIP4AIACABQAAIP4AIACABQAAIP4AIACABQAAIP4AIACABQAAIP4AIACABQAAIP4AIA +2=CABQAAIP4AIACABQAAIP4AIACABQAAIP4AIACABQAAIP4AIACABQAAIP4AIACABQAAIP4A +3=IACABQAAIP4AIACABQAAIP4AIACABQAAIP4AIACABQAAIP4AIACABQAAIP4AIACABQAAIP +4=4AIACABQAAIP4AIACABQAAIP4AIACABQAAIP4AIACABQAAIP4AIACABQAAIP4AIACABQAA +5=IP4AIACABQAAIP4AIACABQAAIP4AIACABQAAIP4AIACABQAAIP4AIACABQAAIP4AIACABQ +6=AAIP4AIACABQAAIP4AIACABQAAIP4AIACA + +[OverlayPack] +1=BQAAIP4AIP+ABQAAIP4AIP+ABQAAIP4AIP+ABQAAIP4AIP+ABQAAIP4AIP+ABQAAIP4AIP +2=+ABQAAIP4AIP+ABQAAIP4AIP+ABQAAIP4AIP+ABQAAIP4AIP+ABQAAIP4AIP+ABQAAIP4A +3=IP+ABQAAIP4AIP+ABQAAIP4AIP+ABQAAIP4AIP+ABQAAIP4AIP+ABQAAIP4AIP+ABQAAIP +4=4AIP+ABQAAIP4AIP+ABQAAIP4AIP+ABQAAIP4AIP+ABQAAIP4AIP+ABQAAIP4AIP+ABQAA +5=IP4AIP+ABQAAIP4AIP+ABQAAIP4AIP+ABQAAIP4AIP+ABQAAIP4AIP+ABQAAIP4AIP+ABQ +6=AAIP4AIP+ABQAAIP4AIP+ABQAAIP4AIP+A + +[Pacific] +IQ=0 +Edge=North +Color=AlliedLoad +Allies=Pacific +Country=Pacific +Credits=0 +NodeCount=0 +TechLevel=1 +PercentBuilt=0 +PlayerControl=no + +[PsiCorps] +IQ=0 +Edge=North +Color=Purple +Allies=PsiCorps +Country=PsiCorps +Credits=0 +NodeCount=0 +TechLevel=1 +PercentBuilt=0 +PlayerControl=no + +[ScorpionCell] +IQ=0 +Edge=North +Color=Purple2 +Allies=ScorpionCell +Country=ScorpionCell +Credits=0 +NodeCount=0 +TechLevel=1 +PercentBuilt=0 +PlayerControl=no + +[Special] +IQ=0 +Edge=North +Color=Grey +Allies=Special +Country=Special +Credits=0 +NodeCount=0 +TechLevel=1 +PercentBuilt=0 +PlayerControl=no + +[SpecialFlags] +Inert=no +FogOfWar=no +IonStorms=no +MCVDeploy=no +Meteorites=no +Visceroids=yes +FixedAlliance=no +TiberiumGrows=yes +InitialVeteran=no +HarvesterImmune=no +TiberiumSpreads=yes +TiberiumExplosive=no +DestroyableBridges=yes + +[Structures] +0=Neutral,YACNST,256,32,76,64,None,1,0,1,0,0,None,None,None,0,0 +1=Neutral,NACNST,256,37,76,64,None,1,0,1,0,0,None,None,None,0,0 +2=Neutral,GACNST,256,27,76,64,None,1,0,1,0,0,None,None,None,0,0 +3=Neutral,CAAIRP,256,22,75,64,None,1,0,1,0,0,None,None,None,0,0 +4=Neutral,CACOMN,256,18,77,64,None,1,0,1,0,0,None,None,None,0,0 +5=Neutral,CAOILD,256,27,98,64,None,1,0,1,0,0,None,None,None,0,0 +6=Neutral,CAOILD,256,30,98,64,None,1,0,1,0,0,None,None,None,0,0 +7=Neutral,CAOILD,256,33,98,64,None,1,0,1,0,0,None,None,None,0,0 +8=Neutral,CAOILD,256,36,98,64,None,1,0,1,0,0,None,None,None,0,0 +9=Neutral,CAOILD,256,39,98,64,None,1,0,1,0,0,None,None,None,0,0 +10=Neutral,CAOILD,256,39,101,64,None,1,0,1,0,0,None,None,None,0,0 +11=Neutral,CAOILD,256,36,101,64,None,1,0,1,0,0,None,None,None,0,0 +12=Neutral,CAOILD,256,33,101,64,None,1,0,1,0,0,None,None,None,0,0 +13=Neutral,CAOILD,256,30,101,64,None,1,0,1,0,0,None,None,None,0,0 +14=Neutral,CAOILD,256,27,101,64,None,1,0,1,0,0,None,None,None,0,0 +15=Neutral,CANRCT,256,22,93,64,None,1,0,1,0,0,None,None,None,0,0 +16=Neutral,CAFORT,256,50,91,64,None,1,0,1,0,0,None,None,None,0,0 +17=Neutral,CAFORT,256,50,82,64,None,1,0,1,0,0,None,None,None,0,0 +18=Neutral,CAFORT,256,50,99,64,None,1,0,1,0,0,None,None,None,0,0 +19=Neutral,CAFORT,256,50,75,64,None,1,0,1,0,0,None,None,None,0,0 +20=Neutral,CAART,256,51,106,64,None,1,0,1,0,0,None,None,None,0,0 +21=Neutral,CAART,256,51,69,64,None,1,0,1,0,0,None,None,None,0,0 + +[TeamTypes] + +[Triggers] + +[USSR] +IQ=0 +Edge=North +Color=SovietLoad +Allies=USSR +Country=USSR +Credits=0 +NodeCount=0 +TechLevel=1 +PercentBuilt=0 +PlayerControl=no + +[UnitedStates] +IQ=0 +Edge=North +Color=AlliedLoad +Allies=UnitedStates +Country=UnitedStates +Credits=0 +NodeCount=0 +TechLevel=1 +PercentBuilt=0 +PlayerControl=no + +[Waypoints] +0=85036 +1=79127 + + + +[Digest] +1=6GWKRqv2n/CqYivwnhLGaJ5SeeE= diff --git a/tests/test_views/test_map_category_api.py b/tests/test_views/test_map_category_api.py index a1aed3f..8750c2d 100644 --- a/tests/test_views/test_map_category_api.py +++ b/tests/test_views/test_map_category_api.py @@ -6,9 +6,7 @@ CATEGORY_URL = "/maps/categories/" -def test_map_category_create( - kirovy_client, client_admin, client_moderator, client_user -): +def test_map_category_create(kirovy_client, client_admin, client_moderator, client_user): """Test that admins can create map categories.""" data = { "name": "Unholy Alliance, Taylor's Version", @@ -26,18 +24,16 @@ def test_map_category_create( assert response.status_code == status.HTTP_201_CREATED post_data: dict = response.data - map_category = MapCategory.objects.get(id=post_data["id"]) + map_category = MapCategory.objects.get(id=post_data["result"]["id"]) assert map_category.name == data["name"] assert map_category.slug == expected_slug - assert post_data["slug"] == expected_slug + assert post_data["result"]["slug"] == expected_slug def test_get_map_categories(client_user, create_cnc_map_category): expected_fields = {"name", "slug", "id", "modified", "created"} - categories: t.Dict[str, MapCategory] = { - str(c.id): c for c in MapCategory.objects.all() - } + categories: t.Dict[str, MapCategory] = {str(c.id): c for c in MapCategory.objects.all()} for category_name in ["Slayer", "Team Slayer", "CTF", "Forge"]: category = create_cnc_map_category(category_name) categories[str(category.id)] = category @@ -50,9 +46,7 @@ def test_get_map_categories(client_user, create_cnc_map_category): assert len(results) == len(categories) for result in results: - assert ( - set(result.keys()) == expected_fields - ), f"We should only read the fields: {expected_fields}" + assert set(result.keys()) == expected_fields, f"We should only read the fields: {expected_fields}" category = categories.get(result["id"]) assert result["name"] == category.name assert result["slug"] == category.slug diff --git a/tests/test_views/test_map_upload.py b/tests/test_views/test_map_upload.py index 4142a5d..c2cbbf0 100644 --- a/tests/test_views/test_map_upload.py +++ b/tests/test_views/test_map_upload.py @@ -1,19 +1,21 @@ import pathlib -import subprocess -from hashlib import md5 from django.core.files.uploadedfile import UploadedFile from rest_framework import status -from kirovy import settings +from kirovy import settings, typing as t +from kirovy.constants.api_codes import UploadApiCodes +from kirovy.utils import file_utils from kirovy.models import CncMap, CncMapFile, MapCategory +from kirovy.response import KirovyResponse from kirovy.services.cnc_gen_2_services import CncGen2MapParser _UPLOAD_URL = "/maps/upload/" +_CLIENT_URL = "/maps/client/upload/" def test_map_file_upload_happy_path(client_user, file_map_desert, game_yuri, extension_map, tmp_media_root): - response = client_user.post( + response: KirovyResponse = client_user.post( _UPLOAD_URL, {"file": file_map_desert, "game_id": str(game_yuri.id)}, format="multipart", @@ -44,9 +46,9 @@ def test_map_file_upload_happy_path(client_user, file_map_desert, game_yuri, ext assert map_object # Note: These won't match an md5 from the commandline because we add the ID to the map file. - assert file_object.hash_md5 == md5(open(uploaded_file, "rb").read()).hexdigest() + assert file_object.hash_md5 == file_utils.hash_file_md5(uploaded_file.open()) file_map_desert.seek(0) - assert file_object.hash_md5 != md5(file_map_desert.read()).hexdigest() + assert file_object.hash_md5 != file_utils.hash_file_md5(file_map_desert.open()) get_response = client_user.get(f"/maps/{map_object.id}/") @@ -66,3 +68,29 @@ def test_map_file_upload_happy_path(client_user, file_map_desert, game_yuri, ext assert not response_map["is_banned"], "Happy path maps should not be banned on upload." assert response_map["legacy_upload_date"] is None, "Non legacy maps should never have this field." assert response_map["id"] == str(map_object.id) + + +def test_map_file_upload_banned_user(file_map_desert, game_yuri, client_banned): + """Test that a banned user cannot upload a new map.""" + response = client_banned.post( + _UPLOAD_URL, + {"file": file_map_desert, "game_id": str(game_yuri.id)}, + format="multipart", + content_type=None, + ) + + assert response.status_code == status.HTTP_403_FORBIDDEN + + +def test_map_file_upload_banned_map(banned_cheat_map, file_map_unfair, client_anonymous): + """Test that an uploaded map will be rejected if the hash matches a banned one.""" + response = client_anonymous.post( + _CLIENT_URL, + {"file": file_map_unfair, "game": banned_cheat_map.cnc_game.slug}, + format="multipart", + content_type=None, + ) + + assert response.status_code == status.HTTP_400_BAD_REQUEST + assert response.data["code"] == UploadApiCodes.DUPLICATE_MAP + assert response.data["additional"]["existing_map_id"] == str(banned_cheat_map.id) diff --git a/tests/test_views/test_search_view.py b/tests/test_views/test_search_view.py new file mode 100644 index 0000000..bc930da --- /dev/null +++ b/tests/test_views/test_search_view.py @@ -0,0 +1,49 @@ +from urllib.parse import urlencode + +from rest_framework import status + +from kirovy.objects.ui_objects import ListResponseData +from kirovy.response import KirovyResponse + +BASE_URL = "/maps/search/" + + +def test_search_map_name(create_cnc_map, client_anonymous): + create_cnc_map("Streets of gold", is_published=True) + expected = create_cnc_map("Silver Road", is_published=True) + + query = urlencode({"search": "silver"}) + + response: KirovyResponse[ListResponseData] = client_anonymous.get(BASE_URL + "?" + query) + + assert response.status_code == status.HTTP_200_OK + assert len(response.data["results"]) == 1 + assert response.data["results"][0]["id"] == str(expected.id) + + +def test_search_map__categories(create_cnc_map, client_anonymous, create_cnc_map_category): + included_category_1 = create_cnc_map_category("Halo Gen") + included_category_2 = create_cnc_map_category("Mental Omega Oil Rush") + # Unincluded isn't grammatically correct, but "excluded" implies filtering this category out. + # "Unincluded" is accurate because we're not specifically including it. + unincluded_category = create_cnc_map_category("Turtler's Paradise") + + # Maps should be included if they have any of the queried categories. + map_both_categories = create_cnc_map(map_categories=[included_category_1, included_category_2]) + map_one_category = create_cnc_map("Delta Halo", map_categories=[included_category_1]) + map_one_category_and_unincluded = create_cnc_map(map_categories=[included_category_1, unincluded_category]) + + # Maps should not be included if they don't have any of the queried categories. + map_unincluded = create_cnc_map(map_categories=[unincluded_category]) + + expected_map_ids = {str(x.id) for x in [map_both_categories, map_one_category, map_one_category_and_unincluded]} + query = urlencode([("categories", str(x.id)) for x in [included_category_1, included_category_2]]) + + response: KirovyResponse[ListResponseData] = client_anonymous.get(f"{BASE_URL}?{query}") + + assert response.status_code == status.HTTP_200_OK + assert len(response.data["results"]) == 3 + + result_ids = {x["id"] for x in response.data["results"]} + + assert result_ids == expected_map_ids