diff --git a/Dockerfile b/Dockerfile index bc8aae5..89a61e5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -19,7 +19,7 @@ ARG MACHINE_EMULATOR_TOOLS_VERSION LABEL io.sunodo.sdk_version=${SUNODORIV_SDK_VERSION} LABEL io.cartesi.rollups.ram_size=128Mi -#LABEL io.cartesi.rollups.data_size=32Mb +LABEL io.cartesi.rollups.data_size=128Mb WORKDIR /opt/tools @@ -27,7 +27,7 @@ WORKDIR /opt/tools RUN < bool: ### # Queries -@query() +@query(splittable_output=True) def cartridge(payload: CartridgePayload) -> bool: query = helpers.select(c for c in Cartridge if c.id == payload.id) @@ -252,7 +265,10 @@ def create_cartridge(cartridge_data,**metadata): cartridge_info_json = json.loads(cartridge_info) Info(**cartridge_info_json) - cartridge_cover = cartridge_info_json.get("cover") + # check if cartridge runs + riv_get_cartridge_outcard(data_hash,0,None,None) + + cartridge_cover = riv_get_cover(data_hash) if cartridge_cover is None or len(cartridge_cover) == 0: cartridge_cover = riv_get_cartridge_screenshot(data_hash,0) diff --git a/app/replay.py b/app/replay.py index 0be3235..69de09a 100644 --- a/app/replay.py +++ b/app/replay.py @@ -13,7 +13,7 @@ from cartesapp.manager import mutation, get_metadata, add_output, event, emit_event, contract_call # TODO: create repo to avoid this relative import hassle from cartesapp.utils import bytes2str -from .setup import AppSettings, ScoreType +from .setup import AppSettings, ScoreType, GameplayHash from .riv import replay_log LOGGER = logging.getLogger(__name__) @@ -55,10 +55,16 @@ def replay(replay: Replay) -> bool: metadata = get_metadata() + if not GameplayHash.check(replay.cartridge_id.hex(),sha256(replay.log).hexdigest()): + msg = f"Gameplay already submitted" + LOGGER.error(msg) + add_output(msg,tags=['error']) + return False + # process replay LOGGER.info("Replaying cartridge...") try: - outcard_raw = replay_log(replay.cartridge_id.hex(),replay.log,replay.args,replay.in_card) + outcard_raw, outhash = replay_log(replay.cartridge_id.hex(),replay.log,replay.args,replay.in_card) except Exception as e: msg = f"Couldn't replay log: {e}" LOGGER.error(msg) @@ -66,8 +72,9 @@ def replay(replay: Replay) -> bool: return False # process outcard - outcard_hash = sha256(outcard_raw.replace(b'\r',b"").replace(b'\t',b"").replace(b'\n',b"").replace(b' ',b"")).digest() - outcard_valid = outcard_hash == replay.outcard_hash + # outcard_hash = sha256(outcard_raw).digest() + # outcard_valid = outcard_hash == replay.outcard_hash + outcard_valid = outhash == replay.outcard_hash outcard_format = outcard_raw[:4] LOGGER.info(f"==== BEGIN OUTCARD ({outcard_format}) ====") @@ -80,7 +87,8 @@ def replay(replay: Replay) -> bool: LOGGER.info("==== END OUTCARD ====") LOGGER.info(f"Expected Outcard Hash: {replay.outcard_hash.hex()}") - LOGGER.info(f"Computed Outcard Hash: {outcard_hash.hex()}") + # LOGGER.info(f"Computed Outcard Hash: {outcard_hash.hex()}") + LOGGER.info(f"Computed Outcard Hash: {outhash.hex()}") LOGGER.info(f"Valid Outcard Hash : {outcard_valid}") if not outcard_valid: @@ -92,7 +100,7 @@ def replay(replay: Replay) -> bool: score = 0 if outcard_format == b"JSON": try: - score = int(json.loads(re.sub(r'\,(?!\s*?[\{\[\"\'\w])', '', outcard_str)).get('score')) or 0 + score = int(json.loads(outcard_str).get('score')) or 0 # re.sub(r'\,(?!\s*?[\{\[\"\'\w])', '', outcard_str) except Exception as e: LOGGER.info(f"Couldn't load score from json: {e}") @@ -106,4 +114,6 @@ def replay(replay: Replay) -> bool: add_output(replay.log,tags=['replay',replay.cartridge_id.hex()]) emit_event(replay_score,tags=['score','general',replay.cartridge_id.hex()]) + GameplayHash.add(replay.cartridge_id.hex(),sha256(replay.log).hexdigest()) + return True diff --git a/app/riv.py b/app/riv.py index f654c93..ff80cd2 100644 --- a/app/riv.py +++ b/app/riv.py @@ -12,21 +12,32 @@ def riv_get_cartridges_path(): def riv_get_cartridge_info(cartridge_id): - args = [] + args = ["sqfscat","-st"] if AppSettings.rivemu_path is None: # use riv os - args.append("riv-chroot") - args.append("/rivos") - args.extend(["sqfscat","-st",f"/{AppSettings.cartridges_path}/{cartridge_id}"]) + args.append(f"/rivos/{AppSettings.cartridges_path}/{cartridge_id}") else: - args.extend(["sqfscat","-st",f"{AppSettings.cartridges_path}/{cartridge_id}"]) + args.append(f"{AppSettings.cartridges_path}/{cartridge_id}") args.append("/info.json") result = subprocess.run(args, capture_output=True, text=True) if result.returncode > 0: - raise Exception("Error getting info") + raise Exception(f"Error getting info: {str(result.stderr)}") return result.stdout +def riv_get_cover(cartridge_id): + args = ["sqfscat","-no-exit"] + if AppSettings.rivemu_path is None: # use riv os + args.append(f"/rivos/{AppSettings.cartridges_path}/{cartridge_id}") + else: + args.append(f"{AppSettings.cartridges_path}/{cartridge_id}") + args.append("/cover.png") + + result = subprocess.run(args, capture_output=True) + if result.returncode > 0: + raise Exception(f"Error getting cover: {str(result.stderr)}") + + return result.stdout def riv_get_cartridge_screenshot(cartridge_id,frame): args = [] @@ -40,15 +51,15 @@ def riv_get_cartridge_screenshot(cartridge_id,frame): args.extend(["--setenv", "RIV_STOP_FRAME", f"{frame}"]) args.extend(["--setenv", "RIV_NO_YIELD", "y"]) args.append("riv-run") - result = subprocess.run(args, capture_output=True, text=True) + result = subprocess.run(args) if result.returncode != 0: - raise Exception("Error getting cover") + raise Exception("Error getting screenshot: {str(result.stderr)}") - cartridge_cover = open(screenshot_path,'rb').read() + cartridge_screenshot = open(screenshot_path,'rb').read() os.remove(screenshot_path) - return cartridge_cover + return cartridge_screenshot # use rivemu screenshot_temp = tempfile.NamedTemporaryFile() @@ -61,29 +72,31 @@ def riv_get_cartridge_screenshot(cartridge_id,frame): args.append(f"-save-screenshot={screenshot_file.name}") args.append(f"-stop-frame={frame}") # args.append(f"-no-yield=y") - result = subprocess.run(args, capture_output=True, text=True, cwd=cwd) + result = subprocess.run(args, cwd=cwd) if result.returncode != 0: - raise Exception(f"Error getting cover") + raise Exception(f"Error getting screenshot: {str(result.stderr)}") - cartridge_cover = open(screenshot_file.name,'rb').read() + cartridge_screenshot = open(screenshot_file.name,'rb').read() screenshot_temp.close() - return cartridge_cover + return cartridge_screenshot def replay_log(cartridge_id,log,riv_args,in_card): if AppSettings.rivemu_path is None: # use riv os replay_path = "/run/replaylog" outcard_path = "/run/outcard" incard_path = "/run/incard" + outhash_path = "/run/outhash" replay_file = open(replay_path,'wb') replay_file.write(log) replay_file.close() if os.path.exists(outcard_path): os.remove(outcard_path) + if os.path.exists(outhash_path): os.remove(outhash_path) - if len(in_card) > 0: + if in_card is not None and len(in_card) > 0: incard_file = open(incard_path,'wb') incard_file.write(in_card) incard_file.close() @@ -94,20 +107,24 @@ def replay_log(cartridge_id,log,riv_args,in_card): run_args.extend(["--setenv", "RIV_CARTRIDGE", f"/{AppSettings.cartridges_path}/{cartridge_id}"]) run_args.extend(["--setenv", "RIV_REPLAYLOG", replay_path]) run_args.extend(["--setenv", "RIV_OUTCARD", outcard_path]) - if len(in_card) > 0: + run_args.extend(["--setenv", "RIV_OUTHASH", outhash_path]) + if in_card is not None and len(in_card) > 0: run_args.extend(["--setenv", "RIV_INCARD", incard_path]) run_args.extend(["--setenv", "RIV_NO_YIELD", "y"]) run_args.append("riv-run") - if riv_args is not None: - run_args.append(riv_args) - result = subprocess.run(run_args, capture_output=True, text=True) + if riv_args is not None and len(riv_args) > 0: + run_args.extend(riv_args.split()) + result = subprocess.run(run_args) if result.returncode != 0: - raise Exception(f"Error processing replay: {result.stderr}") + raise Exception(f"Error processing replay: {str(result.stderr)}") outcard_file = open(outcard_path, 'rb') outcard_raw = outcard_file.read() - return outcard_raw.strip() + outhash_file = open(outhash_path, 'r') + outhash = bytes.fromhex(outhash_file.read()) + + return outcard_raw, outhash # use rivemu replay_temp = tempfile.NamedTemporaryFile() @@ -116,11 +133,13 @@ def replay_log(cartridge_id,log,riv_args,in_card): incard_file = incard_temp.file outcard_temp = tempfile.NamedTemporaryFile() outcard_file = outcard_temp.file + outhash_temp = tempfile.NamedTemporaryFile(mode='w+') + outhash_file = outhash_temp.file replay_file.write(log) replay_file.flush() - if len(in_card) > 0: + if in_card is not None and len(in_card) > 0: incard_file.write(in_card) incard_file.flush() @@ -132,30 +151,27 @@ def replay_log(cartridge_id,log,riv_args,in_card): run_args.append(AppSettings.rivemu_path) run_args.append(f"-cartridge={absolute_cartridge_path}") run_args.append(f"-verify={replay_temp.name}") - # run_args.append(f"-save-outcard={outcard_temp.name}") - if len(in_card): + run_args.append(f"-save-outcard={outcard_temp.name}") + run_args.append(f"-save-outhash={outhash_temp.name}") + run_args.append(f"-speed=1000000") + if in_card is not None and len(in_card): run_args.append(f"-load-incard={incard_temp.name}") - run_args.append(f"-no-yield=y") - if riv_args is not None: - run_args.append(riv_args) - p1 = subprocess.Popen(run_args,stdout=subprocess.PIPE, cwd=cwd) - p2 = subprocess.Popen(["sed","-n","/==== BEGIN OUTCARD ====/,/==== END OUTCARD ====/p"],stdin=p1.stdout,stdout=subprocess.PIPE) - p3 = subprocess.Popen(["head","-n","-1"],stdin=p2.stdout,stdout=subprocess.PIPE) - - fout = open(outcard_temp.name, 'wb') - - result = subprocess.run(["tail","-n","+2"], stdin=p3.stdout,stdout=fout,stderr=subprocess.PIPE) + if riv_args is not None and len(riv_args) > 0: + run_args.extend(riv_args.split()) + result = subprocess.run(run_args, cwd=cwd) if result.returncode != 0: - raise Exception(f"Error processing replay: {result.stderr}") + raise Exception(f"Error processing replay: {str(result.stderr)}") outcard_raw = outcard_file.read() + outhash = bytes.fromhex(outhash_file.read()) replay_temp.close() outcard_temp.close() incard_temp.close() + outhash_temp.close() - return outcard_raw.strip() + return outcard_raw, outhash def riv_get_cartridge_outcard(cartridge_id,frame,riv_args,in_card): if AppSettings.rivemu_path is None: # use riv os @@ -165,7 +181,7 @@ def riv_get_cartridge_outcard(cartridge_id,frame,riv_args,in_card): if os.path.exists(outcard_path): os.remove(outcard_path) - if len(in_card) > 0: + if in_card is not None and len(in_card) > 0: incard_file = open(incard_path,'wb') incard_file.write(in_card) incard_file.close() @@ -177,20 +193,20 @@ def riv_get_cartridge_outcard(cartridge_id,frame,riv_args,in_card): run_args.extend(["--setenv", "RIV_NO_YIELD", "y"]) run_args.extend(["--setenv", "RIV_STOP_FRAME", f"{frame}"]) run_args.extend(["--setenv", "RIV_OUTCARD", outcard_path]) - if len(in_card) > 0: + if in_card is not None and len(in_card) > 0: run_args.extend(["--setenv", "RIV_INCARD", incard_path]) run_args.append("riv-run") - if riv_args is not None: - run_args.append(riv_args) - result = subprocess.run(run_args, capture_output=True, text=True) + if riv_args is not None and len(riv_args) > 0: + run_args.extend(riv_args.split()) + result = subprocess.run(run_args) if result.returncode != 0: - raise Exception("Error running cartridge") + raise Exception(f"Error running cartridge: {str(result.stderr)}") outcard_file = open(outcard_path, 'rb') outcard_raw = outcard_file.read() - return outcard_raw.strip() + return outcard_raw # use rivemu incard_temp = tempfile.NamedTemporaryFile() @@ -198,37 +214,31 @@ def riv_get_cartridge_outcard(cartridge_id,frame,riv_args,in_card): outcard_temp = tempfile.NamedTemporaryFile() outcard_file = outcard_temp.file - if len(in_card) > 0: + if in_card is not None and len(in_card) > 0: incard_file.write(in_card) incard_file.flush() - incard_path = len(in_card) > 0 and incard_temp.name or None + incard_path = in_card is not None and len(in_card) > 0 and incard_temp.name or None absolute_cartridge_path = os.path.abspath(f"{AppSettings.cartridges_path}/{cartridge_id}") cwd = str(Path(AppSettings.rivemu_path).parent.parent.absolute()) run_args = [] run_args.append(AppSettings.rivemu_path) run_args.append(f"-cartridge={absolute_cartridge_path}") - # run_args.append(f"-save-outcard={outcard_temp.name}") - if len(in_card): + run_args.append(f"-save-outcard={outcard_temp.name}") + if in_card is not None and len(in_card): run_args.append(f"-load-incard={incard_temp.name}") - # run_args.append(f"-no-yield=y") run_args.append(f"-stop-frame={frame}") - if riv_args is not None: - run_args.append(riv_args) - p1 = subprocess.Popen(run_args,stdout=subprocess.PIPE, cwd=cwd) - p2 = subprocess.Popen(["grep","-A1","==== BEGIN OUTCARD ===="],stdin=p1.stdout,stdout=subprocess.PIPE) - - fout = open(outcard_temp.name, 'wb') - - result = subprocess.run(["tail","-1"], stdin=p2.stdout,stdout=fout,stderr=subprocess.PIPE) + if riv_args is not None and len(riv_args) > 0: + run_args.extend(riv_args.split()) + result = subprocess.run(run_args, cwd=cwd) if result.returncode != 0: - raise Exception(f"Error running cartridge: {result.stderr}") + raise Exception(f"Error running cartridge: {str(result.stderr)}") outcard_raw = outcard_file.read() outcard_temp.close() incard_temp.close() - return outcard_raw.strip() + return outcard_raw diff --git a/app/scoreboard.py b/app/scoreboard.py index 4355364..d9bffdf 100644 --- a/app/scoreboard.py +++ b/app/scoreboard.py @@ -13,7 +13,7 @@ from cartesapp.manager import mutation, query, get_metadata, output, add_output, event, emit_event, contract_call from cartesapp.utils import hex2bytes, str2bytes, bytes2str -from .setup import AppSettings, ScoreType +from .setup import AppSettings, ScoreType, GameplayHash from .riv import replay_log, riv_get_cartridge_outcard from .cartridge import Cartridge @@ -43,30 +43,30 @@ class Score(Entity): score = helpers.Required(int) scoreboard = helpers.Required(Scoreboard, index=True) -@seed() -def initialize_data(): - name = "simple" - s = Scoreboard( - id = sha256(str2bytes(name)).hexdigest(), - cartridge_id = "907ab088197625939b2137998b0efd59f30b3683093733c1ca4e0a62d638e09f", - name = name, - created_by = "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266", - created_at = 1704078000, - args = "", - in_card = b'', - score_function = "score" - ) - name = "apple 2 seconds" - s = Scoreboard( - id = sha256(str2bytes(name)).hexdigest(), - cartridge_id = "907ab088197625939b2137998b0efd59f30b3683093733c1ca4e0a62d638e09f", - name = name, - created_by = "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266", - created_at = 0, - args = "", - in_card = b'', - score_function = "1000 * apples - 50*frame" - ) +# @seed() +# def initialize_data(): +# name = "simple" +# s = Scoreboard( +# id = sha256(str2bytes(name)).hexdigest(), +# cartridge_id = "907ab088197625939b2137998b0efd59f30b3683093733c1ca4e0a62d638e09f", +# name = name, +# created_by = "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266", +# created_at = 1704078000, +# args = "", +# in_card = b'', +# score_function = "score" +# ) +# name = "apple 2 seconds" +# s = Scoreboard( +# id = sha256(str2bytes(name)).hexdigest(), +# cartridge_id = "907ab088197625939b2137998b0efd59f30b3683093733c1ca4e0a62d638e09f", +# name = name, +# created_by = "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266", +# created_at = 0, +# args = "", +# in_card = b'', +# score_function = "1000 * apples - 50*frame" +# ) # Inputs @@ -258,10 +258,16 @@ def scoreboard_replay(replay: ScoreboardReplayPayload) -> bool: add_output(msg,tags=['error']) return False + if not GameplayHash.check(scoreboard.cartridge_id,sha256(replay.log).hexdigest()): + msg = f"Gameplay already submitted" + LOGGER.error(msg) + add_output(msg,tags=['error']) + return False + # process replay LOGGER.info(f"Processing scoreboard replay...") try: - outcard_raw = replay_log(scoreboard.cartridge_id,replay.log,scoreboard.args,scoreboard.in_card) + outcard_raw, outhash = replay_log(scoreboard.cartridge_id,replay.log,scoreboard.args,scoreboard.in_card) except Exception as e: msg = f"Couldn't replay log: {e}" LOGGER.error(msg) @@ -269,8 +275,9 @@ def scoreboard_replay(replay: ScoreboardReplayPayload) -> bool: return False # process outcard - outcard_hash = sha256(outcard_raw.replace(b'\r',b"").replace(b'\t',b"").replace(b'\n',b"").replace(b' ',b"")).digest() - outcard_valid = outcard_hash == replay.outcard_hash + # outcard_hash = sha256(outcard_raw).digest() + # outcard_valid = outcard_hash == replay.outcard_hash + outcard_valid = outhash == replay.outcard_hash outcard_format = outcard_raw[:4] if outcard_format != b"JSON": @@ -285,7 +292,8 @@ def scoreboard_replay(replay: ScoreboardReplayPayload) -> bool: LOGGER.info(outcard_str) LOGGER.info("==== END OUTCARD ====") LOGGER.info(f"Expected Outcard Hash: {replay.outcard_hash.hex()}") - LOGGER.info(f"Computed Outcard Hash: {outcard_hash.hex()}") + # LOGGER.info(f"Computed Outcard Hash: {outcard_hash.hex()}") + LOGGER.info(f"Computed Outcard Hash: {outhash.hex()}") LOGGER.info(f"Valid Outcard Hash : {outcard_valid}") if not outcard_valid: @@ -295,9 +303,9 @@ def scoreboard_replay(replay: ScoreboardReplayPayload) -> bool: return False try: - outcard_json = json.loads(re.sub(r'\,(?!\s*?[\{\[\"\'\w])', '', outcard_str)) + outcard_json = json.loads(outcard_str) # re.sub(r'\,(?!\s*?[\{\[\"\'\w])', '', outcard_str) parser = Parser() - default_score = outcard_json['scores'] + default_score = outcard_json['score'] score = parser.parse(scoreboard.score_function).evaluate(outcard_json) except Exception as e: msg = f"Couldn't parse score: {e}" @@ -324,6 +332,8 @@ def scoreboard_replay(replay: ScoreboardReplayPayload) -> bool: add_output(replay.log,tags=['replay',scoreboard.cartridge_id,replay.scoreboard_id.hex()]) emit_event(replay_score,tags=['score',scoreboard.cartridge_id,replay.scoreboard_id.hex()]) + GameplayHash.add(scoreboard.cartridge_id,sha256(replay.log).hexdigest()) + return True ### diff --git a/app/setup.py b/app/setup.py index 12be1af..a661df1 100644 --- a/app/setup.py +++ b/app/setup.py @@ -21,3 +21,20 @@ class ScoreType(Enum): default = 0 scoreboard = 1 tournament = 2 + + +class GameplayHash: + cartridge_replays = {} + def __new__(cls): + return cls + + @classmethod + def add(cls, cartridge_id, replay_hash): + if cls.cartridge_replays.get(cartridge_id) is None: cls.cartridge_replays[cartridge_id] = {} + cls.cartridge_replays[cartridge_id][replay_hash] = True + + @classmethod + def check(cls, cartridge_id, replay_hash): + return cls.cartridge_replays.get(cartridge_id) is None \ + or cls.cartridge_replays[cartridge_id].get(replay_hash) is None \ + or cls.cartridge_replays[cartridge_id][replay_hash] == False diff --git a/cartesapp/manager.py b/cartesapp/manager.py index a1a0f83..a5e1c9a 100644 --- a/cartesapp/manager.py +++ b/cartesapp/manager.py @@ -4,7 +4,7 @@ from inspect import getmembers, isfunction, signature import sys, getopt from typing import Optional, List, get_type_hints -from pydantic import BaseModel +from pydantic import BaseModel, create_model from Crypto.Hash import keccak from enum import Enum import json @@ -17,11 +17,10 @@ from .storage import Storage, helpers, add_output_index, OutputType, get_output_indexes from .utils import str2bytes, hex2bytes, bytes2hex - +from .output import MAX_OUTPUT_SIZE, MAX_AGGREGATED_OUTPUT_SIZE, MAX_SPLITTABLE_OUTPUT_SIZE LOGGER = logging.getLogger(__name__) -MAX_OUTPUT_SIZE = 1048567 # (2097152-17)/2 class EmptyClass(BaseModel): pass @@ -57,11 +56,12 @@ def _import_apps(cls): @classmethod def _register_queries(cls, add_to_router=True): query_selectors = [] - for query_fn in Query.queries: - query_name = query_fn.__name__ - module_name = query_fn.__module__.split('.')[0] + for func in Query.queries: + func_name = func.__name__ + module_name = func.__module__.split('.')[0] + configs = Query.configs[f"{module_name}.{func_name}"] - sig = signature(query_fn) + sig = signature(func) if len(sig.parameters) > 1: raise Exception("Queries shouldn't have more than one parameter") @@ -74,24 +74,34 @@ def _register_queries(cls, add_to_router=True): model = EmptyClass # using url router - path = f"{module_name}/{query_name}" + path = f"{module_name}/{func_name}" if path in query_selectors: raise Exception("Duplicate query selector") query_selectors.append(path) + + original_model = model + func_configs = {} + if configs.get("splittable_output") is not None and configs["splittable_output"]: + model_kwargs = splittable_query_params.copy() + model_kwargs["__base__"] = model + model = create_model(model.__name__+'Splittable',**model_kwargs) + func_configs["extended_model"] = model + abi_types = [] # abi.get_abi_types_from_model(model) - cls.queries_info[f"{module_name}.{query_name}"] = {"selector":path,"module":module_name,"method":query_name,"abi_types":abi_types,"model":model} + cls.queries_info[f"{module_name}.{func_name}"] = {"selector":path,"module":module_name,"method":func_name,"abi_types":abi_types,"model":model,"configs":configs} if add_to_router: - LOGGER.info(f"Adding query {module_name}.{query_name} selector={path}, model={model.schema()}") - cls.url_router.inspect(path=path)(_make_query(query_fn,model,param is not None,module=module_name)) + LOGGER.info(f"Adding query {module_name}.{func_name} selector={path}, model={model.__name__}") + cls.url_router.inspect(path=path)(_make_query(func,original_model,param is not None,module_name,**func_configs)) @classmethod def _register_mutations(cls, add_to_router=True): mutation_selectors = [] - for mutation_fn in Mutation.mutations: - mutation_name = mutation_fn.__name__ - module_name = mutation_fn.__module__.split('.')[0] + for func in Mutation.mutations: + func_name = func.__name__ + module_name = func.__module__.split('.')[0] + configs = Mutation.configs[f"{module_name}.{func_name}"] - sig = signature(mutation_fn) + sig = signature(func) if len(sig.parameters) > 1: raise Exception("Mutations shouldn't have more than one parameter") @@ -106,17 +116,17 @@ def _register_mutations(cls, add_to_router=True): # using abi router abi_types = abi.get_abi_types_from_model(model) header = ABIFunctionSelectorHeader( - function=f"{module_name}.{mutation_name}", + function=f"{module_name}.{func_name}", argument_types=abi_types ) header_selector = header.to_bytes().hex() if header_selector in mutation_selectors: raise Exception("Duplicate mutation selector") mutation_selectors.append(header_selector) - cls.mutations_info[f"{module_name}.{mutation_name}"] = {"selector":header,"module":module_name,"method":mutation_name,"abi_types":abi_types,"model":model} + cls.mutations_info[f"{module_name}.{func_name}"] = {"selector":header,"module":module_name,"method":func_name,"abi_types":abi_types,"model":model,"configs":configs} if add_to_router: - LOGGER.info(f"Adding mutation {module_name}.{mutation_name} selector={header_selector}, model={model.schema()}") - cls.abi_router.advance(header=header)(_make_mut(mutation_fn,model,param is not None,module=module_name)) + LOGGER.info(f"Adding mutation {module_name}.{func_name} selector={header_selector}, model={model.schema()}") + cls.abi_router.advance(header=header)(_make_mut(func,model,param is not None,module_name,**Mutation.configs[f"{module_name}.{func_name}"])) @classmethod def _setup_settings(cls): @@ -182,29 +192,38 @@ def create_frontend(cls): # Query class Query: queries = [] + configs = {} def __new__(cls): return cls @classmethod - def add(cls, func): + def add(cls, func, **kwargs): cls.queries.append(func) + func_name = func.__name__ + module_name = func.__module__.split('.')[0] + cls.configs[f"{module_name}.{func_name}"] = kwargs def query(**kwargs): def decorator(func): - Query.add(func) + Query.add(func,**kwargs) return func return decorator +splittable_query_params = {"part":(int,None)} # Mutation class Mutation: mutations = [] + configs = {} def __new__(cls): return cls @classmethod - def add(cls, func): + def add(cls, func, **kwargs): cls.mutations.append(func) + func_name = func.__name__ + module_name = func.__module__.split('.')[0] + cls.configs[f"{module_name}.{func_name}"] = kwargs # TODO: decorator params to allow chunked and compressed mutations def mutation(**kwargs): @@ -215,7 +234,7 @@ def mutation(**kwargs): if kwargs.get('sender_address') is not None: LOGGER.warning("Sender address filtering is not implemented yet") def decorator(func): - Mutation.add(func) + Mutation.add(func,**kwargs) return func return decorator @@ -243,18 +262,21 @@ class Context(object): n_reports: int = 0 n_notices: int = 0 n_vouchers: int = 0 + configs = None + def __new__(cls): return cls @classmethod - def set_context(cls, rollup: Rollup, metadata: RollupMetadata, module: str): + def set_context(cls, rollup: Rollup, metadata: RollupMetadata, module: str, **kwargs): cls.rollup = rollup cls.metadata = metadata cls.module = module cls.n_reports = 0 cls.n_notices = 0 cls.n_vouchers = 0 + cls.configs = kwargs @classmethod def clear_context(cls): @@ -264,6 +286,7 @@ def clear_context(cls): cls.n_reports: 0 cls.n_notices = 0 cls.n_vouchers = 0 + cls.configs = None class Setup: setup_functions = [] @@ -396,6 +419,22 @@ def send_report(payload_data, **kwargs): report_format = OutputFormat[getattr(stg,'report_format')] if hasattr(stg,'report_format') else OutputFormat.json payload,class_name = normalize_output(payload_data,report_format) + extended_params = ctx.configs.get("extended_params") + if extended_params is not None: + if ctx.metadata is None: # inspect + part = extended_params.part + payload_len = len(payload) + if payload_len > MAX_SPLITTABLE_OUTPUT_SIZE and part is not None: + if part >= 0: + startb = MAX_SPLITTABLE_OUTPUT_SIZE*(part) + endb = MAX_SPLITTABLE_OUTPUT_SIZE*(part+1) + payload = payload[startb:endb] + if endb < payload_len: payload += b'0' + + if len(payload) > MAX_AGGREGATED_OUTPUT_SIZE: + LOGGER.warn("Payload Data exceed maximum length. Truncating") + payload = payload[:MAX_AGGREGATED_OUTPUT_SIZE] + # Always chunk if len > MAX_OUTPUT_SIZE # if len(payload) > MAX_OUTPUT_SIZE: raise Exception("Maximum report length violation") @@ -469,13 +508,11 @@ def send_voucher(destination: str, *kargs, **kwargs): ### # Helpers -def _make_query(func,model,has_param, **kwargs): - module = kwargs.get('module') +def _make_query(func,model,has_param,module,**func_configs): @helpers.db_session def query(rollup: Rollup, params: URLParameters) -> bool: try: ctx = Context - ctx.set_context(rollup,None,module) # TODO: accept abi encode or json (for larger post requests, configured in settings) # Decoding url parameters param_list = [] @@ -483,7 +520,8 @@ def query(rollup: Rollup, params: URLParameters) -> bool: hints = get_type_hints(model) fields = [] values = [] - for k in model.__fields__.keys(): + model_fields = model.__fields__.keys() + for k in model_fields: if k in params.query_params: field_str = str(hints[k]) if field_str.startswith('typing.List') or field_str.startswith('typing.Optional[typing.List'): @@ -492,8 +530,23 @@ def query(rollup: Rollup, params: URLParameters) -> bool: else: fields.append(k) values.append(params.query_params[k][0]) - kwargs = dict(zip(fields, values)) - param_list.append(model.parse_obj(kwargs)) + param_list.append(model.parse_obj(dict(zip(fields, values)))) + + extended_model = func_configs.get("extended_model") + if extended_model is not None: + extended_hints = get_type_hints(extended_model) + for k in list(set(extended_model.__fields__.keys()).difference(model_fields)): + if k in params.query_params: + field_str = str(extended_hints[k]) + if field_str.startswith('typing.List') or field_str.startswith('typing.Optional[typing.List'): + fields.append(k) + values.append(params.query_params[k]) + else: + fields.append(k) + values.append(params.query_params[k][0]) + func_configs["extended_params"] = extended_model.parse_obj(dict(zip(fields, values))) + + ctx.set_context(rollup,None,module,**func_configs) res = func(*param_list) except Exception as e: msg = f"Error: {e}" @@ -507,13 +560,12 @@ def query(rollup: Rollup, params: URLParameters) -> bool: return res return query -def _make_mut(func,model,has_param, **kwargs): - module = kwargs.get('module') +def _make_mut(func,model,has_param,module, **kwargs): @helpers.db_session def mut(rollup: Rollup, data: RollupData) -> bool: try: ctx = Context - ctx.set_context(rollup,data.metadata,module) + ctx.set_context(rollup,data.metadata,module,**kwargs) payload = data.bytes_payload()[4:] param_list = [] if has_param: diff --git a/cartesapp/output.py b/cartesapp/output.py index 36b41b3..a74ff77 100644 --- a/cartesapp/output.py +++ b/cartesapp/output.py @@ -1,4 +1,8 @@ +MAX_OUTPUT_SIZE = 1048567 # (2097152-17)/2 +MAX_AGGREGATED_OUTPUT_SIZE = 4194248 # 4194248 = 4194304 (4MB) - 56 B (extra 0x and json formating) +MAX_SPLITTABLE_OUTPUT_SIZE = 4194247 # Extra byte meand there's more data + # TODO add output classes diff --git a/cartesapp/template_frontend_generator.py b/cartesapp/template_frontend_generator.py index 7133ae5..4777e44 100644 --- a/cartesapp/template_frontend_generator.py +++ b/cartesapp/template_frontend_generator.py @@ -6,6 +6,8 @@ from jinja2 import Template from packaging.version import Version +from .output import MAX_SPLITTABLE_OUTPUT_SIZE + FRONTEND_PATH = 'frontend' DEFAULT_LIB_PATH = 'src' PACKAGES_JSON_FILENAME = "package.json" @@ -105,14 +107,16 @@ def render_templates(conf,settings,mutations_info,queries_info,notices_info,repo "add_indexer_query": add_indexer_query, "has_ifaces": has_ifaces, "indexer_query_info": indexer_query_info, - "indexer_output_info": indexer_output_info + "indexer_output_info": indexer_output_info, + "MAX_SPLITTABLE_OUTPUT_SIZE":MAX_SPLITTABLE_OUTPUT_SIZE }) with open(filepath, "w") as f: f.write(helper_template_output) else: imports_template_output = Template(lib_imports).render({ - "has_indexer_query": has_indexer_query + "has_indexer_query": has_indexer_query, + "MAX_SPLITTABLE_OUTPUT_SIZE":MAX_SPLITTABLE_OUTPUT_SIZE }) with open(filepath, "w") as f: @@ -283,6 +287,7 @@ def create_frontend_structure(libs_path=DEFAULT_LIB_PATH): }); const abiCoder = new ethers.utils.AbiCoder(); export const CONVENTIONAL_TYPES: Array = ["bytes","hex","str","int","dict","list","tuple","json"]; +const MAX_SPLITTABLE_OUTPUT_SIZE = {{ MAX_SPLITTABLE_OUTPUT_SIZE }}; /** @@ -721,6 +726,7 @@ def create_frontend_structure(libs_path=DEFAULT_LIB_PATH): const dataTovalidate = data.startsWith('-') ? data.substring(1) : data; return ethers.utils.isHexString(dataTovalidate) && dataTovalidate.length % 2 == 0; }); +const MAX_SPLITTABLE_OUTPUT_SIZE = {{ MAX_SPLITTABLE_OUTPUT_SIZE }}; ''' lib_template = ''' @@ -759,9 +765,28 @@ def create_frontend_structure(libs_path=DEFAULT_LIB_PATH): options?:QueryOptions ):Promise { const route = '{{ info["selector"] }}'; - const data: {{ info['model'].__name__ }} = new {{ info['model'].__name__ }}(inputData); {# return genericInspect(data,route,options); -#} + {% if info["configs"].get("splittable_output") -%} + let part:number = 0; + let hasMoreParts:boolean = false; + const output: InspectReport = {payload: "0x"} + do { + hasMoreParts = false; + let inputDataSplittable = Object.assign({part},inputData); + const data: {{ info['model'].__name__ }} = new {{ info['model'].__name__ }}(inputDataSplittable); + const partOutput: InspectReport = await genericInspect(data,route,options); + let payloadHex = partOutput.payload.substring(2); + if (payloadHex.length/2 > MAX_SPLITTABLE_OUTPUT_SIZE) { + part++; + payloadHex = payloadHex.substring(0, payloadHex.length - 2); + hasMoreParts = true; + } + output.payload += payloadHex; + } while (hasMoreParts) + {% else -%} + const data: {{ info['model'].__name__ }} = new {{ info['model'].__name__ }}(inputData); const output: InspectReport = await genericInspect(data,route,options); + {% endif -%} if (options?.decode) { return decodeToModel(output,options.decodeModel || "json"); } return output; } diff --git a/frontend/.env b/frontend/.env index 3cbbdd9..2b554d0 100644 --- a/frontend/.env +++ b/frontend/.env @@ -1,2 +1,3 @@ NEXT_PUBLIC_DAPP_ADDR="0x70ac08179605AF2D9e75782b8DEcDD3c22aA4D0C" -NEXT_PUBLIC_CARTESI_NODE_URL="http://127.0.0.1:8080" \ No newline at end of file +NEXT_PUBLIC_CARTESI_NODE_URL="http://127.0.0.1:8080" +NEXT_PUBLIC_NETWORK_CHAIN_ID="0x7A69" diff --git a/frontend/app/backend-libs/app/ifaces.d.ts b/frontend/app/backend-libs/app/ifaces.d.ts index 8fb5b2c..8114209 100644 --- a/frontend/app/backend-libs/app/ifaces.d.ts +++ b/frontend/app/backend-libs/app/ifaces.d.ts @@ -6,26 +6,56 @@ */ export interface _Master_ { + CreateScoreboardPayload: CreateScoreboardPayload; + ScoreboardRemoved: ScoreboardRemoved; + RemoveCartridgePayload: RemoveCartridgePayload; + ScoreboardsOutput: ScoreboardsOutput; ScoreboardReplayScore: ScoreboardReplayScore; - ScoreboardsPayload: ScoreboardsPayload; - EmptyClass: EmptyClass; ReplayScore: ReplayScore; + EmptyClass: EmptyClass; + Replay: Replay; + CartridgePayloadSplittable: CartridgePayloadSplittable; + ScoreboardsPayload: ScoreboardsPayload; CartridgeRemoved: CartridgeRemoved; + CartridgesPayload: CartridgesPayload; CartridgeInfo: CartridgeInfo; - ScoresPayload: ScoresPayload; CartridgePayload: CartridgePayload; - CartridgeInserted: CartridgeInserted; - CartridgesOutput: CartridgesOutput; ScoresOutput: ScoresOutput; ScoreboardCreated: ScoreboardCreated; + CartridgeInserted: CartridgeInserted; + CartridgesOutput: CartridgesOutput; + ScoresPayload: ScoresPayload; InserCartridgePayload: InserCartridgePayload; - CartridgesPayload: CartridgesPayload; - ScoreboardRemoved: ScoreboardRemoved; - RemoveCartridgePayload: RemoveCartridgePayload; ScoreboardReplayPayload: ScoreboardReplayPayload; - CreateScoreboardPayload: CreateScoreboardPayload; - Replay: Replay; - ScoreboardsOutput: ScoreboardsOutput; +} +export interface CreateScoreboardPayload { + cartridge_id: string; + name: string; + args: string; + in_card: string; + score_function: string; +} +export interface ScoreboardRemoved { + scoreboard_id: string; + timestamp: number; +} +export interface RemoveCartridgePayload { + id: string; +} +export interface ScoreboardsOutput { + data: ScoreboardInfo[]; + total: number; + page: number; +} +export interface ScoreboardInfo { + id: string; + name: string; + cartridge_id: string; + created_by: string; + created_at: number; + args: string; + in_card: string; + score_function: string; } export interface ScoreboardReplayScore { cartridge_id: string; @@ -36,13 +66,6 @@ export interface ScoreboardReplayScore { extra_score: number; scoreboard_id: string; } -export interface ScoreboardsPayload { - cartridge_id: string; - name?: string; - page?: number; - page_size?: number; -} -export interface EmptyClass {} export interface ReplayScore { cartridge_id: string; user_address: string; @@ -52,10 +75,34 @@ export interface ReplayScore { extra_score?: number; extra?: string; } +export interface EmptyClass {} +export interface Replay { + cartridge_id: string; + outcard_hash: string; + args: string; + in_card: string; + log: string; +} +export interface CartridgePayloadSplittable { + id: string; + part?: number; +} +export interface ScoreboardsPayload { + cartridge_id: string; + name?: string; + page?: number; + page_size?: number; +} export interface CartridgeRemoved { cartridge_id: string; timestamp: number; } +export interface CartridgesPayload { + name?: string; + tags?: string[]; + page?: number; + page_size?: number; +} export interface CartridgeInfo { id: string; name: string; @@ -78,24 +125,9 @@ export interface Author { name: string; link: string; } -export interface ScoresPayload { - scoreboard_id: string; - page?: number; - page_size?: number; -} export interface CartridgePayload { id: string; } -export interface CartridgeInserted { - cartridge_id: string; - user_address: string; - timestamp: number; -} -export interface CartridgesOutput { - data: CartridgeInfo[]; - total: number; - page: number; -} export interface ScoresOutput { data: ScoreInfo[]; total: number; @@ -111,53 +143,26 @@ export interface ScoreboardCreated { created_by: string; created_at: number; } -export interface InserCartridgePayload { - data: string; +export interface CartridgeInserted { + cartridge_id: string; + user_address: string; + timestamp: number; } -export interface CartridgesPayload { - name?: string; - tags?: string[]; - page?: number; - page_size?: number; +export interface CartridgesOutput { + data: CartridgeInfo[]; + total: number; + page: number; } -export interface ScoreboardRemoved { +export interface ScoresPayload { scoreboard_id: string; - timestamp: number; + page?: number; + page_size?: number; } -export interface RemoveCartridgePayload { - id: string; +export interface InserCartridgePayload { + data: string; } export interface ScoreboardReplayPayload { scoreboard_id: string; outcard_hash: string; log: string; } -export interface CreateScoreboardPayload { - cartridge_id: string; - name: string; - args: string; - in_card: string; - score_function: string; -} -export interface Replay { - cartridge_id: string; - outcard_hash: string; - args: string; - in_card: string; - log: string; -} -export interface ScoreboardsOutput { - data: ScoreboardInfo[]; - total: number; - page: number; -} -export interface ScoreboardInfo { - id: string; - name: string; - cartridge_id: string; - created_by: string; - created_at: number; - args: string; - in_card: string; - score_function: string; -} diff --git a/frontend/app/backend-libs/app/lib.ts b/frontend/app/backend-libs/app/lib.ts index d304875..57bf5a5 100644 --- a/frontend/app/backend-libs/app/lib.ts +++ b/frontend/app/backend-libs/app/lib.ts @@ -32,6 +32,7 @@ ajv.addFormat("biginteger", (data) => { const dataTovalidate = data.startsWith('-') ? data.substring(1) : data; return ethers.utils.isHexString(dataTovalidate) && dataTovalidate.length % 2 == 0; }); +const MAX_SPLITTABLE_OUTPUT_SIZE = 4194247; /* * Mutations/Advances @@ -133,12 +134,26 @@ export async function scoreboardReplay( */ export async function cartridge( - inputData: ifaces.CartridgePayload, + inputData: ifaces.CartridgePayloadSplittable, options?:QueryOptions ):Promise { const route = 'app/cartridge'; - const data: CartridgePayload = new CartridgePayload(inputData); - const output: InspectReport = await genericInspect(data,route,options); + let part:number = 0; + let hasMoreParts:boolean = false; + const output: InspectReport = {payload: "0x"} + do { + hasMoreParts = false; + let inputDataSplittable = Object.assign({part},inputData); + const data: CartridgePayloadSplittable = new CartridgePayloadSplittable(inputDataSplittable); + const partOutput: InspectReport = await genericInspect(data,route,options); + let payloadHex = partOutput.payload.substring(2); + if (payloadHex.length/2 > MAX_SPLITTABLE_OUTPUT_SIZE) { + part++; + payloadHex = payloadHex.substring(0, payloadHex.length - 2); + hasMoreParts = true; + } + output.payload += payloadHex; + } while (hasMoreParts) if (options?.decode) { return decodeToModel(output,options.decodeModel || "json"); } return output; } @@ -222,15 +237,9 @@ export function exportToModel(data: any, modelName: string): string { return exporter(data); } -export class Replay extends IOData { constructor(data: ifaces.Replay, validate: boolean = true) { super(models['Replay'],data,validate); } } -export function exportToReplay(data: ifaces.Replay): string { - const dataToExport: Replay = new Replay(data); - return dataToExport.export(); -} - -export class InserCartridgePayload extends IOData { constructor(data: ifaces.InserCartridgePayload, validate: boolean = true) { super(models['InserCartridgePayload'],data,validate); } } -export function exportToInserCartridgePayload(data: ifaces.InserCartridgePayload): string { - const dataToExport: InserCartridgePayload = new InserCartridgePayload(data); +export class CreateScoreboardPayload extends IOData { constructor(data: ifaces.CreateScoreboardPayload, validate: boolean = true) { super(models['CreateScoreboardPayload'],data,validate); } } +export function exportToCreateScoreboardPayload(data: ifaces.CreateScoreboardPayload): string { + const dataToExport: CreateScoreboardPayload = new CreateScoreboardPayload(data); return dataToExport.export(); } @@ -240,27 +249,33 @@ export function exportToScoreboardReplayPayload(data: ifaces.ScoreboardReplayPay return dataToExport.export(); } -export class RemoveCartridgePayload extends IOData { constructor(data: ifaces.RemoveCartridgePayload, validate: boolean = true) { super(models['RemoveCartridgePayload'],data,validate); } } -export function exportToRemoveCartridgePayload(data: ifaces.RemoveCartridgePayload): string { - const dataToExport: RemoveCartridgePayload = new RemoveCartridgePayload(data); +export class EmptyClass extends IOData { constructor(data: ifaces.EmptyClass, validate: boolean = true) { super(models['EmptyClass'],data,validate); } } +export function exportToEmptyClass(data: ifaces.EmptyClass): string { + const dataToExport: EmptyClass = new EmptyClass(data); return dataToExport.export(); } -export class CreateScoreboardPayload extends IOData { constructor(data: ifaces.CreateScoreboardPayload, validate: boolean = true) { super(models['CreateScoreboardPayload'],data,validate); } } -export function exportToCreateScoreboardPayload(data: ifaces.CreateScoreboardPayload): string { - const dataToExport: CreateScoreboardPayload = new CreateScoreboardPayload(data); +export class Replay extends IOData { constructor(data: ifaces.Replay, validate: boolean = true) { super(models['Replay'],data,validate); } } +export function exportToReplay(data: ifaces.Replay): string { + const dataToExport: Replay = new Replay(data); return dataToExport.export(); } -export class EmptyClass extends IOData { constructor(data: ifaces.EmptyClass, validate: boolean = true) { super(models['EmptyClass'],data,validate); } } -export function exportToEmptyClass(data: ifaces.EmptyClass): string { - const dataToExport: EmptyClass = new EmptyClass(data); +export class RemoveCartridgePayload extends IOData { constructor(data: ifaces.RemoveCartridgePayload, validate: boolean = true) { super(models['RemoveCartridgePayload'],data,validate); } } +export function exportToRemoveCartridgePayload(data: ifaces.RemoveCartridgePayload): string { + const dataToExport: RemoveCartridgePayload = new RemoveCartridgePayload(data); return dataToExport.export(); } -export class CartridgesPayload extends IOData { constructor(data: ifaces.CartridgesPayload, validate: boolean = true) { super(models['CartridgesPayload'],data,validate); } } -export function exportToCartridgesPayload(data: ifaces.CartridgesPayload): string { - const dataToExport: CartridgesPayload = new CartridgesPayload(data); +export class InserCartridgePayload extends IOData { constructor(data: ifaces.InserCartridgePayload, validate: boolean = true) { super(models['InserCartridgePayload'],data,validate); } } +export function exportToInserCartridgePayload(data: ifaces.InserCartridgePayload): string { + const dataToExport: InserCartridgePayload = new InserCartridgePayload(data); + return dataToExport.export(); +} + +export class CartridgePayloadSplittable extends IOData { constructor(data: ifaces.CartridgePayloadSplittable, validate: boolean = true) { super(models['CartridgePayloadSplittable'],data,validate); } } +export function exportToCartridgePayloadSplittable(data: ifaces.CartridgePayloadSplittable): string { + const dataToExport: CartridgePayloadSplittable = new CartridgePayloadSplittable(data); return dataToExport.export(); } @@ -276,6 +291,12 @@ export function exportToScoresPayload(data: ifaces.ScoresPayload): string { return dataToExport.export(); } +export class CartridgesPayload extends IOData { constructor(data: ifaces.CartridgesPayload, validate: boolean = true) { super(models['CartridgesPayload'],data,validate); } } +export function exportToCartridgesPayload(data: ifaces.CartridgesPayload): string { + const dataToExport: CartridgesPayload = new CartridgesPayload(data); + return dataToExport.export(); +} + export class CartridgePayload extends IOData { constructor(data: ifaces.CartridgePayload, validate: boolean = true) { super(models['CartridgePayload'],data,validate); } } export function exportToCartridgePayload(data: ifaces.CartridgePayload): string { const dataToExport: CartridgePayload = new CartridgePayload(data); @@ -338,19 +359,12 @@ export function decodeToScoreboardReplayScore(output: CartesiReport | CartesiNot */ export const models: Models = { - 'Replay': { - ioType:IOType.mutationPayload, - abiTypes:['bytes32', 'bytes32', 'string', 'bytes', 'bytes'], - params:['cartridge_id', 'outcard_hash', 'args', 'in_card', 'log'], - exporter: exportToReplay, - validator: ajv.compile(JSON.parse('{"title": "Replay", "type": "object", "properties": {"cartridge_id": {"type": "string", "format": "binary"}, "outcard_hash": {"type": "string", "format": "binary"}, "args": {"type": "string"}, "in_card": {"type": "string", "format": "binary"}, "log": {"type": "string", "format": "binary"}}, "required": ["cartridge_id", "outcard_hash", "args", "in_card", "log"]}')) - }, - 'InserCartridgePayload': { + 'CreateScoreboardPayload': { ioType:IOType.mutationPayload, - abiTypes:['bytes'], - params:['data'], - exporter: exportToInserCartridgePayload, - validator: ajv.compile(JSON.parse('{"title": "InserCartridgePayload", "type": "object", "properties": {"data": {"type": "string", "format": "binary"}}, "required": ["data"]}')) + abiTypes:['bytes32', 'string', 'string', 'bytes', 'string'], + params:['cartridge_id', 'name', 'args', 'in_card', 'score_function'], + exporter: exportToCreateScoreboardPayload, + validator: ajv.compile(JSON.parse('{"title": "CreateScoreboardPayload", "type": "object", "properties": {"cartridge_id": {"type": "string", "format": "binary"}, "name": {"type": "string"}, "args": {"type": "string"}, "in_card": {"type": "string", "format": "binary"}, "score_function": {"type": "string"}}, "required": ["cartridge_id", "name", "args", "in_card", "score_function"]}')) }, 'ScoreboardReplayPayload': { ioType:IOType.mutationPayload, @@ -359,6 +373,20 @@ export const models: Models = { exporter: exportToScoreboardReplayPayload, validator: ajv.compile(JSON.parse('{"title": "ScoreboardReplayPayload", "type": "object", "properties": {"scoreboard_id": {"type": "string", "format": "binary"}, "outcard_hash": {"type": "string", "format": "binary"}, "log": {"type": "string", "format": "binary"}}, "required": ["scoreboard_id", "outcard_hash", "log"]}')) }, + 'EmptyClass': { + ioType:IOType.mutationPayload, + abiTypes:[], + params:[], + exporter: exportToEmptyClass, + validator: ajv.compile(JSON.parse('{"title": "EmptyClass", "type": "object", "properties": {}}')) + }, + 'Replay': { + ioType:IOType.mutationPayload, + abiTypes:['bytes32', 'bytes32', 'string', 'bytes', 'bytes'], + params:['cartridge_id', 'outcard_hash', 'args', 'in_card', 'log'], + exporter: exportToReplay, + validator: ajv.compile(JSON.parse('{"title": "Replay", "type": "object", "properties": {"cartridge_id": {"type": "string", "format": "binary"}, "outcard_hash": {"type": "string", "format": "binary"}, "args": {"type": "string"}, "in_card": {"type": "string", "format": "binary"}, "log": {"type": "string", "format": "binary"}}, "required": ["cartridge_id", "outcard_hash", "args", "in_card", "log"]}')) + }, 'RemoveCartridgePayload': { ioType:IOType.mutationPayload, abiTypes:['bytes32'], @@ -366,26 +394,19 @@ export const models: Models = { exporter: exportToRemoveCartridgePayload, validator: ajv.compile(JSON.parse('{"title": "RemoveCartridgePayload", "type": "object", "properties": {"id": {"type": "string", "format": "binary"}}, "required": ["id"]}')) }, - 'CreateScoreboardPayload': { - ioType:IOType.mutationPayload, - abiTypes:['bytes32', 'string', 'string', 'bytes', 'string'], - params:['cartridge_id', 'name', 'args', 'in_card', 'score_function'], - exporter: exportToCreateScoreboardPayload, - validator: ajv.compile(JSON.parse('{"title": "CreateScoreboardPayload", "type": "object", "properties": {"cartridge_id": {"type": "string", "format": "binary"}, "name": {"type": "string"}, "args": {"type": "string"}, "in_card": {"type": "string", "format": "binary"}, "score_function": {"type": "string"}}, "required": ["cartridge_id", "name", "args", "in_card", "score_function"]}')) - }, - 'EmptyClass': { + 'InserCartridgePayload': { ioType:IOType.mutationPayload, - abiTypes:[], - params:[], - exporter: exportToEmptyClass, - validator: ajv.compile(JSON.parse('{"title": "EmptyClass", "type": "object", "properties": {}}')) + abiTypes:['bytes'], + params:['data'], + exporter: exportToInserCartridgePayload, + validator: ajv.compile(JSON.parse('{"title": "InserCartridgePayload", "type": "object", "properties": {"data": {"type": "string", "format": "binary"}}, "required": ["data"]}')) }, - 'CartridgesPayload': { + 'CartridgePayloadSplittable': { ioType:IOType.queryPayload, abiTypes:[], - params:['name', 'tags', 'page', 'page_size'], - exporter: exportToCartridgesPayload, - validator: ajv.compile(JSON.parse('{"title": "CartridgesPayload", "type": "object", "properties": {"name": {"type": "string"}, "tags": {"type": "array", "items": {"type": "string"}}, "page": {"type": "integer"}, "page_size": {"type": "integer"}}}')) + params:['id', 'part'], + exporter: exportToCartridgePayloadSplittable, + validator: ajv.compile(JSON.parse('{"title": "CartridgePayloadSplittable", "type": "object", "properties": {"id": {"type": "string"}, "part": {"type": "integer"}}, "required": ["id"]}')) }, 'ScoreboardsPayload': { ioType:IOType.queryPayload, @@ -401,6 +422,13 @@ export const models: Models = { exporter: exportToScoresPayload, validator: ajv.compile(JSON.parse('{"title": "ScoresPayload", "type": "object", "properties": {"scoreboard_id": {"type": "string"}, "page": {"type": "integer"}, "page_size": {"type": "integer"}}, "required": ["scoreboard_id"]}')) }, + 'CartridgesPayload': { + ioType:IOType.queryPayload, + abiTypes:[], + params:['name', 'tags', 'page', 'page_size'], + exporter: exportToCartridgesPayload, + validator: ajv.compile(JSON.parse('{"title": "CartridgesPayload", "type": "object", "properties": {"name": {"type": "string"}, "tags": {"type": "array", "items": {"type": "string"}}, "page": {"type": "integer"}, "page_size": {"type": "integer"}}}')) + }, 'CartridgePayload': { ioType:IOType.queryPayload, abiTypes:[], diff --git a/frontend/app/backend-libs/cartesapp/lib.ts b/frontend/app/backend-libs/cartesapp/lib.ts index 2e163a2..6f2ccfb 100644 --- a/frontend/app/backend-libs/cartesapp/lib.ts +++ b/frontend/app/backend-libs/cartesapp/lib.ts @@ -31,6 +31,7 @@ ajv.addFormat("biginteger", (data) => { }); const abiCoder = new ethers.utils.AbiCoder(); export const CONVENTIONAL_TYPES: Array = ["bytes","hex","str","int","dict","list","tuple","json"]; +const MAX_SPLITTABLE_OUTPUT_SIZE = 4194247; /** diff --git a/frontend/app/cartridges/selectedCartridgeProvider.tsx b/frontend/app/cartridges/selectedCartridgeProvider.tsx index 97633e9..f856fc3 100644 --- a/frontend/app/cartridges/selectedCartridgeProvider.tsx +++ b/frontend/app/cartridges/selectedCartridgeProvider.tsx @@ -7,10 +7,10 @@ import { CartridgeInfo as Cartridge } from "../backend-libs/app/ifaces" export const selectedCartridgeContext = createContext<{ selectedCartridge: PlayableCartridge|null, changeCartridge:Function, playCartridge:Function, - setReplayInfo:Function, setCartridgeData:Function, setGamePatrameters:Function, + setReplay:Function, setCartridgeData:Function, setGameParameters:Function, setGameplay:Function, stopCartridge:Function }>({selectedCartridge: null, changeCartridge: () => null, playCartridge: () => null, - setReplayInfo: () => null, setCartridgeData: () => null, setGamePatrameters: () => null, + setReplay: () => null, setCartridgeData: () => null, setGameParameters: () => null, setGameplay: () => null, stopCartridge: () => null}); // export type Cartridge = { @@ -20,23 +20,18 @@ export const selectedCartridgeContext = createContext<{ // desc: string // } -export interface ReplayInfo { - userAddress: string; - timestamp: number; - score: number; - scoreType: string; - extraScore: number; -} export interface PlayableCartridge extends Cartridge { + initCanvas: boolean; play: boolean; playToggle: boolean; cartridgeData: Uint8Array | undefined; inCard: Uint8Array | undefined; args: string | undefined; scoreFunction: string | undefined; - replayInfo: ReplayInfo | undefined; + replay: Uint8Array | undefined; gameplayLog: Uint8Array | undefined; - outcard: string | undefined; + outcard: Uint8Array | undefined; + outhash: string | undefined; } export function SelectedCartridgeProvider({ children }:{ children: React.ReactNode }) { @@ -44,13 +39,14 @@ export function SelectedCartridgeProvider({ children }:{ children: React.ReactNo const changeCartridge = (cartridge:Cartridge) => { const aux = {...cartridge, play:false, cartridgeData:undefined, inCard:undefined, - args:undefined, scoreFunction:undefined, replayInfo:undefined, gameplayLog:undefined, outcard:undefined}; + args:undefined, scoreFunction:undefined, replay:undefined, gameplayLog:undefined, + outcard:undefined, outhash:undefined, initCanvas:selectedCartridge?.initCanvas}; setSelectedCartridge(aux as PlayableCartridge); } const playCartridge = () => { if (selectedCartridge) { - setSelectedCartridge({...selectedCartridge, play:true, replayInfo:undefined, playToggle:!selectedCartridge.playToggle}); + setSelectedCartridge({...selectedCartridge, play:true, gameplayLog:undefined, outcard:undefined, outhash:undefined, replay:undefined, playToggle:!selectedCartridge.playToggle, initCanvas:true}); } } @@ -60,9 +56,9 @@ export function SelectedCartridgeProvider({ children }:{ children: React.ReactNo } } - const setReplayInfo = (replayInfo: ReplayInfo) => { + const setReplay = (replay: Uint8Array) => { if (selectedCartridge) { - setSelectedCartridge({...selectedCartridge, play:true, gameplayLog:undefined, replayInfo}); + setSelectedCartridge({...selectedCartridge, play:true, gameplayLog:undefined, outcard:undefined, outhash:undefined, replay, playToggle:!selectedCartridge.playToggle, initCanvas:true}); } } @@ -72,21 +68,27 @@ export function SelectedCartridgeProvider({ children }:{ children: React.ReactNo } } - const setGamePatrameters = (args: string, inCard: Uint8Array, scoreFunction: string) => { + const setGameParameters = (args: string, inCard: Uint8Array, scoreFunction: string) => { if (selectedCartridge) { - setSelectedCartridge({...selectedCartridge, args, inCard, scoreFunction, gameplayLog:undefined, replayInfo:undefined}); + setSelectedCartridge({...selectedCartridge, args, inCard, scoreFunction, gameplayLog:undefined, replay:undefined}); } } - const setGameplay = (gameplayLog: Uint8Array, outcard: string) => { + const setGameplay = (gameplayLog: Uint8Array, outcard: Uint8Array, outhash: string) => { if (selectedCartridge) { - setSelectedCartridge({...selectedCartridge, gameplayLog, outcard}); + if (outcard == undefined) + if (gameplayLog == undefined) + setSelectedCartridge({...selectedCartridge, gameplayLog, outcard, outhash}); + else + setSelectedCartridge({...selectedCartridge, gameplayLog, outcard, outhash, play: true, playToggle:!selectedCartridge.playToggle, initCanvas:true}); + else + setSelectedCartridge({...selectedCartridge, gameplayLog, outcard, outhash}); } } return ( { children } diff --git a/frontend/app/components/CartridgeInfo.tsx b/frontend/app/components/CartridgeInfo.tsx index b99b15c..55c3b50 100644 --- a/frontend/app/components/CartridgeInfo.tsx +++ b/frontend/app/components/CartridgeInfo.tsx @@ -15,12 +15,11 @@ import StadiumIcon from '@mui/icons-material/Stadium'; import CodeIcon from '@mui/icons-material/Code'; import useDownloader from "react-use-downloader"; import { useConnectWallet } from "@web3-onboard/react"; -import { sha256 } from "js-sha256"; import Cartridge from "../models/cartridge"; import {SciFiPedestal} from "../models/scifi_pedestal"; import Loader from "../components/Loader"; -import { replay } from '../backend-libs/app/lib'; +import { ReplayScore, getOutputs, replay } from '../backend-libs/app/lib'; import { Replay } from '../backend-libs/app/ifaces'; import CartridgeDescription from './CartridgeDescription'; import Link from 'next/link'; @@ -78,6 +77,7 @@ function logFeedback(logStatus:LOG_STATUS, setLogStatus:Function) { } } + function scoreboardFallback() { const arr = Array.from(Array(3).keys()); @@ -94,6 +94,9 @@ function scoreboardFallback() { Score + + + @@ -103,7 +106,7 @@ function scoreboardFallback() {
- 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266 + 0xf39f...2266
@@ -118,6 +121,11 @@ function scoreboardFallback() { 100 + +
+ 100 +
+ ); }) @@ -129,20 +137,27 @@ function scoreboardFallback() { } function CartridgeInfo() { - const {selectedCartridge, playCartridge, setGameplay} = useContext(selectedCartridgeContext); + const {selectedCartridge, playCartridge, setGameplay, setReplay} = useContext(selectedCartridgeContext); const fileRef = useRef(null); - const [{ wallet }, connect] = useConnectWallet(); + const [{ wallet }] = useConnectWallet(); const { download } = useDownloader(); const [submitLogStatus, setSubmitLogStatus] = useState({status: STATUS.READY} as LOG_STATUS); + const reloadScoreboard = submitLogStatus.status === STATUS.VALID? true:false; if (!selectedCartridge) return <>; + var decoder = new TextDecoder("utf-8"); + async function submitLog() { // replay({car}); - if (!selectedCartridge || !selectedCartridge.gameplayLog || !selectedCartridge.outcard){ + if (!selectedCartridge || !selectedCartridge.gameplayLog){ alert("No gameplay data."); return; } + if (!selectedCartridge.outcard || !selectedCartridge.outhash ){ + alert("No gameplay output yet, you should run it."); + return; + } if (!wallet) { alert("Connect first to upload a gameplay log."); return; @@ -151,16 +166,22 @@ function CartridgeInfo() { const signer = new ethers.providers.Web3Provider(wallet.provider, 'any').getSigner(); const inputData: Replay = { cartridge_id:"0x"+selectedCartridge.id, - outcard_hash: "0x"+sha256(selectedCartridge.outcard), + outcard_hash: '0x' + selectedCartridge.outhash, args: selectedCartridge.args || "", in_card: selectedCartridge.inCard ? ethers.utils.hexlify(selectedCartridge.inCard) : "0x", log: ethers.utils.hexlify(selectedCartridge.gameplayLog) } + console.log("Sending Replay:") + if (decoder.decode(selectedCartridge.outcard.slice(0,4)) == 'JSON') { + console.log("Replay Outcard",JSON.parse(decoder.decode(selectedCartridge.outcard).substring(4))) + } else { + console.log("Replay Outcard",selectedCartridge.outcard) + } + console.log("Replay Outcard hash",selectedCartridge.outhash) setSubmitLogStatus({status: STATUS.VALIDATING}); try { - const replayRes = await replay(signer, envClient.DAPP_ADDR, inputData, {decode:true, cartesiNodeUrl: envClient.CARTESI_NODE_URL}); - console.log(replayRes); + await replay(signer, envClient.DAPP_ADDR, inputData, {cartesiNodeUrl: envClient.CARTESI_NODE_URL}); setSubmitLogStatus({status: STATUS.VALID}); } catch (error) { setSubmitLogStatus({status: STATUS.INVALID, error: (error as Error).message}); @@ -188,12 +209,30 @@ function CartridgeInfo() { reader.onload = async (readerEvent) => { const data = readerEvent.target?.result; if (data) { - setGameplay(new Uint8Array(data as ArrayBuffer)); + setGameplay(new Uint8Array(data as ArrayBuffer), undefined); + e.target.value = null; } }; reader.readAsArrayBuffer(e.target.files[0]) } + async function prepareReplay(replayScore: ReplayScore) { + if (selectedCartridge) { + const replayLog: Array = await getOutputs( + { + tags: ["replay", selectedCartridge?.id], + timestamp_gte: replayScore.timestamp.toNumber(), + timestamp_lte: replayScore.timestamp.toNumber(), + msg_sender: replayScore.user_address, + output_type: 'report' + }, + {cartesiNodeUrl: envClient.CARTESI_NODE_URL} + ); + if (replayLog.length > 0) { + setReplay(replayLog[0]); + } + } + } return (
@@ -243,7 +282,8 @@ function CartridgeInfo() { diff --git a/frontend/app/components/Rivemu.tsx b/frontend/app/components/Rivemu.tsx index 7ac6766..c16a317 100644 --- a/frontend/app/components/Rivemu.tsx +++ b/frontend/app/components/Rivemu.tsx @@ -9,18 +9,17 @@ import PlayArrowIcon from '@mui/icons-material/PlayArrow'; import Stop from '@mui/icons-material/Stop'; import ReplayIcon from '@mui/icons-material/Replay'; import OndemandVideoIcon from '@mui/icons-material/OndemandVideo'; -// import DriveFileMoveIcon from '@mui/icons-material/DriveFileMove'; // for cartridge download +import OpenInFullIcon from '@mui/icons-material/OpenInFull'; +import CloseFullscreenIcon from '@mui/icons-material/CloseFullscreen'; import { selectedCartridgeContext } from '../cartridges/selectedCartridgeProvider'; import { cartridge } from '../backend-libs/app/lib'; import { fontPressStart2P } from '../utils/font'; import { envClient } from '../utils/clientEnv'; +import { delay, usePrevious } from '../utils/util'; // let rivlogData: Uint8Array | undefined = undefined; -function delay(ms: number) { - return new Promise( resolve => setTimeout(resolve, ms) ); -} async function movePageToBottom() { await delay(500); window.scrollTo({left: 0, top: document.body.scrollHeight, behavior: 'smooth'}); @@ -31,12 +30,28 @@ async function movePageToTop() { window.scrollTo({left: 0, top: 0, behavior: 'smooth'}); } +function getWindowDimensions() { + const { innerWidth: width, innerHeight: height } = window; + return { width, height }; +} + +const DEFAULT_WIDTH = 640; +const DEFAULT_HEIGTH = 400; + function Rivemu() { - const {selectedCartridge, setCartridgeData, setGameplay, stopCartridge} = useContext(selectedCartridgeContext); + const {selectedCartridge, setCartridgeData, setGameplay } = useContext(selectedCartridgeContext); const [overallScore, setOverallScore] = useState(0); // const [isLoading, setIsLoading] = useState(true); const [isPlaying, setIsPlaying] = useState(false); + const [replayTip, setReplayTip] = useState(false); + const [isReplaying, setIsReplaying] = useState(false); + const [isExpanded, setIsExpanded] = useState(false); + const [playedOnce, setPlayedOnce] = useState(false); + const [canvasHeight, setCanvasHeight] = useState(DEFAULT_HEIGTH); + const [descHeight, setDescHeight] = useState(100); + const [canvasWidth, setCanvasWidth] = useState(DEFAULT_WIDTH); const [replayLog, setReplayLog] = useState(undefined); + const [gameHeigth, setGameHeigth] = useState(0); useEffect(()=>{ if (!selectedCartridge || !selectedCartridge?.play) return; @@ -44,20 +59,71 @@ function Rivemu() { } ,[selectedCartridge?.playToggle]) - function initialize() { + useEffect(()=>{ + let newHeight = DEFAULT_HEIGTH; + let newWidth = DEFAULT_WIDTH; + const canvas: any = document.getElementById("canvas"); + if (canvas) { + const aspectRatio = canvas ? canvas.width / canvas.height : 640/400; + if (isExpanded) { + const windowSizes = getWindowDimensions(); + newHeight = windowSizes.height - 120; + newWidth = Math.floor(aspectRatio * newHeight); + if (newWidth > windowSizes.width - 10) { + newWidth = windowSizes.height - 10; + newHeight = Math.floor(newWidth / aspectRatio); + } + } + + canvas.height = Math.floor(newHeight / gameHeigth) * gameHeigth; + canvas.width = Math.floor(aspectRatio * canvas.height); + } + setCanvasHeight(newHeight); + setCanvasWidth(newWidth); + window.dispatchEvent(new Event("resize")); movePageToBottom(); - loadCartridge(); - if (selectedCartridge?.gameplayLog) setReplayLog(selectedCartridge.gameplayLog); + } + ,[isExpanded]) + + async function initialize() { + if (!selectedCartridge || selectedCartridge.cartridgeData == undefined) { + setGameHeigth(0); + setCanvasHeight(DEFAULT_HEIGTH); + setCanvasWidth(DEFAULT_WIDTH); + setIsPlaying(false); + setIsExpanded(false); + setOverallScore(0); + setReplayLog(undefined); + const windowSizes = getWindowDimensions(); + setDescHeight(windowSizes.height - 150 - DEFAULT_HEIGTH); + const canvas: any = document.getElementById("canvas"); + if (canvas) { + canvas.height = DEFAULT_HEIGTH; + canvas.width = DEFAULT_HEIGTH; + } + } + await loadCartridge(); + movePageToBottom(); + if (selectedCartridge?.replay){ + setReplayLog(selectedCartridge.replay); + setIsReplaying(true); + setReplayTip(true); + } + if (selectedCartridge?.gameplayLog) { + setReplayLog(selectedCartridge.gameplayLog); + setIsReplaying(false); + setReplayTip(true); + } } async function loadCartridge() { if (!selectedCartridge || !selectedCartridge?.play || selectedCartridge.cartridgeData != undefined) return; - const data = await cartridge({id:selectedCartridge.id},{decode:true,decodeModel:"bytes", cartesiNodeUrl: envClient.CARTESI_NODE_URL}); + const data = await cartridge({id:selectedCartridge.id},{decode:true,decodeModel:"bytes", cartesiNodeUrl: envClient.CARTESI_NODE_URL, cache:"force-cache"}); setCartridgeData(data); } - if (!selectedCartridge) { - return <>; + if (!selectedCartridge || !selectedCartridge.initCanvas) { + return <>; } var decoder = new TextDecoder("utf-8"); @@ -66,51 +132,89 @@ function Rivemu() { function coverFallback() { return ( - {"Cover + {"Cover ); } + + function waitEvent(name: string) { + return new Promise((resolve) => { + const listener = (e: any) => { + window.removeEventListener(name, listener); + resolve(e); + } + window.addEventListener(name, listener); + }) + } async function rivemuStart() { + if (!selectedCartridge?.cartridgeData) return; + console.log("rivemuStart"); // setIsLoading(true); + setIsReplaying(false); + setReplayTip(false); + // // @ts-ignore:next-line + // if (Module.quited) { + // // restart wasm when back to page + // // @ts-ignore:next-line + // Module._main(); + // } + await rivemuHalt(); setIsPlaying(true); - // @ts-ignore:next-line - if (Module.quited) { - // restart wasm when back to page - // @ts-ignore:next-line - Module._main(); - } - if (!selectedCartridge?.cartridgeData) return; - console.log("rivemuStart"); if (selectedCartridge.scoreFunction) scoreFunction = parser.parse(selectedCartridge.scoreFunction); // @ts-ignore:next-line let buf = Module._malloc(selectedCartridge.cartridgeData.length); // @ts-ignore:next-line Module.HEAPU8.set(selectedCartridge.cartridgeData, buf); + // @ts-ignore:next-line + let incardBuf = Module._malloc(selectedCartridge.inCard?.length || 0); + // @ts-ignore:next-line + if (selectedCartridge?.inCard) Module.HEAPU8.set(selectedCartridge.inCard, incardBuf); let params = selectedCartridge?.args || ""; // @ts-ignore:next-line Module.ccall( - "rivemu_start_ex", - selectedCartridge.inCard || null, - ["number", "number", "string"], - [buf, selectedCartridge.cartridgeData.length, params] + "rivemu_start_record", + null, + ['number', 'number', 'number', 'number', 'string'], + [ + buf, + selectedCartridge.cartridgeData.length, + incardBuf, + selectedCartridge.inCard?.length || 0, + params + ] ); // @ts-ignore:next-line Module._free(buf); + // @ts-ignore:next-line + Module._free(incardBuf); } async function rivemuReplay() { // TODO: fix rivemuReplay if (!selectedCartridge?.cartridgeData || !replayLog) return; console.log("rivemuReplay"); + setReplayTip(false); + if (selectedCartridge.cartridgeData == undefined || selectedCartridge.outcard != undefined || selectedCartridge.outhash != undefined) + setIsReplaying(true); + // // @ts-ignore:next-line + // if (Module.quited) { + // // restart wasm when back to page + // // @ts-ignore:next-line + // Module._main(); + // } + await rivemuHalt(); setIsPlaying(true); - // @ts-ignore:next-line - if (Module.quited) { - // restart wasm when back to page - // @ts-ignore:next-line - Module._main(); - } if (selectedCartridge.scoreFunction) scoreFunction = parser.parse(selectedCartridge.scoreFunction); @@ -122,49 +226,82 @@ function Rivemu() { Module.HEAPU8.set(selectedCartridge.cartridgeData, cartridgeBuf); // @ts-ignore:next-line Module.HEAPU8.set(replayLog, rivlogBuf); + // @ts-ignore:next-line + let incardBuf = Module._malloc(selectedCartridge.inCard?.length || 0); + // @ts-ignore:next-line + if (selectedCartridge?.inCard) Module.HEAPU8.set(selectedCartridge.inCard, incardBuf); let params = selectedCartridge?.args || ""; // @ts-ignore:next-line Module.ccall( - "rivemu_start_replay_ex", - selectedCartridge.inCard || null, - ["number", "number", "number", "number", "string"], + "rivemu_start_replay", + null, + ['number', 'number', 'number', 'number', 'string', 'number', 'number'], [ cartridgeBuf, selectedCartridge.cartridgeData.length, - rivlogBuf, - replayLog.length, + incardBuf, + selectedCartridge.inCard?.length || 0, params, + rivlogBuf, + replayLog.length ] ); // @ts-ignore:next-line Module._free(cartridgeBuf); // @ts-ignore:next-line Module._free(rivlogBuf); + // @ts-ignore:next-line + Module._free(incardBuf); + } + + async function rivemuHalt() { + // @ts-ignore:next-line + if (Module.ccall('rivemu_stop')) { + await waitEvent('rivemu_on_shutdown'); + } } async function rivemuStop() { console.log("rivemuStop"); - movePageToTop(); setIsPlaying(false); - stopCartridge(); // @ts-ignore:next-line - Module.cwrap("rivemu_stop")(); + rivemuHalt(); + movePageToTop(); + // stopCartridge(); } + if (typeof window !== "undefined") { // @ts-ignore:next-line - window.rivemu_on_outcard_update = function (outcard: any) { - const outcard_str = decoder.decode(outcard); - const outcard_json = JSON.parse(outcard_str.substring(4).replace(/\,(?!\s*?[\{\[\"\'\w])/g, '')); - let score = outcard_json.score; - if (selectedCartridge?.scoreFunction) { - score = scoreFunction.evaluate(outcard_json); + window.rivemu_on_frame = function ( + outcard: ArrayBuffer, + frame: number, + fps: number, + mips: number, + cpu_usage: number, + cycles: number + ) { + let score = 0; + if (decoder.decode(outcard.slice(0,4)) == 'JSON') { + const outcard_str = decoder.decode(outcard); + const outcard_json = JSON.parse(outcard_str.substring(4)); + score = outcard_json.score; + if (selectedCartridge?.scoreFunction) { + score = scoreFunction.evaluate(outcard_json); + } } setOverallScore(score); }; // @ts-ignore:next-line - window.rivemu_on_begin = function (width: any, height: any) { + window.rivemu_on_begin = function (width: number, height: number, target_fps: number, total_frames: number) { + if (!playedOnce) setPlayedOnce(true); + const canvas: any = document.getElementById("canvas"); + setGameHeigth(height); + if (canvas) { + canvas.height = Math.floor(canvasHeight / height) * height; + canvas.width = Math.floor(width / height * canvas.height); + } console.log("rivemu_on_begin"); // setIsLoading(false); // force canvas resize @@ -174,18 +311,21 @@ function Rivemu() { // @ts-ignore:next-line window.rivemu_on_finish = function ( rivlog: ArrayBuffer, - outcard: ArrayBuffer + outcard: ArrayBuffer, + outhash: string ) { - setIsPlaying(false); - setGameplay(new Uint8Array(rivlog),decoder.decode(outcard).replace(/\s|\n|\r|\t/g, '')); - setReplayLog(new Uint8Array(rivlog)); + if (!isReplaying) { + setGameplay(new Uint8Array(rivlog),new Uint8Array(outcard),outhash); + setReplayLog(new Uint8Array(rivlog)); + } + console.log("rivemu_on_finish") }; } return ( -
-
+