about summary refs log tree commit diff stats
path: root/data/maps/the_repetitive
Commit message (Collapse)AuthorAgeFilesLines
* Mark some doors as "latched"Star Rauchenberger2025-10-202-5/+0
|
* Fix vanilla-only oneway door in The RepetitiveStar Rauchenberger2025-09-301-1/+1
|
* Added display names to portsStar Rauchenberger2025-09-283-0/+3
|
* [Data] Fixed some stuff for worldport shuffleStar Rauchenberger2025-09-221-1/+1
|
* [Data] Annotate shuffleable portsStar Rauchenberger2025-09-213-3/+11
|
* [Data] Added the hidden EYE panel in the anti collectable roomStar Rauchenberger2025-09-132-5/+9
|
* Add anti-collectable locationStar Rauchenberger2025-09-132-1/+6
|
* Added door groupsStar Rauchenberger2025-09-072-2/+2
|
* Changed how door location names are formattedStar Rauchenberger2025-08-3010-10/+0
| | | | | | | | | | | | | | | | | | STANDARD type doors with at most four panels in the same map area and no other trigger objects will have their location names generated from the names of the panels used to open the door, similar to Lingo 1. Other door types will use the door's name. In either case, the name can be overridden using the new location_name field. Rooms can also set a panel_display_name field, which will be used in location names for doors, and is used to group panels into areas. Panels themselves can set display names, which differentiates their locations from other panels in the same area. Many maps were updated for this, but note that the_symbolic and the_unyielding have validator failures because of duplicate panel names. This won't matter until panelsanity is implemented.
* [Data] Made proxies with the same answer as the panel explicitStar Rauchenberger2025-08-301-1/+1
|
* Converted puzzle symbols to an enumStar Rauchenberger2025-08-207-99/+99
|
* Maps have display names nowStar Rauchenberger2025-08-201-0/+1
| | | | Also added endings to the apworld.
* Added the_repetitiveStar Rauchenberger2025-08-1813-0/+954
01 302
#include "updater.h"

#include <fmt/core.h>
#include <openssl/evp.h>
#include <openssl/sha.h>
#include <wx/evtloop.h>
#include <wx/progdlg.h>
#include <wx/webrequest.h>
#include <wx/wfstream.h>
#include <wx/zipstrm.h>
#include <yaml-cpp/yaml.h>

#include <cstdio>
#include <deque>
#include <filesystem>
#include <fstream>

#include "global.h"
#include "logger.h"
#include "version.h"

constexpr const char* kVersionFileUrl =
    "https://code.fourisland.com/lingo-ap-tracker/plain/VERSION.yaml";
constexpr const char* kChangelogUrl =
    "https://code.fourisland.com/lingo-ap-tracker/about/CHANGELOG.md";

namespace {

std::string CalculateStringSha256(const wxString& data) {
  unsigned char hash[SHA256_DIGEST_LENGTH];
  EVP_MD_CTX* sha256 = EVP_MD_CTX_new();
  EVP_DigestInit(sha256, EVP_sha256());
  EVP_DigestUpdate(sha256, data.c_str(), data.length());
  EVP_DigestFinal_ex(sha256, hash, nullptr);
  EVP_MD_CTX_free(sha256);

  char output[65] = {0};
  for (int i = 0; i < SHA256_DIGEST_LENGTH; i++) {
    sprintf(output + (i * 2), "%02x", hash[i]);
  }

  return std::string(output);
}

}  // namespace

Updater::Updater(wxFrame* parent) : parent_(parent) {
  Bind(wxEVT_WEBREQUEST_STATE, &Updater::OnWebRequestState, this);
}

void Updater::Cleanup() {
  std::filesystem::path oldDir = GetExecutableDirectory() / "old";
  if (std::filesystem::is_directory(oldDir)) {
    std::filesystem::remove_all(oldDir);
  }
}

void Updater::CheckForUpdates(bool invisible) {
  wxWebRequest versionRequest =
      wxWebSession::GetDefault().CreateRequest(this, kVersionFileUrl);

  if (invisible) {
    update_state_ = UpdateState::GetVersionInvisible;

    versionRequest.Start();
  } else {
    update_state_ = UpdateState::GetVersionManual;

    if (DownloadWithProgress(versionRequest)) {
      if (versionRequest.GetState() == wxWebRequest::State_Failed) {
        wxMessageBox("Could not check for updates.", "Error",
                     wxOK | wxICON_ERROR);
      } else if (versionRequest.GetState() == wxWebRequest::State_Completed) {
        ProcessVersionFile(
            versionRequest.GetResponse().AsString().utf8_string());
      }
    }
  }
}

void Updater::OnWebRequestState(wxWebRequestEvent& evt) {
  if (update_state_ == UpdateState::GetVersionInvisible) {
    if (evt.GetState() == wxWebRequest::State_Completed) {
      ProcessVersionFile(evt.GetResponse().AsString().utf8_string());
    } else if (evt.GetState() == wxWebRequest::State_Failed) {
      parent_->SetStatusText("Could not check for updates.");
    }
  }
}

void Updater::ProcessVersionFile(std::string data) {
  try {
    YAML::Node versionInfo = YAML::Load(data);
    Version latestVersion(versionInfo["version"].as<std::string>());

    if (kTrackerVersion < latestVersion) {
      if (versionInfo["packages"]) {
        std::string platformIdentifier;

        if (wxPlatformInfo::Get().GetOperatingSystemId() == wxOS_WINDOWS_NT) {
          platformIdentifier = "win64";
        }

        if (!platformIdentifier.empty() &&
            versionInfo["packages"][platformIdentifier]) {
          wxMessageDialog dialog(
              nullptr,
              fmt::format("There is a newer version of Lingo AP Tracker "
                          "available. You have {}, and the latest version is "
                          "{}. Would you like to update?",
                          kTrackerVersion.ToString(), latestVersion.ToString()),
              "Update available", wxYES_NO | wxCANCEL);
          dialog.SetYesNoLabels("Install update", "Open changelog");

          int dlgResult = dialog.ShowModal();
          if (dlgResult == wxID_YES) {
            const YAML::Node& packageInfo =
                versionInfo["packages"][platformIdentifier];
            std::string packageUrl = packageInfo["url"].as<std::string>();
            std::string packageChecksum =
                packageInfo["checksum"].as<std::string>();

            std::vector<std::filesystem::path> packageFiles;
            if (packageInfo["files"]) {
              for (const YAML::Node& filename : packageInfo["files"]) {
                packageFiles.push_back(filename.as<std::string>());
              }
            }

            std::vector<std::filesystem::path> deletedFiles;
            if (packageInfo["deleted_files"]) {
              for (const YAML::Node& filename : packageInfo["deleted_files"]) {
                deletedFiles.push_back(filename.as<std::string>());
              }
            }

            InstallUpdate(packageUrl, packageChecksum, packageFiles,
                          deletedFiles);
          } else if (dlgResult == wxID_NO) {
            wxLaunchDefaultBrowser(kChangelogUrl);
          }

          return;
        }
      }

      if (wxMessageBox(
              fmt::format("There is a newer version of Lingo AP Tracker "
                          "available. You have {}, and the latest version is "
                          "{}. Would you like to update?",
                          kTrackerVersion.ToString(), latestVersion.ToString()),
              "Update available", wxYES_NO) == wxYES) {
        wxLaunchDefaultBrowser(kChangelogUrl);
      }
    } else if (update_state_ == UpdateState::GetVersionManual) {
      wxMessageBox("Lingo AP Tracker is up to date!", "Lingo AP Tracker", wxOK);
    }
  } catch (const std::exception& ex) {
    wxMessageBox("Could not check for updates.", "Error", wxOK | wxICON_ERROR);
  }
}

void Updater::InstallUpdate(std::string url, std::string checksum,
                            std::vector<std::filesystem::path> files,
                            std::vector<std::filesystem::path> deletedFiles) {
  update_state_ = UpdateState::GetPackage;

  wxWebRequest packageRequest =
      wxWebSession::GetDefault().CreateRequest(this, url);

  if (!DownloadWithProgress(packageRequest)) {
    return;
  }

  package_path_.clear();
  package_path_.resize(L_tmpnam + 1);
  tmpnam_s(package_path_.data(), L_tmpnam);

  {
    wxFileOutputStream writeOut(package_path_);
    wxString fileData = packageRequest.GetResponse().AsString();
    writeOut.WriteAll(fileData.c_str(), fileData.length());

    std::string downloadedChecksum = CalculateStringSha256(fileData);
    if (downloadedChecksum != checksum) {
      if (wxMessageBox("There was an issue downloading the update. Would you "
                       "like to manually download it instead?",
                       "Error", wxYES_NO | wxICON_ERROR) == wxID_YES) {
        wxLaunchDefaultBrowser(kChangelogUrl);
      }
      return;
    }
  }

  std::filesystem::path newArea = GetExecutableDirectory();
  std::filesystem::path oldArea = newArea / "old";
  std::set<std::filesystem::path> folders;
  std::set<std::filesystem::path> filesToMove;
  for (const std::filesystem::path& existingFile : files) {
    std::filesystem::path movedPath = oldArea / existingFile;
    std::filesystem::path movedDir = movedPath;
    movedDir.remove_filename();
    folders.insert(movedDir);
    filesToMove.insert(existingFile);
  }
  for (const std::filesystem::path& existingFile : deletedFiles) {
    std::filesystem::path movedPath = oldArea / existingFile;
    std::filesystem::path movedDir = movedPath;
    movedDir.remove_filename();
    folders.insert(movedDir);
  }

  for (const std::filesystem::path& newFolder : folders) {
    TrackerLog(fmt::format("Creating directory {}", newFolder.string()));

    std::filesystem::create_directories(newFolder);
  }

  for (const std::filesystem::path& existingFile : files) {
    std::filesystem::path existingPath = newArea / existingFile;

    if (std::filesystem::is_regular_file(existingPath)) {
      std::filesystem::path movedPath = oldArea / existingFile;

      TrackerLog(fmt::format("Moving {} -> {}", existingPath.string(),
                             movedPath.string()));

      std::filesystem::rename(existingPath, movedPath);
    }
  }
  for (const std::filesystem::path& existingFile : deletedFiles) {
    std::filesystem::path existingPath = newArea / existingFile;

    if (std::filesystem::is_regular_file(existingPath)) {
      std::filesystem::path movedPath = oldArea / existingFile;

      TrackerLog(fmt::format("Moving {} -> {}", existingPath.string(),
                             movedPath.string()));

      std::filesystem::rename(existingPath, movedPath);
    }
  }

  wxFileInputStream fileInputStream(package_path_);
  wxZipInputStream zipStream(fileInputStream);
  std::unique_ptr<wxZipEntry> zipEntry;
  while ((zipEntry = std::unique_ptr<wxZipEntry>(zipStream.GetNextEntry())) !=
         nullptr) {
    if (zipEntry->IsDir()) {
      continue;
    }

    std::filesystem::path archivePath = zipEntry->GetName().utf8_string();

    TrackerLog(fmt::format("Found {} in archive", archivePath.string()));

    // Cut off the root folder name
    std::filesystem::path subPath;
    for (auto it = std::next(archivePath.begin()); it != archivePath.end();
         it++) {
      subPath /= *it;
    }

    std::filesystem::path pastePath = newArea / subPath;

    wxFileOutputStream fileOutput(pastePath.string());
    zipStream.Read(fileOutput);
  }

  if (wxMessageBox(
      "Update installed! The tracker must be restarted for the changes to take "
      "effect. Do you want to close the tracker?",
                   "Update installed", wxYES_NO) == wxYES) {
    wxExit();
  }
}

bool Updater::DownloadWithProgress(wxWebRequest& request) {
  request.Start();

  wxProgressDialog dialog("Checking for updates...", "Checking for updates...",
                          100, nullptr,
                          wxPD_APP_MODAL | wxPD_AUTO_HIDE | wxPD_CAN_ABORT |
                              wxPD_ELAPSED_TIME | wxPD_REMAINING_TIME);
  while (request.GetState() != wxWebRequest::State_Completed &&
         request.GetState() != wxWebRequest::State_Failed) {
    if (request.GetBytesExpectedToReceive() == -1) {
      if (!dialog.Pulse()) {
        request.Cancel();
        return false;
      }
    } else {
      dialog.SetRange(request.GetBytesExpectedToReceive());
      if (!dialog.Update(request.GetBytesReceived())) {
        request.Cancel();
        return false;
      }
    }
  }

  return true;
}