Skip to content

Conversation

@phacUFPE
Copy link
Contributor

@phacUFPE phacUFPE commented Aug 17, 2024

Description

SOME CONFIGS WILL BE REMOVED FROM config.lua AND WILL BE USED FROM DATABASE, TABLE worlds!

Implementation of the multiword system.

This PR is in DRAFT due to the necessary modifications:

  • Update login protocol bytes
  • Update schema
  • Create schema migrations
  • Update highscores
  • Load which world the running server is
  • Hirelings spawn and save
  • Test everything

Using MyAAC will need the following PR: opentibiabr/myaac#110

Behaviour

Actual

Multiword system isn't enabled and available

Expected

Multiword system be enabled and available

image

Credits and inspired by: opentibiabr/otservbr-global-archived#929

Type of change

  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • This change requires a documentation update

How Has This Been Tested

  • Test houses
  • Test mailbox
  • Test hirelings
  • Test bank
  • Test highscores
  • Test guilds
  • ...

Test Configuration:

  • Server Version: main (latest)
  • Client: 13.40 (latest)
  • Operating System: Windows 11

Checklist

  • My code follows the style guidelines of this project
  • I have performed a self-review of my own code
  • I checked the PR checks reports
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works

@github-actions

This comment was marked as outdated.

@lBaah

This comment was marked as resolved.

@phacUFPE phacUFPE force-pushed the phacUFPE/multiworld_system branch 2 times, most recently from 08ad270 to 8be898c Compare August 18, 2024 15:35
@phacUFPE phacUFPE force-pushed the phacUFPE/multiworld_system branch from b7c6568 to 0e0eb3b Compare August 22, 2024 18:08
@phacUFPE
Copy link
Contributor Author

phacUFPE commented Aug 26, 2024

This pull request is open for testing, please be aware that it may contain a lot of bugs, if you find anything out of order that was not supposed to happen, please report commenting in here.

@phacUFPE phacUFPE marked this pull request as ready for review August 26, 2024 16:24
@sonarqubecloud
Copy link

@Meth28
Copy link
Contributor

Meth28 commented Feb 5, 2025

works perfectly

@FelipePaluco
Copy link
Contributor

works perfectly

Thanks for testing,

@Methemia Are you in the discord? I want to discuss some tests with you as you are already testing this!
If you are, please let me know in the #english chat channel

@htc16
Copy link
Contributor

htc16 commented Feb 21, 2025

Console errors:

[2025-21-02 04:01:05.256] [error] Query: INSERT INTO `house_lists` (`house_id` , `listid` , `list`, `version`, `world_id`) VALUES  (2658,257,'Xistos\nGalopera\nGuto\n',1740128465256130,�),(2660,257,'Galopera\nXistos\nGuto\n',1740128465256130,�),(2941,256,'Gamboa\nMalucoo\n',
[2025-21-02 04:01:05.256] [error] MySQL error [1064]: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near '\0001),(2660,257,'Galopera\nXistos\nGuto\n',1740128465256130,\0001),(2941,...' at line 1
[2025-21-02 07:01:05.075] [error] Query: INSERT INTO `house_lists` (`house_id` , `listid` , `list`, `version`, `world_id`) VALUES  (2658,257,'Xistos\nGalopera\nGuto\n',1740139265074952,�),(2660,257,'Galopera\nXistos\nGuto\n',1740139265074952,�),(2941,256,'Gamboa\nMalucoo\n', 
[2025-21-02 07:01:05.075] [error] MySQL error [1064]: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near '\0001),(2660,257,'Galopera\nXistos\nGuto\n',1740139265074952,\0001),(2941,...' at line 1 
[2025-21-02 08:52:19.320] [error] Query: SELECT *, @row AS `entries`, 1 AS `page` FROM (SELECT *, (@row := @row + 1) AS `rn` FROM (SELECT `p`.`id`, `p`.`name`, `p`.`level`, `p`.`vocation`, `w`.`name` AS `worldName`, `p`.`experience` AS `points`, @curRank := IF(@prevRank = `experience`, @curRank, IF(@prevRank := `experience`, @curRank + 1, @curRank + 1)) AS `rank` FROM `players` `p` INNER JOIN `worlds` `w` ON `p`.`world_id` = `w`.`id`, (SELECT @curRank := 0, @prevRank := NULL, @row := 0) `r` WHERE `group_id` < 4 Hamble ORDER BY `experience` DESC) `t`) `T` WHERE `rn` > 0 AND `rn` <= 20 
[2025-21-02 08:52:19.320] [error] Message: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near 'Hamble ORDER BY `experience` DESC) `t`) `T` WHERE `rn` > 0 AND `rn` <= 20' at line 1 
[2025-21-02 10:01:05.641] [error] Query: INSERT INTO `house_lists` (`house_id` , `listid` , `list`, `version`, `world_id`) VALUES  (2658,257,'Xistos\nGalopera\nGuto\n',1740150065640791,�),(2660,257,'Galopera\nXistos\nGuto\n',1740150065640791,�),(2941,256,'Gamboa\nMalucoo\n', 
[2025-21-02 10:01:05.641] [error] MySQL error [1064]: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near '\0001),(2660,257,'Galopera\nXistos\nGuto\n',1740150065640791,\0001),(2941,...' at line 1 

Ranking on inactive client
image

@htc16
Copy link
Contributor

htc16 commented Feb 22, 2025

I managed to solve the ranking problem here:

std::string Game::generateHighscoreQuery(
    const std::string &categoryName,
    const std::string &worldName,
    uint32_t page,
    uint8_t entriesPerPage,
    uint32_t vocation,
    uint32_t playerGUID /*= 0*/
) {
    uint32_t startPage = (page - 1) * static_cast<uint32_t>(entriesPerPage);
    uint32_t endPage = startPage + static_cast<uint32_t>(entriesPerPage);

    if (categoryName.empty()) {
        g_logger().error("Category name cannot be empty.");
        return "";
    }

    std::string vocationCondition = (vocation != 0xFFFFFFFF) ? generateVocationConditionHighscore(vocation) : "";

    std::string worldCondition = (!worldName.empty()) ? fmt::format(" AND `w`.`name` = {}", Database::getInstance().escapeString(worldName)) : "";

    std::string query = fmt::format(
        "SELECT *, @row AS `entries`, {} AS `page` FROM ("
        "SELECT *, (@row := @row + 1) AS `rn` FROM ("
        "SELECT `p`.`id`, `p`.`name`, `p`.`level`, `p`.`vocation`, `w`.`name` AS `worldName`, `p`.`{}` AS `points`, "
        "@curRank := IF(@prevRank = `{}`, @curRank, IF(@prevRank := `{}`, @curRank + 1, @curRank + 1)) AS `rank` "
        "FROM `players` `p` INNER JOIN `worlds` `w` ON `p`.`world_id` = `w`.`id`, "
        "(SELECT @curRank := 0, @prevRank := NULL, @row := 0) `r` "
        "WHERE `group_id` < {}{}{} "
        "ORDER BY `{}` DESC) `t`"
        ") `T` WHERE `rn` > {} AND `rn` <= {}",
        page,                         // {0}
        categoryName,                // {1} em `p`.`{}` AS `points`
        categoryName,                // {2} em `@prevRank = '{}'
        categoryName,                // {3} em `@prevRank := '{}'
        static_cast<int>(GROUP_TYPE_GAMEMASTER), // {4}
        vocationCondition,           // {5}
        worldCondition,              // {6}
        categoryName,                // {7} em ORDER BY `{}`
        startPage,                   // {8}
        endPage                      // {9}
    );

    g_logger().debug("Generated highscore query: {}", query);
    return query;
}

The house one seemed more complex to me, I solved one problem and another appeared.

@htc16
Copy link
Contributor

htc16 commented Feb 27, 2025

When there are 2 worlds linked, the players_online table is reset periodically and does not list the players from the second world, it only lists the first.

@htc16
Copy link
Contributor

htc16 commented Feb 28, 2025

I managed to solve this one about the houses

bool IOMapSerialize::SaveHouseInfoGuard() {
	Database &db = Database::getInstance();
	auto currentWorld = g_game().worlds().getCurrentWorld();
	const auto worldId = static_cast<int>(currentWorld->id);

	std::ostringstream query;
	DBInsert houseUpdate("INSERT INTO `houses` (`id`, `owner`, `paid`, `warnings`, `name`, `town_id`, `rent`, `size`, `beds`, `bidder`, `bidder_name`, `highest_bid`, `internal_bid`, `bid_end_date`, `state`, `transfer_status`, `world_id`) VALUES ");
	houseUpdate.upsert({ "owner", "paid", "warnings", "name", "town_id", "rent", "size", "beds", "bidder", "bidder_name", "highest_bid", "internal_bid", "bid_end_date", "state", "transfer_status" });

	for (const auto &[key, house] : g_game().map.houses.getHouses()) {
		auto stateValue = magic_enum::enum_integer(house->getState());
		std::string values = fmt::format("{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{}", house->getId(), house->getOwner(), house->getPaidUntil(), house->getPayRentWarnings(), db.escapeString(house->getName()), house->getTownId(), house->getRent(), house->getSize(), house->getBedCount(), house->getBidder(), db.escapeString(house->getBidderName()), house->getHighestBid(), house->getInternalBid(), house->getBidEndDate(), std::to_string(stateValue), (house->getTransferStatus() ? 1 : 0), worldId);

		if (!houseUpdate.addRow(values)) {
			return false;
		}
	}

	if (!houseUpdate.execute()) {
		return false;
	}

	DBInsert listUpdate("INSERT INTO `house_lists` (`house_id` , `listid` , `list`, `version`, `world_id`) VALUES ");
	listUpdate.upsert({ "list", "version" });
	auto version = getTimeUsNow();

	for (const auto &[key, house] : g_game().map.houses.getHouses()) {
		std::string listText;
		if (house->getAccessList(GUEST_LIST, listText) && !listText.empty()) {
			query << house->getId() << ',' << GUEST_LIST << ',' << db.escapeString(listText) << ',' << version << ',' << worldId;
			if (!listUpdate.addRow(query)) {
				return false;
			}

			listText.clear();
		}

		if (house->getAccessList(SUBOWNER_LIST, listText) && !listText.empty()) {
			query << house->getId() << ',' << SUBOWNER_LIST << ',' << db.escapeString(listText) << ',' << version << ',' << worldId;
			if (!listUpdate.addRow(query)) {
				return false;
			}

			listText.clear();
		}

		for (const std::shared_ptr<Door> &door : house->getDoors()) {
			if (door->getAccessList(listText) && !listText.empty()) {
				query << house->getId() << ',' << door->getDoorId() << ',' << db.escapeString(listText) << ',' << version << ',' << worldId;
				if (!listUpdate.addRow(query)) {
					return false;
				}

				listText.clear();
			}
		}
	}

	if (!listUpdate.execute()) {
		return false;
	}

	if (!db.executeQuery(fmt::format("DELETE FROM `house_lists` WHERE `version` < {} AND `world_id` = {}", version, worldId))) {
		return false;
	}

	return true;
}

resolved from online players

void Game::updatePlayersOnline() const {
	// Function to be executed within the transaction
    auto updateOperation = [this]() {
        const auto &m_players = getPlayers();
        auto currentWorld = worlds().getCurrentWorld();
		const auto worldId = static_cast<int>(currentWorld->id);
        bool changesMade = false;

		// g_metrics().addUpDownCounter("players_online", 1);
		// g_metrics().addUpDownCounter("players_online", -1);

        if (m_players.empty()) {
            std::string query = "SELECT COUNT(*) AS count FROM players_online WHERE world_id = " + std::to_string(worldId) + ";";
            auto result = g_database().storeQuery(query);
            int count = result->getNumber<int>("count");
            if (count > 0) {
                g_database().executeQuery("DELETE FROM `players_online` WHERE world_id = " + std::to_string(worldId) + ";");
                changesMade = true;
            }
        } else {
            DBInsert stmt("INSERT IGNORE INTO `players_online` (player_id, world_id) VALUES ");
			for (const auto &[key, player] : m_players) {
				std::ostringstream playerQuery;
				playerQuery << player->getGUID() << ", " << worldId; // Sem parênteses
				stmt.addRow(playerQuery.str());
			}
			stmt.execute();
            changesMade = true;

            std::ostringstream cleanupQuery;
            cleanupQuery << "DELETE FROM `players_online` WHERE world_id = " << worldId << " AND `player_id` NOT IN (";
            for (const auto &[key, player] : m_players) {
                cleanupQuery << player->getGUID() << ",";
            }
            cleanupQuery.seekp(-1, std::ostringstream::cur);
            cleanupQuery << ");";
            g_database().executeQuery(cleanupQuery.str());
        }

        return changesMade;
    };

    const bool success = DBTransaction::executeWithinTransaction(updateOperation);
    if (!success) {
        const auto worldId = worlds().getCurrentWorld()->id;
        g_logger().error("[Game::updatePlayersOnline] Failed to update players online for world " + std::to_string(worldId));
    }
}

@phacUFPE
Copy link
Contributor Author

phacUFPE commented Mar 1, 2025

SQL queries from Highscores, houses and players online fixed.

@sonarqubecloud
Copy link

sonarqubecloud bot commented Mar 1, 2025

@htc16
Copy link
Contributor

htc16 commented Mar 2, 2025

SQL queries from Highscores, houses and players online fixed.

When you have a world with 10 players and another with 0, according to your update, periodically the 0 will clear the list of the one with 10 since &m_players is empty for that world.

solution

if (m_players.empty()) {
			std::string query = fmt::format("SELECT COUNT(*) AS count FROM players_online WHERE world_id = {};", worldId);
			auto result = g_database().storeQuery(query);
			int count = result->getNumber<int>("count");
			if (count > 0) {
				std::string deleteQuery = fmt::format("DELETE FROM `players_online` WHERE `world_id` = {};", worldId);
				g_database().executeQuery(deleteQuery);
				changesMade = true;
			}
		}

@htc16
Copy link
Contributor

htc16 commented Mar 2, 2025

Also, in the server_initialization.lua file you need to add:

local worldId = configManager.getNumber(configKeys.WORLD_ID)
db.query("DELETE FROM `players_online` WHERE `world_id` = " .. worldId)

@htc16
Copy link
Contributor

htc16 commented Apr 15, 2025

This PR can be merged, I have been testing it for a long time.

}

void House::setOwner(uint32_t guid, bool updateDatabase /* = true*/, const std::shared_ptr<Player> &player /* = nullptr*/) {
const auto worldId = g_game().worlds().getCurrentWorld()->id;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was throwing an exception when someone buys a house, I changed to the line below and fixed the issue.

Suggested change
const auto worldId = g_game().worlds().getCurrentWorld()->id;
const auto worldId = static_cast<int>(g_game().worlds().getCurrentWorld()->id);


const auto worldId = static_cast<int>(g_game().worlds().getCurrentWorld()->id);

if (m_players.empty()) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider doing this change too:

image
std::ostringstream query;
			query << "SELECT COUNT(*) AS count FROM players_online WHERE world_id = " << worldId << ";";

			auto result = g_database().storeQuery(query.str());
			int count = result->getNumber<int>("count");
			if (count > 0) {
				std::ostringstream ss;
				ss << "DELETE FROM players_online WHERE world_id = " << worldId << ";";
				g_database().executeQuery(ss.str());
				changesMade = true;
			}

There's a conflict in players' online count if there are more than 1 world online.
With this change, one world doesn't impact the other.

@Codemaufeito
Copy link

With the change in the status port, the otservlist no longer finds the server. Is there a solution?

@htc16
Copy link
Contributor

htc16 commented May 11, 2025

With the change in the status port, the otservlist no longer finds the server. Is there a solution?

Instead of 97173 you just use 7173, 7174...

@htc16
Copy link
Contributor

htc16 commented May 21, 2025

worldType = "pvp" worldLocation = "South America" o servidor parece que fica como pvp enfor

image

Dude, when you change the config.lua you need to change the dB worlds too, I believe that's it.

@htc16
Copy link
Contributor

htc16 commented Aug 2, 2025

This PR could have been merged a long time ago, and at this moment I have been testing it without any problems with the latest updates.

@htc16
Copy link
Contributor

htc16 commented Aug 2, 2025

I even found that it is possible to use the status port directly on 7171 just like before without any problems.

@Codemaufeito
Copy link

I even found that it is possible to use the status port directly on 7171 just like before without any problems.

I've been using this system for a long time without any problems... but I'm already thinking about an improved version
https://discord.com/channels/965178465915633674/965188047132049418/1385134715547160607

@htc16
Copy link
Contributor

htc16 commented Aug 5, 2025

I even found that it is possible to use the status port directly on 7171 just like before without any problems.

I've been using this system for a long time without any problems... but I'm already thinking about an improved version https://discord.com/channels/965178465915633674/965188047132049418/1385134715547160607

goooood

@imagenet

This comment has been minimized.

@imagenet

This comment has been minimized.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.