Repository navigation
Merge pull request #298 from Milan4ikdudewtf/main #309
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Update Timestamp and Sort Properties Files | |
| on: | |
| push: | |
| paths: | |
| - '**/*.properties' | |
| jobs: | |
| update-timestamp-and-sort: | |
| # Only run in the main repository | |
| if: github.repository == 'EuphoriaPatches/propertiesFiles' | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.ref }} | |
| cancel-in-progress: false | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Fetch latest changes | |
| if: github.event_name == 'push' | |
| run: | | |
| # Fetch the latest changes from the remote repository | |
| echo "Fetching latest changes from repository..." | |
| git fetch origin | |
| - name: Get and process modified files | |
| run: | | |
| # Use GITHUB_SHA to ensure we compare the correct commits | |
| MODIFIED_FILES=$(git diff --name-only $GITHUB_SHA^ $GITHUB_SHA | grep -E '\.properties$' || true) | |
| echo "Modified files: $MODIFIED_FILES" | |
| # Only proceed if there are modified files | |
| if [ -n "$MODIFIED_FILES" ]; then | |
| DATE=$(date '+%B %d, %Y, %H:%M UTC') | |
| YEAR=$(date '+%Y') | |
| echo "Updating timestamps to: $DATE" | |
| # Process each file | |
| for file in $MODIFIED_FILES; do | |
| if [ -f "$file" ]; then | |
| echo "Processing file: $file" | |
| # Remove trailing whitespace | |
| sed -i 's/[[:space:]]*$//' "$file" | |
| echo " - Removed trailing whitespaces" | |
| # Check if header exists | |
| HAS_TIMESTAMP=$(grep -q "^### Last updated:" "$file" && echo "yes" || echo "no") | |
| HAS_COPYRIGHT=$(grep -q "^# Copyright" "$file" && echo "yes" || echo "no") | |
| if [ "$HAS_TIMESTAMP" = "yes" ] && [ "$HAS_COPYRIGHT" = "yes" ]; then | |
| # Update existing timestamp line | |
| sed -i "s/^### Last updated:.*$/### Last updated: $DATE/" "$file" | |
| # Update copyright year | |
| sed -i "s/^# Copyright (c) [0-9]\{4\}/# Copyright (c) $YEAR/" "$file" | |
| echo " - Updated existing timestamp and copyright" | |
| elif [ "$HAS_TIMESTAMP" = "yes" ] && [ "$HAS_COPYRIGHT" = "no" ]; then | |
| # Update timestamp and add copyright after it | |
| sed -i "s/^### Last updated:.*$/### Last updated: $DATE/" "$file" | |
| sed -i "/^### Last updated:/a # Copyright (c) $YEAR SpacEagle17 – Licensed under the MIT License: https://opensource.org/licenses/MIT" "$file" | |
| echo " - Updated timestamp and added copyright" | |
| else | |
| # Add both lines at the beginning | |
| sed -i "1i### Last updated: $DATE\n# Copyright (c) $YEAR SpacEagle17 – Licensed under the MIT License: https://opensource.org/licenses/MIT" "$file" | |
| echo " - Added new timestamp and copyright" | |
| fi | |
| else | |
| echo "File not found: $file (may have been deleted)" | |
| fi | |
| done | |
| echo "Timestamp updates and whitespace removal complete" | |
| else | |
| echo "No .properties files were modified" | |
| fi | |
| - name: Set up Python for sorting script | |
| uses: actions/setup-python@v4 | |
| with: | |
| python-version: '3.x' | |
| - name: Create and run duplicate remover script | |
| run: | | |
| # Create the duplicate remover Python script | |
| echo "Creating duplicate remover script..." | |
| cat > remove_duplicates.py << 'EOF' | |
| import sys | |
| import re | |
| def tokenize(line): | |
| """Split a line on whitespace, keeping "quoted entries with spaces" as a single token""" | |
| return re.findall(r'"[^"]*"|\S+', line) | |
| def strip_quotes(s): | |
| """Strip surrounding quotes from a token, if present""" | |
| if len(s) >= 2 and s.startswith('"') and s.endswith('"'): | |
| return s[1:-1] | |
| return s | |
| def remove_duplicates_in_section(section_lines): | |
| """Remove duplicate mod entries within a section, preserving different blockstates""" | |
| if not section_lines: | |
| return section_lines | |
| # Get the section header (first line) | |
| header_line = section_lines[0].rstrip() | |
| # Extract all lines | |
| result = [header_line] | |
| seen_entries = set() | |
| for i, line in enumerate(section_lines[1:], 1): | |
| line_stripped = line.rstrip() | |
| # Preserve comments and empty lines | |
| if line_stripped.startswith('#') or not line_stripped.strip() or line_stripped.strip() == '\\': | |
| result.append(line_stripped) | |
| continue | |
| # Check if this is a tag line | |
| if 'tags_' in line_stripped: | |
| result.append(line_stripped) | |
| continue | |
| # Process mod entries | |
| parts = tokenize(line_stripped) | |
| filtered_parts = [] | |
| for part in parts: | |
| # Skip backslashes | |
| if part == '\\': | |
| continue | |
| # If this is a mod entry (contains colon) | |
| if ':' in part and not part.startswith('#'): | |
| # Use the full part (including blockstate) as the key to check for duplicates | |
| unique_key = part.rstrip('\\').strip() | |
| if unique_key not in seen_entries: | |
| seen_entries.add(unique_key) | |
| filtered_parts.append(part) | |
| else: | |
| # Not a mod entry, keep as-is | |
| filtered_parts.append(part) | |
| # Reconstruct the line if we have filtered parts | |
| if filtered_parts: | |
| # Check if original line had a backslash at the end | |
| has_backslash = line_stripped.rstrip().endswith('\\') | |
| reconstructed = ' '.join(filtered_parts) | |
| if has_backslash and not reconstructed.endswith('\\'): | |
| reconstructed += ' \\' | |
| result.append(reconstructed) | |
| elif line_stripped.strip() == '\\': | |
| # Preserve standalone backslashes | |
| result.append(line_stripped) | |
| # Add newlines back | |
| return [line + '\n' for line in result] | |
| def process_properties_file(file_path): | |
| """Process a properties file to remove duplicates within each section""" | |
| try: | |
| with open(file_path, 'r', encoding='utf-8') as f: | |
| lines = f.readlines() | |
| except FileNotFoundError: | |
| print(f"Error: File '{file_path}' not found") | |
| return False | |
| except Exception as e: | |
| print(f"Error reading file: {e}") | |
| return False | |
| result_lines = [] | |
| i = 0 | |
| duplicates_removed = 0 | |
| while i < len(lines): | |
| line = lines[i] | |
| # Check if this line starts a section (block.XXXXX =, item.XXXXX =, entity.XXXXX =) | |
| section_match = re.match(r'^(item|block|entity)\.\d+\s*=', line) | |
| if section_match: | |
| # Start of a section - collect all lines in this section | |
| current_section = [line] | |
| section_start = i | |
| section_end = i | |
| # Find the end of this section | |
| for j in range(i + 1, len(lines)): | |
| current_section.append(lines[j]) | |
| # Check if this line ends the section (no backslash at end or empty line) | |
| stripped = lines[j].strip() | |
| if not stripped.endswith('\\') and stripped != '\\': | |
| section_end = j | |
| break | |
| section_end = j | |
| # Count entries before deduplication | |
| original_count = sum(1 for line in current_section for part in line.split() if ':' in part and part != '\\') | |
| # Remove duplicates in this section | |
| processed_section = remove_duplicates_in_section(current_section) | |
| # Count entries after deduplication | |
| new_count = sum(1 for line in processed_section for part in line.split() if ':' in part and part != '\\') | |
| if original_count > new_count: | |
| duplicates_removed += (original_count - new_count) | |
| print(f"Section {section_match.group()}: Removed {original_count - new_count} duplicate(s)") | |
| result_lines.extend(processed_section) | |
| # Skip past this section in the main loop | |
| i = section_end + 1 | |
| else: | |
| # Not the start of a section, keep as-is | |
| result_lines.append(line) | |
| i += 1 | |
| # Write back to file | |
| try: | |
| with open(file_path, 'w', encoding='utf-8') as f: | |
| f.writelines(result_lines) | |
| if duplicates_removed > 0: | |
| print(f"Total duplicates removed: {duplicates_removed}") | |
| return True | |
| except Exception as e: | |
| print(f"Error writing file: {e}") | |
| return False | |
| def main(): | |
| if len(sys.argv) < 2: | |
| print("Usage: python remove_duplicates.py <path_to_properties_file>") | |
| return | |
| file_path = sys.argv[1] | |
| process_properties_file(file_path) | |
| if __name__ == "__main__": | |
| main() | |
| EOF | |
| # Process each modified properties file to remove duplicates | |
| MODIFIED_FILES=$(git diff --name-only $GITHUB_SHA^ $GITHUB_SHA | grep -E '\.properties$' || true) | |
| if [ -n "$MODIFIED_FILES" ]; then | |
| echo "Running duplicate remover on modified properties files..." | |
| for file in $MODIFIED_FILES; do | |
| if [ -f "$file" ]; then | |
| echo "Removing duplicates in file: $file" | |
| python remove_duplicates.py "$file" | |
| fi | |
| done | |
| fi | |
| - name: Create and run properties sorting script | |
| run: | | |
| # Create the Python script | |
| echo "Creating properties file sorting script..." | |
| cat > sort_properties.py << 'EOF' | |
| import os | |
| import sys | |
| import re | |
| def tokenize(line): | |
| """Split a line on whitespace, keeping "quoted entries with spaces" as a single token""" | |
| return re.findall(r'"[^"]*"|\S+', line) | |
| def strip_quotes(s): | |
| """Strip surrounding quotes from a token, if present""" | |
| if len(s) >= 2 and s.startswith('"') and s.endswith('"'): | |
| return s[1:-1] | |
| return s | |
| def sort_properties_file(file_path): | |
| # Read the entire file | |
| with open(file_path, 'r', encoding='utf-8') as f: | |
| original_lines = f.readlines() | |
| result_lines = [] | |
| i = 0 | |
| while i < len(original_lines): | |
| line = original_lines[i] | |
| # Check if this line starts a section (item.XXXXX = or block.XXXXX =) | |
| section_match = re.match(r'^(item|block|entity)\.\d+\s*=', line) | |
| if section_match: | |
| # Start of a section - collect all lines in this section | |
| current_section = [line] | |
| section_end = i | |
| # Find the end of this section | |
| for j in range(i + 1, len(original_lines)): | |
| section_end = j | |
| if not original_lines[j].strip().endswith('\\'): | |
| current_section.append(original_lines[j]) | |
| break | |
| current_section.append(original_lines[j]) | |
| # Process the section | |
| processed_section = process_section(current_section) | |
| result_lines.extend(processed_section) | |
| # Skip past this section in the main loop | |
| i = section_end + 1 | |
| else: | |
| # Not the start of a section, keep as-is | |
| result_lines.append(line) | |
| i += 1 | |
| return result_lines | |
| def process_section(section_lines): | |
| """Process a section to sort mod entries alphabetically both vertically and horizontally""" | |
| # Special case: Check if the section only has a header that contains a mod | |
| if len(section_lines) <= 2 and ':' in section_lines[0]: | |
| return section_lines | |
| # First check if it has any mod entries beyond the header (lines with colons) | |
| has_mod_entries = False | |
| for line in section_lines[1:]: # Skip header | |
| if ':' in line and not line.strip().startswith('#'): | |
| has_mod_entries = True | |
| break | |
| # If no mod entries or only in header, return the section unchanged | |
| if not has_mod_entries: | |
| return section_lines | |
| # Get the section header (first line with item.XXXXX = or block.XXXXX =) | |
| header_line = section_lines[0].rstrip() | |
| # Check if header contains mod entries | |
| header_has_mod = ':' in header_line and not header_line.startswith('#') | |
| header_mod_entries = [] | |
| if header_has_mod: | |
| # Extract mod entries from header to add them later | |
| parts = header_line.split('=') | |
| section_id = parts[0].strip() | |
| mod_parts = tokenize(parts[1].strip().rstrip('\\').strip()) | |
| for part in mod_parts: | |
| if ':' in part: | |
| header_mod_entries.append(part) | |
| # Extract tag lines and comments (if any) | |
| tag_lines = [] | |
| comment_lines = [] | |
| i = 1 | |
| while i < len(section_lines) and 'tags_' in section_lines[i]: | |
| tag_lines.append(section_lines[i].rstrip()) | |
| i += 1 | |
| # Extract all mod entries grouped by mod name | |
| mod_entries = {} | |
| # First add header mod entries if any | |
| for entry in header_mod_entries: | |
| mod_name = strip_quotes(entry).split(':')[0] | |
| if mod_name not in mod_entries: | |
| mod_entries[mod_name] = [] | |
| mod_entries[mod_name].append(entry) | |
| # Process all lines to collect mods and preserve comments | |
| for i, line in enumerate(section_lines): | |
| line = line.strip() | |
| # Skip empty lines and backslashes | |
| if not line or line == '\\': | |
| continue | |
| # Preserve comments | |
| if line.startswith('#'): | |
| comment_lines.append((i, section_lines[i].rstrip())) | |
| continue | |
| # Skip header line (already processed) | |
| if i == 0: | |
| continue | |
| # Skip tag lines (already processed) | |
| if 'tags_' in line: | |
| continue | |
| # If this is a mod entry (has a colon) | |
| if ':' in line and not line.startswith('#'): | |
| mod_parts = tokenize(line) | |
| for part in mod_parts: | |
| if ':' in part and not part.startswith('#'): | |
| mod_name = strip_quotes(part).split(':')[0] | |
| # Add to mod entries dictionary | |
| if mod_name not in mod_entries: | |
| mod_entries[mod_name] = [] | |
| # Clean the entry (remove trailing backslash) | |
| entry = part.rstrip('\\').strip() | |
| mod_entries[mod_name].append(entry) | |
| # Sort mod names | |
| sorted_mod_names = sorted(mod_entries.keys()) | |
| # Count effective mod names (excluding those that should be skipped) | |
| effective_mod_count = 0 | |
| for mod_name in sorted_mod_names: | |
| if not (header_has_mod and all(entry in header_mod_entries for entry in mod_entries[mod_name])): | |
| effective_mod_count += 1 | |
| # Reconstruct the section | |
| result = [] | |
| # Add the header line with a backslash if we have mod entries | |
| if sorted_mod_names and not header_line.endswith('\\'): | |
| result.append(f"{header_line} \\\n") | |
| else: | |
| result.append(f"{header_line}\n") | |
| # Add tag lines with backslashes | |
| for tag in tag_lines: | |
| tag = tag.rstrip('\\').strip() | |
| if sorted_mod_names: | |
| result.append(f"{tag} \\\n") | |
| else: | |
| result.append(f"{tag}\n") | |
| # Add comments that appear before mod entries | |
| for i, comment in comment_lines: | |
| # Only add comments that appear before the mod entries section | |
| if i < len(tag_lines) + 1: # +1 for header | |
| result.append(f"{comment}\n") | |
| # Add backslash separator if needed | |
| if effective_mod_count > 0: | |
| result.append("\\\n") | |
| # Add sorted mod entries | |
| processed_count = 0 | |
| for i, mod_name in enumerate(sorted_mod_names): | |
| # Skip mod entries that were in the header if preserving header | |
| if header_has_mod and all(entry in header_mod_entries for entry in mod_entries[mod_name]): | |
| continue | |
| processed_count += 1 | |
| # Sort entries within this mod alphabetically | |
| sorted_entries = sorted(mod_entries[mod_name], key=strip_quotes) | |
| # Join all entries for this mod | |
| entries = ' '.join(sorted_entries) | |
| if processed_count < effective_mod_count: | |
| # Not the last effective mod - add backslash | |
| result.append(f"{entries} \\\n") | |
| result.append("\\\n") | |
| else: | |
| # Last mod - no backslash | |
| result.append(f"{entries}\n") | |
| # Add comments that appear after mod entries | |
| for i, comment in comment_lines: | |
| # Only add comments that appear after the mod entries section | |
| if i >= len(tag_lines) + 1: # +1 for header | |
| result.append(f"{comment}\n") | |
| return result | |
| def main(): | |
| if len(sys.argv) < 2: | |
| print("Usage: python sort_properties.py <path_to_properties_file>") | |
| return | |
| file_path = sys.argv[1] | |
| if not os.path.exists(file_path): | |
| print(f"Error: File {file_path} not found") | |
| return | |
| # Sort file | |
| sorted_lines = sort_properties_file(file_path) | |
| # Write sorted content back | |
| with open(file_path, 'w', encoding='utf-8') as f: | |
| f.writelines(sorted_lines) | |
| print(f"File {file_path} sorted successfully!") | |
| if __name__ == "__main__": | |
| main() | |
| EOF | |
| # Process each modified properties file | |
| MODIFIED_FILES=$(git diff --name-only $GITHUB_SHA^ $GITHUB_SHA | grep -E '\.properties$' || true) | |
| if [ -n "$MODIFIED_FILES" ]; then | |
| echo "Running the sort script on modified properties files..." | |
| for file in $MODIFIED_FILES; do | |
| if [ -f "$file" ]; then | |
| echo "Sorting file: $file" | |
| python sort_properties.py "$file" | |
| fi | |
| done | |
| fi | |
| - name: Count modded blocks and update README | |
| run: | | |
| echo "Creating block counting script..." | |
| cat > count_blocks.py << 'EOF' | |
| import os | |
| import re | |
| def tokenize(line): | |
| """Split a line on whitespace, keeping "quoted entries with spaces" as a single token""" | |
| return re.findall(r'"[^"]*"|\S+', line) | |
| def strip_quotes(s): | |
| """Strip surrounding quotes from a token, if present""" | |
| if len(s) >= 2 and s.startswith('"') and s.endswith('"'): | |
| return s[1:-1] | |
| return s | |
| def update_readme_with_block_count(count): | |
| """Update README.md with the current block count""" | |
| import re | |
| readme_path = "README.md" | |
| if not os.path.exists(readme_path): | |
| print(f"Warning: {readme_path} not found") | |
| return False | |
| try: | |
| # Read the current README | |
| with open(readme_path, 'r', encoding='utf-8') as f: | |
| content = f.read() | |
| # Format the count for display | |
| if count >= 1000000: | |
| formatted_count = f"{count/1000000:.2f}M" | |
| elif count >= 1000: | |
| formatted_count = f"{count/1000:.2f}K" | |
| else: | |
| formatted_count = str(count) | |
| print(f"Formatted count: {formatted_count}") | |
| # Find the section markers | |
| start_marker = "## Current Total Modded Blocks: <ins>" | |
| end_marker = "</ins>" | |
| start_index = content.find(start_marker) | |
| if start_index != -1: | |
| start_index += len(start_marker) | |
| end_index = content.find(end_marker, start_index) | |
| if end_index != -1: | |
| # Replace just the count part | |
| updated_content = content[:start_index] + formatted_count + content[end_index:] | |
| # Now update the exact count line below (format: [54382]: #) | |
| exact_count_pattern = r'\[\d+\]: #' | |
| match = re.search(exact_count_pattern, updated_content[end_index:]) | |
| if match: | |
| # Calculate the position in the full content | |
| match_start = end_index + match.start() | |
| match_end = end_index + match.end() | |
| # Replace with new exact count | |
| updated_content = updated_content[:match_start] + f"[{count}]: #" + updated_content[match_end:] | |
| print(f"Updated exact count reference: [{count}]: #") | |
| else: | |
| print("Warning: Exact count reference line not found") | |
| # Write the updated content | |
| with open(readme_path, 'w', encoding='utf-8') as f: | |
| f.write(updated_content) | |
| print(f"README.md updated with block count: {formatted_count}") | |
| return True | |
| print(f"Warning: Count section not found in README.md") | |
| return False | |
| except Exception as e: | |
| print(f"Error updating README.md: {str(e)}") | |
| return False | |
| def main(): | |
| # Count blocks from all .properties files in "blockProperties" folder | |
| block_properties_folder = "blockProperties" | |
| print(f"Counting unique modded blocks in {block_properties_folder}...") | |
| total_count = 0 | |
| unique_blocks = set() | |
| # Walk through all subdirectories | |
| for root, dirs, files in os.walk(block_properties_folder): | |
| for file in files: | |
| if file.endswith('.properties'): | |
| file_path = os.path.join(root, file) | |
| print(f"Processing: {file_path}") | |
| try: | |
| with open(file_path, 'r', encoding='utf-8') as f: | |
| lines = f.readlines() | |
| for line in lines: | |
| line = line.strip() | |
| # Skip empty lines, comments | |
| if not line or line.startswith('#'): | |
| continue | |
| # Extract everything after equals sign if it's a block definition | |
| if line.startswith('block.'): | |
| equal_sign_index = line.find('=') | |
| if equal_sign_index > 0: | |
| line = line[equal_sign_index + 1:].strip() | |
| # Skip if just a backslash | |
| if line == '\\': | |
| continue | |
| # Split into potential block IDs | |
| potential_ids = tokenize(line) | |
| for block_id in potential_ids: | |
| # Skip empty, backslash, or tag identifiers | |
| if not block_id or block_id == '\\' or block_id.startswith('%') or block_id.startswith('tags_'): | |
| continue | |
| # Check if it could be a modded block (contains a colon) | |
| if ':' in block_id: | |
| # Extract the mod name and block name | |
| parts = strip_quotes(block_id).split(':') | |
| # If it has at least 2 parts and the first part isn't a blockstate | |
| if len(parts) >= 2 and '=' not in parts[0]: | |
| # Create the base mod:block identifier without blockstate | |
| mod_block_id = f"{parts[0]}:{parts[1]}" | |
| unique_blocks.add(mod_block_id) | |
| except Exception as e: | |
| print(f"Error processing {file_path}: {str(e)}") | |
| total_count = len(unique_blocks) | |
| print(f"Found {total_count} unique modded blocks across all files") | |
| # Update README | |
| update_readme_with_block_count(total_count) | |
| if __name__ == "__main__": | |
| main() | |
| EOF | |
| echo "Running block counting script..." | |
| python count_blocks.py | |
| - name: Configure Git | |
| if: github.event_name == 'push' | |
| run: | | |
| git config --global user.name 'GitHub Action' | |
| git config --global user.email 'action@github.com' | |
| - name: Commit and push with retry | |
| if: github.event_name == 'push' | |
| run: | | |
| # Add all properties files recursively and README | |
| git add '**/*.properties' README.md | |
| # Check if there are changes to commit | |
| if git diff --staged --quiet; then | |
| echo "No changes to commit" | |
| exit 0 | |
| fi | |
| echo "Committing timestamp, duplicate removal, sorting changes, and block count update..." | |
| git commit -m "Update timestamps, remove duplicates, sort mod entries, and update block count [skip ci]" | |
| # Try pushing with retries for potential conflicts | |
| MAX_RETRIES=3 | |
| RETRY_COUNT=0 | |
| until git push origin ${GITHUB_REF#refs/heads/} || [ $RETRY_COUNT -ge $MAX_RETRIES ]; do | |
| RETRY_COUNT=$((RETRY_COUNT + 1)) | |
| echo "Push failed, retrying (Attempt $RETRY_COUNT of $MAX_RETRIES)..." | |
| # Stash any unstaged changes before pulling | |
| git stash --include-untracked | |
| git pull --rebase origin ${GITHUB_REF#refs/heads/} | |
| # Pop stashed changes back if there were any | |
| git stash list | grep -q "stash@{0}" && git stash pop || true | |
| done | |
| if [ $RETRY_COUNT -ge $MAX_RETRIES ]; then | |
| echo "Failed to push after $MAX_RETRIES attempts, using force push" | |
| git push --force origin ${GITHUB_REF#refs/heads/} | |
| else | |
| echo "Timestamp updates, duplicate removal, mod sorting, and block count update pushed successfully" | |
| fi |