Jump to content


  • Content count

  • Joined

  • Last visited

Everything posted by Sanchez

  1. Hi, In this thread I'm going to show you how to make a game-client or client-game communication with packets, instead of using the old quest-client, client-quest communication. Lets start with the game-client, in this example I will send 1 variable to the client. First start with the HEADER, open your binary source and navigate to UserInterface/Packet.h. Now you will see many headers, create a new one, but search for an empty number. I will use 57, because its not used. GC means it's used for Game -> Client packet, it's just a prefix. HEADER_GC_METIN2DEV Now add the structure for the packet, this is most important part. Structure is the "body" of the packet, it contains the HEADER as BYTE and the other optional variables. As I said I just want to send one int type to the client, so add it. typedef struct command_metin2dev_packet { BYTE bHeader; int M2int; } TPacketGCMetin2Dev; Now navigate to UserInterface/PythonNetworkStream.cpp and add your header to the CMainPacketHeaderMap class. The first parameter of the Set is the HEADER, second is the size of the structure. We will use just static size packets in this tutorial, but the third argument can be dynamic size too. Set(HEADER_GC_METIN2DEV, CNetworkPacketHeaderMap::TPacketType(sizeof(TPacketGCMetin2Dev), STATIC_SIZE_PACKET)); Now navigate to UserInterface/PythonNtworkStreamPhaseGame.cpp and add the function to the switch. case HEADER_GC_METIN2DEV: ret = RecvM2DevPacket(); break; The name of the function will be RecvM2DevPacket: Now declarate the function, navigate to UserInterface/PythonNetworkStream.h and add it as public: bool RecvM2DevPacket(); Now add the receiver part of the code. Recv "picks" out xy bytes from the buffer and the return type of it is false if there was no data in the buffer by that size otherwise true, which means it was successful. xy = size of the structure bool CPythonNetworkStream::RecvM2DevPacket() { TPacketGCMetin2Dev Metin2DevGC; if (!Recv(sizeof(TPacketGCMetin2Dev), &Metin2DevGC)) { Tracen("Recv Metin2DevGC Packet Error"); return false; } } Now we are calling the BINARY_M2DEV_Test function in game.py and passing the received data. PyCallClassMemberFunc(m_apoPhaseWnd[PHASE_WINDOW_GAME], "BINARY_M2DEV_Test", Py_BuildValue("(i)", Metin2DevGC.M2int)); This was the client-side of the game-client communication, lets start the server-side: First of all we need to add the header again, navigate to game/packet.h and add this: And the structure: typedef struct packet_metin2dev_packet { BYTE byHeader; int M2int; } TPacketGCMetin2Dev; Now navigate to game/char.cpp and create a function which sends the packet. void CHARACTER::SendMetin2DevPacket() { } Declare it in the game/char.h: void SendMetin2DevPacket(); Now lets add the content of the function. Create a new instance of the structure, set the values of it and send it to the client. void CHARACTER::SendMetin2DevPacket() { if (!GetDesc()) { return; } TPacketGCMetin2Dev Metin2DevGC; Metin2DevGC.byHeader = HEADER_GC_METIN2DEV; Metin2DevGC.M2int = GetPlayerID(); GetDesc()->Packet(&Metin2DevGC, sizeof(TPacketGCMetin2Dev)); } Now add the last function to game.py, this will be called by the binary: def BINARY_M2DEV_Test(self, M2int): import dbg dbg.LogBox(str(M2int)) Finally, lets check how it works: If you have any question or suggestion, please just reply to this topic. Kind Regards, Sanchez
  2. Hi, In this thread I will show you how to do an in-game ban for your GMs. First of all, you need to make a new column in the account.account. The name of the field should be reason and select varchar as type. Or just use this in your console to create the field: ALTER TABLE account ADD reason VARCHAR(256); Now open game/cmd.cpp and search for this: ACMD(do_block_chat); Add this under that: ACMD(do_ban); Search for this still in the game/cmd.cpp: { "block_chat_list",do_block_chat_list, 0, POS_DEAD, GM_PLAYER }, Make a new line and add this under that: { "ban", do_ban, 0, POS_DEAD, GM_IMPLEMENTOR }, At this point you can change the rights for the command: GM_PLAYER - do NOT choose this! GM_LOW_WIZARD GM_WIZARD GM_HIGH_WIZARD GM_GOD GM_IMPLEMENTOR Search for this event in game/cmd_gm.cpp: ACMD(do_block_chat) Add this under that: ACMD(do_ban) Now time to add the complete code to ACMD(do_ban): // Args char arg1[256], arg2[256], arg3[256]; // Local variables const char* szName; const char* szReason; int iDuration; one_argument(two_arguments(argument, arg1, sizeof(arg1), arg2, sizeof(arg2)), arg3, sizeof(arg3)); // Invalid syntax if (!*arg1 || !*arg2 || !*arg3) { ch->ChatPacket(CHAT_TYPE_INFO, "Invalid Syntax, usage: <player name> <time in hours> <reason> tip: don't use spaces in the reason, use _"); return; } szName = arg1; iDuration = atoi(arg2); szReason = arg3; if (iDuration <= 0) { ch->ChatPacket(CHAT_TYPE_INFO, "Duration can't be 0 or minus."); return; } LPCHARACTER tch = CHARACTER_MANAGER::instance().FindPC(szName); if (!tch) { ch->ChatPacket(CHAT_TYPE_INFO, "%s is not playing", szName); return; } if (!tch->GetDesc()) { ch->ChatPacket(CHAT_TYPE_INFO, "%s don't have desc", szName); return; } if (tch == ch) { ch->ChatPacket(CHAT_TYPE_INFO, "What's wrong with you? Don't ban yourself"); return; } if (tch->GetGMLevel() > GM_PLAYER) { ch->ChatPacket(CHAT_TYPE_INFO, "Do not ban GMs"); return; } std::auto_ptr<SQLMsg> msg(DBManager::instance().DirectQuery("UPDATE account.account SET availDt = FROM_UNIXTIME(UNIX_TIMESTAMP(CURRENT_TIMESTAMP()) + %i), reason = '%s' WHERE id = %d", iDuration * 3600, szReason, tch->GetDesc()->GetAccountTable().id)); tch->GetDesc()->DelayedDisconnect(5); sys_log(0, "%s[%d] banned %s for %i hours with reason: %s", ch->GetName(), ch->GetPlayerID(), szName, iDuration, szReason); ch->ChatPacket(CHAT_TYPE_INFO, "%s has been banned for %i hours with reason: %s", szName, iDuration, szReason); Check how it works: /ban <player name> <duration in hours> <reason> Example: /ban Doe 24 Hacking In-game: Database, account table: syslog: Feb 10 03:30:15.890000 :: Sanchez[57735] banned Doe for 24 hours with reason: Hacking If you have any question or suggestion, please just reply to this topic. Kind Regards, Sanchez
  3. Hi, In this thread I will show you how you can implement an advanced spam protection to your server. The player can write the same message just after 5 seconds. The player will receive 1 minute of chat ban if he is trying to send the same message more than 3 times in 5 seconds. You can specify a blockspamlist.lst file which contains words. If the user sending a message which contains a words from the list, the player will receive 5 minutes of chat ban. (You can specify the time of the chat ban) You can specify a bannspamlist.lst file which contains words. If the user sending a message which contains a words from the list, the player will receive a ban. (You can specify the time of the ban) Open game/input_main.cpp and search for this: if (ch->IncreaseChatCounter() >= 10) Add this over that: if (!strcmp(ch->LastPlayerMessage, buf) && (thecore_pulse() < (ch->LastMessageAt + SPAM_WAIT_SEC * 25)) && !ch->SpamAllowBuf(buf) && ch->GetGMLevel() < GM_LOW_WIZARD) { if (ch->BlockChatAfter < 2) { ch->ChatPacket(CHAT_TYPE_INFO, ("You must wait 5 seconds to repeat your message")); ch->BlockChatAfter++; return iExtraLen; } else { ch->BlockChatAfter = 0; ch->PlayerPunish(false, SPAM_CHAT_BAN_TIME); return iExtraLen; } } else { if (!ch->BannListCheck(buf) && ch->GetGMLevel() < GM_LOW_WIZARD) { ch->PlayerPunish(true, SPAM_BAN_TIME); return iExtraLen; } if (!ch->SpamListCheck(buf) && ch->GetGMLevel() < GM_LOW_WIZARD) { ch->ChatPacket(CHAT_TYPE_INFO, ("You wrote a not allowed words!")); ch->PlayerPunish(false, SPAM_CHAT_BAN_TIME); return iExtraLen; } } Still in input_main.cpp search for this: ch->GetMapIndex(), strlen(ch->GetName()))); Add this under that: strcpy(ch->LastPlayerMessage, buf); ch->LastMessageAt = thecore_pulse(); ch->BlockChatAfter = 0; Open game/char.h and search for this: BYTE GetChatCounter() const; Add this under that: int LastMessageAt; int BlockChatAfter; char LastPlayerMessage[CHAT_MAX_LEN + 1]; void PlayerPunish(bool PowerPunish, int Duration); bool SpamListCheck(const char *Message); bool BannListCheck(const char *Message); bool SpamAllowBuf(const char *Message); Open game/char.cpp and add these events: void CHARACTER::PlayerPunish(bool PowerPunish, int Duration) { if (!PowerPunish) { AddAffect(AFFECT_BLOCK_CHAT, POINT_NONE, 0, AFF_NONE, Duration, 0, true); sys_log(0, "%s[%d] has been chatbanned because of spamming/writing words which are in the spamlist.txt", GetName(), GetPlayerID()); } else { std::auto_ptr<SQLMsg> msg(DBManager::instance().DirectQuery("UPDATE account.account SET availDt = FROM_UNIXTIME(UNIX_TIMESTAMP(CURRENT_TIMESTAMP()) + %i) WHERE id = %d", Duration, GetAID())); sys_log(0, "%s[%d] has been banned because of saying blacklisted word", GetName(), GetPlayerID()); GetDesc()->DelayedDisconnect(5); } } bool CHARACTER::SpamAllowBuf(const char *Message) { if (!strcmp(Message, "(Ȳ´ç)") || !strcmp(Message, "(µ·)") || !strcmp(Message, "(±â»Ý)") || !strcmp(Message, "(ÁÁľĆ)") || !strcmp(Message, "(»ç¶ű)") || !strcmp(Message, "(şĐłë)") || !strcmp(Message, "(ľĆÇĎ)") || !strcmp(Message, "(żěżď)") || !strcmp(Message, "(ÁËĽŰ)")) { return true; } return false; } bool CHARACTER::SpamListCheck(const char *Message) { for (int i = 0; i < SpamBlockListArray.size(); i++) { if (!strcmp(Message, SpamBlockListArray[i].c_str())) { return false; } } return true; } bool CHARACTER::BannListCheck(const char *Message) { for (int i = 0; i < SpamBannListArray.size(); i++) { if (!strcmp(Message, SpamBannListArray[i].c_str())) { return false; } } return true; } Add 2 new files to your project: spamblock.cpp spamblock.h Add these to the spamblock.cpp #include "fstream" #include "string" #include "sstream" #include "stdafx.h" #include "../../common/length.h" std::vector<std::string> SpamBlockListArray; std::vector<std::string> SpamBannListArray; void LoadBlockSpamList() { std::string TempBlockList; std::ifstream File("chat/blockspamlist.lst"); if (!File.is_open()) { sys_log(0, "WARNING: cannot open chat/blockspamlist.lst"); return; } SpamBlockListArray.clear(); while (!File.eof()) { File >> TempBlockList; SpamBlockListArray.push_back(TempBlockList); } File.close(); } void LoadBannSpamList() { std::string TempBannList; std::ifstream File("chat/bannspamlist.lst"); if (!File.is_open()) { sys_log(0, "WARNING: cannot open chat/bannspamlist.lst"); return; } SpamBannListArray.clear(); while (!File.eof()) { File >> TempBannList; SpamBannListArray.push_back(TempBannList); } File.close(); } Add these to the spamblock.h #include "string" #include "../../common/length.h" extern void LoadBlockSpamList(); extern void LoadBannSpamList(); extern std::vector<std::string> SpamBlockListArray; extern std::vector<std::string> SpamBannListArray; Add this to game/char.cpp #include "spamblock.h" Add this to game/main.cpp #include "spamblock.h" Search for this in game/main.cpp: PanamaLoad(); Add these under that: LoadBlockSpamList(); LoadBannSpamList(); Open common/length.h and add these: SPAM_WAIT_SEC = 5, // The player can duplicate his message after 5 sec SPAM_CHAT_BAN_TIME = 60, // The player will receive 60 seconds chat ban, if he is saying a spamlist word SPAM_BAN_TIME = 3600, // The player will receive 1 hour ban, if he is saying a banlist word Open game/cmd_gm.cpp and search for this event: ACMD(do_reload) Add this to the switch function: case 'b': ch->ChatPacket(CHAT_TYPE_INFO, "Reloading bann/spam list infomations."); LoadBlockSpamList(); LoadBannSpamList(); sys_log(0, "Reloading bann/spam list infomations."); break; Add this to the top of the file: #include "spamblock.h" How to set up: Make sure you added everything to your game Create a new folder called chat in your channels Create 2 files, blockspamlist.lst and bannspamlist.lst Upload the words to these files Restart your server blockspamlist.lst example: obsceneword1 obsceneword2 obsceneword3 obsceneword4 bannspamlist.lst example: WWW.CHEAP-FARM-SERVICE.COM WWW.CHEAP-GOLD.COM WWW.EASY-HACKS.COM WWW.FREE-YANG.COM If you have any question or suggestion, please just reply to this topic. Kind Regards, Sanchez
  4. [FIX] PM flooder kick hack

    Hi everyone, Maybe just in my country, but it looks so many people started using this annoying PM flooder which cause a buffer overflow in the target client. It can be fixed easily on server-side, so let's do it: Add these functions as public to char.h: void ClearPMCounter(void) { m_iPMCounter = 0; } void IncreasePMCounter(void) { m_iPMCounter++; } void SetLastPMPulse(void); int GetPMCounter(void) const { return m_iPMCounter; } int GetLastPMPulse(void) const { return m_iLastPMPulse; } Add these to char.h too, but as protected: int m_iLastPMPulse; int m_iPMCounter; Add this function to char.cpp: void CHARACTER::SetLastPMPulse(void) { m_iLastPMPulse = thecore_pulse() + 25; } Still in char.cpp search for the Initialize and add these to the function: m_iLastPMPulse = 0; m_iPMCounter = 0; Now navigate to the Whisper function in input_main.cpp and add this after the iExtraLen variable checking at the top: if (ch->GetLastPMPulse() < thecore_pulse()) ch->ClearPMCounter(); if (ch->GetPMCounter() > 3 && ch->GetLastPMPulse() > thecore_pulse()) { ch->GetDesc()->SetPhase(PHASE_CLOSE); return -1; } Search for this still in the Whisper function: if (pkChr == ch) return (iExtraLen); Add these after that: ch->IncreasePMCounter(); ch->SetLastPMPulse();
  5. Hi everyone, In this thread I will show you how to use the compiled python files in the client. First of all we need to download Python 2.2 or 2.7 Python 2.2 download Python 2.7 download Let's start the tutorial: 1. Rename system.py to main.py or to anything you want, it doesn't matter. 2. Change these stuffs in main.py: Search for this: filename = name + '.py' Replace with this: filename = name + '.pyc' Search for this: newmodule = _process_result(compile(pack_file(filename,'r').read(),filename,'exec'),name) Replace with this: newmodule = _process_result(marshal.loads(pack_file(filename,'rb').read()[8:]),name) Search for this: RunMainScript("prototype.py") Replace with this: RunMainScript("prototype.pyc") 3. Create a new file called system.py 4. Copy this to system.py: import pack import imp import marshal data = pack.Get('main.pyc') if data == None: raise IOError, 'Error happened...' if data[:4] != imp.get_magic(): raise ImportError, 'Error happened...' exec marshal.loads(data[8:]) 5. Let's continue with the compiling. Download this file and put to the Python 2.2 or 2.7 folder. Create a new folder called files and put all .py files from the root and the main.py to the folder. Drop the downloaded Compiler.py to the python.exe and wait few seconds, now it will compile the files. After the compiling finished copy back the .pyc files to the root and delete the old .py files. 6. Copy the system.py to the root. (Example picture) 7. Pack the root.eix/epk If you have any question or suggestion please just reply to this topic. Kind Regards, Sanchez
  6. Open source CRC patcher

    June 17 2014 - I rewrote the whole the source. FAQ: How can I open the project, which version of Visual Studio do I need? I used Visual Studio 2013, but I'm sure you can open in 2012 too. What is CRC? You can learn more about Cyclic redundancy check here. How can I make a list for the patcher? You can use the lister tool, it's in the source. HOW TO MAKE IT WORKS: 1. Change the ServerURL variable in Globals.cs to your url 2. Build the project 3. Create a list with the lister tool (Example of the list) 4. Upload the files and the patchlist to your server (Example of the folder structure) Downloads: MEGA.CO.NZ If you have any question or suggestion please just reply to this topic. Kind Regards, Sanchez
  7. Good afternoon guys, As far as I know there is no available up to date client at the moment, so it's finally time to release one. Can I modify the root? It have .py files? Yes, you can modify it without any problems. Is it contains the updates from the last few weeks? Yes, everything till today. Can I unpack the patches or they are archived with type 4? Every patch has been repacked with type 0-1-2 without any modifications. What archiver should I use? It depends on you, but you can use the r3869/r2806 by Tim, which is available on this board. Is the client have any modifications? No, I just had to modify some lines in the root to get it work with the binary. Is there any way to use again the "pong"? Yes! The client contains 2 binary and the secondary one support it. (Please note: Without the modification of the (40250)game file the secondary binary will not work) metin2client_without_pong.exe metin2client_with_pong.exe Is the binary use Python 2.7? Sure, it's using 2.7. Is the the client have any known bug? I tested many time the client and I didn't found anything. Please let me know if you found an issue. I don't trust you. Can you prepare a VirusTotal? I can't. The size of the client is way too big for VT, but you can find the results of the binaries on pastebin. Download: PASTEBIN
  8. Hi everyone, Microsoft 3 years ago in 2011 released the first version of the Python Tools for Visual Studio and they are still working on it,. Now everybody thinking how is it better to use Visual Studio instead of Notepad++? My opinions are: Better Syntax highlighting IntelliSense Many useful options like these: Go To Definition, Find All References Visual Studio GUI is amazing, It's dream to work with it Example of the Syntax highlighting: Example of the Find All References: Example of the IntelliSense: How to download & install: Go to the Python Tools for Visual Studio Codeplex page and follow the tutorial or just follow these steps: 1. Download Visual Studio, Express version does not supported! 2. Install PTVS for for your Visual Studio version: 2010, 2012, 2013 3. Install Python 2.7 That's all, now you are ready to import the py files: Extract root, locale and uiscript or anything you wan't and separate them to folders. Example: Open Visual Studio and create a new project using the "From Existing Python code" option. On the next window add the path of the 3 folders. Now click on Next and select Python 2.7 from the dropdown list. That's all, now Visual Studio creating your new project with the added files. If you have any question or suggestion, please just reply to this topic. Kind Regards, Sanchez
  9. Hi everyone, In this tutorial i'm going to show you how to unpack the type 4 protected files from the official client. File requirements: [RELEASE] Current Metin2.DE/EN Client This binary fileextractor.py by DaRealFreak (First of all drop the type 4 protected files from the official client to the downloaded client.) Let's start the tutorial with the serverinfo. Unpack the root of the downloaded client and modify the serverinfo.py with these informations: IP address: World port: 12105 Auth port: 11150 Now save the serverinfo.py and open the intrologin.py, then search for example this function: def __OnClickSelectServerButton(self): Add this to the begin of the function: extractor ={} execfile("fileextractor.py", extractor) Now add the fileextractor.py to the root and pack it. Let's continue and drop the downloaded metin2client_extractor.exe to the client folder and start it. Register a new account here, login and create a new character. Now press the exit key on your keyboard to go back to the channel select, because we are already received the keys for the type 4 protected files. And...that's all. Select again a server to show up the python extractor window and we can extract every type 4 protected file.
  10. I can't buy VIP membership

    I'm sorry, but we are still working on it to resolve this issue. Please be to patient, we are trying to do the best.
  11. The title says everything, what's your last movie? Today morning I watched the Blue Is the Warmest Color movie and I really enjoyed it.
  12. VIP

    We are working on it to resolve this issue, but I can't give you an ETA.
  13. backdoor

    Then there's no reason to create a backdoor.
  14. backdoor

    No sorry. You can do it easily, just follow the steps.
  15. backdoor

    It depends on the type of the backdoor, but the easiest and probably the best will be the following: 1. Create or modify a packet which sends text (char array) 2. Write a small tool which can be injected into the process to set the content and send the backdoor packet. 2. Execute the content of the packet by using the system(content goes here) function.
  16. I can clearly read every comment, just use Korean - Windows 949 encoding. Google translate:
  17. Game not saving player info

    Firstly I recommend you to do a graceful db shutdown or just call the CClientManager::Quit() function to see what's going on with the cache when shutting down the server.
  18. Hi everyone, In this thread I will show you how to change the color of the chat messages without any client modification. I will use the 2 new ring slots in this example. First of all, the color codes: RED: |cFFFF0000|H|h GREEN: |cFF00FF00|H|h BLUE: |cFF0080FF|H|h YELLOW: |cFFFFFF00|H|h Open game/unique_item.h and add these: TEXT_COLOR_RED = ID_OF_THE_ITEM, TEXT_COLOR_GREEN = ID_OF_THE_ITEM, TEXT_COLOR_BLUE = ID_OF_THE_ITEM, TEXT_COLOR_YELLOW = ID_OF_THE_ITEM, Open game/input_main.cpp and search for this: const TPacketCGChat* pinfo = reinterpret_cast<const TPacketCGChat*>(data); Replace with this: int len; const TPacketCGChat* pinfo = reinterpret_cast<const TPacketCGChat*>(data); Remove this: int len = snprintf(chatbuf, sizeof(chatbuf), "%s : %s", ch->GetName(), buf); Search for this: if (CHAT_TYPE_SHOUT == pinfo->type) Add this over that: if (ch->IsEquipNewRingItem(TEXT_COLOR_RED)) { // RED len = snprintf(chatbuf, sizeof(chatbuf), "%s : %s %s", ch->GetName(), "|cFFFF0000|H|h", buf); } else if (ch->IsEquipNewRingItem(TEXT_COLOR_GREEN)) { // GREEN len = snprintf(chatbuf, sizeof(chatbuf), "%s : %s %s", ch->GetName(), "|cFF00FF00|H|h", buf); } else if (ch->IsEquipNewRingItem(TEXT_COLOR_BLUE)) { // BLUE len = snprintf(chatbuf, sizeof(chatbuf), "%s : %s %s", ch->GetName(), "|cFF0080FF|H|h", buf); } else if (ch->IsEquipNewRingItem(TEXT_COLOR_YELLOW)) { // YELLOW len = snprintf(chatbuf, sizeof(chatbuf), "%s : %s %s", ch->GetName(), "|cFFFFFF00|H|h", buf); } else { // DEFAULT COLOR len = snprintf(chatbuf, sizeof(chatbuf), "%s : %s", ch->GetName(), buf); } Open game/char.cpp and add this event: bool CHARACTER::IsEquipNewRingItem(DWORD dwItemVnum) const { { LPITEM u = GetWear(WEAR_RING1); if (u && u->GetVnum() == dwItemVnum) { return true; } } { LPITEM u = GetWear(WEAR_RING2); if (u && u->GetVnum() == dwItemVnum) { return true; } } return false; } Open game/char.h and search for this: bool IsEquipUniqueGroup(DWORD dwGroupVnum) const; Add this under that: bool IsEquipNewRingItem(DWORD dwItemVnum) const; And now how it looks in the game: If you have any question or suggestion, please just reply to this topic. Kind Regards, Sanchez
  19. Download is not working

    We are working on it to resolve this issue, please be patient.
  20. Save dogs

    I'm totally agree with this petition. I ate so many things in my life, such as shark, squid, clam, grasshopper and many more animals, but I would never ever eat a dog. I know that I'm from the Europe and I will never understand their culture, but... PS: I also own a dog and she is part of my life, not my lunch.
  21. Metin2 Client Bug

    Next time please open your topic to the Questions and Answers section.
  22. Source discussions, questions, answers

    Please, use spoiler tag for long posts!
  23. CyberGhost VPN Premium for free

    Hi everyone, As the title says you can get the premium version of CyberGhost for free, for a limited time. To get your key just go here: http://www.cyberghostvpn.com/malwaretips and enter your Email address. Unlimited Traffic Unlimited Bandwidth 305+ servers P2P support and many more....
  24. CyberGhost VPN Premium for free

    Try it with a temporary E-mail address. https://www.guerrillamail.com http://getairmail.com http://10minutemail.com/10MinuteMail/index.html