Conversation
|
LGTM I do think that we need to stop hard coding the quests in code. We should come up with a system that enable us to define quest from a external file (and maybe even hot reload?) |
sepalani
left a comment
There was a problem hiding this comment.
I'm not a huge fan of the way this is handled as the "temporal slots" are hardcoded and hooking more things (and lambda) into a not flexible already hardcoded system doesn't feel clean at all.
In other words, we shouldn't hardcode quests, nor its rotation system, nor tight it to the jhen event. Ideally, we should be able to change these settings without rebooting the server.
I'm open to suggestions regarding how this can be implemented. I've been thinking about hosting binary quest files and using a config file to schedule which quest files should be served.
|
I agree with you, but most if not all the requirement that you set would require a fairly robust config system. If we are going to "stop" the progression of game <-> server feature we should start asap trying to figure out the direction that we want to go |
cc7701c to
3bc8e5e
Compare
sepalani
left a comment
There was a problem hiding this comment.
Some comments regarding the code. Could you please also address the linter issues.
|
|
||
|
|
||
| def create_server(server_class, server_handler, | ||
| def create_server(server_class, server_handler, binary_loader, |
There was a problem hiding this comment.
Can't the binary_loader be loaded from its module (using the module as singleton and/or providing a get_instance) when needed?
There was a problem hiding this comment.
It's currently initialized a single time in master_server.py and a reference to that object is passed down to the children that require it. Since it's stateful (currently in that it tracks an incrementing version number, and in the future in that we might have it on an auto refresh timer to check for updated files periodically) I'd rather do it this way.
There was a problem hiding this comment.
If its purpose is to be initialised once, then you should use the singleton pattern rather than passing it to every functions. I also suspect that it introduced a regression as server can't be launched independently anymore (e.g. python fmp_server.py).
dump_quest.py
Outdated
| http://divinewh.im/q/c/Grudge_Match:_Bird_and_Brute | ||
| """ | ||
|
|
||
| from mh.quest_utils import * |
There was a problem hiding this comment.
Please address this linter comment.
| @@ -0,0 +1,2937 @@ | |||
| """Quest dumper. | |||
There was a problem hiding this comment.
This is missing a shebang and the copyright notice.
dump_quest.py
Outdated
| def EXPORT(quest): | ||
| import json | ||
| name = quest['quest_info']['name'].replace(':', '') | ||
| with open('event/{}.json'.format(name), "w") as outfile: | ||
| json.dump(quest, outfile, indent=4) | ||
|
|
||
|
|
||
| QUESTS = [] |
There was a problem hiding this comment.
This can be done in a main function. Creating a QUEST list isn't necessary here. Its creation can be deferred later when all quests are created. locals() and a list/dict comprehension (or similar) can be used if you want the quests to be automatically included in a list.
dump_quest.py
Outdated
| for q in QUESTS: | ||
| EXPORT(q) |
There was a problem hiding this comment.
Please use if __name__ == "__main__" if the intended behaviour for this file is to be run as a script. Otherwise, this behaviour will be present if this is imported. You might also want to create a main function and use the argparse module if you intend to add more options to this script.
There was a problem hiding this comment.
This has been fixed, as with above
| version = binary["version"] | ||
| if callable(version): | ||
| version = version(self.server.binary_loader) | ||
| content = get_pat_binary_from_version(binary_type, version) | ||
| if callable(content): | ||
| content = content() | ||
| data = struct.pack(">II", binary["version"], len(content)) | ||
| content = content(self.server.binary_loader) | ||
| data = struct.pack(">II", version, len(content)) |
There was a problem hiding this comment.
Can't this logic/part be offloaded to the binary_loader?
There was a problem hiding this comment.
Not all binaries currently come from the binary_loader, only the ones that have the potential to change from moment to moment. In other words yes, it could be moved (and probably should, once we have things like a rotating trading post), but since this PR is specifically about the event quests, I'll leave that to a future update.
| class QuestLoader: | ||
| def __init__(self, file_loc='event/quest_rotation.json'): | ||
| self.version = 0 | ||
| self.version_incrementer = 28 |
There was a problem hiding this comment.
This gives us the ability to expand the quest rotation to up to 4 weeks (7 * 4 days) per update.
There was a problem hiding this comment.
You might want to offload a version, base_version or whatever to the quest_rotation.json as it shouldn't be hardcoded.
mh/quest_utils.py
Outdated
| self.load_quests() | ||
| self.version = 0 | ||
|
|
||
| def load_quests(self, file_loc=None): |
mh/quest_utils.py
Outdated
| def get_quest(self, idx): | ||
| return self.quests(idx) |
There was a problem hiding this comment.
- Is this ever used?
- Isn't
self.questsa list rather than a callable?
mh/quest_utils.py
Outdated
| trueIdx = idx - 1 | ||
| return [self.quests[trueIdx][j][1] for j in range(len(self.quests[trueIdx]))] | ||
|
|
||
| def __len__(self): | ||
| # Get number of days | ||
| return len(self.quests) | ||
|
|
||
| def __getitem__(self, idx): | ||
| # Get quests for the day | ||
| trueIdx = idx - self.version - 1 | ||
| print("idx is {}".format(trueIdx)) | ||
| print("idx: {}, len is {}".format(trueIdx, len(self.quests[trueIdx]))) | ||
| return [self.quests[trueIdx][j][0] for j in range(len(self.quests[trueIdx]))] |
There was a problem hiding this comment.
- Please use
snake_case. - Can't
enumeratebe used instead ofrange(len(...))? - These debug prints should be removed.
7ea1320 to
96b52b6
Compare
…ates and .json files for each quest Removed unneeded default parameter Finalized Jump Four Jaggi, added tentative Rage Match, and added more to the quest formatting system. Added "Flooded Forest Free-For-All" and fixed bug Changed quest rotation system to a .json-based system that's refreshable via the interactive (-i) menu Added bytes for small monster wave transition criteria Added ability to programatically add small monsters Added more programatically-added small monster information Addressed Sepalani comments
96b52b6 to
1a17585
Compare
sepalani
left a comment
There was a problem hiding this comment.
Here is a cursory review. The PR can't be run on python3 nor a single server, only master_server is working.
| self.info("Running on {} port {}".format(*address)) | ||
| self.debug_con = [] | ||
| self.debug_mode = debug_mode | ||
| self.binary_loader = binary_loader |
There was a problem hiding this comment.
Can't the binary_loader be instantiated here rather than passing as parameter to all of its parent functions (however, I'd rather prefer using a singleton if possible)?
from mh.quest_utils import QuestLoader
# ...
self.binary_loader = QuestLoader("event/quest_rotation.json")| BowgunFrame, BowgunStock, BowgunBarrel | ||
|
|
||
|
|
||
| def EXPORT(quest): |
There was a problem hiding this comment.
Function names should be snake_case'd. Feel free to use a more explicit name such as export_to_json or similar.
| QUESTS = [] | ||
| QUESTS.append(QUEST_EVENT_JUMP_FOUR_JAGGI) | ||
| QUESTS.append(QUEST_EVENT_THE_PHANTOM_URAGAAN) | ||
| QUESTS.append(QUEST_EVENT_BLOOD_SPORT) | ||
| QUESTS.append(QUEST_EVENT_MERCY_MISSION) | ||
| QUESTS.append(QUEST_EVENT_FF_FREE_FOR_ALL) | ||
| QUESTS.append(QUEST_EVENT_RAGE_MATCH) | ||
| QUESTS.append(QUEST_EVENT_WORLD_EATER) | ||
| QUESTS.append(QUEST_EVENT_WHERE_GODS_FEAR_TO_TREAD) | ||
| QUESTS.append(QUEST_EVENT_GREEN_EGGS) | ||
| QUESTS.append(QUEST_EVENT_JUMP_FOURTY_EIGHT_JAGGI) | ||
| QUESTS.append(GRUDGE_MATCH_ROYAL_LUDROTH) | ||
| QUESTS.append(GRUDGE_MATCH_BIRD_BRUTE) | ||
| QUESTS.append(GRUDGE_MATCH_TWO_FLAMES) | ||
| QUESTS.append(GRUDGE_MATCH_LAND_LORDS) |
There was a problem hiding this comment.
| QUESTS = [] | |
| QUESTS.append(QUEST_EVENT_JUMP_FOUR_JAGGI) | |
| QUESTS.append(QUEST_EVENT_THE_PHANTOM_URAGAAN) | |
| QUESTS.append(QUEST_EVENT_BLOOD_SPORT) | |
| QUESTS.append(QUEST_EVENT_MERCY_MISSION) | |
| QUESTS.append(QUEST_EVENT_FF_FREE_FOR_ALL) | |
| QUESTS.append(QUEST_EVENT_RAGE_MATCH) | |
| QUESTS.append(QUEST_EVENT_WORLD_EATER) | |
| QUESTS.append(QUEST_EVENT_WHERE_GODS_FEAR_TO_TREAD) | |
| QUESTS.append(QUEST_EVENT_GREEN_EGGS) | |
| QUESTS.append(QUEST_EVENT_JUMP_FOURTY_EIGHT_JAGGI) | |
| QUESTS.append(GRUDGE_MATCH_ROYAL_LUDROTH) | |
| QUESTS.append(GRUDGE_MATCH_BIRD_BRUTE) | |
| QUESTS.append(GRUDGE_MATCH_TWO_FLAMES) | |
| QUESTS.append(GRUDGE_MATCH_LAND_LORDS) | |
| QUESTS = [ | |
| QUEST_EVENT_JUMP_FOUR_JAGGI, | |
| QUEST_EVENT_THE_PHANTOM_URAGAAN, | |
| QUEST_EVENT_BLOOD_SPORT, | |
| QUEST_EVENT_MERCY_MISSION, | |
| QUEST_EVENT_FF_FREE_FOR_ALL, | |
| QUEST_EVENT_RAGE_MATCH, | |
| QUEST_EVENT_WORLD_EATER, | |
| QUEST_EVENT_WHERE_GODS_FEAR_TO_TREAD, | |
| QUEST_EVENT_GREEN_EGGS, | |
| QUEST_EVENT_JUMP_FOURTY_EIGHT_JAGGI, | |
| GRUDGE_MATCH_ROYAL_LUDROTH, | |
| GRUDGE_MATCH_BIRD_BRUTE, | |
| GRUDGE_MATCH_TWO_FLAMES, | |
| GRUDGE_MATCH_LAND_LORDS | |
| ] |
| class Enum(object): | ||
| def __getitem__(self, idx): | ||
| return getattr(self, idx) |
There was a problem hiding this comment.
This doesn't seem necessary and is very error prone:
- You're hiding the name of the Enum derived classes you instantiated (but you forgot to instantiate
WaveTypewhich seems to be an oversight) - You can do
Monster.barrothwhich should autocomplete and check on modern IDE whereasMonsters['barroth']won't. - It doesn't seem worth it since you will have to parse the json anyway, so you'll need to check every places where the string refers to a resource (monster, location, ...), then get its value (which is single line of code no matter which way you choose: calling getattr or the brackets operator on the enum instance). Worse your current method add one line of code per enum which is overkill.
I have nothing against enum, python3 also has its own enum module. Similar implementations also exist in python2 and use a cleaner approach. Regardless, I don't think the approach you're suggesting is worth its overhead, especially if it's not currently used.
| def byteify(input): | ||
| if isinstance(input, dict): | ||
| return {byteify(key): byteify(value) | ||
| for key, value in input.iteritems()} | ||
| elif isinstance(input, list): | ||
| return [byteify(element) for element in input] | ||
| elif isinstance(input, unicode): | ||
| return input.encode('utf-8') | ||
| else: | ||
| return input |
There was a problem hiding this comment.
This will fail on python3 as reported by Ruff.
IIRC, comparing str and unicode should be safe, so I don't see why such a conversion is needed as you should be able to get your json data properly regardless.
| version = binary["version"] | ||
| if callable(version): | ||
| version = version(self.server.binary_loader) | ||
| content = get_pat_binary_from_version(binary_type, version) | ||
| if callable(content): | ||
| content = content() | ||
| data = struct.pack(">II", binary["version"], len(content)) | ||
| content = content(self.server.binary_loader) | ||
| data = struct.pack(">II", version, len(content)) |
| class QuestLoader: | ||
| def __init__(self, file_loc='event/quest_rotation.json'): | ||
| self.version = 0 | ||
| self.version_incrementer = 28 |
There was a problem hiding this comment.
You might want to offload a version, base_version or whatever to the quest_rotation.json as it shouldn't be hardcoded.
|
|
||
|
|
||
| def create_server(server_class, server_handler, | ||
| def create_server(server_class, server_handler, binary_loader, |
There was a problem hiding this comment.
If its purpose is to be initialised once, then you should use the singleton pattern rather than passing it to every functions. I also suspect that it introduced a regression as server can't be launched independently anymore (e.g. python fmp_server.py).
|
Hello, first of all thank you for setting up this MH3SP project and for bringing this magnificent game back to life, I don't know if I'm in the right place to talk about this but a lot of important event quests are missing, let me allow myself so to inform you that I have just found "all" the event quests of the game at this link : (https://monsterhunter.fandom.com/wiki/MH3:_Event_Quests#Rage_Match) hoping that this will help you, (I could possibly translate the quests into French), unfortunately I cannot "yet" help you with the development because I am learning, therefore my skills will not be sufficient, in any case good luck to you and thank you again, hoping that the information will be transmitted ^^ |
This PR adds the foundation for an event/arena quest cycle, with a new list of event/arena quests able to be scheduled to be sent each day. Currently, the event cycle contains 14 unique temporal slots, for 14 days of available scheduling. This is in order to keep the schedule aligned with the Sandstorm event, which is also on a 14-day cycle.